@tanstack/powersync-db-collection 0.1.67 (latest), @tanstack/db 0.9.0, @powersync/common 1.57.3, @powersync/node 0.19.5. The relevant code is unchanged on main in both repositories.
Describe the bug
PowerSync syncs rows as schemaless JSON and applies the client schema as views, so a backend can add a column before every installed app version knows about it — older clients simply don't see the new column. With the collection, that column breaks writes instead:
- The collection creates its diff trigger without a
columns list, so TriggerManagerImpl records the raw NEW.data ("Track all columns"), including keys the client schema does not declare.
- When a row changes, the collection deserializes that JSON into memory, extra key included. (Rows loaded by the initial
SELECT * FROM <view> don't carry it, so rows that never change are unaffected.)
collection.update serializes the in-memory row with serializeForSQLite, which throws Could not find schema for <key> column. for the undeclared key.
This is the normal situation for mobile apps, where older versions stay installed while the backend moves on: one backend migration and every update on a changed row fails for those users.
To Reproduce
A local @powersync/node database. Writing the synced JSON in ps_data__todos directly stands in for the sync client applying a row from a newer backend; we first hit this with a real PowerSync service.
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PowerSyncDatabase, Schema, Table, column } from '@powersync/node'
import { createCollection } from '@tanstack/db'
import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
// This app version's schema: no `priority` column.
const AppSchema = new Schema({ todos: new Table({ title: column.text }) })
const db = new PowerSyncDatabase({
schema: AppSchema,
database: { dbFilename: 'repro.sqlite', dbLocation: mkdtempSync(join(tmpdir(), 'ps-undeclared-')) },
})
await db.init()
await db.execute(`INSERT INTO todos (id, title) VALUES ('t1', 'Write report')`)
const todos = createCollection(powerSyncCollectionOptions({ database: db, table: AppSchema.props.todos }))
await todos.preload()
// A newer backend adds `priority`; the synced row now carries it.
await db.writeTransaction(async (tx) => {
await tx.execute(`UPDATE ps_data__todos SET data = json_set(data, '$.priority', 'high') WHERE id = 't1'`)
})
await new Promise((r) => setTimeout(r, 200)) // let the collection flush the change into memory
console.log('collection row keys:', Object.keys(todos.get('t1') ?? {}).filter((k) => !k.startsWith('$')))
console.log('view row:', await db.get(`SELECT * FROM todos WHERE id = 't1'`))
try {
const tx = todos.update('t1', (draft) => {
draft.title = 'Write report today'
})
await tx.isPersisted.promise
console.log('OK: update persisted')
} catch (err) {
console.log(`BUG: ${(err as Error).message}`)
}
Output:
collection row keys: [ 'id', 'title', 'priority' ]
view row: { id: 't1', title: 'Write report' }
BUG: Could not find schema for priority column.
Expected behavior
The collection ignores columns the schema does not declare, as the view does, and the update persists.
Additional context
Passing the declared column names to createDiffTrigger (it accepts a columns option) or dropping undeclared keys when a sync row is deserialized would avoid it. Persisting only mutation.changes in handleUpdate (#1817, a lost-update problem) would also stop this particular throw, but the undeclared keys would still show up in query results.
Environment: Node 22.22.1 on macOS 26.6 (arm64).
@tanstack/powersync-db-collection0.1.67 (latest),@tanstack/db0.9.0,@powersync/common1.57.3,@powersync/node0.19.5. The relevant code is unchanged onmainin both repositories.Describe the bug
PowerSync syncs rows as schemaless JSON and applies the client schema as views, so a backend can add a column before every installed app version knows about it — older clients simply don't see the new column. With the collection, that column breaks writes instead:
columnslist, soTriggerManagerImplrecords the rawNEW.data("Track all columns"), including keys the client schema does not declare.SELECT * FROM <view>don't carry it, so rows that never change are unaffected.)collection.updateserializes the in-memory row withserializeForSQLite, which throwsCould not find schema for <key> column.for the undeclared key.This is the normal situation for mobile apps, where older versions stay installed while the backend moves on: one backend migration and every update on a changed row fails for those users.
To Reproduce
A local
@powersync/nodedatabase. Writing the synced JSON inps_data__todosdirectly stands in for the sync client applying a row from a newer backend; we first hit this with a real PowerSync service.Output:
Expected behavior
The collection ignores columns the schema does not declare, as the view does, and the update persists.
Additional context
Passing the declared column names to
createDiffTrigger(it accepts acolumnsoption) or dropping undeclared keys when a sync row is deserialized would avoid it. Persisting onlymutation.changesinhandleUpdate(#1817, a lost-update problem) would also stop this particular throw, but the undeclared keys would still show up in query results.Environment: Node 22.22.1 on macOS 26.6 (arm64).