diff --git a/.changeset/small-tables-listen.md b/.changeset/small-tables-listen.md new file mode 100644 index 0000000000..fbb62047b7 --- /dev/null +++ b/.changeset/small-tables-listen.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db-sqlite-persistence-core': patch +--- + +Fixed bug where internal metadata would get reset on insert causing stale items to remain diff --git a/packages/db-sqlite-persistence-core/src/persisted.ts b/packages/db-sqlite-persistence-core/src/persisted.ts index 12cf8319c3..ca60a050ab 100644 --- a/packages/db-sqlite-persistence-core/src/persisted.ts +++ b/packages/db-sqlite-persistence-core/src/persisted.ts @@ -2290,9 +2290,21 @@ function createWrappedSyncConfig< message.type === `insert` && normalization.operation.metadata === undefined ) { - openTransaction.rowMetadataWrites.set(normalization.operation.key, { - type: `delete`, - }) + // Reset stale metadata for a fresh insert, but don't clobber an + // explicit metadata write already queued for this key in the same + // transaction (e.g. query reconcile stamps owners, then inserts). + if ( + !openTransaction.rowMetadataWrites.has( + normalization.operation.key, + ) + ) { + openTransaction.rowMetadataWrites.set( + normalization.operation.key, + { + type: `delete`, + }, + ) + } } else if (normalization.operation.metadata !== undefined) { openTransaction.rowMetadataWrites.set(normalization.operation.key, { type: `set`, diff --git a/packages/db-sqlite-persistence-core/tests/persisted.test.ts b/packages/db-sqlite-persistence-core/tests/persisted.test.ts index 57c11d8442..42bbcf5633 100644 --- a/packages/db-sqlite-persistence-core/tests/persisted.test.ts +++ b/packages/db-sqlite-persistence-core/tests/persisted.test.ts @@ -816,6 +816,78 @@ describe(`persistedCollectionOptions`, () => { ) }) + it(`preserves row metadata set before a metadata-less insert in the same sync transaction`, async () => { + const adapter = createRecordingAdapter() + const ownership = { queryCollection: { owners: [`gc:q1`] } } + const sync: SyncConfig = { + sync: ({ begin, write, commit, markReady, metadata }) => { + begin() + metadata?.row.set(`remote-1`, ownership) + write({ + type: `insert`, + value: { + id: `remote-1`, + title: `From remote`, + }, + }) + commit() + markReady() + }, + } + + const collection = createCollection( + persistedCollectionOptions({ + id: `sync-present`, + getKey: (item: Todo) => item.id, + sync, + persistence: { + adapter, + }, + }), + ) + + await collection.stateWhenReady() + await flushAsyncWork() + + expect(adapter.rowMetadata.get(`remote-1`)).toEqual(ownership) + expect(collection._state.syncedMetadata.get(`remote-1`)).toEqual(ownership) + }) + + it(`resets stale row metadata for a metadata-less insert with no queued metadata`, async () => { + const adapter = createRecordingAdapter() + adapter.rowMetadata.set(`remote-1`, { stale: true }) + const sync: SyncConfig = { + sync: ({ begin, write, commit, markReady }) => { + begin() + write({ + type: `insert`, + value: { + id: `remote-1`, + title: `From remote`, + }, + }) + commit() + markReady() + }, + } + + const collection = createCollection( + persistedCollectionOptions({ + id: `sync-present`, + getKey: (item: Todo) => item.id, + sync, + persistence: { + adapter, + }, + }), + ) + + await collection.stateWhenReady() + await flushAsyncWork() + + expect(adapter.rowMetadata.has(`remote-1`)).toBe(false) + }) + it(`uses a stable generated collection id in sync-present mode when id is omitted`, async () => { const adapter = createRecordingAdapter() const options = persistedCollectionOptions({