actualbudget/actual · error · CompileError

Field not joinable on table ${tableName}: "${field}"

Error message

Field not joinable on table ${tableName}: "${field}"

What it means

makePath (compiler.ts:88) only allows path segments that are joinable — i.e. schema fields with a `ref` pointing at another table. If an intermediate path segment names a field that either doesn't exist or exists but is a plain column (no ref), this CompileError is thrown because the compiler cannot build a JOIN for it.

Source

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

function makePath(state, path) {
  const { schema, paths } = state;

  const parts = path.split('.');
  if (parts.length < 2) {
    throw new CompileError('Invalid path: ' + path);
  }

  const initialTable = parts[0];

  const tableName = parts.slice(1).reduce((tableName, field) => {
    const table = schema[tableName];

    if (table == null) {
      throw new CompileError(`Path error: ${tableName} table does not exist`);
    }

    if (!table[field] || table[field].ref == null) {
      throw new CompileError(
        `Field not joinable on table ${tableName}: "${field}"`,
      );
    }

    return table[field].ref;
  }, initialTable);

  let joinTable;
  const parentParts = parts.slice(0, -1);
  if (parentParts.length === 1) {
    joinTable = parentParts[0];
  } else {
    const parentPath = parentParts.join('.');
    const parentDesc = paths.get(parentPath);
    if (!parentDesc) {
      throw new CompileError('Path does not exist: ' + parentPath);
    }
    joinTable = parentDesc.tableId;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Replace the intermediate segment with a field that has a `ref` in the schema (a real relationship, e.g. 'payee', 'account', 'category').
  2. If you only need the column value, stop the path at that field: use 'amount' rather than 'amount.something'.
  3. Check schema.ts to confirm which fields on the table define `ref` before composing multi-segment paths.

Example fix

// before
q('transactions').select('amount.name')
// after
q('transactions').select('payee.name')
Defensive patterns

Strategy: validation

Validate before calling

import { schema } from './aql/schema';
function isJoinable(table, field) {
  return Boolean(schema[table] && schema[table][field] && schema[table][field].ref != null);
}

Type guard

function hasRef(fieldDesc) {
  return fieldDesc != null && typeof fieldDesc === 'object' && 'ref' in fieldDesc && fieldDesc.ref != null;
}

Try / catch

try {
  runQuery(q);
} catch (e) {
  if (e.message.includes('Field not joinable')) {
    throw new Error('Intermediate path segments must be relationships (ref fields)');
  } else throw e;
}

Prevention

When it happens

Trigger: q('transactions').select('amount.name') — 'amount' is a plain number column with no ref; or q('transactions').filter({ 'notes.value': ... }) where 'notes' has no ref in the schema.

Common situations: Attempting to traverse through scalar columns as if they were relationships; inventing relationship names not present in the schema; old queries written against schema versions where the field was joinable and later changed.

Related errors


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