Visitar URL original
Use upsert instead of REPLACE INTO when applying synced rows by khawarizmus · Pull Request #236 · powersync-ja/powersync-sqlite-core · GitHub
Skip to content

Use upsert instead of REPLACE INTO when applying synced rows - #236

Draft
khawarizmus wants to merge 1 commit into
mainfrom
fix/sync-local-upsert
Draft

khawarizmus wants to merge 1 commit into
mainfrom
fix/sync-local-upsert

Conversation

@khawarizmus

Copy link
Copy Markdown

Problem

When a synced row already exists, core writes it with REPLACE INTO. SQLite then:

  • deletes the old row and inserts a new one with a new rowid
  • runs DELETE triggers only if recursive_triggers is on (JS and Dart native turn it on; Swift, Kotlin and Dart web don't)

Without the pragma, triggers on ps_data__* tables break. Example: FTS shows outdated/duplicate results and inserts fail with constraint failed.
Reported in powersync-ja/powersync-swift#93 and thunderbird/thunderbolt#1512.

Fix

Use an upsert (INSERT ... ON CONFLICT DO UPDATE) for ps_data__* and ps_untyped. Direct and raw tables already do this.

Why not turn on recursive_triggers in every SDK?

recursive_triggers Upsert
Where Every SDK and every connection Once, in core
Synced edit looks like DELETE + INSERT UPDATE
Rowid Changes Stays the same
Cascading-delete triggers Fire on every edit Fire only on real deletes

Keep the pragma in JS/Dart: user-defined raw table put statements may still use REPLACE.

Behavior change

A synced edit to an existing row now runs UPDATE triggers and keeps the rowid, on every SDK.
Triggers that only listen for INSERT or DELETE no longer see synced edits. This needs an upgrade note.

JS diff triggers (trackTableDiff) are affected. A synced edit was a DELETE + INSERT pair, and is now one UPDATE with previous_value:

  • tracking INSERT only: synced edits no longer appear, so add UPDATE
  • tracking DELETE: no more false deletes on edits
  • tracking UPDATE: synced edits now appear

Apps without custom triggers on internal tables: no change.

Tests

  • Added: a synced update runs the UPDATE trigger and keeps the rowid (ps_data__* and ps_untyped).

Open question

  • Release in a minor version with an upgrade note, or in the next major?

Used Claude to plan and execute the fix.

@rkistner rkistner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in favor of this change, and think a minor version update should be fine.

It would however be good to do a basic performance test of this. These two benchmarks should help here:

test('sync_local (full)', () {
final timer = runSyncLocal(null);
// These are fairly generous limits, to catch significant regressions only.
expect(vfs.tempWrites, lessThan(count / 50));
expect(timer.elapsed,
lessThan(Duration(milliseconds: 100 + (count / 50).round())));
});
test('sync_local (partial)', () {
final timer = runSyncLocal(2);
expect(vfs.tempWrites, lessThan(count / 50));
expect(timer.elapsed,
lessThan(Duration(milliseconds: 100 + (count / 50).round())));
});
. They measure number of filesystem operations rather than actual throughput, but it should give a good indication of potential performance impact.

I also think it would be good to additionally enable recursive triggers in all our SDKs, since it's quite unexpected if triggers don't fire after synced changes.

It would be good to get @simolus3's input on this as well.

@simolus3

Copy link
Copy Markdown
Contributor

I agree that using an upsert here makes sense, and that we can use a minor/patch version (changing the behavior of diff triggers should be okay since it's an improvement and they're not stable yet).

I also think it would be good to additionally enable recursive triggers in all our SDKs, since it's quite unexpected if triggers don't fire after synced changes.

Yeah, judging by the fact that Dart/native enables this and Dart/web doesn't, I think the missing SDKs are an oversight.


Test results:

Test name main fix/sync-local-upsert
unique ids full Reads: 1290591 + 0 ; Writes: 69031 + 0 Reads: 1291626 + 0 ; Writes: 69031 + 0
unique ids partial Reads: 521257 + 0 ; Writes: 27613 + 0 Reads: 521340 + 0 ; Writes: 27613 + 0
duplicate ids full Reads: 431637 + 0 ; Writes: 38 + 0 Reads: 431637 + 0 ; Writes: 38 + 0
duplicate ids partial Reads: 199397 + 0 ; Writes: 34 + 0 Reads: 199397 + 0 ; Writes: 34 + 0
small number of changes full Reads: 5680 + 0 ; Writes: 736 + 0 Reads: 5681 + 0 ; Writes: 736 + 0
small number of changes partial Reads: 2958 + 0 ; Writes: 312 + 0 Reads: 2988 + 0 ; Writes: 312 + 0
large number of buckets full Reads: 116062 + 0 ; Writes: 12729 + 0 Reads: 116113 + 0 ; Writes: 12729 + 0
large number of buckets partial Reads: 863 + 0 ; Writes: 83 + 0 Reads: 865 + 0 ; Writes: 83 + 0

The slight regression in the amount of reads doesn't seem that concerning to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants