diff --git a/core/Makefile b/core/Makefile index cedf63fbc..6584a1273 100644 --- a/core/Makefile +++ b/core/Makefile @@ -242,4 +242,4 @@ $(TARGET_FUZZ): $(prefix) $(TARGET_SQLITE3_EXTRA_C) src/fuzzer.cc $(ext_files) valgrind \ ubsan analyzer fuzz asan static -FORCE: ; \ No newline at end of file +FORCE: ; diff --git a/core/rs/core/src/backfill.rs b/core/rs/core/src/backfill.rs index 5eeb3310b..cbf6c543d 100644 --- a/core/rs/core/src/backfill.rs +++ b/core/rs/core/src/backfill.rs @@ -14,8 +14,11 @@ pub fn backfill_table( pk_cols: Vec<&str>, non_pk_cols: Vec<&str>, is_commit_alter: bool, + no_tx: bool, ) -> Result { - db.exec_safe("SAVEPOINT backfill")?; + if !no_tx { + db.exec_safe("SAVEPOINT backfill")?; + } let sql = format!( "SELECT {pk_cols} FROM \"{table}\" AS t1 @@ -46,16 +49,26 @@ pub fn backfill_table( }; if let Err(e) = result { - db.exec_safe("ROLLBACK TO backfill")?; + if !no_tx { + db.exec_safe("ROLLBACK TO backfill")?; + } + return Err(e); } if let Err(e) = backfill_missing_columns(db, table, &pk_cols, &non_pk_cols, is_commit_alter) { - db.exec_safe("ROLLBACK TO backfill")?; + if !no_tx { + db.exec_safe("ROLLBACK TO backfill")?; + } + return Err(e); } - db.exec_safe("RELEASE backfill") + if !no_tx { + db.exec_safe("RELEASE backfill") + } else { + Ok(ResultCode::OK) + } } /** diff --git a/core/rs/core/src/c.rs b/core/rs/core/src/c.rs index cacad44fd..f0fb04ea1 100644 --- a/core/rs/core/src/c.rs +++ b/core/rs/core/src/c.rs @@ -134,6 +134,14 @@ extern "C" { ext_data: *mut crsql_ExtData, err_msg: *mut *mut c_char, ) -> c_int; + pub fn crsql_createCrr( + db: *mut sqlite::sqlite3, + schemaName: *const c_char, + tblName: *const c_char, + isCommitAlter: c_int, + noTx: c_int, + err: *mut *mut c_char, + ) -> c_int; } #[test] diff --git a/core/rs/core/src/create_cl_set_vtab.rs b/core/rs/core/src/create_cl_set_vtab.rs new file mode 100644 index 000000000..51b82f1cf --- /dev/null +++ b/core/rs/core/src/create_cl_set_vtab.rs @@ -0,0 +1,263 @@ +extern crate alloc; + +use core::ffi::{c_char, c_int, c_void}; + +use crate::alloc::borrow::ToOwned; +use crate::c::crsql_createCrr; +use alloc::boxed::Box; +use alloc::ffi::CString; +use alloc::format; +use alloc::string::String; +use sqlite::{convert_rc, sqlite3, Connection, CursorRef, StrRef, VTabArgs, VTabRef}; +use sqlite_nostd as sqlite; +use sqlite_nostd::ResultCode; + +// Virtual table definition to create a causal length set backed table. + +#[repr(C)] +struct CLSetTab { + base: sqlite::vtab, + base_table_name: String, + db_name: String, + db: *mut sqlite3, +} + +// used in response to `create virtual table ... using clset` +extern "C" fn create( + db: *mut sqlite::sqlite3, + _aux: *mut c_void, + argc: c_int, + argv: *const *const c_char, + vtab: *mut *mut sqlite::vtab, + err: *mut *mut c_char, +) -> c_int { + match create_impl(db, argc, argv, vtab, err) { + Ok(rc) => rc as c_int, + Err(rc) => { + // deallocate the vtab on error. + unsafe { + if *vtab != core::ptr::null_mut() { + let tab = Box::from_raw((*vtab).cast::()); + drop(tab); + *vtab = core::ptr::null_mut(); + } + } + rc as c_int + } + } +} + +fn create_impl( + db: *mut sqlite::sqlite3, + argc: c_int, + argv: *const *const c_char, + vtab: *mut *mut sqlite::vtab, + err: *mut *mut c_char, +) -> Result { + // This is the schema component + let vtab_args = sqlite::parse_vtab_args(argc, argv)?; + connect_create_shared(db, vtab, &vtab_args)?; + + // We can't wrap this in a savepoint for some reason. I guess because the `CREATE VIRTUAL TABLE..` + // statement is processing? 🤷‍♂️ + create_clset_storage(db, &vtab_args, err)?; + let db_name_c = CString::new(vtab_args.database_name)?; + let table_name_c = CString::new(base_name_from_virtual_name(vtab_args.table_name))?; + + // TODO: move `createCrr` to Rust + let rc = unsafe { crsql_createCrr(db, db_name_c.as_ptr(), table_name_c.as_ptr(), 0, 1, err) }; + convert_rc(rc) +} + +fn create_clset_storage( + db: *mut sqlite::sqlite3, + args: &VTabArgs, + err: *mut *mut c_char, +) -> Result { + // Is the _last_ arg all the args? Or is it comma separated in some way? + // What about index definitions... + // Let the user later create them against the base table? Or via insertions into our vtab schema? + let table_def = args.arguments.join(","); + if !args.table_name.ends_with("_schema") { + err.set("CLSet virtual table names must end with `_schema`"); + return Err(ResultCode::MISUSE); + } + + db.exec_safe(&format!( + "CREATE TABLE \"{db_name}\".\"{table_name}\" ({table_def})", + db_name = crate::util::escape_ident(args.database_name), + table_name = crate::util::escape_ident(base_name_from_virtual_name(args.table_name)), + table_def = table_def + )) +} + +fn base_name_from_virtual_name(virtual_name: &str) -> &str { + &virtual_name[0..(virtual_name.len() - "_schema".len())] +} + +// connect to an existing virtual table previously created by `create virtual table` +extern "C" fn connect( + db: *mut sqlite::sqlite3, + _aux: *mut c_void, + argc: c_int, + argv: *const *const c_char, + vtab: *mut *mut sqlite::vtab, + _err: *mut *mut c_char, +) -> c_int { + let vtab_args = sqlite::parse_vtab_args(argc, argv); + match vtab_args { + Ok(vtab_args) => match connect_create_shared(db, vtab, &vtab_args) { + Ok(rc) | Err(rc) => rc as c_int, + }, + Err(_e) => { + // free the tab if it was allocated + unsafe { + if *vtab != core::ptr::null_mut() { + let tab = Box::from_raw((*vtab).cast::()); + drop(tab); + *vtab = core::ptr::null_mut(); + } + }; + ResultCode::FORMAT as c_int + } + } +} + +fn connect_create_shared( + db: *mut sqlite::sqlite3, + vtab: *mut *mut sqlite::vtab, + args: &VTabArgs, +) -> Result { + sqlite::declare_vtab( + db, + "CREATE TABLE x(alteration TEXT HIDDEN, schema TEXT HIDDEN);", + )?; + let tab = Box::new(CLSetTab { + base: sqlite::vtab { + nRef: 0, + pModule: core::ptr::null(), + zErrMsg: core::ptr::null_mut(), + }, + base_table_name: base_name_from_virtual_name(args.table_name).to_owned(), + db_name: args.database_name.to_owned(), + db: db, + }); + vtab.set(tab); + Ok(ResultCode::OK) +} + +extern "C" fn best_index(_vtab: *mut sqlite::vtab, _index_info: *mut sqlite::index_info) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int { + if vtab != core::ptr::null_mut() { + let tab = unsafe { Box::from_raw(vtab.cast::()) }; + drop(tab); + } + ResultCode::OK as c_int +} + +extern "C" fn destroy(vtab: *mut sqlite::vtab) -> c_int { + let tab = unsafe { Box::from_raw(vtab.cast::()) }; + let ret = tab.db.exec_safe(&format!( + "DROP TABLE \"{db_name}\".\"{table_name}\"; + DROP TABLE \"{db_name}\".\"{table_name}__crsql_clock\";", + table_name = crate::util::escape_ident(&tab.base_table_name), + db_name = crate::util::escape_ident(&tab.db_name) + )); + match ret { + Err(rc) | Ok(rc) => rc as c_int, + } +} + +extern "C" fn open(_vtab: *mut sqlite::vtab, cursor: *mut *mut sqlite::vtab_cursor) -> c_int { + cursor.set(Box::new(sqlite::vtab_cursor { + pVtab: core::ptr::null_mut(), + })); + ResultCode::OK as c_int +} + +extern "C" fn close(cursor: *mut sqlite::vtab_cursor) -> c_int { + unsafe { + drop(Box::from_raw(cursor)); + } + 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 next(_cursor: *mut sqlite::vtab_cursor) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn eof(_cursor: *mut sqlite::vtab_cursor) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn column( + _cursor: *mut sqlite::vtab_cursor, + _ctx: *mut sqlite::context, + _col_num: c_int, +) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn rowid(_cursor: *mut sqlite::vtab_cursor, _row_id: *mut sqlite::int64) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn begin(_vtab: *mut sqlite::vtab) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn commit(_vtab: *mut sqlite::vtab) -> c_int { + ResultCode::OK as c_int +} + +extern "C" fn rollback(_vtab: *mut sqlite::vtab) -> c_int { + ResultCode::OK as c_int +} + +static MODULE: sqlite_nostd::module = sqlite_nostd::module { + iVersion: 0, + xCreate: Some(create), + xConnect: Some(connect), + xBestIndex: Some(best_index), + xDisconnect: Some(disconnect), + xDestroy: Some(destroy), + xOpen: Some(open), + xClose: Some(close), + xFilter: Some(filter), + xNext: Some(next), + xEof: Some(eof), + xColumn: Some(column), + xRowid: Some(rowid), + xUpdate: None, + xBegin: Some(begin), + xSync: None, + xCommit: Some(commit), + xRollback: Some(rollback), + xFindFunction: None, + xRename: None, + xSavepoint: None, + xRelease: None, + xRollbackTo: None, + xShadowName: None, +}; + +pub fn create_module(db: *mut sqlite::sqlite3) -> Result { + db.create_module_v2("clset", &MODULE, None, None)?; + + // xCreate(|x| 0); + + Ok(ResultCode::OK) +} diff --git a/core/rs/core/src/lib.rs b/core/rs/core/src/lib.rs index 9bedbcf30..732952d3b 100644 --- a/core/rs/core/src/lib.rs +++ b/core/rs/core/src/lib.rs @@ -11,6 +11,7 @@ mod changes_vtab_read; mod changes_vtab_write; mod compare_values; mod consts; +mod create_cl_set_vtab; mod is_crr; mod pack_columns; mod stmt_cache; @@ -31,7 +32,7 @@ pub use pack_columns::unpack_columns; pub use pack_columns::ColumnValue; use sqlite::ResultCode; use sqlite_nostd as sqlite; -use sqlite_nostd::{context, Connection, Context, Value}; +use sqlite_nostd::{Connection, Context, Value}; pub use teardown::*; pub extern "C" fn crsql_as_table( @@ -123,18 +124,23 @@ pub extern "C" fn sqlite3_crsqlcore_init( } let rc = unpack_columns_vtab::create_module(db).unwrap_or(sqlite::ResultCode::ERROR); + if rc != ResultCode::OK { + return rc as c_int; + } + let rc = create_cl_set_vtab::create_module(db).unwrap_or(ResultCode::ERROR); return rc as c_int; } #[no_mangle] pub extern "C" fn crsql_backfill_table( - context: *mut context, + db: *mut sqlite::sqlite3, table: *const c_char, pk_cols: *const *const c_char, pk_cols_len: c_int, non_pk_cols: *const *const c_char, non_pk_cols_len: c_int, is_commit_alter: c_int, + no_tx: c_int, ) -> c_int { let table = unsafe { CStr::from_ptr(table).to_str() }; let pk_cols = unsafe { @@ -153,10 +159,14 @@ pub extern "C" fn crsql_backfill_table( }; let result = match (table, pk_cols, non_pk_cols) { - (Ok(table), Ok(pk_cols), Ok(non_pk_cols)) => { - let db = context.db_handle(); - backfill_table(db, table, pk_cols, non_pk_cols, is_commit_alter != 0) - } + (Ok(table), Ok(pk_cols), Ok(non_pk_cols)) => backfill_table( + db, + table, + pk_cols, + non_pk_cols, + is_commit_alter != 0, + no_tx != 0, + ), _ => Err(ResultCode::ERROR), }; diff --git a/core/rs/core/src/unpack_columns_vtab.rs b/core/rs/core/src/unpack_columns_vtab.rs index 80a27c2d3..357f71fd4 100644 --- a/core/rs/core/src/unpack_columns_vtab.rs +++ b/core/rs/core/src/unpack_columns_vtab.rs @@ -28,13 +28,10 @@ extern "C" fn connect( _err: *mut *mut c_char, ) -> c_int { // TODO: more ergonomic rust binding for this - let rc = sqlite::declare_vtab( - db, - sqlite::strlit!("CREATE TABLE x(cell ANY, package BLOB hidden);"), - ); - if rc != 0 { - return rc; + if let Err(rc) = sqlite::declare_vtab(db, "CREATE TABLE x(cell ANY, package BLOB hidden);") { + return rc as c_int; } + unsafe { // TODO: more ergonomic rust bindings *vtab = Box::into_raw(Box::new(sqlite::vtab { @@ -42,7 +39,7 @@ extern "C" fn connect( pModule: core::ptr::null(), zErrMsg: core::ptr::null_mut(), })); - sqlite::vtab_config(db, sqlite::INNOCUOUS); + let _ = sqlite::vtab_config(db, sqlite::INNOCUOUS); } ResultCode::OK as c_int } diff --git a/core/rs/integration-check/tests/test_cl_set_vtab.rs b/core/rs/integration-check/tests/test_cl_set_vtab.rs new file mode 100644 index 000000000..03bdf6d42 --- /dev/null +++ b/core/rs/integration-check/tests/test_cl_set_vtab.rs @@ -0,0 +1,102 @@ +use sqlite::{Connection, ManagedConnection, ResultCode}; +use sqlite_nostd as sqlite; + +/* +Test: +- create crr +- destroy crr +- use crr that was created +- create if not exist vtab +- +*/ + +#[test] +fn create_crr_via_vtab() { + create_crr_via_vtab_impl().unwrap(); +} + +fn create_crr_via_vtab_impl() -> Result<(), ResultCode> { + let db = integration_utils::opendb()?; + let conn = &db.db; + + conn.exec_safe("CREATE VIRTUAL TABLE foo_schema USING CLSet (a primary key, b);")?; + conn.exec_safe("INSERT INTO foo VALUES (1, 2);")?; + let stmt = conn.prepare_v2("SELECT count(*) FROM crsql_changes")?; + stmt.step()?; + let count = stmt.column_int(0)?; + assert_eq!(count, 1); + Ok(()) +} + +#[test] +fn destroy_crr_via_vtab() { + destroy_crr_via_vtab_impl().unwrap(); +} + +fn destroy_crr_via_vtab_impl() -> Result<(), ResultCode> { + let db = integration_utils::opendb()?; + let conn = &db.db; + + conn.exec_safe("CREATE VIRTUAL TABLE foo_schema USING CLSet (a primary key, b);")?; + conn.exec_safe("DROP TABLE foo_schema")?; + let stmt = conn.prepare_v2("SELECT count(*) FROM sqlite_master WHERE name LIKE '%foo%'")?; + stmt.step()?; + let count = stmt.column_int(0)?; + assert_eq!(count, 0); + Ok(()) +} + +#[test] +fn create_invalid_crr() { + create_invalid_crr_impl().unwrap(); +} + +fn create_invalid_crr_impl() -> Result<(), ResultCode> { + let db = integration_utils::opendb()?; + let conn = &db.db; + + let result = conn.exec_safe("CREATE VIRTUAL TABLE foo_schema USING CLSet (a, b);"); + assert_eq!(result, Err(ResultCode::ERROR)); + let msg = conn.errmsg().unwrap(); + assert_eq!( + msg, + "Table foo has no primary key. CRRs must have a primary key" + ); + Ok(()) +} + +#[test] +fn create_if_not_exists() { + create_if_not_exists_impl().unwrap(); +} + +fn create_if_not_exists_impl() -> Result<(), ResultCode> { + let db = integration_utils::opendb()?; + let conn = &db.db; + + conn.exec_safe( + "CREATE VIRTUAL TABLE IF NOT EXISTS foo_schema USING CLSet (a primary key, b);", + )?; + conn.exec_safe("INSERT INTO foo VALUES (1, 2);")?; + let stmt = conn.prepare_v2("SELECT count(*) FROM crsql_changes")?; + stmt.step()?; + let count = stmt.column_int(0)?; + assert_eq!(count, 1); + drop(stmt); + // second create is a no-op + conn.exec_safe( + "CREATE VIRTUAL TABLE IF NOT EXISTS foo_schema USING CLSet (a primary key, b);", + )?; + let stmt = conn.prepare_v2("SELECT count(*) FROM crsql_changes")?; + stmt.step()?; + let count = stmt.column_int(0)?; + assert_eq!(count, 1); + Ok(()) +} + +// and later migration tests +// UPDATE foo SET schema = '...'; +// INSERT INTO foo (alter) VALUES ('...'); +// and auto-migrate tests for whole schema. +// auto-migrate would... +// - re-write `create vtab` things as `update foo set schema = ...` where those vtabs did not exist. diff --git a/core/rs/sqlite-rs-embedded b/core/rs/sqlite-rs-embedded index 530e62678..638554d4f 160000 --- a/core/rs/sqlite-rs-embedded +++ b/core/rs/sqlite-rs-embedded @@ -1 +1 @@ -Subproject commit 530e62678eda53278c21632033f5af2aebf1885f +Subproject commit 638554d4f00c58c122740f2f20e6cd77cf31d977 diff --git a/core/src/crsqlite.c b/core/src/crsqlite.c index 2110caf28..f7a0490a6 100644 --- a/core/src/crsqlite.c +++ b/core/src/crsqlite.c @@ -99,9 +99,8 @@ static void getSeqFunc(sqlite3_context *context, int argc, * Create a new crr -- * all triggers, views, tables */ -static int createCrr(sqlite3_context *context, sqlite3 *db, - const char *schemaName, const char *tblName, - int isCommitAlter, char **err) { +int crsql_createCrr(sqlite3 *db, const char *schemaName, const char *tblName, + int isCommitAlter, int noTx, char **err) { int rc = SQLITE_OK; crsql_TableInfo *tableInfo = 0; @@ -141,8 +140,8 @@ static int createCrr(sqlite3_context *context, sqlite3 *db, for (size_t i = 0; i < tableInfo->nonPksLen; i++) { nonPkNames[i] = tableInfo->nonPks[i].name; } - rc = crsql_backfill_table(context, tblName, pkNames, tableInfo->pksLen, - nonPkNames, tableInfo->nonPksLen, isCommitAlter); + rc = crsql_backfill_table(db, tblName, pkNames, tableInfo->pksLen, nonPkNames, + tableInfo->nonPksLen, isCommitAlter, noTx); sqlite3_free(pkNames); sqlite3_free(nonPkNames); @@ -203,7 +202,7 @@ static void crsqlMakeCrrFunc(sqlite3_context *context, int argc, return; } - rc = createCrr(context, db, schemaName, tblName, 0, &errmsg); + rc = crsql_createCrr(db, schemaName, tblName, 0, 0, &errmsg); if (rc != SQLITE_OK) { sqlite3_result_error(context, errmsg, -1); sqlite3_result_error_code(context, rc); @@ -287,7 +286,7 @@ static void crsqlCommitAlterFunc(sqlite3_context *context, int argc, crsql_ExtData *pExtData = (crsql_ExtData *)sqlite3_user_data(context); rc = crsql_compact_post_alter(db, tblName, pExtData, &errmsg); if (rc == SQLITE_OK) { - rc = createCrr(context, db, schemaName, tblName, 1, &errmsg); + rc = crsql_createCrr(db, schemaName, tblName, 1, 0, &errmsg); } if (rc == SQLITE_OK) { rc = sqlite3_exec(db, "RELEASE alter_crr", 0, 0, &errmsg); diff --git a/core/src/rust.h b/core/src/rust.h index 6929d3b08..bf37411d3 100644 --- a/core/src/rust.h +++ b/core/src/rust.h @@ -8,10 +8,10 @@ // structures to the old C-code that hasn't been converted yet. // These are those definitions. -int crsql_backfill_table(sqlite3_context *context, const char *tblName, +int crsql_backfill_table(sqlite3 *db, const char *tblName, const char **zpkNames, int pkCount, const char **zNonPkNames, int nonPkCount, - int isCommitAlter); + int isCommitAlter, int noTx); int crsql_is_crr(sqlite3 *db, const char *tblName); int crsql_compare_sqlite_values(const sqlite3_value *l, const sqlite3_value *r); int crsql_create_crr_triggers(sqlite3 *db, crsql_TableInfo *tableInfo,