diff --git a/core/rs/bundle/Cargo.lock b/core/rs/bundle/Cargo.lock index e1f4bdb01..8b964eb60 100644 --- a/core/rs/bundle/Cargo.lock +++ b/core/rs/bundle/Cargo.lock @@ -121,9 +121,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" diff --git a/core/rs/core/Cargo.lock b/core/rs/core/Cargo.lock index 244a76aa7..d2006ae23 100644 --- a/core/rs/core/Cargo.lock +++ b/core/rs/core/Cargo.lock @@ -105,9 +105,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" diff --git a/core/rs/core/src/c.rs b/core/rs/core/src/c.rs index de116be49..333fbb372 100644 --- a/core/rs/core/src/c.rs +++ b/core/rs/core/src/c.rs @@ -46,8 +46,8 @@ pub enum ChangeRowType { } #[repr(C)] -#[derive(Debug, Copy, Clone)] #[allow(non_snake_case, non_camel_case_types)] +#[derive(Debug, Copy, Clone)] pub struct crsql_ExtData { pub pPragmaSchemaVersionStmt: *mut sqlite::stmt, pub pPragmaDataVersionStmt: *mut sqlite::stmt, @@ -67,6 +67,7 @@ pub struct crsql_ExtData { pub pSetSiteIdOrdinalStmt: *mut sqlite::stmt, pub pSelectSiteIdOrdinalStmt: *mut sqlite::stmt, pub pSelectClockTablesStmt: *mut sqlite::stmt, + pub mergeEqualValues: ::core::ffi::c_int, } #[repr(C)] @@ -262,7 +263,7 @@ fn bindgen_test_layout_crsql_ExtData() { let ptr = UNINIT.as_ptr(); assert_eq!( ::core::mem::size_of::(), - 128usize, + 136usize, concat!("Size of: ", stringify!(crsql_ExtData)) ); assert_eq!( @@ -452,4 +453,14 @@ fn bindgen_test_layout_crsql_ExtData() { stringify!(pSelectClockTablesStmt) ) ); + assert_eq!( + unsafe { ::core::ptr::addr_of!((*ptr).mergeEqualValues) as usize - ptr as usize }, + 128usize, + concat!( + "Offset of field: ", + stringify!(crsql_ExtData), + "::", + stringify!(mergeEqualValues) + ) + ); } diff --git a/core/rs/core/src/changes_vtab_write.rs b/core/rs/core/src/changes_vtab_write.rs index de9b0b965..3f26b07ab 100644 --- a/core/rs/core/src/changes_vtab_write.rs +++ b/core/rs/core/src/changes_vtab_write.rs @@ -19,19 +19,21 @@ use crate::util::slab_rowid; /** * did_cid_win does not take into account the causal length. - * The expectation is that all cuasal length concerns have already been handle + * The expectation is that all causal length concerns have already been handle * via: * - early return because insert_cl < local_cl * - automatic win because insert_cl > local_cl - * - come here to did_cid_win iff insert_cl = local_cl + * - come here to did_cid_win if insert_cl = local_cl */ fn did_cid_win( db: *mut sqlite3, + ext_data: *mut crsql_ExtData, insert_tbl: &str, tbl_info: &TableInfo, unpacked_pks: &Vec, key: sqlite::int64, insert_val: *mut sqlite::value, + insert_site_id: &[u8], col_name: &str, col_version: sqlite::int64, errmsg: *mut *mut c_char, @@ -89,10 +91,15 @@ fn did_cid_win( match step_result { Ok(ResultCode::ROW) => { let local_value = col_val_stmt.column_value(0)?; - let ret = crsql_compare_sqlite_values(insert_val, local_value); + let mut ret = crsql_compare_sqlite_values(insert_val, local_value); reset_cached_stmt(col_val_stmt.stmt)?; - // value won, take value - // if values are the same (ret == 0) then we return false and do not take the update + if ret == 0 && unsafe { (*ext_data).mergeEqualValues == 1 } { + // values are the same (ret == 0) and the option to tie break on site_id is true + ret = unsafe { + let my_site_id = core::slice::from_raw_parts((*ext_data).siteId, 16); + insert_site_id.cmp(my_site_id) as c_int + }; + } return Ok(ret > 0); } _ => { @@ -586,17 +593,19 @@ unsafe fn merge_insert( || !row_exists_locally || did_cid_win( db, + (*tab).pExtData, insert_tbl, &tbl_info, &unpacked_pks, key, insert_val, + insert_site_id, insert_col, insert_col_vrsn, errmsg, )?; - if does_cid_win == false { + if !does_cid_win { // doesCidWin == 0? compared against our clocks, nothing wins. OK and // Done. return Ok(ResultCode::OK); diff --git a/core/rs/core/src/config.rs b/core/rs/core/src/config.rs new file mode 100644 index 000000000..e53fc3336 --- /dev/null +++ b/core/rs/core/src/config.rs @@ -0,0 +1,85 @@ +use alloc::format; + +use sqlite::{Connection, Context}; +use sqlite_nostd as sqlite; +use sqlite_nostd::{ResultCode, Value}; + +use crate::c::crsql_ExtData; + +pub const MERGE_EQUAL_VALUES: &str = "merge-equal-values"; + +pub extern "C" fn crsql_config_set( + ctx: *mut sqlite::context, + argc: i32, + argv: *mut *mut sqlite::value, +) { + let args = sqlite::args!(argc, argv); + + let name = args[0].text(); + + let value = match name { + MERGE_EQUAL_VALUES => { + let value = args[1]; + let ext_data = ctx.user_data() as *mut crsql_ExtData; + unsafe { (*ext_data).mergeEqualValues = value.int() }; + value + } + _ => { + ctx.result_error("Unknown setting name"); + ctx.result_error_code(ResultCode::ERROR); + return; + } + }; + + let db = ctx.db_handle(); + match insert_config_setting(db, name, value) { + Ok(value) => { + ctx.result_value(value); + } + Err(rc) => { + ctx.result_error("Could not persist config in database"); + ctx.result_error_code(rc); + return; + } + } +} + +fn insert_config_setting( + db: *mut sqlite_nostd::sqlite3, + name: &str, + value: *mut sqlite::value, +) -> Result<*mut sqlite::value, ResultCode> { + let stmt = + db.prepare_v2("INSERT OR REPLACE INTO crsql_master VALUES (?, ?) RETURNING value")?; + + stmt.bind_text(1, &format!("config.{name}"), sqlite::Destructor::TRANSIENT)?; + stmt.bind_value(2, value)?; + + if let ResultCode::ROW = stmt.step()? { + stmt.column_value(0) + } else { + Err(ResultCode::ERROR) + } +} + +pub extern "C" fn crsql_config_get( + ctx: *mut sqlite::context, + argc: i32, + argv: *mut *mut sqlite::value, +) { + let args = sqlite::args!(argc, argv); + + let name = args[0].text(); + + match name { + MERGE_EQUAL_VALUES => { + let ext_data = ctx.user_data() as *mut crsql_ExtData; + ctx.result_int(unsafe { (*ext_data).mergeEqualValues }); + } + _ => { + ctx.result_error("Unknown setting name"); + ctx.result_error_code(ResultCode::ERROR); + return; + } + } +} diff --git a/core/rs/core/src/lib.rs b/core/rs/core/src/lib.rs index b7f699d51..17b1ec49d 100644 --- a/core/rs/core/src/lib.rs +++ b/core/rs/core/src/lib.rs @@ -18,6 +18,7 @@ mod changes_vtab; mod changes_vtab_read; mod changes_vtab_write; mod compare_values; +mod config; mod consts; mod create_cl_set_vtab; mod create_crr; @@ -53,6 +54,7 @@ use alter::crsql_compact_post_alter; use automigrate::*; use backfill::*; use c::{crsql_freeExtData, crsql_newExtData}; +use config::{crsql_config_get, crsql_config_set}; use core::ffi::{c_int, c_void, CStr}; use create_crr::create_crr; use db_version::{crsql_fill_db_version_if_needed, crsql_next_db_version}; @@ -212,7 +214,7 @@ pub extern "C" fn sqlite3_crsqlcore_init( let ext_data = unsafe { crsql_newExtData(db, site_id_buffer as *mut c_char) }; if ext_data.is_null() { - sqlite::free(site_id_buffer as *mut c_void); + // no need to free the site id buffer here, this is cleaned up already. return null_mut(); } @@ -471,6 +473,40 @@ pub extern "C" fn sqlite3_crsqlcore_init( return null_mut(); } + let rc = db + .create_function_v2( + "crsql_config_set", + 2, + sqlite::UTF8, + Some(ext_data as *mut c_void), + Some(crsql_config_set), + None, + None, + None, + ) + .unwrap_or(sqlite::ResultCode::ERROR); + if rc != ResultCode::OK { + unsafe { crsql_freeExtData(ext_data) }; + return null_mut(); + } + + let rc = db + .create_function_v2( + "crsql_config_get", + 1, + sqlite::UTF8 | sqlite::INNOCUOUS | sqlite::DETERMINISTIC, + Some(ext_data as *mut c_void), + Some(crsql_config_get), + None, + None, + None, + ) + .unwrap_or(sqlite::ResultCode::ERROR); + if rc != ResultCode::OK { + unsafe { crsql_freeExtData(ext_data) }; + return null_mut(); + } + return ext_data as *mut c_void; } diff --git a/core/rs/core/src/sha.rs b/core/rs/core/src/sha.rs index df78688cf..496cb4ccf 100644 --- a/core/rs/core/src/sha.rs +++ b/core/rs/core/src/sha.rs @@ -1,2 +1,2 @@ // The sha of the commit that this version of crsqlite was built from. -pub const SHA: &'static str = ""; +pub const SHA: &'static str = "3a01980562615001d765eec61e3ed58147a93f93"; diff --git a/core/rs/fractindex-core/Cargo.lock b/core/rs/fractindex-core/Cargo.lock index cf2f953f0..0635768d0 100644 --- a/core/rs/fractindex-core/Cargo.lock +++ b/core/rs/fractindex-core/Cargo.lock @@ -10,9 +10,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bindgen" -version = "0.63.0" +version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ "bitflags", "cexpr", @@ -21,6 +21,7 @@ dependencies = [ "lazycell", "log", "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", @@ -32,9 +33,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "cexpr" @@ -154,9 +155,9 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", @@ -165,9 +166,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -190,20 +191,30 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -291,9 +302,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.107" +version = "2.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" dependencies = [ "proc-macro2", "quote", diff --git a/core/src/ext-data.c b/core/src/ext-data.c index 34c06b777..f8715f4f9 100644 --- a/core/src/ext-data.c +++ b/core/src/ext-data.c @@ -1,5 +1,8 @@ #include "ext-data.h" +#include +#include + #include "consts.h" void crsql_clear_stmt_cache(crsql_ExtData *pExtData); @@ -9,6 +12,8 @@ void crsql_drop_table_info_vec(crsql_ExtData *pExtData); crsql_ExtData *crsql_newExtData(sqlite3 *db, unsigned char *siteIdBuffer) { crsql_ExtData *pExtData = sqlite3_malloc(sizeof *pExtData); + pExtData->siteId = siteIdBuffer; + pExtData->pPragmaSchemaVersionStmt = 0; int rc = sqlite3_prepare_v3(db, "PRAGMA schema_version", -1, SQLITE_PREPARE_PERSISTENT, @@ -45,13 +50,47 @@ crsql_ExtData *crsql_newExtData(sqlite3 *db, unsigned char *siteIdBuffer) { pExtData->pragmaSchemaVersion = -1; pExtData->pragmaDataVersion = -1; pExtData->pragmaSchemaVersionForTableInfos = -1; - pExtData->siteId = siteIdBuffer; pExtData->pDbVersionStmt = 0; pExtData->tableInfos = 0; pExtData->rowsImpacted = 0; pExtData->updatedTableInfosThisTx = 0; crsql_init_table_info_vec(pExtData); + sqlite3_stmt *pStmt; + + rc += sqlite3_prepare_v2(db, + "SELECT ltrim(key, 'config.'), value FROM " + "crsql_master WHERE key LIKE 'config.%';", + -1, &pStmt, 0); + + if (rc != SQLITE_OK) { + crsql_freeExtData(pExtData); + return 0; + } + + // set defaults! + pExtData->mergeEqualValues = 0; + + while (sqlite3_step(pStmt) == SQLITE_ROW) { + const unsigned char *name = sqlite3_column_text(pStmt, 0); + int colType = sqlite3_column_type(pStmt, 1); + + if (strcmp("merge-equal-values", (char *)name) == 0) { + if (colType == SQLITE_INTEGER) { + const int value = sqlite3_column_int(pStmt, 1); + pExtData->mergeEqualValues = value; + } else { + // broken setting... + crsql_freeExtData(pExtData); + return 0; + } + } else { + // unhandled config setting + } + } + + sqlite3_finalize(pStmt); + int pv = crsql_fetchPragmaDataVersion(db, pExtData); if (pv == -1 || rc != SQLITE_OK) { crsql_freeExtData(pExtData); diff --git a/core/src/ext-data.h b/core/src/ext-data.h index a24f61836..c4ff801d0 100644 --- a/core/src/ext-data.h +++ b/core/src/ext-data.h @@ -42,6 +42,8 @@ struct crsql_ExtData { sqlite3_stmt *pSetSiteIdOrdinalStmt; sqlite3_stmt *pSelectSiteIdOrdinalStmt; sqlite3_stmt *pSelectClockTablesStmt; + + int mergeEqualValues; }; crsql_ExtData *crsql_newExtData(sqlite3 *db, unsigned char *siteIdBuffer); diff --git a/py/correctness/src/crsql_correctness.egg-info/SOURCES.txt b/py/correctness/src/crsql_correctness.egg-info/SOURCES.txt index 42eedac20..b1ec0f626 100644 --- a/py/correctness/src/crsql_correctness.egg-info/SOURCES.txt +++ b/py/correctness/src/crsql_correctness.egg-info/SOURCES.txt @@ -7,9 +7,11 @@ src/crsql_correctness.egg-info/top_level.txt tests/test_as_ordered.py tests/test_cl_merging.py tests/test_cl_triggers.py +tests/test_commit_alter_perf.py tests/test_crsql_changes_filters.py tests/test_dbversion.py tests/test_insert_new_rows.py +tests/test_lookaside_key_creation.py tests/test_prior_versions.py tests/test_sandbox.py tests/test_schema_modification.py @@ -18,5 +20,6 @@ tests/test_seq.py tests/test_site_id_lookaside.py tests/test_siteid.py tests/test_sync.py +tests/test_sync_bit.py tests/test_sync_prop.py tests/test_update_rows.py \ No newline at end of file diff --git a/py/correctness/tests/test_config.py b/py/correctness/tests/test_config.py new file mode 100644 index 000000000..3291000d2 --- /dev/null +++ b/py/correctness/tests/test_config.py @@ -0,0 +1,25 @@ +from crsql_correctness import connect, close +import pathlib + +def test_config_merge_equal_values(): + dbfile = "./config.db" + pathlib.Path(dbfile).unlink(missing_ok=True) + db = connect(dbfile) + value = db.execute("SELECT crsql_config_set('merge-equal-values', 1);").fetchone() + assert (value == (1,)) + db.commit() + + value = db.execute("SELECT value FROM crsql_master WHERE key = 'config.merge-equal-values'").fetchone() + assert (value == (1,)) + + value = db.execute("SELECT crsql_config_get('merge-equal-values');").fetchone() + assert (value == (1,)) + + close(db) + db = connect(dbfile) + + value = db.execute("SELECT value FROM crsql_master WHERE key = 'config.merge-equal-values'").fetchone() + assert (value == (1,)) + + value = db.execute("SELECT crsql_config_get('merge-equal-values')").fetchone() + assert (value == (1,)) diff --git a/py/correctness/tests/test_sync.py b/py/correctness/tests/test_sync.py index c7f801ff4..a1d74d916 100644 --- a/py/correctness/tests/test_sync.py +++ b/py/correctness/tests/test_sync.py @@ -380,6 +380,26 @@ def make_dbs(): site_id = get_site_id(db2) assert (changes == [('foo', b'\x01\t\x01', 'b', 2, 1, 1, site_id, 1, 0)]) +def test_merge_same_w_tie_breaker(): + db1 = create_basic_db() + db2 = create_basic_db() + + db1.execute("INSERT INTO foo (a,b) VALUES (1,2);") + db1.execute("SELECT crsql_config_set('merge-equal-values', 1);") + db1.commit() + + db2.execute("INSERT INTO foo (a,b) VALUES (1,2);") + db2.execute("SELECT crsql_config_set('merge-equal-values', 1);") + db2.commit() + + sync_left_to_right(db1, db2, 0) + changes12 = db2.execute("SELECT \"table\", pk, cid, val, col_version, site_id FROM crsql_changes").fetchall() + + sync_left_to_right(db2, db1, 0) + changes21 = db1.execute("SELECT \"table\", pk, cid, val, col_version, site_id FROM crsql_changes").fetchall() + + assert (changes12 == changes21) + def test_merge_matching_clocks_lesser_value(): def make_dbs():