{"record":{"id":"4ca7b687a34b7ba4","repo":"actualbudget/actual","slug":"field-not-joinable-on-table-tablename-field","errorCode":null,"errorMessage":"Field not joinable on table ${tableName}: \"${field}\"","messagePattern":"Field not joinable on table (.+?): \"(.+?)\"","errorType":"exception","errorClass":"CompileError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/aql/compiler.ts","lineNumber":89,"sourceCode":"function makePath(state, path) {\n  const { schema, paths } = state;\n\n  const parts = path.split('.');\n  if (parts.length < 2) {\n    throw new CompileError('Invalid path: ' + path);\n  }\n\n  const initialTable = parts[0];\n\n  const tableName = parts.slice(1).reduce((tableName, field) => {\n    const table = schema[tableName];\n\n    if (table == null) {\n      throw new CompileError(`Path error: ${tableName} table does not exist`);\n    }\n\n    if (!table[field] || table[field].ref == null) {\n      throw new CompileError(\n        `Field not joinable on table ${tableName}: \"${field}\"`,\n      );\n    }\n\n    return table[field].ref;\n  }, initialTable);\n\n  let joinTable;\n  const parentParts = parts.slice(0, -1);\n  if (parentParts.length === 1) {\n    joinTable = parentParts[0];\n  } else {\n    const parentPath = parentParts.join('.');\n    const parentDesc = paths.get(parentPath);\n    if (!parentDesc) {\n      throw new CompileError('Path does not exist: ' + parentPath);\n    }\n    joinTable = parentDesc.tableId;","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/aql/compiler.ts#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Replace the intermediate segment with a field that has a `ref` in the schema (a real relationship, e.g. 'payee', 'account', 'category').","If you only need the column value, stop the path at that field: use 'amount' rather than 'amount.something'.","Check schema.ts to confirm which fields on the table define `ref` before composing multi-segment paths."],"exampleFix":"// before\nq('transactions').select('amount.name')\n// after\nq('transactions').select('payee.name')","handlingStrategy":"validation","validationCode":"import { schema } from './aql/schema';\nfunction isJoinable(table, field) {\n  return Boolean(schema[table] && schema[table][field] && schema[table][field].ref != null);\n}","typeGuard":"function hasRef(fieldDesc) {\n  return fieldDesc != null && typeof fieldDesc === 'object' && 'ref' in fieldDesc && fieldDesc.ref != null;\n}","tryCatchPattern":"try {\n  runQuery(q);\n} catch (e) {\n  if (e.message.includes('Field not joinable')) {\n    throw new Error('Intermediate path segments must be relationships (ref fields)');\n  } else throw e;\n}","preventionTips":["Only traverse fields with `ref` in the schema","Stop paths at scalar fields; never extend them","Keep a map of joinable relationships per table for query generation"],"tags":["aql","query","join","path","compile-error"],"backgroundTag":"field-not-joinable","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}