From 4a03e7bf57b47c23824b21c11272ac7b16083cdf Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 13 Aug 2026 10:36:13 +0200 Subject: [PATCH 1/2] Fix `tx_id` mismatch for insert-only tables --- crates/core/src/crud_vtab.rs | 41 +++++++++++++---- crates/core/src/state.rs | 1 + dart/test/crud_test.dart | 58 +++++++++++++++++++++++++ dart/test/utils/migration_fixtures.dart | 51 +++++++++++----------- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/crates/core/src/crud_vtab.rs b/crates/core/src/crud_vtab.rs index 0f91db6..b8917b1 100644 --- a/crates/core/src/crud_vtab.rs +++ b/crates/core/src/crud_vtab.rs @@ -4,6 +4,7 @@ use alloc::boxed::Box; use alloc::rc::Rc; use const_format::formatcp; use core::ffi::{CStr, c_char, c_int, c_void}; +use core::mem; use serde::Serialize; use serde_json::value::RawValue; @@ -50,6 +51,15 @@ struct VirtualTable { struct ActiveCrudTransaction { tx_id: i64, mode: CrudTransactionMode, + /// Whether the virtual table owning this transaction observed the begin, or whether we re-used + /// a transaction we already knew existed. + /// + /// We have two virtual tables to record local mutations as crud entries. They both need a begin + /// hook to increment `ps_tx` when a transaction operates on that table. For transactions + /// operating on both tables, only one table must increment the counter though. We store whether + /// we're in a transaction in [DatabaseState::current_transaction_id], if that is true when + /// `xBegin` is called then we skip incrementing the counter. + observed_begin: bool, } enum CrudTransactionMode { @@ -184,17 +194,28 @@ impl VirtualTable { fn begin(&mut self) -> Result<()> { let db = self.db; - // language=SQLite - let statement = - db.prepare_v2("UPDATE ps_tx SET next_tx = next_tx + 1 WHERE id = 1 RETURNING next_tx")?; - let tx_id = if statement.step()? { - statement.column_int64(0) - 1 - } else { - return Err(PowerSyncError::unknown_internal()); + let (tx_id, observed_begin) = { + if let Some(existing_tx) = self.state.current_transaction_id.get() { + // Re-use existing transaction, the other table is responsible for clearing that + // field on commit. + (existing_tx, false) + } else { + let statement = db.prepare_v2( + "UPDATE ps_tx SET next_tx = next_tx + 1 WHERE id = 1 RETURNING next_tx", + )?; + let tx_id = if statement.step()? { + statement.column_int64(0) - 1 + } else { + return Err(PowerSyncError::unknown_internal()); + }; + self.state.current_transaction_id.set(Some(tx_id)); + (tx_id, true) + } }; self.current_tx = Some(ActiveCrudTransaction { tx_id, + observed_begin, mode: if self.is_simple { CrudTransactionMode::Simple(Default::default()) } else { @@ -206,7 +227,11 @@ impl VirtualTable { } fn end_transaction(&mut self) { - self.current_tx = None; + if let Some(tx) = mem::take(&mut self.current_tx) { + if tx.observed_begin { + self.state.current_transaction_id.set(None); + } + } } } diff --git a/crates/core/src/state.rs b/crates/core/src/state.rs index 27b1107..a0a66df 100644 --- a/crates/core/src/state.rs +++ b/crates/core/src/state.rs @@ -36,6 +36,7 @@ pub struct DatabaseState { /// Cached put and delete statements for raw tables, used by the `sync_local` step of the sync /// client. pub inferred_schema_cache: InferredSchemaCache, + pub current_transaction_id: Cell>, } impl DatabaseState { diff --git a/dart/test/crud_test.dart b/dart/test/crud_test.dart index 74ca574..fe268bc 100644 --- a/dart/test/crud_test.dart +++ b/dart/test/crud_test.dart @@ -980,5 +980,63 @@ INSERT INTO ps_kv(key, value) VALUES expect(db.select('SELECT * FROM ps_crud'), isEmpty); }); }); + + group('transaction ids', () { + setUp(() { + db.executeInTx('select powersync_replace_schema(?)', [ + json.encode({ + 'tables': [ + { + 'name': 'regular', + 'columns': [ + {'name': 'a', 'type': 'integer'} + ] + }, + { + 'name': 'insertonly', + 'insert_only': true, + 'columns': [ + {'name': 'a', 'type': 'integer'} + ] + } + ], + }) + ]); + }); + + for (final table in ['regular', 'insertonly']) { + test('for write into $table table', () { + for (var tx = 1; tx < 10; tx++) { + db.execute('BEGIN'); + final numWrites = tx * 2; + for (var i = 0; i < numWrites; i++) { + db.execute('INSERT INTO $table (id, a) VALUES (uuid(), 1234)'); + } + db.execute('COMMIT'); + + expect( + db.select('SELECT * FROM ps_crud WHERE tx_id = ?', [tx]), + hasLength(numWrites), + ); + } + }); + } + + test('for writes in both types of tables', () { + for (var tx = 1; tx < 10; tx++) { + db.execute('BEGIN'); + for (var i = 0; i < tx; i++) { + db.execute('INSERT INTO regular (id, a) VALUES (uuid(), 1234)'); + db.execute('INSERT INTO insertonly (id, a) VALUES (uuid(), 1234)'); + } + db.execute('COMMIT'); + + expect( + db.select('SELECT * FROM ps_crud WHERE tx_id = ?', [tx]), + hasLength(tx * 2), + ); + } + }); + }); }); } diff --git a/dart/test/utils/migration_fixtures.dart b/dart/test/utils/migration_fixtures.dart index 35c23b7..949c092 100644 --- a/dart/test/utils/migration_fixtures.dart +++ b/dart/test/utils/migration_fixtures.dart @@ -7,7 +7,7 @@ final expectedState = _expectedState(); Map _expectedState() { final state = { - 2: r''' + 2: r''' ;CREATE TABLE ps_buckets( name TEXT PRIMARY KEY, last_applied_op INTEGER NOT NULL DEFAULT 0, @@ -36,7 +36,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(1, null) ;INSERT INTO ps_migration(id, down_migrations) VALUES(2, '[{"sql":"DELETE FROM ps_migration WHERE id >= 2","params":[]},{"sql":"DROP TABLE ps_tx","params":[]},{"sql":"ALTER TABLE ps_crud DROP COLUMN tx_id","params":[]}]') ''', - 3: r''' + 3: r''' ;CREATE TABLE ps_buckets( name TEXT PRIMARY KEY, last_applied_op INTEGER NOT NULL DEFAULT 0, @@ -67,7 +67,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(2, '[{"sql":"DELETE FROM ps_migration WHERE id >= 2","params":[]},{"sql":"DROP TABLE ps_tx","params":[]},{"sql":"ALTER TABLE ps_crud DROP COLUMN tx_id","params":[]}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(3, '[{"sql":"DELETE FROM ps_migration WHERE id >= 3"},{"sql":"DROP TABLE ps_kv"}]') ''', - 4: r''' + 4: r''' ;CREATE TABLE ps_buckets( name TEXT PRIMARY KEY, last_applied_op INTEGER NOT NULL DEFAULT 0, @@ -99,7 +99,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(3, '[{"sql":"DELETE FROM ps_migration WHERE id >= 3"},{"sql":"DROP TABLE ps_kv"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(4, '[{"sql":"DELETE FROM ps_migration WHERE id >= 4"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN op_checksum"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN remove_operations"}]') ''', - 5: r''' + 5: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -137,7 +137,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(4, '[{"sql":"DELETE FROM ps_migration WHERE id >= 4"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN op_checksum"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN remove_operations"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(5, '[{"sql":"SELECT powersync_drop_view(view.name)\n FROM sqlite_master view\n WHERE view.type = ''view''\n AND view.sql GLOB ''*-- powersync-auto-generated''"},{"sql":"ALTER TABLE ps_buckets RENAME TO ps_buckets_5"},{"sql":"ALTER TABLE ps_oplog RENAME TO ps_oplog_5"},{"sql":"CREATE TABLE ps_buckets(\n name TEXT PRIMARY KEY,\n last_applied_op INTEGER NOT NULL DEFAULT 0,\n last_op INTEGER NOT NULL DEFAULT 0,\n target_op INTEGER NOT NULL DEFAULT 0,\n add_checksum INTEGER NOT NULL DEFAULT 0,\n pending_delete INTEGER NOT NULL DEFAULT 0\n, op_checksum INTEGER NOT NULL DEFAULT 0, remove_operations INTEGER NOT NULL DEFAULT 0)"},{"sql":"INSERT INTO ps_buckets(name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete)\n SELECT name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete FROM ps_buckets_5"},{"sql":"CREATE TABLE ps_oplog(\n bucket TEXT NOT NULL,\n op_id INTEGER NOT NULL,\n op INTEGER NOT NULL,\n row_type TEXT,\n row_id TEXT,\n key TEXT,\n data TEXT,\n hash INTEGER NOT NULL,\n superseded INTEGER NOT NULL)"},{"sql":"CREATE INDEX ps_oplog_by_row ON ps_oplog (row_type, row_id) WHERE superseded = 0"},{"sql":"CREATE INDEX ps_oplog_by_opid ON ps_oplog (bucket, op_id)"},{"sql":"CREATE INDEX ps_oplog_by_key ON ps_oplog (bucket, key) WHERE superseded = 0"},{"sql":"INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, key, data, hash, superseded)\n SELECT ps_buckets_5.name, oplog.op_id, 3, oplog.row_type, oplog.row_id, oplog.key, oplog.data, oplog.hash, 0\n FROM ps_oplog_5 oplog\n JOIN ps_buckets_5\n ON ps_buckets_5.id = oplog.bucket"},{"sql":"DROP TABLE ps_oplog_5"},{"sql":"DROP TABLE ps_buckets_5"},{"sql":"INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded)\n SELECT ''$local'', 1, 4, r.row_type, r.row_id, 0, 0\n FROM ps_updated_rows r"},{"sql":"INSERT OR REPLACE INTO ps_buckets(name, pending_delete, last_op, target_op) VALUES(''$local'', 1, 0, 9223372036854775807)"},{"sql":"DROP TABLE ps_updated_rows"},{"sql":"DELETE FROM ps_migration WHERE id >= 5"}]') ''', - 6: r''' + 6: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -176,7 +176,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(5, '[{"sql":"SELECT powersync_drop_view(view.name)\n FROM sqlite_master view\n WHERE view.type = ''view''\n AND view.sql GLOB ''*-- powersync-auto-generated''"},{"sql":"ALTER TABLE ps_buckets RENAME TO ps_buckets_5"},{"sql":"ALTER TABLE ps_oplog RENAME TO ps_oplog_5"},{"sql":"CREATE TABLE ps_buckets(\n name TEXT PRIMARY KEY,\n last_applied_op INTEGER NOT NULL DEFAULT 0,\n last_op INTEGER NOT NULL DEFAULT 0,\n target_op INTEGER NOT NULL DEFAULT 0,\n add_checksum INTEGER NOT NULL DEFAULT 0,\n pending_delete INTEGER NOT NULL DEFAULT 0\n, op_checksum INTEGER NOT NULL DEFAULT 0, remove_operations INTEGER NOT NULL DEFAULT 0)"},{"sql":"INSERT INTO ps_buckets(name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete)\n SELECT name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete FROM ps_buckets_5"},{"sql":"CREATE TABLE ps_oplog(\n bucket TEXT NOT NULL,\n op_id INTEGER NOT NULL,\n op INTEGER NOT NULL,\n row_type TEXT,\n row_id TEXT,\n key TEXT,\n data TEXT,\n hash INTEGER NOT NULL,\n superseded INTEGER NOT NULL)"},{"sql":"CREATE INDEX ps_oplog_by_row ON ps_oplog (row_type, row_id) WHERE superseded = 0"},{"sql":"CREATE INDEX ps_oplog_by_opid ON ps_oplog (bucket, op_id)"},{"sql":"CREATE INDEX ps_oplog_by_key ON ps_oplog (bucket, key) WHERE superseded = 0"},{"sql":"INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, key, data, hash, superseded)\n SELECT ps_buckets_5.name, oplog.op_id, 3, oplog.row_type, oplog.row_id, oplog.key, oplog.data, oplog.hash, 0\n FROM ps_oplog_5 oplog\n JOIN ps_buckets_5\n ON ps_buckets_5.id = oplog.bucket"},{"sql":"DROP TABLE ps_oplog_5"},{"sql":"DROP TABLE ps_buckets_5"},{"sql":"INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded)\n SELECT ''$local'', 1, 4, r.row_type, r.row_id, 0, 0\n FROM ps_updated_rows r"},{"sql":"INSERT OR REPLACE INTO ps_buckets(name, pending_delete, last_op, target_op) VALUES(''$local'', 1, 0, 9223372036854775807)"},{"sql":"DROP TABLE ps_updated_rows"},{"sql":"DELETE FROM ps_migration WHERE id >= 5"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(6, '[{"sql":"DELETE FROM ps_migration WHERE id >= 6"}]') ''', - 7: r''' + 7: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -220,7 +220,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(6, '[{"sql":"DELETE FROM ps_migration WHERE id >= 6"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(7, '[{"sql":"INSERT OR REPLACE INTO ps_kv(key, value) SELECT ''last_synced_at'', last_synced_at FROM ps_sync_state WHERE priority = 2147483647"},{"sql":"DROP TABLE ps_sync_state"},{"sql":"DELETE FROM ps_migration WHERE id >= 7"}]') ''', - 8: r''' + 8: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -265,7 +265,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(7, '[{"sql":"INSERT OR REPLACE INTO ps_kv(key, value) SELECT ''last_synced_at'', last_synced_at FROM ps_sync_state WHERE priority = 2147483647"},{"sql":"DROP TABLE ps_sync_state"},{"sql":"DELETE FROM ps_migration WHERE id >= 7"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(8, '[{"sql":"ALTER TABLE ps_sync_state RENAME TO ps_sync_state_new"},{"sql":"CREATE TABLE ps_sync_state (\n priority INTEGER NOT NULL,\n last_synced_at TEXT NOT NULL\n) STRICT"},{"sql":"INSERT INTO ps_sync_state SELECT * FROM ps_sync_state_new"},{"sql":"DROP TABLE ps_sync_state_new"},{"sql":"DELETE FROM ps_migration WHERE id >= 8"}]') ''', - 9: r''' + 9: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -311,7 +311,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(8, '[{"sql":"ALTER TABLE ps_sync_state RENAME TO ps_sync_state_new"},{"sql":"CREATE TABLE ps_sync_state (\n priority INTEGER NOT NULL,\n last_synced_at TEXT NOT NULL\n) STRICT"},{"sql":"INSERT INTO ps_sync_state SELECT * FROM ps_sync_state_new"},{"sql":"DROP TABLE ps_sync_state_new"},{"sql":"DELETE FROM ps_migration WHERE id >= 8"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(9, '[{"sql":"ALTER TABLE ps_buckets DROP COLUMN count_at_last"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN count_since_last"},{"sql":"DELETE FROM ps_migration WHERE id >= 9"}]') ''', - 10: r''' + 10: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -358,7 +358,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(9, '[{"sql":"ALTER TABLE ps_buckets DROP COLUMN count_at_last"},{"sql":"ALTER TABLE ps_buckets DROP COLUMN count_since_last"},{"sql":"DELETE FROM ps_migration WHERE id >= 9"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(10, '[{"sql":"SELECT powersync_drop_view(view.name)\n FROM sqlite_master view\n WHERE view.type = ''view''\n AND view.sql GLOB ''*-- powersync-auto-generated''"},{"sql":"DELETE FROM ps_migration WHERE id >= 10"}]') ''', - 11: r''' + 11: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -418,7 +418,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(10, '[{"sql":"SELECT powersync_drop_view(view.name)\n FROM sqlite_master view\n WHERE view.type = ''view''\n AND view.sql GLOB ''*-- powersync-auto-generated''"},{"sql":"DELETE FROM ps_migration WHERE id >= 10"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(11, '[{"sql":"DROP TABLE ps_stream_subscriptions"},{"sql":"DELETE FROM ps_migration WHERE id >= 11"}]') ''', - 12: r''' + 12: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -479,7 +479,7 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(11, '[{"sql":"DROP TABLE ps_stream_subscriptions"},{"sql":"DELETE FROM ps_migration WHERE id >= 11"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(12, '[{"sql":"ALTER TABLE ps_buckets DROP COLUMN downloaded_size"},{"sql":"DELETE FROM ps_migration WHERE id >= 12"}]') ''', - 13: r''' + 13: r''' ;CREATE TABLE ps_buckets( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -541,7 +541,8 @@ Map _expectedState() { ;INSERT INTO ps_migration(id, down_migrations) VALUES(12, '[{"sql":"ALTER TABLE ps_buckets DROP COLUMN downloaded_size"},{"sql":"DELETE FROM ps_migration WHERE id >= 12"}]') ;INSERT INTO ps_migration(id, down_migrations) VALUES(13, '[{"sql":"UPDATE ps_stream_subscriptions SET expires_at = expires_at / 1000000, last_synced_at = last_synced_at / 1000000"},{"sql":"ALTER TABLE ps_sync_state RENAME TO ps_sync_state_new"},{"sql":"CREATE TABLE ps_sync_state (\n priority INTEGER NOT NULL PRIMARY KEY,\n last_synced_at TEXT NOT NULL\n) STRICT;"},{"sql":"INSERT INTO ps_sync_state (priority, last_synced_at) SELECT priority, datetime(last_synced_at / 1000000, ''unixepoch'') FROM ps_sync_state_new"},{"sql":"DROP TABLE ps_sync_state_new"},{"sql":"DELETE FROM ps_migration WHERE id >= 13"}]')''', }; - state[14] = '''${state[13]!.trim().replaceFirst(' target_op INTEGER NOT NULL DEFAULT 0,\n', '')} + state[14] = + '''${state[13]!.trim().replaceFirst(' target_op INTEGER NOT NULL DEFAULT 0,\n', '')} ;INSERT INTO ps_migration(id, down_migrations) VALUES(14, '[{"sql":"ALTER TABLE ps_buckets RENAME TO ps_buckets_14"},{"sql":"DROP INDEX ps_buckets_name"},{"sql":"CREATE TABLE ps_buckets(\\n id INTEGER PRIMARY KEY,\\n name TEXT NOT NULL,\\n last_applied_op INTEGER NOT NULL DEFAULT 0,\\n last_op INTEGER NOT NULL DEFAULT 0,\\n target_op INTEGER NOT NULL DEFAULT 0,\\n add_checksum INTEGER NOT NULL DEFAULT 0,\\n op_checksum INTEGER NOT NULL DEFAULT 0,\\n pending_delete INTEGER NOT NULL DEFAULT 0\\n) STRICT"},{"sql":"CREATE UNIQUE INDEX ps_buckets_name ON ps_buckets (name)"},{"sql":"ALTER TABLE ps_buckets ADD COLUMN count_at_last INTEGER NOT NULL DEFAULT 0"},{"sql":"ALTER TABLE ps_buckets ADD COLUMN count_since_last INTEGER NOT NULL DEFAULT 0"},{"sql":"ALTER TABLE ps_buckets ADD COLUMN downloaded_size INTEGER NOT NULL DEFAULT 0"},{"sql":"INSERT INTO ps_buckets(\\n id,\\n name,\\n last_applied_op,\\n last_op,\\n add_checksum,\\n op_checksum,\\n pending_delete,\\n count_at_last,\\n count_since_last,\\n downloaded_size\\n)\\nSELECT\\n id,\\n name,\\n last_applied_op,\\n last_op,\\n add_checksum,\\n op_checksum,\\n pending_delete,\\n count_at_last,\\n count_since_last,\\n downloaded_size\\nFROM ps_buckets_14"},{"sql":"DROP TABLE ps_buckets_14"},{"sql":"INSERT INTO ps_buckets(name, pending_delete, last_op, last_applied_op, target_op)\\nSELECT ''\$local'', 1, seen, applied, target\\n FROM (\\n SELECT\\n IFNULL((SELECT CAST(value AS INTEGER) FROM ps_kv WHERE key = ''last_seen_checkpoint_request_id''), 0) AS seen,\\n IFNULL((SELECT CAST(value AS INTEGER) FROM ps_kv WHERE key = ''last_applied_checkpoint_request_id''), 0) AS applied,\\n (SELECT CAST(value AS INTEGER) FROM ps_kv WHERE key = ''target_checkpoint_request_id'') AS target\\n )\\n WHERE EXISTS (\\n SELECT 1 FROM ps_kv WHERE key = ''target_checkpoint_request_id''\\n )\\nON CONFLICT(name) DO UPDATE SET\\n pending_delete = excluded.pending_delete,\\n last_op = excluded.last_op,\\n last_applied_op = excluded.last_applied_op,\\n target_op = excluded.target_op"},{"sql":"DELETE FROM ps_migration WHERE id >= 14"}]')'''; return state; } @@ -553,7 +554,7 @@ final data1 = _data1(); Map _data1() { final data = { - 2: r''' + 2: r''' ;INSERT INTO ps_buckets(name, last_applied_op, last_op, target_op, add_checksum, pending_delete) VALUES ('b1', 0, 0, 0, 0, 0), ('b2', 0, 0, 0, 1000, 0) @@ -563,7 +564,7 @@ Map _data1() { ('b2', 3, 3, 'lists', 'l1', '', '{}', 3, 0), ('b2', 4, 4, 'lists', 'l2', '', null, 5, 0) ''', - 3: r''' + 3: r''' ;INSERT INTO ps_buckets(name, last_applied_op, last_op, target_op, add_checksum, pending_delete) VALUES ('b1', 0, 0, 0, 0, 0), ('b2', 0, 0, 0, 1000, 0) @@ -573,7 +574,7 @@ Map _data1() { ('b2', 3, 3, 'lists', 'l1', '', '{}', 3, 0), ('b2', 4, 4, 'lists', 'l2', '', null, 5, 0) ''', - 4: r''' + 4: r''' ;INSERT INTO ps_buckets(name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete) VALUES ('b1', 0, 0, 0, 0, 120, 0), ('b2', 0, 0, 0, 1000, 8, 0) @@ -583,7 +584,7 @@ Map _data1() { ('b2', 3, 3, 'lists', 'l1', '', '{}', 3, 0), ('b2', 4, 4, 'lists', 'l2', '', null, 5, 0) ''', - 5: r''' + 5: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0) @@ -594,7 +595,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 6: r''' + 6: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0) @@ -605,7 +606,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 7: r''' + 7: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0) @@ -616,7 +617,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 8: r''' + 8: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0) @@ -627,7 +628,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 9: r''' + 9: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete, count_at_last, count_since_last) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0, 0, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0, 0, 0) @@ -638,7 +639,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 10: r''' + 10: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete, count_at_last, count_since_last) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0, 0, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0, 0, 0) @@ -649,7 +650,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 11: r''' + 11: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete, count_at_last, count_since_last) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0, 0, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0, 0, 0) @@ -660,7 +661,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 12: r''' + 12: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete, count_at_last, count_since_last, downloaded_size) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0, 0, 0, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0, 0, 0, 0) @@ -671,7 +672,7 @@ Map _data1() { ;INSERT INTO ps_updated_rows(row_type, row_id) VALUES ('lists', 'l2') ''', - 13: r''' + 13: r''' ;INSERT INTO ps_buckets(id, name, last_applied_op, last_op, target_op, add_checksum, op_checksum, pending_delete, count_at_last, count_since_last, downloaded_size) VALUES (1, 'b1', 0, 0, 0, 0, 120, 0, 0, 0, 0), (2, 'b2', 0, 0, 0, 1005, 3, 0, 0, 0, 0) From 7ec16c62473ebfa5b90ea2a8b04652c711a9dac2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 13 Aug 2026 10:47:43 +0200 Subject: [PATCH 2/2] Simplify slightly --- crates/core/src/crud_vtab.rs | 3 +-- dart/test/crud_test.dart | 33 +++++++++++++++++++-------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/core/src/crud_vtab.rs b/crates/core/src/crud_vtab.rs index b8917b1..b092068 100644 --- a/crates/core/src/crud_vtab.rs +++ b/crates/core/src/crud_vtab.rs @@ -4,7 +4,6 @@ use alloc::boxed::Box; use alloc::rc::Rc; use const_format::formatcp; use core::ffi::{CStr, c_char, c_int, c_void}; -use core::mem; use serde::Serialize; use serde_json::value::RawValue; @@ -227,7 +226,7 @@ impl VirtualTable { } fn end_transaction(&mut self) { - if let Some(tx) = mem::take(&mut self.current_tx) { + if let Some(tx) = self.current_tx.take() { if tx.observed_begin { self.state.current_transaction_id.set(None); } diff --git a/dart/test/crud_test.dart b/dart/test/crud_test.dart index fe268bc..d8a9b1b 100644 --- a/dart/test/crud_test.dart +++ b/dart/test/crud_test.dart @@ -1022,21 +1022,26 @@ INSERT INTO ps_kv(key, value) VALUES }); } - test('for writes in both types of tables', () { - for (var tx = 1; tx < 10; tx++) { - db.execute('BEGIN'); - for (var i = 0; i < tx; i++) { - db.execute('INSERT INTO regular (id, a) VALUES (uuid(), 1234)'); - db.execute('INSERT INTO insertonly (id, a) VALUES (uuid(), 1234)'); - } - db.execute('COMMIT'); + for (final (first, second) in [ + ('regular', 'insertonly'), + ('insertonly', 'regular') + ]) { + test('write $first then $second', () { + for (var tx = 1; tx < 10; tx++) { + db.execute('BEGIN'); + for (var i = 0; i < tx; i++) { + db.execute('INSERT INTO $first (id, a) VALUES (uuid(), 1234)'); + db.execute('INSERT INTO $second (id, a) VALUES (uuid(), 1234)'); + } + db.execute('COMMIT'); - expect( - db.select('SELECT * FROM ps_crud WHERE tx_id = ?', [tx]), - hasLength(tx * 2), - ); - } - }); + expect( + db.select('SELECT * FROM ps_crud WHERE tx_id = ?', [tx]), + hasLength(tx * 2), + ); + } + }); + } }); }); }