actualbudget/actual · error · CompileError

Invalid internal table filter: only object filters are suppo

Error message

Invalid internal table filter: only object filters are supported

What it means

When AQL queries internal tables, `tableFilters` supplies implicit filters for that table. These filters must be simple `{field: value}` objects because they are compiled without join support; if a filter is an array (array-form condition), CompileError is thrown since array filters may reference joins that internal table filters cannot handle.

Source

Thrown at packages/loot-core/src/server/aql/compiler.ts:1094

export function compileQuery(
  queryState: QueryState,
  schema: Schema,
  schemaConfig: SchemaConfig = {},
) {
  const { withDead, validateRefs = true, tableOptions, rawMode } = queryState;

  const {
    tableViews = {},
    tableFilters = () => [],
    customizeQuery = queryState => queryState,
  } = schemaConfig;

  const internalTableFilters = name => {
    const filters = tableFilters(name);
    // These filters cannot join tables and must be simple strings
    for (const filter of filters) {
      if (Array.isArray(filter)) {
        throw new CompileError(
          'Invalid internal table filter: only object filters are supported',
        );
      }
      if (Object.keys(filter)[0].indexOf('.') !== -1) {
        throw new CompileError(
          'Invalid internal table filter: field names cannot contain paths',
        );
      }
    }
    return filters;
  };

  const tableRef = (name: string, isJoin?: boolean) => {
    const view =
      typeof tableViews === 'function'
        ? tableViews(name, { withDead, isJoin, tableOptions })
        : tableViews[name];
    return view || name;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Rewrite the filter as a plain object: `{field: value}` or `{field: {$op: value}}`.
  2. Move complex join-dependent conditions out of tableFilters into the query itself.
  3. Add a preprocessing step that converts array-form conditions to object form before registering table filters.

Example fix

// before
tableFilters = { tombstone: ['tombstone', '=', 0] }
// after
tableFilters = { tombstone: { tombstone: 0 } }
Defensive patterns

Strategy: validation

Validate before calling

function assertObjectFilters(filters) {
  for (const f of filters) {
    if (Array.isArray(f)) throw new Error('Internal table filters must be object form');
  }
}

Type guard

const isObjectFilter = (f) => typeof f === 'object' && f !== null && !Array.isArray(f);

Prevention

When it happens

Trigger: Providing a custom `tableFilters` mapping for an internal table where a filter is written in the array/condition-list form `[['amount', '$gt', 5]]` instead of the object form `{amount: 5}`.

Common situations: Extending the AQL schema with views/dead-keys filters and reusing condition arrays from elsewhere, or constructing filters programmatically and mixing the two accepted condition forms.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/a849ab431462be7a. Report an issue: GitHub.