diff --git a/core/rs/core/src/bootstrap.rs b/core/rs/core/src/bootstrap.rs index ed798f021..b4649840e 100644 --- a/core/rs/core/src/bootstrap.rs +++ b/core/rs/core/src/bootstrap.rs @@ -204,7 +204,7 @@ pub extern "C" fn crsql_create_clock_table( } } -fn create_clock_table( +pub fn create_clock_table( db: *mut sqlite3, table_info: *mut crsql_TableInfo, _err: *mut *mut c_char, diff --git a/core/rs/core/src/c.rs b/core/rs/core/src/c.rs index 348b389c4..c0e1405a4 100644 --- a/core/rs/core/src/c.rs +++ b/core/rs/core/src/c.rs @@ -1,5 +1,9 @@ extern crate alloc; +use alloc::boxed::Box; +use alloc::ffi::CString; +use alloc::vec::Vec; use core::ffi::{c_char, c_int}; +use core::ptr::null_mut; #[cfg(not(feature = "std"))] use num_derive::FromPrimitive; @@ -135,14 +139,6 @@ 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] @@ -640,3 +636,36 @@ fn bindgen_test_layout_crsql_ExtData() { ) ); } + +pub trait CPointer { + /** + * Returns a C compatible pointer to the underlying data. + * After calling this function, the caller is responsible for the memory. + */ + fn into_c_ptr(self) -> *mut T; +} + +impl CPointer for Vec { + fn into_c_ptr(mut self) -> *mut T { + if self.len() == 0 { + null_mut() + } else { + self.shrink_to(0); + self.into_raw_parts().0 + } + } +} + +impl CPointer for &str { + fn into_c_ptr(self) -> *mut c_char { + CString::new(self) + .map(|x| x.into_raw()) + .unwrap_or(null_mut()) + } +} + +impl CPointer for crsql_TableInfo { + fn into_c_ptr(self) -> *mut crsql_TableInfo { + Box::into_raw(Box::from(self)) + } +} diff --git a/core/rs/core/src/create_cl_set_vtab.rs b/core/rs/core/src/create_cl_set_vtab.rs index 1686deb1a..a4ddce4d3 100644 --- a/core/rs/core/src/create_cl_set_vtab.rs +++ b/core/rs/core/src/create_cl_set_vtab.rs @@ -3,12 +3,11 @@ extern crate alloc; use core::ffi::{c_char, c_int, c_void}; use crate::alloc::borrow::ToOwned; -use crate::c::crsql_createCrr; +use crate::create_crr::create_crr; 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::{sqlite3, Connection, CursorRef, StrRef, VTabArgs, VTabRef}; use sqlite_nostd as sqlite; use sqlite_nostd::ResultCode; @@ -68,12 +67,9 @@ fn create_impl( // 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) + let schema = vtab_args.database_name; + let table = base_name_from_virtual_name(vtab_args.table_name); + create_crr(db, schema, table, false, true, err) } fn create_clset_storage( diff --git a/core/rs/core/src/create_crr.rs b/core/rs/core/src/create_crr.rs new file mode 100644 index 000000000..6121b8d0f --- /dev/null +++ b/core/rs/core/src/create_crr.rs @@ -0,0 +1,70 @@ +use alloc::vec::Vec; +use core::ffi::{c_char, CStr}; +use sqlite_nostd as sqlite; +use sqlite_nostd::ResultCode; + +use crate::bootstrap::create_clock_table; +use crate::c::crsql_TableInfo; +use crate::tableinfo::{free_table_info, is_table_compatible, pull_table_info}; +use crate::triggers::create_triggers; +use crate::{backfill_table, is_crr, remove_crr_triggers_if_exist}; + +/** + * Create a new crr -- + * all triggers, views, tables + */ +pub fn create_crr( + db: *mut sqlite::sqlite3, + _schema: &str, + table: &str, + is_commit_alter: bool, + no_tx: bool, + err: *mut *mut c_char, +) -> Result { + if !is_table_compatible(db, table, err)? { + return Err(ResultCode::ERROR); + } + if is_crr(db, table)? { + return Ok(ResultCode::OK); + } + + let mut table_info: *mut crsql_TableInfo = core::ptr::null_mut(); + pull_table_info(db, table, &mut table_info, err)?; + + let cleanup = |err: ResultCode| unsafe { + free_table_info(table_info); + err + }; + + create_clock_table(db, table_info, err) + .and_then(|_| remove_crr_triggers_if_exist(db, table)) + .and_then(|_| create_triggers(db, table_info, err)) + .map_err(cleanup)?; + + let (non_pk_cols, pk_cols) = unsafe { + let info = table_info + .as_ref() + .ok_or(ResultCode::ERROR) + .map_err(cleanup)?; + let (pks, non_pks) = (info.pksLen as usize, info.nonPksLen as usize); + // Iterate without ownership transfer + ( + (0..non_pks) + .map(|i| &*info.nonPks.offset(i as isize)) + .map(|x| CStr::from_ptr(x.name).to_str()) + .collect::, _>>() + .map_err(|_| ResultCode::ERROR) + .map_err(cleanup)?, + (0..pks) + .map(|i| &*info.pks.offset(i as isize)) + .map(|x| CStr::from_ptr(x.name).to_str()) + .collect::, _>>() + .map_err(|_| ResultCode::ERROR) + .map_err(cleanup)?, + ) + }; + + backfill_table(db, table, pk_cols, non_pk_cols, is_commit_alter, no_tx).map_err(cleanup)?; + + Ok(cleanup(ResultCode::OK)) +} diff --git a/core/rs/core/src/lib.rs b/core/rs/core/src/lib.rs index 732952d3b..89c96acff 100644 --- a/core/rs/core/src/lib.rs +++ b/core/rs/core/src/lib.rs @@ -12,20 +12,24 @@ mod changes_vtab_write; mod compare_values; mod consts; mod create_cl_set_vtab; +mod create_crr; mod is_crr; mod pack_columns; mod stmt_cache; +mod tableinfo; mod teardown; mod triggers; mod unpack_columns_vtab; mod util; +use crate::c::crsql_TableInfo; use core::{ffi::c_char, slice}; extern crate alloc; use alloc::vec::Vec; pub use automigrate::*; pub use backfill::*; use core::ffi::{c_int, CStr}; +use create_crr::create_crr; pub use is_crr::*; use pack_columns::crsql_pack_columns; pub use pack_columns::unpack_columns; @@ -33,6 +37,9 @@ pub use pack_columns::ColumnValue; use sqlite::ResultCode; use sqlite_nostd as sqlite; use sqlite_nostd::{Connection, Context, Value}; +use tableinfo::free_table_info; +use tableinfo::is_table_compatible; +use tableinfo::pull_table_info; pub use teardown::*; pub extern "C" fn crsql_as_table( @@ -209,3 +216,58 @@ pub extern "C" fn crsql_is_crr(db: *mut sqlite::sqlite3, table: *const c_char) - (ResultCode::NOMEM as c_int) * -1 } } + +#[no_mangle] +pub extern "C" fn crsql_is_table_compatible( + db: *mut sqlite::sqlite3, + table: *const c_char, + err: *mut *mut c_char, +) -> c_int { + if let Ok(table) = unsafe { CStr::from_ptr(table).to_str() } { + is_table_compatible(db, table, err) + .map(|x| x as c_int) + .unwrap_or_else(|err| (err as c_int) * -1) + } else { + (ResultCode::NOMEM as c_int) * -1 + } +} + +#[no_mangle] +pub extern "C" fn crsql_pull_table_info( + db: *mut sqlite::sqlite3, + table: *const c_char, + table_info: *mut *mut crsql_TableInfo, + err: *mut *mut c_char, +) -> c_int { + if let Ok(table) = unsafe { CStr::from_ptr(table).to_str() } { + pull_table_info(db, table, table_info, err).unwrap_or_else(|err| err) as c_int + } else { + (ResultCode::NOMEM as c_int) * -1 + } +} + +#[no_mangle] +pub extern "C" fn crsql_free_table_info(table_info: *mut crsql_TableInfo) { + unsafe { free_table_info(table_info) }; +} + +#[no_mangle] +pub extern "C" fn crsql_create_crr( + db: *mut sqlite::sqlite3, + schema: *const c_char, + table: *const c_char, + is_commit_alter: c_int, + no_tx: c_int, + err: *mut *mut c_char, +) -> c_int { + let schema = unsafe { CStr::from_ptr(schema).to_str() }; + let table = unsafe { CStr::from_ptr(table).to_str() }; + + return match (table, schema) { + (Ok(table), Ok(schema)) => { + create_crr(db, schema, table, is_commit_alter != 0, no_tx != 0, err) + .unwrap_or_else(|err| err) as c_int + } + _ => ResultCode::NOMEM as c_int, + }; +} diff --git a/core/rs/core/src/tableinfo.rs b/core/rs/core/src/tableinfo.rs new file mode 100644 index 000000000..5468c0671 --- /dev/null +++ b/core/rs/core/src/tableinfo.rs @@ -0,0 +1,213 @@ +use crate::c::CPointer; +use crate::c::{crsql_ColumnInfo, crsql_TableInfo}; +use crate::util::Countable; +use alloc::boxed::Box; +use alloc::ffi::CString; +use alloc::format; +use alloc::vec; +use alloc::vec::Vec; +use core::ffi::c_char; +use num_traits::ToPrimitive; +use sqlite_nostd as sqlite; +use sqlite_nostd::Connection; +use sqlite_nostd::ResultCode; +use sqlite_nostd::StrRef; + +/** + * Given a table name, return the table info that describes that table. + * TableInfo is a struct that represents the results + * of pragma_table_info, pragma_index_list, pragma_index_info on a given table + * and its indices as well as some extra fields to facilitate crr creation. + */ +pub fn pull_table_info( + db: *mut sqlite::sqlite3, + table: &str, + table_info: *mut *mut crsql_TableInfo, + err: *mut *mut c_char, +) -> Result { + let sql = format!("SELECT count(*) FROM pragma_table_info('{table}')"); + let columns_len = match db.prepare_v2(&sql).and_then(|stmt| { + stmt.step()?; + stmt.column_int(0)?.to_usize().ok_or(ResultCode::ERROR) + }) { + Ok(count) => count, + Err(code) => { + err.set(&format!("Failed to find columns for crr -- {table}")); + return Err(code); + } + }; + + let sql = format!( + "SELECT \"cid\", \"name\", \"type\", \"notnull\", \"pk\" + FROM pragma_table_info('{table}') ORDER BY cid ASC" + ); + let column_infos = match db.prepare_v2(&sql) { + Ok(stmt) => { + let mut cols: Vec = vec![]; + + while stmt.step()? == ResultCode::ROW { + cols.push(crsql_ColumnInfo { + type_: stmt.column_text(2).map_err(free_cols(&cols))?.into_c_ptr(), + name: stmt.column_text(1).map_err(free_cols(&cols))?.into_c_ptr(), + notnull: stmt.column_int(3)?, + cid: stmt.column_int(0)?, + pk: stmt.column_int(4)?, + }); + } + + if cols.len() != columns_len { + err.set("Number of fetched columns did not match expected number of columns"); + return Err(free_cols(&cols)(ResultCode::ERROR)); + } + cols + } + Err(code) => { + err.set(&format!("Failed to prepare select for crr -- {table}")); + return Err(code); + } + }; + + let (mut pks, non_pks): (Vec<_>, Vec<_>) = + column_infos.clone().into_iter().partition(|x| x.pk > 0); + pks.sort_by_key(|x| x.pk); + + unsafe { + *table_info = crsql_TableInfo { + baseCols: column_infos.into_c_ptr(), + baseColsLen: columns_len as i32, + tblName: table.into_c_ptr(), + nonPksLen: non_pks.len() as i32, + nonPks: non_pks.into_c_ptr(), + pksLen: pks.len() as i32, + pks: pks.into_c_ptr(), + } + .into_c_ptr(); + } + + return Ok(ResultCode::OK); +} + +fn free_cols<'a>(cols: &'a Vec) -> impl Fn(ResultCode) -> ResultCode + 'a { + move |err: ResultCode| { + for info in cols { + drop(unsafe { CString::from_raw(info.type_) }); + drop(unsafe { CString::from_raw(info.name) }); + } + err + } +} + +pub unsafe fn free_table_info(table_info: *mut crsql_TableInfo) { + if table_info.is_null() { + return; + } + + let info = *table_info; + if !info.tblName.is_null() { + drop(CString::from_raw(info.tblName)); + } + if !info.baseCols.is_null() { + free_cols(&Vec::from_raw_parts( + info.baseCols, + info.baseColsLen as usize, + info.baseColsLen as usize, + ))(ResultCode::OK); + } + if !info.pks.is_null() { + drop(Vec::from_raw_parts( + info.pks, + info.pksLen as usize, + info.pksLen as usize, + )); + } + if !info.nonPks.is_null() { + drop(Vec::from_raw_parts( + info.nonPks, + info.nonPksLen as usize, + info.nonPksLen as usize, + )); + } + drop(Box::from_raw(table_info)); +} + +pub fn is_table_compatible( + db: *mut sqlite::sqlite3, + table: &str, + err: *mut *mut c_char, +) -> Result { + // No unique indices besides primary key + if db.count(&format!( + "SELECT count(*) FROM pragma_index_list('{table}') + WHERE \"origin\" != 'pk' AND \"unique\" = 1" + ))? != 0 + { + err.set(&format!( + "Table {table} has unique indices besides\ + the primary key. This is not allowed for CRRs" + )); + return Ok(false); + } + + // Must have a primary key + if db.count(&format!( + // pragma_index_list does not include primary keys that alias rowid... + // hence why we cannot use + // `select * from pragma_index_list where origin = pk` + "SELECT count(*) FROM pragma_table_info('{table}') + WHERE \"pk\" > 0" + ))? == 0 + { + err.set(&format!( + "Table {table} has no primary key. \ + CRRs must have a primary key" + )); + return Ok(false); + } + + // No auto-increment primary keys + let stmt = db.prepare_v2(&format!( + "SELECT 1 FROM sqlite_master WHERE name = ? AND type = 'table' AND sql + LIKE '%autoincrement%' limit 1" + ))?; + stmt.bind_text(1, table, sqlite::Destructor::STATIC)?; + if stmt.step()? == ResultCode::ROW { + err.set(&format!( + "{table} has auto-increment primary keys. This is likely a mistake as two \ + concurrent nodes will assign unrelated rows the same primary key. \ + Either use a primary key that represents the identity of your row or \ + use a database friendly UUID such as UUIDv7" + )); + return Ok(false); + }; + + // No checked foreign key constraints + if db.count(&format!( + "SELECT count(*) FROM pragma_foreign_key_list('{table}')" + ))? != 0 + { + err.set(&format!( + "Table {table} has checked foreign key constraints. \ + CRRs may have foreign keys but must not have \ + checked foreign key constraints as they can be violated \ + by row level security or replication." + )); + return Ok(false); + } + + // Check for default value or nullable + if db.count(&format!( + "SELECT count(*) FROM pragma_table_xinfo('{table}') + WHERE \"notnull\" = 1 AND \"dflt_value\" IS NULL AND \"pk\" = 0" + ))? != 0 + { + err.set(&format!( + "Table {table} has a NOT NULL column without a DEFAULT VALUE. \ + This is not allowed as it prevents forwards and backwards \ + compatibility between schema versions. Make the column \ + nullable or assign a default value to it." + )); + return Ok(false); + } + + return Ok(true); +} diff --git a/core/rs/core/src/triggers.rs b/core/rs/core/src/triggers.rs index 8a9351b25..85a329344 100644 --- a/core/rs/core/src/triggers.rs +++ b/core/rs/core/src/triggers.rs @@ -26,7 +26,7 @@ pub extern "C" fn crsql_create_crr_triggers( } } -fn create_triggers( +pub fn create_triggers( db: *mut sqlite3, table_info: *mut crsql_TableInfo, err: *mut *mut c_char, diff --git a/core/rs/core/src/util.rs b/core/rs/core/src/util.rs index 9daab231c..f9b0188d1 100644 --- a/core/rs/core/src/util.rs +++ b/core/rs/core/src/util.rs @@ -144,3 +144,16 @@ pub fn escape_ident(ident: &str) -> String { pub fn escape_ident_as_value(ident: &str) -> String { return ident.replace("'", "''"); } + +pub trait Countable { + fn count(self, sql: &str) -> Result; +} + +impl Countable for *mut sqlite::sqlite3 { + fn count(self, sql: &str) -> Result { + self.prepare_v2(sql).and_then(|stmt| { + stmt.step()?; + stmt.column_int(0) + }) + } +} diff --git a/core/src/changes-vtab-read.test.c b/core/src/changes-vtab-read.test.c index cf1088c1b..174f0861e 100644 --- a/core/src/changes-vtab-read.test.c +++ b/core/src/changes-vtab-read.test.c @@ -23,8 +23,8 @@ static void testChangesUnionQuery() { &err); rc += sqlite3_exec(db, "select crsql_as_crr('foo');", 0, 0, &err); rc += sqlite3_exec(db, "select crsql_as_crr('bar');", 0, 0, &err); - rc += crsql_getTableInfo(db, "foo", &tblInfos[0], &err); - rc += crsql_getTableInfo(db, "bar", &tblInfos[1], &err); + rc += crsql_pull_table_info(db, "foo", &tblInfos[0], &err); + rc += crsql_pull_table_info(db, "bar", &tblInfos[1], &err); assert(rc == SQLITE_OK); char *query = crsql_changes_union_query(tblInfos, 2, ""); @@ -119,7 +119,7 @@ static void testRowPatchDataQuery() { rc += sqlite3_exec(db, "select crsql_as_crr('foo');", 0, 0, &err); rc += sqlite3_exec(db, "insert into foo values(1, 'cb', 'cc', 'cd')", 0, 0, &err); - rc += crsql_getTableInfo(db, "foo", &tblInfo, &err); + rc += crsql_pull_table_info(db, "foo", &tblInfo, &err); assert(rc == SQLITE_OK); // TC1: single pk table, 1 col change @@ -131,7 +131,7 @@ static void testRowPatchDataQuery() { printf("\t\e[0;32mSuccess\e[0m\n"); sqlite3_free(err); - crsql_freeTableInfo(tblInfo); + crsql_free_table_info(tblInfo); crsql_close(db); assert(rc == SQLITE_OK); } diff --git a/core/src/crsqlite.c b/core/src/crsqlite.c index 498f54df0..52c55f09e 100644 --- a/core/src/crsqlite.c +++ b/core/src/crsqlite.c @@ -114,60 +114,6 @@ static void getSeqFunc(sqlite3_context *context, int argc, sqlite3_result_int(context, pExtData->seq); } -/** - * Create a new crr -- - * all triggers, views, tables - */ -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; - - if (!crsql_isTableCompatible(db, tblName, err)) { - return SQLITE_ERROR; - } - - rc = crsql_is_crr(db, tblName); - if (rc < 0) { - return rc * -1; - } - if (rc == 1) { - return SQLITE_OK; - } - - rc = crsql_getTableInfo(db, tblName, &tableInfo, err); - - if (rc != SQLITE_OK) { - crsql_freeTableInfo(tableInfo); - return rc; - } - - rc = crsql_create_clock_table(db, tableInfo, err); - if (rc == SQLITE_OK) { - rc = crsql_remove_crr_triggers_if_exist(db, tableInfo->tblName); - if (rc == SQLITE_OK) { - rc = crsql_create_crr_triggers(db, tableInfo, err); - } - } - - const char **pkNames = sqlite3_malloc(sizeof(char *) * tableInfo->pksLen); - for (size_t i = 0; i < tableInfo->pksLen; i++) { - pkNames[i] = tableInfo->pks[i].name; - } - const char **nonPkNames = - sqlite3_malloc(sizeof(char *) * tableInfo->nonPksLen); - for (size_t i = 0; i < tableInfo->nonPksLen; i++) { - nonPkNames[i] = tableInfo->nonPks[i].name; - } - rc = crsql_backfill_table(db, tblName, pkNames, tableInfo->pksLen, nonPkNames, - tableInfo->nonPksLen, isCommitAlter, noTx); - sqlite3_free(pkNames); - sqlite3_free(nonPkNames); - - crsql_freeTableInfo(tableInfo); - return rc; -} - static void crsqlSyncBit(sqlite3_context *context, int argc, sqlite3_value **argv) { int *syncBit = (int *)sqlite3_user_data(context); @@ -221,7 +167,7 @@ static void crsqlMakeCrrFunc(sqlite3_context *context, int argc, return; } - rc = crsql_createCrr(db, schemaName, tblName, 0, 0, &errmsg); + rc = crsql_create_crr(db, schemaName, tblName, 0, 0, &errmsg); if (rc != SQLITE_OK) { sqlite3_result_error(context, errmsg, -1); sqlite3_result_error_code(context, rc); @@ -305,7 +251,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 = crsql_createCrr(db, schemaName, tblName, 1, 0, &errmsg); + rc = crsql_create_crr(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/crsqlite.test.c b/core/src/crsqlite.test.c index b7348fd46..d923e2d9c 100644 --- a/core/src/crsqlite.test.c +++ b/core/src/crsqlite.test.c @@ -98,13 +98,13 @@ static void testCreateClockTable() { sqlite3_exec(db, "CREATE TABLE baz (a primary key, b)", 0, 0, 0); sqlite3_exec(db, "CREATE TABLE boo (a primary key, b, c)", 0, 0, 0); - rc = crsql_getTableInfo(db, "foo", &tc1, &err); + rc = crsql_pull_table_info(db, "foo", &tc1, &err); CHECK_OK - rc = crsql_getTableInfo(db, "bar", &tc2, &err); + rc = crsql_pull_table_info(db, "bar", &tc2, &err); CHECK_OK - rc = crsql_getTableInfo(db, "baz", &tc3, &err); + rc = crsql_pull_table_info(db, "baz", &tc3, &err); CHECK_OK - rc = crsql_getTableInfo(db, "boo", &tc4, &err); + rc = crsql_pull_table_info(db, "boo", &tc4, &err); CHECK_OK rc = crsql_create_clock_table(db, tc1, &err); @@ -116,10 +116,10 @@ static void testCreateClockTable() { rc = crsql_create_clock_table(db, tc4, &err); CHECK_OK - crsql_freeTableInfo(tc1); - crsql_freeTableInfo(tc2); - crsql_freeTableInfo(tc3); - crsql_freeTableInfo(tc4); + crsql_free_table_info(tc1); + crsql_free_table_info(tc2); + crsql_free_table_info(tc3); + crsql_free_table_info(tc4); // TODO: check that the tables have the expected schema diff --git a/core/src/rust.h b/core/src/rust.h index bf37411d3..a9398819e 100644 --- a/core/src/rust.h +++ b/core/src/rust.h @@ -27,5 +27,11 @@ int crsql_init_site_id(sqlite3 *db, unsigned char *ret); int crsql_init_peer_tracking_table(sqlite3 *db); int crsql_create_schema_table_if_not_exists(sqlite3 *db); int crsql_maybe_update_db(sqlite3 *db, char **pzErrMsg); +int crsql_is_table_compatible(sqlite3 *db, const char *tblName, char **err); +int crsql_pull_table_info(sqlite3 *db, const char *tblName, + crsql_TableInfo **tableInfo, char **err); +void crsql_free_table_info(crsql_TableInfo *tableInfo); +int crsql_create_crr(sqlite3 *db, const char *schemaName, const char *tblName, + int isCommitAlter, int noTx, char **err); #endif diff --git a/core/src/tableinfo.c b/core/src/tableinfo.c index 3682a8dc6..b7f2ca3c4 100644 --- a/core/src/tableinfo.c +++ b/core/src/tableinfo.c @@ -8,222 +8,12 @@ #include "consts.h" #include "crsqlite.h" #include "get-table.h" +#include "rust.h" #include "util.h" -void crsql_freeColumnInfoContents(crsql_ColumnInfo *columnInfo) { - sqlite3_free(columnInfo->name); - sqlite3_free(columnInfo->type); -} - -static void crsql_freeColumnInfos(crsql_ColumnInfo *columnInfos, int len) { - if (columnInfos == 0) { - return; - } - - int i = 0; - for (i = 0; i < len; ++i) { - crsql_freeColumnInfoContents(&columnInfos[i]); - } - - sqlite3_free(columnInfos); -} - -int crsql_numPks(crsql_ColumnInfo *colInfos, int colInfosLen) { - int ret = 0; - int i = 0; - - for (i = 0; i < colInfosLen; ++i) { - if (colInfos[i].pk > 0) { - ++ret; - } - } - - return ret; -} - -static int cmpPks(const void *a, const void *b) { - return (((crsql_ColumnInfo *)a)->pk - ((crsql_ColumnInfo *)b)->pk); -} - -crsql_ColumnInfo *crsql_pks(crsql_ColumnInfo *colInfos, int colInfosLen, - int *pPksLen) { - int numPks = crsql_numPks(colInfos, colInfosLen); - crsql_ColumnInfo *ret = 0; - int i = 0; - int j = 0; - *pPksLen = numPks; - - if (numPks == 0) { - return 0; - } - - ret = sqlite3_malloc(numPks * sizeof *ret); - for (i = 0; i < colInfosLen; ++i) { - if (colInfos[i].pk > 0) { - assert(j < numPks); - ret[j] = colInfos[i]; - ++j; - } - } - - qsort(ret, numPks, sizeof(crsql_ColumnInfo), cmpPks); - - assert(j == numPks); - return ret; -} - -crsql_ColumnInfo *crsql_nonPks(crsql_ColumnInfo *colInfos, int colInfosLen, - int *pNonPksLen) { - int nonPksLen = colInfosLen - crsql_numPks(colInfos, colInfosLen); - crsql_ColumnInfo *ret = 0; - int i = 0; - int j = 0; - *pNonPksLen = nonPksLen; - - if (nonPksLen == 0) { - return 0; - } - - ret = sqlite3_malloc(nonPksLen * sizeof *ret); - for (i = 0; i < colInfosLen; ++i) { - if (colInfos[i].pk == 0) { - assert(j < nonPksLen); - ret[j] = colInfos[i]; - ++j; - } - } - - assert(j == nonPksLen); - return ret; -} - -/** - * Constructs a table info based on the results of pragma - * statements against the base table. - */ -static crsql_TableInfo *crsql_tableInfo(const char *tblName, - crsql_ColumnInfo *colInfos, - int colInfosLen) { - crsql_TableInfo *ret = sqlite3_malloc(sizeof *ret); - - ret->baseCols = colInfos; - ret->baseColsLen = colInfosLen; - - ret->tblName = crsql_strdup(tblName); - - ret->nonPks = - crsql_nonPks(ret->baseCols, ret->baseColsLen, &(ret->nonPksLen)); - ret->pks = crsql_pks(ret->baseCols, ret->baseColsLen, &(ret->pksLen)); - - return ret; -} - -/** - * Given a table name, return the table info that describes that table. - * TableInfo is a struct that represents the results - * of pragma_table_info, pragma_index_list, pragma_index_info on a given table - * and its inidces as well as some extra fields to facilitate crr creation. - */ -int crsql_getTableInfo(sqlite3 *db, const char *tblName, - crsql_TableInfo **pTableInfo, char **pErrMsg) { - char *zSql = 0; - int rc = SQLITE_OK; - sqlite3_stmt *pStmt = 0; - int numColInfos = 0; - int i = 0; - crsql_ColumnInfo *columnInfos = 0; - - zSql = - sqlite3_mprintf("select count(*) from pragma_table_info('%s')", tblName); - numColInfos = crsql_getCount(db, zSql); - sqlite3_free(zSql); - - if (numColInfos < 0) { - *pErrMsg = sqlite3_mprintf("Failed to find columns for crr -- %s", tblName); - return numColInfos; - } - - zSql = sqlite3_mprintf( - "select \"cid\", \"name\", \"type\", \"notnull\", \"pk\" from " - "pragma_table_info('%s') order by cid asc", - tblName); - rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - - if (rc != SQLITE_OK) { - *pErrMsg = - sqlite3_mprintf("Failed to prepare select for crr -- %s", tblName); - sqlite3_finalize(pStmt); - return rc; - } - - rc = sqlite3_step(pStmt); - if (rc != SQLITE_ROW) { - *pErrMsg = sqlite3_mprintf("Failed to parse crr definition -- %s", tblName); - sqlite3_finalize(pStmt); - return rc; - } - columnInfos = sqlite3_malloc(numColInfos * sizeof *columnInfos); - while (rc == SQLITE_ROW) { - if (i >= numColInfos) { - sqlite3_finalize(pStmt); - for (int j = 0; j < i; ++j) { - crsql_freeColumnInfoContents(&columnInfos[j]); - } - sqlite3_free(columnInfos); - return SQLITE_ERROR; - } - - columnInfos[i].cid = sqlite3_column_int(pStmt, 0); - - columnInfos[i].name = - crsql_strdup((const char *)sqlite3_column_text(pStmt, 1)); - columnInfos[i].type = - crsql_strdup((const char *)sqlite3_column_text(pStmt, 2)); - - columnInfos[i].notnull = sqlite3_column_int(pStmt, 3); - columnInfos[i].pk = sqlite3_column_int(pStmt, 4); - - ++i; - rc = sqlite3_step(pStmt); - } - sqlite3_finalize(pStmt); - - if (i < numColInfos) { - for (int j = 0; j < i; ++j) { - crsql_freeColumnInfoContents(&columnInfos[j]); - } - sqlite3_free(columnInfos); - *pErrMsg = sqlite3_mprintf( - "Number of fetched columns did not match expected number of " - "columns"); - return SQLITE_ERROR; - } - - *pTableInfo = crsql_tableInfo(tblName, columnInfos, numColInfos); - - return SQLITE_OK; -} - -void crsql_freeTableInfo(crsql_TableInfo *tableInfo) { - if (tableInfo == 0) { - return; - } - // baseCols is a superset of all other col arrays - // and will free their contents. - crsql_freeColumnInfos(tableInfo->baseCols, tableInfo->baseColsLen); - - // the arrays themselves of course still need freeing - sqlite3_free(tableInfo->tblName); - sqlite3_free(tableInfo->pks); - sqlite3_free(tableInfo->nonPks); - - sqlite3_free(tableInfo); -} - void crsql_freeAllTableInfos(crsql_TableInfo **tableInfos, int len) { for (int i = 0; i < len; ++i) { - crsql_freeTableInfo(tableInfos[i]); + crsql_free_table_info(tableInfos[i]); } sqlite3_free(tableInfos); } @@ -295,7 +85,7 @@ int crsql_pullAllTableInfos(sqlite3 *db, crsql_TableInfo ***pzpTableInfos, char *baseTableName = crsql_strndup(zzClockTableNames[i + 1], strlen(zzClockTableNames[i + 1]) - __CRSQL_CLOCK_LEN); - rc = crsql_getTableInfo(db, baseTableName, &tableInfos[i], errmsg); + rc = crsql_pull_table_info(db, baseTableName, &tableInfos[i], errmsg); sqlite3_free(baseTableName); if (rc != SQLITE_OK) { @@ -311,162 +101,4 @@ int crsql_pullAllTableInfos(sqlite3 *db, crsql_TableInfo ***pzpTableInfos, *rTableInfosLen = rNumRows; return SQLITE_OK; -} - -int crsql_isTableCompatible(sqlite3 *db, const char *tblName, char **errmsg) { - // No unique indices besides primary key - sqlite3_stmt *pStmt = 0; - char *zSql = sqlite3_mprintf( - "SELECT count(*) FROM pragma_index_list('%s') WHERE \"origin\" != 'pk' " - "AND \"unique\" = 1", - tblName); - int rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - - if (rc != SQLITE_OK) { - *errmsg = - sqlite3_mprintf("Failed to analyze index information for %s", tblName); - return 0; - } - - rc = sqlite3_step(pStmt); - if (rc == SQLITE_ROW) { - int count = sqlite3_column_int(pStmt, 0); - sqlite3_finalize(pStmt); - if (count != 0) { - *errmsg = sqlite3_mprintf( - "Table %s has unique indices besides the primary key. This is " - "not " - "allowed for CRRs", - tblName); - return 0; - } - } else { - sqlite3_finalize(pStmt); - return 0; - } - - // Must have a primary key - zSql = sqlite3_mprintf( - // pragma_index_list does not include primary keys that alias rowid... - // hence why we cannot use `select * from pragma_index_list where origin = - // pk` - "SELECT count(*) FROM pragma_table_info('%s') WHERE \"pk\" > 0", tblName); - rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - - if (rc != SQLITE_OK) { - *errmsg = sqlite3_mprintf( - "Failed to analyze primary key information for %s", tblName); - return 0; - } - - rc = sqlite3_step(pStmt); - if (rc == SQLITE_ROW) { - int count = sqlite3_column_int(pStmt, 0); - sqlite3_finalize(pStmt); - if (count == 0) { - *errmsg = sqlite3_mprintf( - "Table %s has no primary key. CRRs must have a primary key", tblName); - return 0; - } - } else { - sqlite3_finalize(pStmt); - return 0; - } - - // No auto-increment primary keys - zSql = - "SELECT 1 FROM sqlite_master WHERE name = ? AND type = 'table' AND sql " - "LIKE '%autoincrement%' limit 1"; - rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - - rc += sqlite3_bind_text(pStmt, 1, tblName, -1, SQLITE_STATIC); - if (rc != SQLITE_OK) { - *errmsg = sqlite3_mprintf("Failed to analyze autoincrement status for %s", - tblName); - return 0; - } - rc = sqlite3_step(pStmt); - sqlite3_finalize(pStmt); - if (rc == SQLITE_ROW) { - *errmsg = sqlite3_mprintf( - "%s has auto-increment primary keys. This is likely a mistake as two " - "concurrent nodes will assign unrelated rows the same primary key. " - "Either use a primary key that represents the identity of your row or " - "use a database friendly UUID such as UUIDv7", - tblName); - return 0; - } else if (rc != SQLITE_DONE) { - return 0; - } - - // No checked foreign key constraints - zSql = sqlite3_mprintf("SELECT count(*) FROM pragma_foreign_key_list('%s')", - tblName); - rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - - if (rc != SQLITE_OK) { - *errmsg = sqlite3_mprintf( - "Failed to analyze primary key information for %s", tblName); - return 0; - } - - rc = sqlite3_step(pStmt); - if (rc == SQLITE_ROW) { - int count = sqlite3_column_int(pStmt, 0); - sqlite3_finalize(pStmt); - if (count != 0) { - *errmsg = sqlite3_mprintf( - "Table %s has checked foreign key constraints. CRRs may have foreign " - "keys but must not have " - "checked foreign key constraints as they can be violated by row " - "level " - "security or replication.", - tblName); - return 0; - } - } else { - sqlite3_finalize(pStmt); - return 0; - } - - // check for default value or nullable - zSql = sqlite3_mprintf( - "SELECT count(*) FROM pragma_table_xinfo('%s') WHERE \"notnull\" = 1 " - "AND " - "\"dflt_value\" IS NULL AND \"pk\" = 0", - tblName); - rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0); - sqlite3_free(zSql); - - if (rc != SQLITE_OK) { - *errmsg = sqlite3_mprintf( - "Failed to analyze default value information for %s", tblName); - return 0; - } - - rc = sqlite3_step(pStmt); - if (rc == SQLITE_ROW) { - int count = sqlite3_column_int(pStmt, 0); - sqlite3_finalize(pStmt); - if (count != 0) { - *errmsg = sqlite3_mprintf( - "Table %s has a NOT NULL column without a DEFAULT VALUE. This " - "is not " - "allowed as it prevents forwards and backwards compatability " - "between " - "schema versions. Make the column nullable or assign a default " - "value " - "to it.", - tblName); - return 0; - } - } else { - sqlite3_finalize(pStmt); - return 0; - } - - return 1; -} +} \ No newline at end of file diff --git a/core/src/tableinfo.h b/core/src/tableinfo.h index 0809c6f51..1fae8de96 100644 --- a/core/src/tableinfo.h +++ b/core/src/tableinfo.h @@ -37,13 +37,6 @@ struct crsql_TableInfo { crsql_ColumnInfo *crsql_extractBaseCols(crsql_ColumnInfo *colInfos, int colInfosLen, int *pBaseColsLen); -void crsql_freeColumnInfoContents(crsql_ColumnInfo *columnInfo); -void crsql_freeTableInfo(crsql_TableInfo *tableInfo); - -// TODO: this should be pullTableInfo -int crsql_getTableInfo(sqlite3 *db, const char *tblName, - crsql_TableInfo **pTableInfo, char **pErrMsg); - void crsql_freeAllTableInfos(crsql_TableInfo **tableInfos, int len); crsql_TableInfo *crsql_findTableInfo(crsql_TableInfo **tblInfos, int len, const char *tblName); @@ -52,6 +45,5 @@ int crsql_indexofTableInfo(crsql_TableInfo **tblInfos, int len, sqlite3_int64 crsql_slabRowid(int idx, sqlite3_int64 rowid); int crsql_pullAllTableInfos(sqlite3 *db, crsql_TableInfo ***pzpTableInfos, int *rTableInfosLen, char **errmsg); -int crsql_isTableCompatible(sqlite3 *db, const char *tblName, char **errmsg); #endif \ No newline at end of file diff --git a/core/src/tableinfo.test.c b/core/src/tableinfo.test.c index 1fce4f2b3..e713d5eea 100644 --- a/core/src/tableinfo.test.c +++ b/core/src/tableinfo.test.c @@ -7,6 +7,7 @@ #include "consts.h" #include "crsqlite.h" +#include "rust.h" #include "util.h" int crsql_close(sqlite3 *db); @@ -21,7 +22,7 @@ static void testGetTableInfo() { rc = sqlite3_open(":memory:", &db); sqlite3_exec(db, "CREATE TABLE foo (a INT NOT NULL, b)", 0, 0, 0); - rc = crsql_getTableInfo(db, "foo", &tableInfo, &errMsg); + rc = crsql_pull_table_info(db, "foo", &tableInfo, &errMsg); if (rc != SQLITE_OK) { printf("err: %s %d\n", errMsg, rc); @@ -48,10 +49,10 @@ static void testGetTableInfo() { assert(tableInfo->nonPks[0].notnull == 1); assert(tableInfo->nonPks[0].pk == 0); - crsql_freeTableInfo(tableInfo); + crsql_free_table_info(tableInfo); sqlite3_exec(db, "CREATE TABLE bar (a PRIMARY KEY, b)", 0, 0, 0); - rc = crsql_getTableInfo(db, "bar", &tableInfo, &errMsg); + rc = crsql_pull_table_info(db, "bar", &tableInfo, &errMsg); if (rc != SQLITE_OK) { printf("err: %s %d\n", errMsg, rc); sqlite3_free(errMsg); @@ -70,7 +71,7 @@ static void testGetTableInfo() { assert(tableInfo->pksLen == 1); assert(tableInfo->nonPksLen == 1); - crsql_freeTableInfo(tableInfo); + crsql_free_table_info(tableInfo); printf("\t\e[0;32mSuccess\e[0m\n"); crsql_close(db); @@ -138,44 +139,47 @@ static void testIsTableCompatible() { // no pks rc += sqlite3_exec(db, "CREATE TABLE foo (a)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "foo", &errmsg); + rc = crsql_is_table_compatible(db, "foo", &errmsg); assert(rc == 0); sqlite3_free(errmsg); + errmsg = 0; // pks rc = sqlite3_exec(db, "CREATE TABLE bar (a primary key)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "bar", &errmsg); + rc = crsql_is_table_compatible(db, "bar", &errmsg); assert(rc == 1); // pks + other non unique indices rc = sqlite3_exec(db, "CREATE TABLE baz (a primary key, b)", 0, 0, 0); rc += sqlite3_exec(db, "CREATE INDEX bar_i ON baz (b)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "bar", &errmsg); + rc = crsql_is_table_compatible(db, "bar", &errmsg); assert(rc == 1); // pks + other unique indices rc = sqlite3_exec(db, "CREATE TABLE fuzz (a primary key, b)", 0, 0, 0); rc += sqlite3_exec(db, "CREATE UNIQUE INDEX fuzz_i ON fuzz (b)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "fuzz", &errmsg); + rc = crsql_is_table_compatible(db, "fuzz", &errmsg); assert(rc == 0); sqlite3_free(errmsg); + errmsg = 0; // not null and no dflt rc = sqlite3_exec(db, "CREATE TABLE buzz (a primary key, b NOT NULL)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "buzz", &errmsg); + rc = crsql_is_table_compatible(db, "buzz", &errmsg); assert(rc == 0); sqlite3_free(errmsg); + errmsg = 0; // not null and dflt rc = sqlite3_exec( db, "CREATE TABLE boom (a primary key, b NOT NULL DEFAULT 1)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "boom", &errmsg); + rc = crsql_is_table_compatible(db, "boom", &errmsg); assert(rc == 1); // fk constraint @@ -184,36 +188,38 @@ static void testIsTableCompatible() { "CREATE TABLE zoom (a primary key, b, FOREIGN KEY(b) REFERENCES foo(a))", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "zoom", &errmsg); + rc = crsql_is_table_compatible(db, "zoom", &errmsg); assert(rc == 0); sqlite3_free(errmsg); + errmsg = 0; // strict mode should be ok rc = sqlite3_exec(db, "CREATE TABLE atable (\"id\" TEXT PRIMARY KEY) STRICT", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "atable", &errmsg); + rc = crsql_is_table_compatible(db, "atable", &errmsg); assert(rc == 1); // no autoincrement rc = sqlite3_exec( db, "CREATE TABLE woom (a integer primary key autoincrement)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "woom", &errmsg); + rc = crsql_is_table_compatible(db, "woom", &errmsg); assert(rc == 0); sqlite3_free(errmsg); + errmsg = 0; // aliased rowid rc = sqlite3_exec(db, "CREATE TABLE loom (a integer primary key)", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "loom", &errmsg); + rc = crsql_is_table_compatible(db, "loom", &errmsg); assert(rc == 1); rc = sqlite3_exec( db, "CREATE TABLE atable2 (\"id\" TEXT PRIMARY KEY, x TEXT) STRICT;", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "atable2", &errmsg); + rc = crsql_is_table_compatible(db, "atable2", &errmsg); assert(rc == 1); rc = sqlite3_exec(db, @@ -225,7 +231,7 @@ static void testIsTableCompatible() { ) STRICT;", 0, 0, 0); assert(rc == SQLITE_OK); - rc = crsql_isTableCompatible(db, "atable2", &errmsg); + rc = crsql_is_table_compatible(db, "atable2", &errmsg); assert(rc == 1); printf("\t\e[0;32mSuccess\e[0m\n"); diff --git a/core/src/triggers.test.c b/core/src/triggers.test.c index fb653053c..164bac15d 100644 --- a/core/src/triggers.test.c +++ b/core/src/triggers.test.c @@ -26,13 +26,13 @@ static void testCreateTriggers() { rc = sqlite3_exec(db, "CREATE TABLE \"foo\" (\"a\" PRIMARY KEY, \"b\", \"c\")", 0, 0, &errMsg); - rc = crsql_getTableInfo(db, "foo", &tableInfo, &errMsg); + rc = crsql_pull_table_info(db, "foo", &tableInfo, &errMsg); if (rc == SQLITE_OK) { rc = crsql_create_crr_triggers(db, tableInfo, &errMsg); } - crsql_freeTableInfo(tableInfo); + crsql_free_table_info(tableInfo); if (rc != SQLITE_OK) { crsql_close(db); printf("err: %s | rc: %d\n", errMsg, rc);