From c1ce30b142e07ab0f957f6b6bf51c1966de1d86a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 23 Sep 2025 10:04:57 +0200 Subject: [PATCH] Add soft clear preserving oplog state --- crates/core/src/view_admin.rs | 31 +++++++++++++++++++++++++++---- dart/test/sync_test.dart | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/core/src/view_admin.rs b/crates/core/src/view_admin.rs index e85c47d..b767ed9 100644 --- a/crates/core/src/view_admin.rs +++ b/crates/core/src/view_admin.rs @@ -150,14 +150,21 @@ fn powersync_clear_impl( ) -> Result { let local_db = ctx.db_handle(); - let clear_local = args[0].int(); + let flags = PowerSyncClearFlags(args[0].int()); + + if !flags.soft_clear() { + // With a soft clear, we want to delete public data while keeping internal data around. When + // connect() is called with compatible JWTs yielding a large overlap of buckets, this can + // speed up the next sync. + local_db.exec_safe("DELETE FROM ps_oplog; DELETE FROM ps_buckets")?; + } else { + local_db.exec_safe("UPDATE ps_buckets SET last_applied_op = 0")?; + } // language=SQLite local_db.exec_safe( "\ -DELETE FROM ps_oplog; DELETE FROM ps_crud; -DELETE FROM ps_buckets; DELETE FROM ps_untyped; DELETE FROM ps_updated_rows; DELETE FROM ps_kv WHERE key != 'client_id'; @@ -166,7 +173,7 @@ DELETE FROM ps_stream_subscriptions; ", )?; - let table_glob = if clear_local != 0 { + let table_glob = if flags.clear_local() { "ps_data_*" } else { "ps_data__*" @@ -199,6 +206,22 @@ DELETE FROM {table};", Ok(String::from("")) } +#[derive(Clone, Copy)] +struct PowerSyncClearFlags(i32); + +impl PowerSyncClearFlags { + const MASK_CLEAR_LOCAL: i32 = 0x01; + const MASK_SOFT_CLEAR: i32 = 0x02; + + fn clear_local(self) -> bool { + self.0 & Self::MASK_CLEAR_LOCAL != 0 + } + + fn soft_clear(self) -> bool { + self.0 & Self::MASK_SOFT_CLEAR != 0 + } +} + create_auto_tx_function!(powersync_clear_tx, powersync_clear_impl); create_sqlite_text_fn!(powersync_clear, powersync_clear_tx, "powersync_clear"); diff --git a/dart/test/sync_test.dart b/dart/test/sync_test.dart index 60c6c35..e5f6cf0 100644 --- a/dart/test/sync_test.dart +++ b/dart/test/sync_test.dart @@ -370,6 +370,38 @@ void _syncTests({ expect(db.select('SELECT * FROM ps_sync_state'), hasLength(0)); }); + test('can soft clear', () { + invokeControl('start', null); + pushCheckpoint(buckets: [bucketDescription('a', count: 1)]); + pushSyncData('a', '1', 'row-0', 'PUT', {'col': 'hi'}); + pushCheckpointComplete(); + + expect(db.select('SELECT * FROM items'), hasLength(1)); + + // Soft clear + db.execute('SELECT powersync_clear(2)'); + db.select('select powersync_replace_schema(?)', [json.encode(testSchema)]); + expect(db.select('SELECT * FROM items'), hasLength(0)); + + final request = invokeControl('start', null); + expect( + request, + contains(containsPair( + 'EstablishSyncStream', + { + // Should request state from before clear + 'request': containsPair('buckets', [ + {'name': 'a', 'after': '1'} + ]), + }, + )), + ); + + pushCheckpoint(buckets: [bucketDescription('a', count: 1)]); + pushCheckpointComplete(); + expect(db.select('SELECT * FROM items'), hasLength(1)); + }); + test('persists download progress', () { const bucket = 'bkt'; void expectProgress(int atLast, int sinceLast) {