Skip to content

Commit

Permalink
move db_version methods to Rust
Browse files Browse the repository at this point in the history
  • Loading branch information
tantaman committed Sep 20, 2023
1 parent 28ece42 commit b6863bd
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 188 deletions.
16 changes: 7 additions & 9 deletions core/rs/core/src/alter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use core::ffi::{c_char, c_int, CStr};
use core::mem;
#[cfg(not(feature = "std"))]
use num_traits::FromPrimitive;
use sqlite_nostd::{sqlite3, Connection, ResultCode};
use sqlite_nostd::{sqlite3, Connection, ResultCode, StrRef};

use crate::c::{crsql_ExtData, crsql_getDbVersion};
use crate::c::crsql_ExtData;
use crate::db_version::fill_db_version_if_needed;
use crate::tableinfo::{crsql_ensure_table_infos_are_up_to_date, TableInfo};

#[no_mangle]
Expand All @@ -32,13 +33,10 @@ unsafe fn compact_post_alter(
errmsg: *mut *mut c_char,
) -> Result<ResultCode, ResultCode> {
let tbl_name_str = CStr::from_ptr(tbl_name).to_str()?;
let c_rc = crsql_getDbVersion(db, ext_data, errmsg);
if c_rc != ResultCode::OK as c_int {
if let Some(rc) = ResultCode::from_i32(c_rc) {
return Err(rc);
}
return Err(ResultCode::ERROR);
}
fill_db_version_if_needed(db, ext_data).or_else(|msg| {
errmsg.set(&msg);
Err(ResultCode::ERROR)
})?;
let current_db_version = (*ext_data).dbVersion;

// If primary key columns change (in the schema)
Expand Down
11 changes: 5 additions & 6 deletions core/rs/core/src/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use sqlite_nostd as sqlite;

pub static INSERT_SENTINEL: &str = "-1";
pub static DELETE_SENTINEL: &str = "-1";
// pub static DB_VERSION_SCHEMA_VERSION: c_int = 0;
pub static DB_VERSION_SCHEMA_VERSION: c_int = 0;
pub static TABLE_INFO_SCHEMA_VERSION: c_int = 1;

#[derive(FromPrimitive, PartialEq, Debug)]
Expand Down Expand Up @@ -92,16 +92,15 @@ pub struct crsql_Changes_cursor {
}

extern "C" {
pub fn crsql_getDbVersion(
db: *mut sqlite::sqlite3,
ext_data: *mut crsql_ExtData,
err_msg: *mut *mut c_char,
) -> c_int;
pub fn crsql_fetchPragmaSchemaVersion(
db: *mut sqlite::sqlite3,
pExtData: *mut crsql_ExtData,
which: c_int,
) -> c_int;
pub fn crsql_fetchPragmaDataVersion(
db: *mut sqlite::sqlite3,
pExtData: *mut crsql_ExtData,
) -> c_int;
pub fn crsql_newExtData(
db: *mut sqlite::sqlite3,
siteIdBuffer: *mut c_char,
Expand Down
3 changes: 3 additions & 0 deletions core/rs/core/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ pub const TBL_SCHEMA: &'static str = "crsql_master";
pub const CRSQLITE_VERSION: i32 = 15_00_00;
pub const SITE_ID_LEN: i32 = 16;
pub const ROWID_SLAB_SIZE: i64 = 10000000000000;
// db version is a signed 64bit int since sqlite doesn't support saving and
// retrieving unsigned 64bit ints. (2^64 / 2) is a big enough number to write 1
// million entries per second for 3,000 centuries.
pub const MIN_POSSIBLE_DB_VERSION: i64 = 0;
pub const MAX_TBL_NAME_LEN: i32 = 2048;
147 changes: 147 additions & 0 deletions core/rs/core/src/db_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use core::ptr;

use crate::alloc::string::ToString;
use alloc::format;
use alloc::string::String;
use core::ffi::{c_char, c_int};
use sqlite::ResultCode;
use sqlite::StrRef;
use sqlite::{sqlite3, Stmt};
use sqlite_nostd as sqlite;

use crate::c::crsql_ExtData;
use crate::c::crsql_fetchPragmaDataVersion;
use crate::c::crsql_fetchPragmaSchemaVersion;
use crate::c::DB_VERSION_SCHEMA_VERSION;
use crate::consts::MIN_POSSIBLE_DB_VERSION;
use crate::ext_data::recreate_db_version_stmt;

#[no_mangle]
pub extern "C" fn crsql_fill_db_version_if_needed(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
errmsg: *mut *mut c_char,
) -> c_int {
match fill_db_version_if_needed(db, ext_data) {
Ok(rc) => rc as c_int,
Err(msg) => {
errmsg.set(&msg);
ResultCode::ERROR as c_int
}
}
}

#[no_mangle]
pub extern "C" fn crsql_next_db_version(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
merging_version: sqlite::int64,
errmsg: *mut *mut c_char,
) -> sqlite::int64 {
match next_db_version(db, ext_data, Some(merging_version)) {
Ok(version) => version,
Err(msg) => {
errmsg.set(&msg);
-1
}
}
}

pub fn next_db_version(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
merging_version: Option<i64>,
) -> Result<i64, String> {
fill_db_version_if_needed(db, ext_data)?;

let mut ret = unsafe { (*ext_data).dbVersion + 1 };
if ret < unsafe { (*ext_data).pendingDbVersion } {
ret = unsafe { (*ext_data).pendingDbVersion };
}
if let Some(merging_version) = merging_version {
if ret < merging_version {
ret = merging_version;
}
}
unsafe {
(*ext_data).pendingDbVersion = ret;
}
Ok(ret)
}

pub fn fill_db_version_if_needed(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
) -> Result<ResultCode, String> {
unsafe {
let rc = crsql_fetchPragmaDataVersion(db, ext_data);
if rc == -1 {
return Err("failed to fetch PRAGMA data_version".to_string());
}
if (*ext_data).dbVersion != -1 && rc == 0 {
return Ok(ResultCode::OK);
}
fetch_db_version_from_storage(db, ext_data)
}
}

pub fn fetch_db_version_from_storage(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
) -> Result<ResultCode, String> {
unsafe {
let mut schema_changed = 0;
if (*ext_data).pDbVersionStmt == ptr::null_mut() {
schema_changed = 1;
} else {
schema_changed =
crsql_fetchPragmaSchemaVersion(db, ext_data, DB_VERSION_SCHEMA_VERSION);
}

if schema_changed < 0 {
return Err("failed to fetch the pragma schema version".to_string());
}

if schema_changed > 0 {
match recreate_db_version_stmt(db, ext_data) {
Ok(ResultCode::DONE) => {
// this means there are no clock tables / this is a clean db
(*ext_data).dbVersion = 0;
return Ok(ResultCode::OK);
}
Ok(_) => {}
Err(rc) => return Err(format!("failed to recreate db version stmt: {}", rc)),
}
}

let db_version_stmt = (*ext_data).pDbVersionStmt;
let rc = db_version_stmt.step();
match rc {
// no rows? We're a fresh db with the min starting version
Ok(ResultCode::DONE) => {
db_version_stmt.reset().or_else(|rc| {
Err(format!(
"failed to reset db version stmt after DONE: {}",
rc
))
})?;
(*ext_data).dbVersion = MIN_POSSIBLE_DB_VERSION;
Ok(ResultCode::OK)
}
// got a row? It is our db version.
Ok(ResultCode::ROW) => {
(*ext_data).dbVersion = db_version_stmt.column_int64(0);
db_version_stmt
.reset()
.or_else(|rc| Err(format!("failed to reset db version stmt after ROW: {}", rc)))
}
// Not row or done? Something went wrong.
Ok(rc) | Err(rc) => {
db_version_stmt.reset().or_else(|rc| {
Err(format!("failed to reset db version stmt after ROW: {}", rc))
})?;
Err(format!("failed to step db version stmt: {}", rc))
}
}
}
}
2 changes: 1 addition & 1 deletion core/rs/core/src/ext_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub extern "C" fn crsql_recreate_db_version_stmt(
}
}

fn recreate_db_version_stmt(
pub fn recreate_db_version_stmt(
db: *mut sqlite3,
ext_data: *mut crsql_ExtData,
) -> Result<ResultCode, ResultCode> {
Expand Down
4 changes: 4 additions & 0 deletions core/rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ mod compare_values;
mod consts;
mod create_cl_set_vtab;
mod create_crr;
#[cfg(feature = "test")]
pub mod db_version;
#[cfg(not(feature = "test"))]
mod db_version;
mod ext_data;
mod is_crr;
#[cfg(feature = "test")]
Expand Down
1 change: 1 addition & 0 deletions core/rs/core/src/test_exports.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub use crate::bootstrap;
pub use crate::c;
pub use crate::db_version;
pub use crate::pack_columns;
pub use crate::tableinfo;
2 changes: 2 additions & 0 deletions core/rs/integration_check/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub extern "C" fn crsql_integration_check() {
t::teardown::run_suite().expect("tear down suite");
println!("Running cl_set_vtab");
t::test_cl_set_vtab::run_suite().expect("test cl set vtab suite");
println!("Running db_version");
t::test_db_version::run_suite().expect("test db version suite");
}

pub fn opendb() -> Result<CRConnection, ResultCode> {
Expand Down
1 change: 1 addition & 0 deletions core/rs/integration_check/src/t/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pub mod sync_bit_honored;
pub mod tableinfo;
pub mod teardown;
pub mod test_cl_set_vtab;
pub mod test_db_version;
111 changes: 111 additions & 0 deletions core/rs/integration_check/src/t/test_db_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
extern crate alloc;
use alloc::{ffi::CString, string::String};
use core::ffi::c_char;
use crsql_bundle::test_exports;
use sqlite::{Connection, ResultCode};
use sqlite_nostd as sqlite;

fn make_site() -> *mut c_char {
let inner_ptr: *mut c_char = CString::new("0000000000000000").unwrap().into_raw();
inner_ptr
}

fn test_fetch_db_version_from_storage() -> Result<ResultCode, String> {
let c = crate::opendb().expect("db opened");
let db = &c.db;
let raw_db = db.db;
let ext_data = unsafe { test_exports::c::crsql_newExtData(raw_db, make_site()) };

test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
// no clock tables, no version.
assert_eq!(0, unsafe { (*ext_data).dbVersion });

// this was a bug where calling twice on a fresh db would fail the second
// time.
test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
// should still return same data on a subsequent call with no schema
assert_eq!(0, unsafe { (*ext_data).dbVersion });

// create some schemas
db.exec_safe("CREATE TABLE foo (a primary key not null, b);")
.expect("made foo");
db.exec_safe("SELECT crsql_as_crr('foo');")
.expect("made foo crr");
test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
// still v0 since no rows are inserted
assert_eq!(0, unsafe { (*ext_data).dbVersion });

// version is bumped due to insert
db.exec_safe("INSERT INTO foo (a, b) VALUES (1, 2);")
.expect("inserted");
test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
assert_eq!(1, unsafe { (*ext_data).dbVersion });

db.exec_safe("CREATE TABLE bar (a primary key not null, b);")
.expect("created bar");
db.exec_safe("SELECT crsql_as_crr('bar');")
.expect("bar as crr");
db.exec_safe("INSERT INTO bar VALUES (1, 2)")
.expect("inserted into bar");
test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
assert_eq!(2, unsafe { (*ext_data).dbVersion });

test_exports::db_version::fetch_db_version_from_storage(raw_db, ext_data)?;
assert_eq!(2, unsafe { (*ext_data).dbVersion });

unsafe {
test_exports::c::crsql_freeExtData(ext_data);
};

Ok(ResultCode::OK)
}

fn test_next_db_version() -> Result<(), String> {
let c = crate::opendb().expect("db opened");
let db = &c.db;
let raw_db = db.db;
let ext_data = unsafe { test_exports::c::crsql_newExtData(raw_db, make_site()) };

// is current + 1
// doesn't bump forward on successive calls
assert_eq!(
1,
test_exports::db_version::next_db_version(raw_db, ext_data, None)?
);
assert_eq!(
1,
test_exports::db_version::next_db_version(raw_db, ext_data, None)?
);
// doesn't roll back with new provideds
assert_eq!(
1,
test_exports::db_version::next_db_version(raw_db, ext_data, Some(-1))?
);
assert_eq!(
1,
test_exports::db_version::next_db_version(raw_db, ext_data, Some(0))?
);
// sets to max of current and provided
assert_eq!(
3,
test_exports::db_version::next_db_version(raw_db, ext_data, Some(3))?
);
assert_eq!(
3,
test_exports::db_version::next_db_version(raw_db, ext_data, Some(2))?
);

// existing db version not touched
assert_eq!(0, unsafe { (*ext_data).dbVersion });

unsafe {
test_exports::c::crsql_freeExtData(ext_data);
};
Ok(())
}

pub fn run_suite() -> Result<(), String> {
test_fetch_db_version_from_storage()?;
test_next_db_version()?;
Ok(())
}
Loading

0 comments on commit b6863bd

Please sign in to comment.