Skip to content

Commit 260817c

Browse files
Marcus PousetteMarcus Pousette
authored andcommitted
fix(sqlite): persist local proofs atomically
1 parent fd77168 commit 260817c

3 files changed

Lines changed: 377 additions & 36 deletions

File tree

packages/treecrdt-sqlite-ext/src/extension/functions/local_ops.rs

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,15 @@ struct JsonLocalOpTransactionError {
5555
enum LocalOpMode {
5656
Immediate,
5757
Prepare,
58-
Commit(String),
58+
Commit {
59+
precondition: String,
60+
proof: LocalOpProof,
61+
},
62+
}
63+
64+
struct LocalOpProof {
65+
sig: Vec<u8>,
66+
proof_ref: Option<Vec<u8>>,
5967
}
6068

6169
enum LocalOpDispatchResult {
@@ -109,20 +117,42 @@ fn parse_local_op_mode(
109117
args: &[*mut sqlite3_value],
110118
base_argc: usize,
111119
) -> Result<LocalOpMode, ()> {
112-
if argc as usize == base_argc {
113-
return Ok(LocalOpMode::Immediate);
120+
match argc as usize {
121+
n if n == base_argc => return Ok(LocalOpMode::Immediate),
122+
n if n == base_argc + 1 => {
123+
return (read_text(args[base_argc]) == "prepare")
124+
.then_some(LocalOpMode::Prepare)
125+
.ok_or(());
126+
}
127+
n if n == base_argc + 3 => {}
128+
_ => return Err(()),
114129
}
115-
if argc as usize != base_argc + 1 {
130+
131+
let precondition = read_text(args[base_argc]);
132+
if !precondition.starts_with("v1:") {
116133
return Err(());
117134
}
118-
let phase = read_text(args[base_argc]);
119-
if phase == "prepare" {
120-
Ok(LocalOpMode::Prepare)
121-
} else if phase.starts_with("v1:") {
122-
Ok(LocalOpMode::Commit(phase))
123-
} else {
124-
Err(())
135+
let sig = read_required_blob(args[base_argc + 1])?;
136+
if sig.len() != 64 {
137+
return Err(());
125138
}
139+
let proof_ref = if unsafe { sqlite_value_type(args[base_argc + 2]) } == SQLITE_NULL as c_int {
140+
None
141+
} else {
142+
let value = read_required_blob(args[base_argc + 2])?;
143+
if value.len() != 16 {
144+
return Err(());
145+
}
146+
Some(value)
147+
};
148+
Ok(LocalOpMode::Commit {
149+
precondition,
150+
proof: LocalOpProof { sig, proof_ref },
151+
})
152+
}
153+
154+
fn valid_local_op_argc(argc: c_int, base_argc: usize) -> bool {
155+
matches!(argc as usize, n if n == base_argc || n == base_argc + 1 || n == base_argc + 3)
126156
}
127157

128158
fn sqlite_busy_or_locked(rc: c_int) -> bool {
@@ -285,6 +315,75 @@ fn local_precondition(
285315
Ok(Some(format!("v1:{}", hasher.finalize().to_hex())))
286316
}
287317

318+
fn persist_local_op_proof(
319+
db: *mut sqlite3,
320+
doc_id: &[u8],
321+
op: &Operation,
322+
proof: &LocalOpProof,
323+
) -> Result<(), c_int> {
324+
let sql = CString::new(
325+
"INSERT OR REPLACE INTO treecrdt_sync_op_auth \
326+
(doc_id, op_ref, sig, proof_ref, created_at_ms) \
327+
VALUES (?1, ?2, ?3, ?4, CAST(strftime('%s','now') AS INTEGER) * 1000)",
328+
)
329+
.expect("local op proof sql");
330+
let mut stmt: *mut sqlite3_stmt = null_mut();
331+
let rc = sqlite_prepare_v2(db, sql.as_ptr(), -1, &mut stmt, null_mut());
332+
if rc != SQLITE_OK as c_int {
333+
return Err(rc);
334+
}
335+
336+
let op_ref = derive_op_ref_v0(doc_id, op.meta.id.replica.as_bytes(), op.meta.id.counter);
337+
let mut bind_err = false;
338+
unsafe {
339+
bind_err |= sqlite_bind_text(
340+
stmt,
341+
1,
342+
doc_id.as_ptr() as *const c_char,
343+
doc_id.len() as c_int,
344+
None,
345+
) != SQLITE_OK as c_int;
346+
bind_err |= sqlite_bind_blob(
347+
stmt,
348+
2,
349+
op_ref.as_ptr() as *const c_void,
350+
op_ref.len() as c_int,
351+
None,
352+
) != SQLITE_OK as c_int;
353+
bind_err |= sqlite_bind_blob(
354+
stmt,
355+
3,
356+
proof.sig.as_ptr() as *const c_void,
357+
proof.sig.len() as c_int,
358+
None,
359+
) != SQLITE_OK as c_int;
360+
bind_err |= match proof.proof_ref.as_ref() {
361+
Some(value) => sqlite_bind_blob(
362+
stmt,
363+
4,
364+
value.as_ptr() as *const c_void,
365+
value.len() as c_int,
366+
None,
367+
),
368+
None => sqlite_bind_null(stmt, 4),
369+
} != SQLITE_OK as c_int;
370+
}
371+
if bind_err {
372+
unsafe { sqlite_finalize(stmt) };
373+
return Err(SQLITE_ERROR as c_int);
374+
}
375+
376+
let step_rc = unsafe { sqlite_step(stmt) };
377+
let finalize_rc = unsafe { sqlite_finalize(stmt) };
378+
if step_rc != SQLITE_DONE as c_int {
379+
return Err(step_rc);
380+
}
381+
if finalize_rc != SQLITE_OK as c_int {
382+
return Err(finalize_rc);
383+
}
384+
Ok(())
385+
}
386+
288387
fn begin_local_core_op(
289388
db: *mut sqlite3,
290389
doc_id: &[u8],
@@ -493,7 +592,10 @@ where
493592
};
494593
finish_local_core_op(session, op, plan).map(LocalOpDispatchResult::Committed)
495594
}
496-
LocalOpMode::Commit(expected_precondition) => {
595+
LocalOpMode::Commit {
596+
precondition: expected_precondition,
597+
proof,
598+
} => {
497599
let mut session = match begin_optimistic_local_core_op(db, &replica, savepoint_name) {
498600
Ok(v) => v,
499601
Err(OptimisticBeginError::Conflict) => return Ok(LocalOpDispatchResult::Conflict),
@@ -525,6 +627,9 @@ where
525627
Ok(v) => v,
526628
Err(err) => return Err(session.rollback(sqlite_err_from_core(err))),
527629
};
630+
if let Err(rc) = persist_local_op_proof(session.db, &session.doc_id, &op, &proof) {
631+
return Err(session.rollback(rc));
632+
}
528633
finish_local_core_op(session, op, plan).map(LocalOpDispatchResult::Committed)
529634
}
530635
}
@@ -556,10 +661,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_insert(
556661
return;
557662
}
558663

559-
if argc != 6 && argc != 7 {
664+
if !valid_local_op_argc(argc, 6) {
560665
sqlite_result_error(
561666
ctx,
562-
b"treecrdt_local_insert expects 6 args plus optional prepare/precondition\0".as_ptr()
667+
b"treecrdt_local_insert expects 6 args, 7 for prepare, or 9 for commit\0".as_ptr()
563668
as *const c_char,
564669
);
565670
return;
@@ -571,7 +676,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_insert(
571676
Err(_) => {
572677
sqlite_result_error(
573678
ctx,
574-
b"treecrdt_local_insert: invalid prepare/precondition argument\0".as_ptr()
679+
b"treecrdt_local_insert: invalid prepare or commit arguments\0".as_ptr()
575680
as *const c_char,
576681
);
577682
return;
@@ -655,10 +760,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_move(
655760
return;
656761
}
657762

658-
if argc != 5 && argc != 6 {
763+
if !valid_local_op_argc(argc, 5) {
659764
sqlite_result_error(
660765
ctx,
661-
b"treecrdt_local_move expects 5 args plus optional prepare/precondition\0".as_ptr()
766+
b"treecrdt_local_move expects 5 args, 6 for prepare, or 8 for commit\0".as_ptr()
662767
as *const c_char,
663768
);
664769
return;
@@ -670,7 +775,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_move(
670775
Err(_) => {
671776
sqlite_result_error(
672777
ctx,
673-
b"treecrdt_local_move: invalid prepare/precondition argument\0".as_ptr()
778+
b"treecrdt_local_move: invalid prepare or commit arguments\0".as_ptr()
674779
as *const c_char,
675780
);
676781
return;
@@ -752,10 +857,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_delete(
752857
return;
753858
}
754859

755-
if argc != 2 && argc != 3 {
860+
if !valid_local_op_argc(argc, 2) {
756861
sqlite_result_error(
757862
ctx,
758-
b"treecrdt_local_delete expects 2 args plus optional prepare/precondition\0".as_ptr()
863+
b"treecrdt_local_delete expects 2 args, 3 for prepare, or 5 for commit\0".as_ptr()
759864
as *const c_char,
760865
);
761866
return;
@@ -767,7 +872,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_delete(
767872
Err(_) => {
768873
sqlite_result_error(
769874
ctx,
770-
b"treecrdt_local_delete: invalid prepare/precondition argument\0".as_ptr()
875+
b"treecrdt_local_delete: invalid prepare or commit arguments\0".as_ptr()
771876
as *const c_char,
772877
);
773878
return;
@@ -818,10 +923,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_payload(
818923
return;
819924
}
820925

821-
if argc != 3 && argc != 4 {
926+
if !valid_local_op_argc(argc, 3) {
822927
sqlite_result_error(
823928
ctx,
824-
b"treecrdt_local_payload expects 3 args plus optional prepare/precondition\0".as_ptr()
929+
b"treecrdt_local_payload expects 3 args, 4 for prepare, or 6 for commit\0".as_ptr()
825930
as *const c_char,
826931
);
827932
return;
@@ -833,7 +938,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_payload(
833938
Err(_) => {
834939
sqlite_result_error(
835940
ctx,
836-
b"treecrdt_local_payload: invalid prepare/precondition argument\0".as_ptr()
941+
b"treecrdt_local_payload: invalid prepare or commit arguments\0".as_ptr()
837942
as *const c_char,
838943
);
839944
return;

packages/treecrdt-sqlite-ext/src/extension/functions/schema.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,16 @@ CREATE TABLE IF NOT EXISTS tree_payload (
306306
last_replica BLOB NOT NULL,
307307
last_counter INTEGER NOT NULL
308308
);
309+
"#;
310+
const OP_AUTH: &str = r#"
311+
CREATE TABLE IF NOT EXISTS treecrdt_sync_op_auth (
312+
doc_id TEXT NOT NULL,
313+
op_ref BLOB NOT NULL,
314+
sig BLOB NOT NULL,
315+
proof_ref BLOB,
316+
created_at_ms INTEGER NOT NULL,
317+
PRIMARY KEY (doc_id, op_ref)
318+
);
309319
"#;
310320

311321
let rc_meta = {
@@ -352,6 +362,13 @@ CREATE TABLE IF NOT EXISTS tree_payload (
352362
return Err(rc_tree_payload);
353363
}
354364

365+
let rc_op_auth = {
366+
let sql = CString::new(OP_AUTH).expect("op auth schema");
367+
sqlite_exec(db, sql.as_ptr(), None, null_mut(), null_mut())
368+
};
369+
if rc_op_auth != SQLITE_OK as c_int {
370+
return Err(rc_op_auth);
371+
}
355372
const INDEXES: &str = r#"
356373
CREATE INDEX IF NOT EXISTS idx_ops_lamport ON ops(lamport, replica, counter);
357374
CREATE INDEX IF NOT EXISTS idx_ops_op_ref ON ops(op_ref);

0 commit comments

Comments
 (0)