diff --git a/crates/core/src/pre_close_vtab.rs b/crates/core/src/pre_close_vtab.rs index 7f11671..79dfa4e 100644 --- a/crates/core/src/pre_close_vtab.rs +++ b/crates/core/src/pre_close_vtab.rs @@ -71,24 +71,59 @@ extern "C" fn update( _argv: *mut *mut sqlite::value, _p_row_id: *mut sqlite::int64, ) -> c_int { - 0 + // This table isn't meant to be written to. + ResultCode::MISUSE as c_int } -// Insert-only virtual table. -// The primary functionality here is in update. -// connect and disconnect configures the table and allocates the required resources. +#[repr(transparent)] +struct EmptyCursor(sqlite::vtab_cursor); + +extern "C" fn best_index(_vtab: *mut sqlite::vtab, _index_info: *mut sqlite::index_info) -> c_int { + // No rows are ever returned, so there's nothing to plan for. + ResultCode::OK as c_int +} + +extern "C" fn open(vtab: *mut sqlite::vtab, cursor: *mut *mut sqlite::vtab_cursor) -> c_int { + let c = Box::into_raw(Box::new(EmptyCursor(sqlite::vtab_cursor { pVtab: vtab }))); + unsafe { *cursor = c.cast::() }; + + ResultCode::OK as c_int +} + +extern "C" fn close(cursor: *mut sqlite::vtab_cursor) -> c_int { + unsafe { + drop(Box::from_raw(cursor as *mut EmptyCursor)); + } + ResultCode::OK as c_int +} + +extern "C" fn filter( + _cursor: *mut sqlite::vtab_cursor, + _idx_num: c_int, + _idx_str: *const c_char, + _argc: c_int, + _argv: *mut *mut sqlite::value, +) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn eof(_cursor: *mut sqlite::vtab_cursor) -> c_int { + 1 +} + +// Select-only virtual table, the primary functionality here is in disconnect. static MODULE: sqlite::module = sqlite::module { iVersion: 0, xCreate: None, xConnect: Some(connect), - xBestIndex: Some(vtab_no_best_index), + xBestIndex: Some(best_index), xDisconnect: Some(disconnect), xDestroy: None, - xOpen: Some(vtab_no_open), - xClose: Some(vtab_no_close), - xFilter: Some(vtab_no_filter), + xOpen: Some(open), + xClose: Some(close), + xFilter: Some(filter), xNext: Some(vtab_no_next), - xEof: Some(vtab_no_eof), + xEof: Some(eof), xColumn: Some(vtab_no_column), xRowid: Some(vtab_no_rowid), xUpdate: Some(update), @@ -105,6 +140,11 @@ static MODULE: sqlite::module = sqlite::module { xIntegrity: None, }; +pub fn ensure_has_internal_close_vtab(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { + db.exec(c"SELECT 1 FROM powersync_internal_close;")?; + Ok(()) +} + pub fn register(db: *mut sqlite::sqlite3, state: Rc) -> Result<(), ResultCode> { db.create_module_v2( "powersync_internal_close", diff --git a/crates/core/src/sync/storage_adapter.rs b/crates/core/src/sync/storage_adapter.rs index 76757b9..74ef428 100644 --- a/crates/core/src/sync/storage_adapter.rs +++ b/crates/core/src/sync/storage_adapter.rs @@ -7,6 +7,7 @@ use serde::Serialize; use crate::{ error::{PSResult, PowerSyncError}, ext::SafeManagedStmt, + pre_close_vtab::ensure_has_internal_close_vtab, schema::Schema, state::DatabaseState, sync::{ @@ -50,6 +51,11 @@ pub struct StorageAdapter { impl StorageAdapter { pub fn new(db: *mut sqlite::sqlite3) -> Result { + // The cached statements here prevent sqlite3_close from completing. sqlite3_close invokes + // the xDisconnect callback on attached virtual tables, which we use to implement a + // "pre-close hook". See `pre_close_vtab.rs` for more details on how that works. + ensure_has_internal_close_vtab(db)?; + // language=SQLite let progress = db .prepare_v2("SELECT name, count_at_last, count_since_last FROM ps_buckets") diff --git a/crates/core/src/update_hooks.rs b/crates/core/src/update_hooks.rs index 1501aa0..4702cf7 100644 --- a/crates/core/src/update_hooks.rs +++ b/crates/core/src/update_hooks.rs @@ -8,7 +8,10 @@ use powersync_sqlite_nostd::{ self as sqlite, Connection, Context, ResultCode, Value, bindings::SQLITE_RESULT_SUBTYPE, }; -use crate::{constants::SUBTYPE_JSON, error::PowerSyncError, state::DatabaseState}; +use crate::{ + constants::SUBTYPE_JSON, error::PowerSyncError, pre_close_vtab::ensure_has_internal_close_vtab, + state::DatabaseState, +}; /// The `powersync_update_hooks` methods works like this: /// @@ -57,6 +60,11 @@ extern "C" fn powersync_update_hooks( match op { "install" => { + if let Err(e) = ensure_has_internal_close_vtab(db) { + ctx.result_error_code(e); + return; + }; + let state = unsafe { user_data.as_ref().unwrap_unchecked() }; let db_state = &state.state; diff --git a/crates/core/src/view_admin.rs b/crates/core/src/view_admin.rs index 3ef012b..7c1d517 100644 --- a/crates/core/src/view_admin.rs +++ b/crates/core/src/view_admin.rs @@ -39,11 +39,6 @@ fn powersync_init_impl( verify_in_transaction(db)?; powersync_migrate(ctx, LATEST_VERSION)?; - // Register the powersync_internal_close vtab to implement a "pre-close hook". - // See `pre_close_vtab.rs` for more details on how that works. - ctx.db_handle() - .exec(c"INSERT INTO powersync_internal_close(_) VALUES (null)")?; - Ok(String::from("")) } diff --git a/dart/test/sync_test.dart b/dart/test/sync_test.dart index ad93396..31e68c5 100644 --- a/dart/test/sync_test.dart +++ b/dart/test/sync_test.dart @@ -2132,6 +2132,30 @@ CREATE TRIGGER users_ref_delete expect(vfs.openFiles, isZero); }); + test('resolving the offline sync status allows closing the database', () { + final vfs = TrackingFileSystem( + parent: InMemoryFileSystem(), name: 'sync-test-resolve-offline'); + sqlite3.registerVirtualFileSystem(vfs); + addTearDown(() => sqlite3.unregisterVirtualFileSystem(vfs)); + + { + db = openTestDatabase(vfs: vfs, fileName: '/test.db') + ..executeInTx('select powersync_init();'); + expect(vfs.openFiles, isNonZero); + db.close(); + expect(vfs.openFiles, isZero); + } + + db = openTestDatabase( + vfs: vfs, fileName: '/test.db', mode: OpenMode.readOnly) + ..executeInTx('select powersync_init();'); + + db.execute('SELECT powersync_offline_sync_status();'); + expect(vfs.openFiles, isNonZero); + db.close(); + expect(vfs.openFiles, isZero); + }); + test('tracks download size', () { invokeControl('start', null); pushCheckpoint(buckets: [bucketDescription('a', count: 2)]); diff --git a/dart/test/utils/native_test_utils.dart b/dart/test/utils/native_test_utils.dart index 3eeca1e..3839cc2 100644 --- a/dart/test/utils/native_test_utils.dart +++ b/dart/test/utils/native_test_utils.dart @@ -17,8 +17,11 @@ var didLoadExtension = false; String? testingWithSanitizers = null; -CommonDatabase openTestDatabase( - {VirtualFileSystem? vfs, String fileName = ':memory:'}) { +CommonDatabase openTestDatabase({ + VirtualFileSystem? vfs, + String fileName = ':memory:', + OpenMode mode = OpenMode.readWriteCreate, +}) { if (!didLoadExtension) { loadExtension(); } @@ -35,7 +38,7 @@ CommonDatabase openTestDatabase( vfs = inMemory; } - final db = sqlite3.open(fileName, vfs: vfs?.name); + final db = sqlite3.open(fileName, vfs: vfs?.name, mode: mode); addTearDown(db.close); return db; } diff --git a/dart/test/utils/tracking_vfs.dart b/dart/test/utils/tracking_vfs.dart index a2d411b..fd66a57 100644 --- a/dart/test/utils/tracking_vfs.dart +++ b/dart/test/utils/tracking_vfs.dart @@ -32,7 +32,7 @@ final class TrackingFileSystem extends BaseVirtualFileSystem { final result = parent.xOpen(path, flags); openFiles++; return ( - outFlags: result.outFlags, + outFlags: flags, file: TrackingFile( result.file, this, flags & SqlFlag.SQLITE_OPEN_DELETEONCLOSE != 0), );