{"record":{"id":"06515c3e1172d98e","repo":"actualbudget/actual","slug":"invalid-schema","errorCode":"invalid-schema","errorMessage":"invalid-schema","messagePattern":"invalid-schema","errorType":"error_code","errorClass":"SyncError","httpStatus":null,"severity":"critical","filePath":"packages/loot-core/src/server/sync/index.ts","lineNumber":102,"sourceCode":"    // Do nothing, it doesn't exist in the db\n  } else {\n    let query;\n    try {\n      if (prev) {\n        query = {\n          sql: `UPDATE ${dataset} SET ${column} = ? WHERE id = ?`,\n          params: [value, row],\n        };\n      } else {\n        query = {\n          sql: `INSERT INTO ${dataset} (id, ${column}) VALUES (?, ?)`,\n          params: [row, value],\n        };\n      }\n\n      db.runQuery(db.cache(query.sql), query.params);\n    } catch (error) {\n      throw new SyncError('invalid-schema', {\n        error: { message: error.message, stack: error.stack },\n        query,\n      });\n    }\n  }\n}\n\n// TODO: convert to `whereIn`\nasync function fetchAll(table, ids) {\n  let results = [];\n\n  // was 500, but that caused a stack overflow in Safari\n  const batchSize = 100;\n\n  for (let i = 0; i < ids.length; i += batchSize) {\n    const partIds = ids.slice(i, i + batchSize);\n    let sql;\n    let column = `${table}.id`;","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/sync/index.ts#L84-L120","documentation":"A SyncError thrown by the internal apply() function when the SQL generated from an incoming sync message (INSERT INTO <dataset> or UPDATE <dataset> SET <column>) fails against the local SQLite database. It wraps the underlying SQL error (message/stack) plus the offending query. It almost always means the local schema does not match the schema the remote message was created against.","triggerScenarios":"applyMessages/applyMessagesForImport replay a message whose dataset is a table that no longer exists, whose column was renamed/removed, or whose value violates constraints (e.g. NOT NULL, UNIQUE) in the local schema; typically after a version downgrade or upgrading an old budget file to a newer schema.","commonSituations":"Opening a budget with a newer app version synced its schema-changing messages, then opening it with an older app; custom/modified database files; migrations partially applied so messages reference tables/columns missing locally.","solutions":["Update to the latest Actual version so the local schema matches the messages being applied","Back up the budget file, then check the wrapped error.query in the log to identify the missing table/column and repair the schema","Restore the budget from a backup made with a matching app version","If a migration failed, re-run migrations by reloading/resetting the budget db from the server copy"],"exampleFix":"// before: downgraded app applying unknown column\napplyMessages(msgs); // invalid-schema: no such column: sort_order\n// after: upgrade first\nyarn upgrade @actual-app/web@latest // then reopen budget and sync","handlingStrategy":"try-catch","validationCode":"const tables = await db.runQuery(\n  \"SELECT name FROM sqlite_master WHERE type='table'\",\n);\nif (!tables.some(t => t.name === msg.dataset)) {\n  throw new Error(`Local schema missing table: ${msg.dataset}`);\n}","typeGuard":"function isInvalidSchema(e: unknown): e is SyncError & { reason: { error: { message: string }, query: { sql: string } } } {\n  return e instanceof SyncError && e.reason?.code === 'invalid-schema' && !!e.reason.query;\n}","tryCatchPattern":"try {\n  await applyMessages(messages);\n} catch (e) {\n  if (isInvalidSchema(e)) {\n    logger.error('Schema mismatch at:', e.reason.query.sql, e.reason.error.message);\n    // restore from backup or upgrade app\n  } else throw e;\n}","preventionTips":["Never downgrade the app below the version that wrote the budget","Take budget backups before upgrades","Let migrations finish before syncing","Inspect e.reason.query in logs to pinpoint the schema drift"],"tags":["sync","sqlite","schema","migration"],"backgroundTag":"schema-version-mismatch","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}