You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[PowerSync] Support integer primary keys: the collection key must be a string id#2391
@tanstack/powersync-db-collection requires the collection key to be the string id column. A backend with integer primary keys (a Postgres identity or serial column) therefore has to use string keys in TanStack DB. Its integer foreign keys then no longer match: eq(part.id, line.partId) compares "5" with 5.
The obvious fix is to convert id back to a number in deserializationSchema. The adapter rejects that:
Error: Record must have a string id field
at asPowerSyncRecord (helpers.js)
at Object.getKey (powersync.js)
Reproduction
@tanstack/db and @tanstack/powersync-db-collection1.0.0-rc.1.4, @powersync/node0.21.0, @powersync/common2.5.0, Node 24. The same code is on main (873a560).
constAPP_SCHEMA=newSchema({products: newTable({name: column.text,category: column.text})})// db = new PowerSyncDatabase({ schema: APP_SCHEMA, database: { implementation: { type: 'node:sqlite' }, ... }})awaitdb.execute(`INSERT INTO products (id, name, category) VALUES ('1', 'A', 'x'), ('2', 'B', 'x')`)constrow=z.object({id: z.coerce.number(),name: z.string().nullable(),category: z.string().nullable()})constcollection=createCollection(powerSyncCollectionOptions({database: db,table: APP_SCHEMA.props.products,schema: row,deserializationSchema: row,onDeserializationError: ()=>{},}),)awaitcreateLiveQueryCollection({query: (q)=>q.from({product: collection}).where(({ product })=>eq(product.category,'x')),}).preload()// Eager: "Source collection '…' failed its first load: Record must have a string id field"// On-demand: "Initial subset load failed: Record must have a string id field"
The same script with id: z.string() loads and reloads correctly.
Where the string key is assumed
helpers.ts#L69: asPowerSyncRecord throws for a non-string id, and getKey goes through it. The types enforce the same thing: AnyTableColumnType has id: string, and the config type omits getKey.
Removing that check alone wouldn't be enough. Two on-demand paths compare the raw SQLite id (always text) with collection keys:
powersync.ts#L818: collection.has(rowId) never matches a numeric key. A held row that changed would then be written as an insert and throw DuplicateKeySyncError.
powersync.ts#L830: missing?.delete(row.id) would remove nothing, so recovering from a tracking outage would delete every held row.
Ongoing changes (flushDiffRecordsWithContext) and deletes already derive the key from the deserialized row.
Proposal
Let the key come from the deserialized row. For example, allow id to deserialize to string | number, or accept a getKey option. Then use that key in the two on-demand paths above, e.g. getKey(deserializeSyncRow(row)) instead of row.id.
Local writes through the collection would still need a string id for SQLite. So this could be limited to collections that convert id in deserializationSchema and only read, with writes going through the app's API, which is the case for server-generated integer IDs. PowerSync's service already converts integer id columns to text when syncing (idFromData in sync-rules/src/cast.ts), so only the client adapter needs the change.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
@tanstack/powersync-db-collectionrequires the collection key to be the stringidcolumn. A backend with integer primary keys (a Postgres identity or serial column) therefore has to use string keys in TanStack DB. Its integer foreign keys then no longer match:eq(part.id, line.partId)compares"5"with5.The obvious fix is to convert
idback to a number indeserializationSchema. The adapter rejects that:Reproduction
@tanstack/dband@tanstack/powersync-db-collection1.0.0-rc.1.4,@powersync/node0.21.0,@powersync/common2.5.0, Node 24. The same code is onmain(873a560).The same script with
id: z.string()loads and reloads correctly.Where the string key is assumed
helpers.ts#L69:asPowerSyncRecordthrows for a non-stringid, andgetKeygoes through it. The types enforce the same thing:AnyTableColumnTypehasid: string, and the config type omitsgetKey.Removing that check alone wouldn't be enough. Two on-demand paths compare the raw SQLite
id(always text) with collection keys:powersync.ts#L818:collection.has(rowId)never matches a numeric key. A held row that changed would then be written as aninsertand throwDuplicateKeySyncError.powersync.ts#L830:missing?.delete(row.id)would remove nothing, so recovering from a tracking outage would delete every held row.Ongoing changes (
flushDiffRecordsWithContext) and deletes already derive the key from the deserialized row.Proposal
Let the key come from the deserialized row. For example, allow
idto deserialize tostring | number, or accept agetKeyoption. Then use that key in the two on-demand paths above, e.g.getKey(deserializeSyncRow(row))instead ofrow.id.Local writes through the collection would still need a string
idfor SQLite. So this could be limited to collections that convertidindeserializationSchemaand only read, with writes going through the app's API, which is the case for server-generated integer IDs. PowerSync's service already converts integeridcolumns to text when syncing (idFromDatainsync-rules/src/cast.ts), so only the client adapter needs the change.All reactions