From 505a202760f5d490720aa05c7ca16b1d0344a047 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 22 Oct 2025 11:44:01 +0200 Subject: [PATCH 1/2] Allow clearing raw tables --- crates/core/src/crud_vtab.rs | 25 ++++------ crates/core/src/lib.rs | 8 +-- crates/core/src/migrations.rs | 5 +- crates/core/src/operations_vtab.rs | 21 +++----- crates/core/src/schema/management.rs | 19 ++++--- crates/core/src/schema/mod.rs | 8 +-- crates/core/src/schema/table_info.rs | 2 + crates/core/src/state.rs | 69 +++++++++++++++++++++----- crates/core/src/sync/interface.rs | 3 +- crates/core/src/sync/mod.rs | 4 +- crates/core/src/sync/streaming_sync.rs | 9 ++-- crates/core/src/update_hooks.rs | 18 +++---- crates/core/src/view_admin.rs | 25 ++++++++-- dart/test/error_test.dart | 15 ++---- dart/test/sync_test.dart | 11 ++++ 15 files changed, 151 insertions(+), 91 deletions(-) diff --git a/crates/core/src/crud_vtab.rs b/crates/core/src/crud_vtab.rs index 68dc69d7..8674c05f 100644 --- a/crates/core/src/crud_vtab.rs +++ b/crates/core/src/crud_vtab.rs @@ -1,7 +1,7 @@ extern crate alloc; use alloc::boxed::Box; -use alloc::sync::Arc; +use alloc::rc::Rc; use const_format::formatcp; use core::ffi::{CStr, c_char, c_int, c_void}; use core::sync::atomic::Ordering; @@ -42,7 +42,7 @@ struct VirtualTable { db: *mut sqlite::sqlite3, current_tx: Option, is_simple: bool, - state: Arc, + state: Rc, } struct ActiveCrudTransaction { @@ -301,14 +301,7 @@ extern "C" fn connect( pModule: core::ptr::null(), zErrMsg: core::ptr::null_mut(), }, - state: { - // Increase refcount - we can't use from_raw alone because we don't own the aux - // data (connect could be called multiple times). - let state = Arc::from_raw(aux as *mut DatabaseState); - let clone = state.clone(); - core::mem::forget(state); - clone - }, + state: DatabaseState::clone_from(aux), db, current_tx: None, is_simple, @@ -321,7 +314,7 @@ extern "C" fn connect( extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int { unsafe { - drop(Box::from_raw(vtab)); + drop(Box::from_raw(vtab as *mut VirtualTable)); } ResultCode::OK as c_int } @@ -400,20 +393,20 @@ static MODULE: sqlite_nostd::module = sqlite_nostd::module { xIntegrity: None, }; -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { sqlite::convert_rc(sqlite::create_module_v2( db, SIMPLE_NAME.as_ptr(), &MODULE, - Arc::into_raw(state.clone()) as *mut c_void, - Some(DatabaseState::destroy_arc), + Rc::into_raw(state.clone()) as *mut c_void, + Some(DatabaseState::destroy_rc), ))?; sqlite::convert_rc(sqlite::create_module_v2( db, MANUAL_NAME.as_ptr(), &MODULE, - Arc::into_raw(state) as *mut c_void, - Some(DatabaseState::destroy_arc), + Rc::into_raw(state) as *mut c_void, + Some(DatabaseState::destroy_rc), ))?; Ok(()) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 3d5e7eac..b9e662db 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -8,7 +8,7 @@ extern crate alloc; use core::ffi::{c_char, c_int}; -use alloc::{ffi::CString, format, sync::Arc}; +use alloc::{ffi::CString, format, rc::Rc}; use sqlite::ResultCode; use sqlite_nostd as sqlite; @@ -66,7 +66,7 @@ pub extern "C" fn sqlite3_powersync_init( fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> { PowerSyncError::check_sqlite3_version()?; - let state = Arc::new(DatabaseState::new()); + let state = Rc::new(DatabaseState::new()); crate::version::register(db)?; crate::views::register(db)?; @@ -74,14 +74,14 @@ fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> { crate::diff::register(db)?; crate::fix_data::register(db)?; crate::json_util::register(db)?; - crate::view_admin::register(db)?; + crate::view_admin::register(db, state.clone())?; crate::checkpoint::register(db)?; crate::kv::register(db)?; crate::state::register(db, state.clone())?; sync::register(db, state.clone())?; update_hooks::register(db, state.clone())?; - crate::schema::register(db)?; + crate::schema::register(db, state.clone())?; crate::operations_vtab::register(db, state.clone())?; crate::crud_vtab::register(db, state)?; diff --git a/crates/core/src/migrations.rs b/crates/core/src/migrations.rs index 6adde8e7..64faf7bc 100644 --- a/crates/core/src/migrations.rs +++ b/crates/core/src/migrations.rs @@ -27,8 +27,9 @@ CREATE TABLE IF NOT EXISTS ps_migration(id INTEGER PRIMARY KEY, down_migrations )?; // language=SQLite - let current_version_stmt = - local_db.prepare_v2("SELECT ifnull(max(id), 0) as version FROM ps_migration")?; + let current_version_stmt = local_db + .prepare_v2("SELECT ifnull(max(id), 0) as version FROM ps_migration") + .into_db_result(local_db)?; let rc = current_version_stmt.step()?; if rc != ResultCode::ROW { return Err(PowerSyncError::unknown_internal()); diff --git a/crates/core/src/operations_vtab.rs b/crates/core/src/operations_vtab.rs index bb60308d..3f58794b 100644 --- a/crates/core/src/operations_vtab.rs +++ b/crates/core/src/operations_vtab.rs @@ -1,7 +1,7 @@ extern crate alloc; use alloc::boxed::Box; -use alloc::sync::Arc; +use alloc::rc::Rc; use core::ffi::{c_char, c_int, c_void}; use sqlite::{Connection, ResultCode, Value}; @@ -18,7 +18,7 @@ use crate::vtab_util::*; struct VirtualTable { base: sqlite::vtab, db: *mut sqlite::sqlite3, - state: Arc, + state: Rc, target_applied: bool, target_validated: bool, @@ -46,14 +46,7 @@ extern "C" fn connect( zErrMsg: core::ptr::null_mut(), }, db, - state: { - // Increase refcount - we can't use from_raw alone because we don't own the aux - // data (connect could be called multiple times). - let state = Arc::from_raw(aux as *mut DatabaseState); - let clone = state.clone(); - core::mem::forget(state); - clone - }, + state: DatabaseState::clone_from(aux), target_validated: false, target_applied: false, })); @@ -65,7 +58,7 @@ extern "C" fn connect( extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int { unsafe { - drop(Box::from_raw(vtab)); + drop(Box::from_raw(vtab as *mut VirtualTable)); } ResultCode::OK as c_int } @@ -150,12 +143,12 @@ static MODULE: sqlite_nostd::module = sqlite_nostd::module { xIntegrity: None, }; -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { db.create_module_v2( "powersync_operations", &MODULE, - Some(Arc::into_raw(state) as *mut c_void), - Some(DatabaseState::destroy_arc), + Some(Rc::into_raw(state) as *mut c_void), + Some(DatabaseState::destroy_rc), )?; Ok(()) diff --git a/crates/core/src/schema/management.rs b/crates/core/src/schema/management.rs index b8d0fd2f..112615da 100644 --- a/crates/core/src/schema/management.rs +++ b/crates/core/src/schema/management.rs @@ -1,5 +1,6 @@ extern crate alloc; +use alloc::rc::Rc; use alloc::string::String; use alloc::vec::Vec; use alloc::{format, vec}; @@ -11,6 +12,7 @@ use sqlite_nostd::Context; use crate::error::{PSResult, PowerSyncError}; use crate::ext::ExtendedDatabase; +use crate::state::DatabaseState; use crate::util::{quote_identifier, quote_json_path}; use crate::{create_auto_tx_function, create_sqlite_text_fn}; @@ -138,10 +140,8 @@ SELECT name, internal_name, local_only FROM powersync_tables WHERE name NOT IN ( Ok(()) } -fn update_indexes(db: *mut sqlite::sqlite3, schema: &str) -> Result<(), PowerSyncError> { +fn update_indexes(db: *mut sqlite::sqlite3, schema: &Schema) -> Result<(), PowerSyncError> { let mut statements: Vec = alloc::vec![]; - let schema = - serde_json::from_str::(schema).map_err(PowerSyncError::as_argument_error)?; let mut expected_index_names: Vec = vec![]; { @@ -298,15 +298,20 @@ fn powersync_replace_schema_impl( args: &[*mut sqlite::value], ) -> Result { let schema = args[0].text(); + let state = unsafe { DatabaseState::from_context(&ctx) }; + let parsed_schema = + serde_json::from_str::(schema).map_err(PowerSyncError::as_argument_error)?; + let db = ctx.db_handle(); // language=SQLite db.exec_safe("SELECT powersync_init()").into_db_result(db)?; update_tables(db, schema)?; - update_indexes(db, schema)?; + update_indexes(db, &parsed_schema)?; update_views(db, schema)?; + state.set_schema(parsed_schema); Ok(String::from("")) } @@ -317,16 +322,16 @@ create_sqlite_text_fn!( "powersync_replace_schema" ); -pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { db.create_function_v2( "powersync_replace_schema", 1, sqlite::UTF8, - None, + Some(Rc::into_raw(state) as *mut _), Some(powersync_replace_schema), None, None, - None, + Some(DatabaseState::destroy_rc), )?; Ok(()) diff --git a/crates/core/src/schema/mod.rs b/crates/core/src/schema/mod.rs index ec04753d..d916964f 100644 --- a/crates/core/src/schema/mod.rs +++ b/crates/core/src/schema/mod.rs @@ -1,7 +1,7 @@ mod management; mod table_info; -use alloc::vec::Vec; +use alloc::{rc::Rc, vec::Vec}; use serde::Deserialize; use sqlite::ResultCode; use sqlite_nostd as sqlite; @@ -10,6 +10,8 @@ pub use table_info::{ TableInfoFlags, }; +use crate::state::DatabaseState; + #[derive(Deserialize, Default)] pub struct Schema { pub tables: Vec, @@ -17,6 +19,6 @@ pub struct Schema { pub raw_tables: Vec, } -pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { - management::register(db) +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { + management::register(db, state) } diff --git a/crates/core/src/schema/table_info.rs b/crates/core/src/schema/table_info.rs index a81d0162..363859cd 100644 --- a/crates/core/src/schema/table_info.rs +++ b/crates/core/src/schema/table_info.rs @@ -24,6 +24,8 @@ pub struct RawTable { pub name: String, pub put: PendingStatement, pub delete: PendingStatement, + #[serde(default)] + pub clear: Option, } impl Table { diff --git a/crates/core/src/state.rs b/crates/core/src/state.rs index e46f87e6..dcf7f65d 100644 --- a/crates/core/src/state.rs +++ b/crates/core/src/state.rs @@ -1,37 +1,51 @@ use core::{ - cell::RefCell, + cell::{Ref, RefCell}, ffi::{c_int, c_void}, sync::atomic::{AtomicBool, Ordering}, }; use alloc::{ collections::btree_set::BTreeSet, + rc::Rc, string::{String, ToString}, - sync::Arc, }; use sqlite::{Connection, ResultCode}; use sqlite_nostd::{self as sqlite, Context}; +use crate::schema::Schema; + /// State that is shared for a SQLite database connection after the core extension has been /// registered on it. /// /// `init_extension` allocates an instance of this in an `Arc` that is shared as user-data for /// functions/vtabs that need access to it. +#[derive(Default)] pub struct DatabaseState { pub is_in_sync_local: AtomicBool, + schema: RefCell>, pending_updates: RefCell>, commited_updates: RefCell>, } impl DatabaseState { pub fn new() -> Self { - DatabaseState { - is_in_sync_local: AtomicBool::new(false), - pending_updates: Default::default(), - commited_updates: Default::default(), + Self::default() + } + + pub fn view_schema(&self) -> Option> { + let schema_ref = self.schema.borrow(); + if schema_ref.is_none() { + None + } else { + Some(Ref::map(schema_ref, |f| f.as_ref().unwrap())) } } + /// Marks the given [Schema] as being the one currently installed to the database. + pub fn set_schema(&self, schema: Schema) { + self.schema.replace(Some(schema)); + } + pub fn sync_local_guard<'a>(&'a self) -> impl Drop + use<'a> { self.is_in_sync_local .compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire) @@ -72,19 +86,48 @@ impl DatabaseState { core::mem::replace(&mut *committed, Default::default()) } - pub unsafe extern "C" fn destroy_arc(ptr: *mut c_void) { - drop(unsafe { Arc::from_raw(ptr.cast::()) }); + /// ## Safety + /// + /// This is only safe to call when an `Rc` has been installed as the `user_data` + /// pointer when registering the function. + pub unsafe fn from_context(context: &impl Context) -> &Self { + let user_data = context.user_data().cast::(); + unsafe { + // Safety: user_data() points to valid DatabaseState reference alive as long as the + // context. + &*user_data + } + } + + /// ## Safety + /// + /// This is only save to call if `context` is the user-data pointer of a function or virtual + /// table created with an `Rc Rc { + let context = context as *mut DatabaseState; + + unsafe { + // Safety: It's a valid pointer that has at least one reference (owned by SQLite while + // the function is registered). + Rc::increment_strong_count(context); + // Safety: Moves the clone we've just created into Rust. + Rc::from_raw(context) + } + } + + pub unsafe extern "C" fn destroy_rc(ptr: *mut c_void) { + drop(unsafe { Rc::from_raw(ptr.cast::()) }); } } -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { unsafe extern "C" fn func( ctx: *mut sqlite::context, _argc: c_int, _argv: *mut *mut sqlite::value, ) { - let data = ctx.user_data().cast::(); - let data = unsafe { data.as_ref() }.unwrap(); + let data = unsafe { DatabaseState::from_context(&ctx) }; ctx.result_int(if data.is_in_sync_local.load(Ordering::Relaxed) { 1 @@ -97,11 +140,11 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<( "powersync_in_sync_operation", 0, 0, - Some(Arc::into_raw(state) as *mut c_void), + Some(Rc::into_raw(state) as *mut c_void), Some(func), None, None, - Some(DatabaseState::destroy_arc), + Some(DatabaseState::destroy_rc), )?; Ok(()) } diff --git a/crates/core/src/sync/interface.rs b/crates/core/src/sync/interface.rs index c5731de5..a5c5de94 100644 --- a/crates/core/src/sync/interface.rs +++ b/crates/core/src/sync/interface.rs @@ -13,7 +13,6 @@ use crate::sync::subscriptions::{StreamKey, apply_subscriptions}; use alloc::borrow::Cow; use alloc::boxed::Box; use alloc::rc::Rc; -use alloc::sync::Arc; use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; use sqlite::{ResultCode, Value}; @@ -191,7 +190,7 @@ struct SqlController { client: SyncClient, } -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { extern "C" fn control( ctx: *mut sqlite::context, argc: c_int, diff --git a/crates/core/src/sync/mod.rs b/crates/core/src/sync/mod.rs index f43a2a5a..1377890a 100644 --- a/crates/core/src/sync/mod.rs +++ b/crates/core/src/sync/mod.rs @@ -1,4 +1,4 @@ -use alloc::sync::Arc; +use alloc::rc::Rc; use sqlite_nostd::{self as sqlite, ResultCode}; mod bucket_priority; @@ -17,6 +17,6 @@ pub use checksum::Checksum; use crate::state::DatabaseState; -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { interface::register(db, state) } diff --git a/crates/core/src/sync/streaming_sync.rs b/crates/core/src/sync/streaming_sync.rs index ef48a93b..68184635 100644 --- a/crates/core/src/sync/streaming_sync.rs +++ b/crates/core/src/sync/streaming_sync.rs @@ -13,7 +13,6 @@ use alloc::{ format, rc::Rc, string::{String, ToString}, - sync::Arc, vec::Vec, }; use futures_lite::FutureExt; @@ -52,13 +51,13 @@ use super::{ /// initialized. pub struct SyncClient { db: *mut sqlite::sqlite3, - db_state: Arc, + db_state: Rc, /// The current [ClientState] (essentially an optional [StreamingSyncIteration]). state: ClientState, } impl SyncClient { - pub fn new(db: *mut sqlite::sqlite3, state: Arc) -> Self { + pub fn new(db: *mut sqlite::sqlite3, state: Rc) -> Self { Self { db, db_state: state, @@ -146,7 +145,7 @@ impl SyncIterationHandle { fn new( db: *mut sqlite::sqlite3, options: StartSyncStream, - state: Arc, + state: Rc, ) -> Result { let runner = StreamingSyncIteration { db, @@ -225,7 +224,7 @@ impl<'a> ActiveEvent<'a> { struct StreamingSyncIteration { db: *mut sqlite::sqlite3, - state: Arc, + state: Rc, adapter: StorageAdapter, options: StartSyncStream, status: SyncStatusContainer, diff --git a/crates/core/src/update_hooks.rs b/crates/core/src/update_hooks.rs index 0642a66d..996719e9 100644 --- a/crates/core/src/update_hooks.rs +++ b/crates/core/src/update_hooks.rs @@ -4,7 +4,7 @@ use core::{ sync::atomic::{AtomicBool, Ordering}, }; -use alloc::{boxed::Box, sync::Arc}; +use alloc::{boxed::Box, rc::Rc}; use sqlite_nostd::{ self as sqlite, Connection, Context, ResultCode, Value, bindings::SQLITE_RESULT_SUBTYPE, }; @@ -20,7 +20,7 @@ use crate::{constants::SUBTYPE_JSON, error::PowerSyncError, state::DatabaseState /// /// The update hooks don't have to be uninstalled manually, that happens when the connection is /// closed and the function is unregistered. -pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { let state = Box::new(HookState { has_registered_hooks: AtomicBool::new(false), db, @@ -43,7 +43,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<( struct HookState { has_registered_hooks: AtomicBool, db: *mut sqlite::sqlite3, - state: Arc, + state: Rc, } extern "C" fn destroy_function(ctx: *mut c_void) { @@ -88,7 +88,7 @@ extern "C" fn powersync_update_hooks( db_state, db.update_hook( Some(update_hook_impl), - Arc::into_raw(db_state.clone()) as *mut c_void, + Rc::into_raw(db_state.clone()) as *mut c_void, ), ); check_previous( @@ -96,7 +96,7 @@ extern "C" fn powersync_update_hooks( db_state, db.commit_hook( Some(commit_hook_impl), - Arc::into_raw(db_state.clone()) as *mut c_void, + Rc::into_raw(db_state.clone()) as *mut c_void, ), ); check_previous( @@ -104,7 +104,7 @@ extern "C" fn powersync_update_hooks( db_state, db.rollback_hook( Some(rollback_hook_impl), - Arc::into_raw(db_state.clone()) as *mut c_void, + Rc::into_raw(db_state.clone()) as *mut c_void, ), ); state.has_registered_hooks.store(true, Ordering::Relaxed); @@ -155,8 +155,8 @@ unsafe extern "C" fn rollback_hook_impl(ctx: *mut c_void) { state.track_rollback(); } -fn check_previous(desc: &'static str, expected: &Arc, previous: *const c_void) { - let expected = Arc::as_ptr(expected); +fn check_previous(desc: &'static str, expected: &Rc, previous: *const c_void) { + let expected = Rc::as_ptr(expected); assert!( previous.is_null() || previous == expected.cast(), @@ -165,7 +165,7 @@ fn check_previous(desc: &'static str, expected: &Arc, previous: * if !previous.is_null() { // The hook callbacks own an Arc that needs to be dropped now. unsafe { - Arc::decrement_strong_count(previous); + Rc::decrement_strong_count(previous); } } } diff --git a/crates/core/src/view_admin.rs b/crates/core/src/view_admin.rs index b767ed9c..77b40aca 100644 --- a/crates/core/src/view_admin.rs +++ b/crates/core/src/view_admin.rs @@ -1,9 +1,10 @@ extern crate alloc; use alloc::format; +use alloc::rc::Rc; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use core::ffi::c_int; +use core::ffi::{c_int, c_void}; use sqlite::{ResultCode, Value}; use sqlite_nostd as sqlite; @@ -11,6 +12,7 @@ use sqlite_nostd::{Connection, Context}; use crate::error::PowerSyncError; use crate::migrations::{LATEST_VERSION, powersync_migrate}; +use crate::state::DatabaseState; use crate::util::quote_identifier; use crate::{create_auto_tx_function, create_sqlite_text_fn}; @@ -149,6 +151,7 @@ fn powersync_clear_impl( args: &[*mut sqlite::value], ) -> Result { let local_db = ctx.db_handle(); + let state = unsafe { DatabaseState::from_context(&ctx) }; let flags = PowerSyncClearFlags(args[0].int()); @@ -203,6 +206,20 @@ DELETE FROM {table};", local_db.exec_safe(&delete_sql)?; } + if let Some(schema) = state.view_schema() { + for raw_table in &schema.raw_tables { + if let Some(stmt) = &raw_table.clear { + local_db.exec_safe(&stmt).map_err(|e| { + PowerSyncError::from_sqlite( + local_db, + e, + format!("Clearing raw table {}", raw_table.name), + ) + })?; + } + } + } + Ok(String::from("")) } @@ -294,7 +311,7 @@ fn setup_internal_views(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { Ok(()) } -pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { +pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { // This entire module is just making it easier to edit sqlite_master using queries. // The primary interfaces exposed are: // 1. Individual views: @@ -368,11 +385,11 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { "powersync_clear", 1, sqlite::UTF8, - None, + Some(Rc::into_raw(state) as *mut c_void), Some(powersync_clear), None, None, - None, + Some(DatabaseState::destroy_rc), )?; db.create_function_v2( diff --git a/dart/test/error_test.dart b/dart/test/error_test.dart index a84c20bf..550bf5de 100644 --- a/dart/test/error_test.dart +++ b/dart/test/error_test.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:sqlite3/common.dart'; import 'package:test/test.dart'; @@ -19,17 +17,14 @@ void main() { }); test('contain inner SQLite descriptions', () { + // Create a wrong migrations table for the core extension to trip over. + db.execute('CREATE TABLE IF NOT EXISTS ps_migration(foo TEXT)'); + expect( - () => db.execute('SELECT powersync_replace_schema(?)', [ - json.encode({ - // This fails because we're trying to json_extract from the string - // in e.g. update_tables. - 'tables': ['invalid entry'], - }) - ]), + () => db.execute('SELECT powersync_init()'), throwsA(isSqliteException( 1, - 'powersync_replace_schema: internal SQLite call returned ERROR: malformed JSON', + 'powersync_init: internal SQLite call returned ERROR: no such column: id', )), ); }); diff --git a/dart/test/sync_test.dart b/dart/test/sync_test.dart index 9fa92676..aa7c8668 100644 --- a/dart/test/sync_test.dart +++ b/dart/test/sync_test.dart @@ -1068,6 +1068,7 @@ END; 'sql': 'DELETE FROM users WHERE id = ?', 'params': ['Id'], }, + 'clear': 'DELETE FROM users;', } ], 'tables': [], @@ -1224,6 +1225,16 @@ END; pushCheckpointComplete(); expect(db.select('SELECT * FROM ps_crud'), isEmpty); }); + + test('clear', () { + setupRawTables(); + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema)]); + db.execute('INSERT INTO users (id, name) VALUES (uuid(), ?)', ['test']); + + expect(db.select('SELECT * FROM users'), hasLength(1)); + db.execute('SELECT powersync_clear(0)'); + expect(db.select('SELECT * FROM users'), hasLength(0)); + }); }); } From cc2e561bd28e1eb76de6d270417b6edf79afe7a0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 22 Oct 2025 11:52:17 +0200 Subject: [PATCH 2/2] Remove more atomcis --- crates/core/src/crud_vtab.rs | 3 +-- crates/core/src/state.rs | 19 +++++++------------ crates/core/src/update_hooks.rs | 10 +++++----- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/crates/core/src/crud_vtab.rs b/crates/core/src/crud_vtab.rs index 8674c05f..24d651ae 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::sync::atomic::Ordering; use serde::Serialize; use serde_json::value::RawValue; @@ -88,7 +87,7 @@ impl VirtualTable { .ok_or_else(|| PowerSyncError::state_error("Not in tx"))?; let db = self.db; - if self.state.is_in_sync_local.load(Ordering::Relaxed) { + if self.state.is_in_sync_local.get() { // Don't collect CRUD writes while we're syncing the local database - writes made here // aren't writes we should upload. // This normally doesn't happen because we insert directly into the data tables, but diff --git a/crates/core/src/state.rs b/crates/core/src/state.rs index dcf7f65d..72f68b3e 100644 --- a/crates/core/src/state.rs +++ b/crates/core/src/state.rs @@ -1,7 +1,6 @@ use core::{ - cell::{Ref, RefCell}, + cell::{Cell, Ref, RefCell}, ffi::{c_int, c_void}, - sync::atomic::{AtomicBool, Ordering}, }; use alloc::{ @@ -21,7 +20,7 @@ use crate::schema::Schema; /// functions/vtabs that need access to it. #[derive(Default)] pub struct DatabaseState { - pub is_in_sync_local: AtomicBool, + pub is_in_sync_local: Cell, schema: RefCell>, pending_updates: RefCell>, commited_updates: RefCell>, @@ -47,15 +46,15 @@ impl DatabaseState { } pub fn sync_local_guard<'a>(&'a self) -> impl Drop + use<'a> { - self.is_in_sync_local - .compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire) - .expect("should not be syncing already"); + if self.is_in_sync_local.replace(true) { + panic!("Should ont be syncing already"); + } struct ClearOnDrop<'a>(&'a DatabaseState); impl Drop for ClearOnDrop<'_> { fn drop(&mut self) { - self.0.is_in_sync_local.store(false, Ordering::Release); + self.0.is_in_sync_local.set(false); } } @@ -129,11 +128,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<() ) { let data = unsafe { DatabaseState::from_context(&ctx) }; - ctx.result_int(if data.is_in_sync_local.load(Ordering::Relaxed) { - 1 - } else { - 0 - }); + ctx.result_int(if data.is_in_sync_local.get() { 1 } else { 0 }); } db.create_function_v2( diff --git a/crates/core/src/update_hooks.rs b/crates/core/src/update_hooks.rs index 996719e9..75d73a64 100644 --- a/crates/core/src/update_hooks.rs +++ b/crates/core/src/update_hooks.rs @@ -1,7 +1,7 @@ use core::{ + cell::Cell, ffi::{CStr, c_char, c_int, c_void}, ptr::null_mut, - sync::atomic::{AtomicBool, Ordering}, }; use alloc::{boxed::Box, rc::Rc}; @@ -22,7 +22,7 @@ use crate::{constants::SUBTYPE_JSON, error::PowerSyncError, state::DatabaseState /// closed and the function is unregistered. pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { let state = Box::new(HookState { - has_registered_hooks: AtomicBool::new(false), + has_registered_hooks: Cell::new(false), db, state, }); @@ -41,7 +41,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<() } struct HookState { - has_registered_hooks: AtomicBool, + has_registered_hooks: Cell, db: *mut sqlite::sqlite3, state: Rc, } @@ -49,7 +49,7 @@ struct HookState { extern "C" fn destroy_function(ctx: *mut c_void) { let state = unsafe { Box::from_raw(ctx as *mut HookState) }; - if state.has_registered_hooks.load(Ordering::Relaxed) { + if state.has_registered_hooks.get() { check_previous( "update", &state.state, @@ -107,7 +107,7 @@ extern "C" fn powersync_update_hooks( Rc::into_raw(db_state.clone()) as *mut c_void, ), ); - state.has_registered_hooks.store(true, Ordering::Relaxed); + state.has_registered_hooks.set(true); } "get" => { let state = unsafe { user_data.as_ref().unwrap_unchecked() };