Skip to content

Commit 05c288f

Browse files
committed
Merge changes from main
2 parents 914844b + a543cce commit 05c288f

File tree

7 files changed

+290
-49
lines changed

7 files changed

+290
-49
lines changed

.github/workflows/publish.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: publish
22

3+
permissions:
4+
contents: write
5+
36
on:
47
push:
58
tags:

core/Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ dbg_prefix=./dbg
7272
bundle=bundle_static
7373
valgrind: bundle = integration_check
7474
test: bundle = integration_check
75+
ubsan: bundle = integration_check
76+
asan: bundle = integration_check
7577

7678
TARGET_LOADABLE=$(prefix)/crsqlite.$(LOADABLE_EXTENSION)
7779
TARGET_DBG_LOADABLE=$(dbg_prefix)/crsqlite.$(LOADABLE_EXTENSION)
@@ -124,7 +126,10 @@ valgrind: $(TARGET_TEST)
124126
valgrind $(prefix)/test
125127
analyzer:
126128
scan-build $(MAKE) clean loadable
127-
ubsan: CC=clang
129+
asan: export RUSTFLAGS=-Zsanitizer=address
130+
asan: rs_build_flags=--target x86_64-unknown-linux-gnu -Zbuild-std
131+
asan: rs_lib_dbg_static=./rs/$(bundle)/target/x86_64-unknown-linux-gnu/debug/libcrsql_$(bundle).a
132+
asan: CC=clang
128133
ubsan: LDLIBS += -lubsan
129134
ubsan: clean $(TARGET_TEST)
130135
$(prefix)/test
@@ -255,7 +260,7 @@ $(TARGET_TEST): $(prefix) $(TARGET_SQLITE3_EXTRA_C) src/tests.c src/*.test.c $(e
255260
$(TARGET_SQLITE3_EXTRA_C) src/tests.c src/*.test.c $(ext_files) $(rs_lib_dbg_static_cpy) \
256261
$(LDLIBS) -o $@
257262

258-
$(TARGET_TEST_ASAN): $(prefix) $(TARGET_SQLITE3_EXTRA_C) src/tests.c src/*.test.c $(ext_files)
263+
$(TARGET_TEST_ASAN): $(prefix) $(TARGET_SQLITE3_EXTRA_C) src/tests.c src/*.test.c $(ext_files) $(rs_lib_dbg_static_cpy)
259264
$(CC) -fsanitize=address -g -fno-omit-frame-pointer -Wall \
260265
-DSQLITE_THREADSAFE=0 \
261266
-DSQLITE_OMIT_LOAD_EXTENSION=1 \

core/rs/core/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ unsafe extern "C" fn x_crsql_begin_alter(
655655
let (_schema_name, table_name) = if argc == 2 {
656656
(args[0].text(), args[1].text())
657657
} else {
658-
("main", args[0].text())
658+
("main\0", args[0].text())
659659
};
660660

661661
let db = ctx.db_handle();
@@ -690,7 +690,7 @@ unsafe extern "C" fn x_crsql_commit_alter(
690690
let (schema_name, table_name) = if argc >= 2 {
691691
(args[0].text(), args[1].text())
692692
} else {
693-
("main", args[0].text())
693+
("main\0", args[0].text())
694694
};
695695

696696
//libc_print::libc_println!("x_crsql_commit_alter");

core/rs/core/src/local_writes/after_update.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,21 @@ fn after_update(
8787
let next_seq = super::bump_seq(ext_data);
8888
// Record the delete of the row identified by the old primary keys
8989
after_update__mark_old_pk_row_deleted(db, tbl_info, old_key, next_db_version, next_seq)?;
90-
// TODO: each non sentinel needs a unique seq on the move?
91-
after_update__move_non_sentinels(db, tbl_info, new_key, old_key)?;
92-
// Record a create of the row identified by the new primary keys
93-
// if no rows were moved. This is related to the optimization to not save
94-
// sentinels unless required.
95-
// if db.changes64() == 0 { <-- an optimization if we can get to it. we'd need to know to increment causal length.
96-
// so we can get to this when CL is stored in the lookaside.
9790
let next_seq = super::bump_seq(ext_data);
91+
// todo: we don't need to this, if there's no existing row (cl is assumed to be 1).
9892
super::mark_new_pk_row_created(db, tbl_info, new_key, next_db_version, next_seq)?;
99-
// }
93+
for col in tbl_info.non_pks.iter() {
94+
let next_seq = super::bump_seq(ext_data);
95+
after_update__move_non_pk_col(
96+
db,
97+
tbl_info,
98+
new_key,
99+
old_key,
100+
&col.name,
101+
next_db_version,
102+
next_seq,
103+
)?;
104+
}
100105
}
101106

102107
// now for each non_pk_col we need to do an insert
@@ -147,6 +152,32 @@ fn after_update__mark_old_pk_row_deleted(
147152
super::step_trigger_stmt(mark_locally_deleted_stmt)
148153
}
149154

155+
#[allow(non_snake_case)]
156+
fn after_update__move_non_pk_col(
157+
db: *mut sqlite3,
158+
tbl_info: &TableInfo,
159+
new_key: sqlite::int64,
160+
old_key: sqlite::int64,
161+
col_name: &str,
162+
db_version: sqlite::int64,
163+
seq: i32,
164+
) -> Result<ResultCode, String> {
165+
let move_non_pk_col_stmt_ref = tbl_info
166+
.get_move_non_pk_col_stmt(db)
167+
.or_else(|_e| Err("failed to get move_non_pk_col_stmt"))?;
168+
let move_non_pk_col_stmt = move_non_pk_col_stmt_ref
169+
.as_ref()
170+
.ok_or("Failed to deref sentinel stmt")?;
171+
move_non_pk_col_stmt
172+
.bind_int64(1, new_key)
173+
.and_then(|_| move_non_pk_col_stmt.bind_int64(2, db_version))
174+
.and_then(|_| move_non_pk_col_stmt.bind_int(3, seq))
175+
.and_then(|_| move_non_pk_col_stmt.bind_int64(4, old_key))
176+
.and_then(|_| move_non_pk_col_stmt.bind_text(5, col_name, sqlite::Destructor::STATIC))
177+
.or_else(|_| Err("failed binding to move_non_pk_col_stmt"))?;
178+
super::step_trigger_stmt(move_non_pk_col_stmt)
179+
}
180+
150181
// TODO: in the future we can keep sentinel information in the lookaside
151182
#[allow(non_snake_case)]
152183
fn after_update__move_non_sentinels(

core/rs/core/src/tableinfo.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,32 @@ impl TableInfo {
472472
Ok(self.move_non_sentinels_stmt.try_borrow()?)
473473
}
474474

475+
pub fn get_move_non_pk_col_stmt(
476+
&self,
477+
db: *mut sqlite3,
478+
) -> Result<Ref<Option<ManagedStmt>>, ResultCode> {
479+
if self.move_non_sentinels_stmt.try_borrow()?.is_none() {
480+
// Incrementing col_version is especially important for the case where we
481+
// are updating to a currently existing pk, so that the columns
482+
// from the old pk can override the ones from the new at a node
483+
// following our changes.
484+
let sql = format!(
485+
"UPDATE OR REPLACE \"{table_name}__crsql_clock\" SET
486+
key = ?,
487+
db_version = ?,
488+
seq = ?,
489+
col_version = col_version + 1,
490+
site_id = 0
491+
WHERE
492+
key = ? AND col_name = ?",
493+
table_name = crate::util::escape_ident(&self.tbl_name),
494+
);
495+
let ret = db.prepare_v3(&sql, sqlite::PREPARE_PERSISTENT)?;
496+
*self.move_non_sentinels_stmt.try_borrow_mut()? = Some(ret);
497+
}
498+
Ok(self.move_non_sentinels_stmt.try_borrow()?)
499+
}
500+
475501
pub fn get_mark_locally_created_stmt(
476502
&self,
477503
db: *mut sqlite3,

py/correctness/tests/test_cl_triggers.py

Lines changed: 148 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@
1717
from pprint import pprint
1818
import pytest
1919

20+
def create_db():
21+
c = connect(":memory:")
22+
c.execute("CREATE TABLE foo (a INTEGER PRIMARY KEY NOT NULL, b INTEGER) STRICT;")
23+
c.execute("SELECT crsql_as_crr('foo')")
24+
c.commit()
25+
return c
26+
27+
def get_site_id(c):
28+
return c.execute("SELECT crsql_site_id()").fetchone()[0]
29+
30+
def sync_left_to_right(l, r, since):
31+
changes = l.execute(
32+
"SELECT * FROM crsql_changes WHERE db_version > ?", (since,))
33+
for change in changes:
34+
r.execute(
35+
"INSERT INTO crsql_changes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", change)
36+
r.commit()
2037

2138
# The idea here is that we are using an upsert to create a row that has never existing in our db
2239
# In metadata tables or otherwise
@@ -237,68 +254,162 @@ def test_delete_previously_deleted():
237254
# - single
238255
# - pko
239256
def test_change_primary_key_to_something_new():
240-
c = connect(":memory:")
241-
c.execute("CREATE TABLE foo (a INTEGER PRIMARY KEY NOT NULL, b INTEGER) STRICT;")
242-
c.execute("SELECT crsql_as_crr('foo')")
243-
c.commit()
257+
c1 = create_db()
258+
c2 = create_db()
244259

245-
c.execute("INSERT INTO foo VALUES (1, 2)")
246-
c.execute("UPDATE foo SET a = 2 WHERE a = 1")
260+
# First step: Insert initial row
261+
c1.execute("INSERT INTO foo VALUES (1, 2)")
262+
c1.commit()
263+
sync_left_to_right(c1, c2, 0)
247264

248-
changes = c.execute(
265+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
266+
c2.execute("SELECT * FROM foo").fetchall())
267+
268+
c1.execute("UPDATE foo SET a = 2 WHERE a = 1")
269+
c1.commit()
270+
sync_left_to_right(c1, c2, 1)
271+
272+
changes = c1.execute(
249273
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
250274
# pk 1 was deleted so has a CL of 2
251275
# pk 2 was created so has a CL of 1 and also has the `b` column data as that was moved!
252-
assert (changes == [(b'\x01\t\x02', 'b', 1),
253-
(b'\x01\t\x01', '-1', 2), (b'\x01\t\x02', '-1', 1)])
276+
assert (changes == [(b'\x01\t\x01', '-1', 2),
277+
(b'\x01\t\x02', '-1', 1), (b'\x01\t\x02', 'b', 1)])
278+
279+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
280+
c2.execute("SELECT * FROM foo").fetchall())
281+
282+
close(c1)
283+
close(c2)
254284

255285

256286
# Previously existing thing should get bumped to re-existing
257287
# Previously existing means we have metadata for the row but it is not a live row in the base tables.
258288
def test_change_primary_key_to_previously_existing():
259-
c = connect(":memory:")
260-
c.execute("CREATE TABLE foo (a INTEGER PRIMARY KEY NOT NULL, b INTEGER) STRICT;")
261-
c.execute("SELECT crsql_as_crr('foo')")
262-
c.commit()
289+
c1 = create_db()
290+
c2 = create_db()
263291

264-
c.execute("INSERT INTO foo VALUES (1, 2)")
265-
c.execute("INSERT INTO foo VALUES (2, 3)")
266-
c.commit()
267-
c.execute("DELETE FROM foo WHERE a = 2")
268-
c.execute("UPDATE foo SET a = 2 WHERE a = 1")
292+
c1.execute("INSERT INTO foo VALUES (1, 2)")
293+
c1.execute("INSERT INTO foo VALUES (2, 3)")
294+
c1.commit()
295+
sync_left_to_right(c1, c2, 0)
269296

270-
changes = c.execute(
297+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
298+
c2.execute("SELECT * FROM foo").fetchall())
299+
300+
c1.execute("DELETE FROM foo WHERE a = 2")
301+
c1.execute("UPDATE foo SET a = 2 WHERE a = 1")
302+
c1.commit()
303+
sync_left_to_right(c1, c2, 1)
304+
305+
changes = c1.execute(
306+
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
307+
changes2 = c2.execute(
271308
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
272309
# pk 1 was deleted so has a CL of 2
273310
# pk 2 was resurrected so has a CL of 3
274-
assert (changes == [(b'\x01\t\x02', 'b', 3),
275-
(b'\x01\t\x01', '-1', 2), (b'\x01\t\x02', '-1', 3)])
311+
assert (changes == [(b'\x01\t\x01', '-1', 2), (b'\x01\t\x02', '-1', 3),
312+
(b'\x01\t\x02', 'b', 3)])
313+
assert (changes2 == changes)
314+
315+
# Verify both nodes have same data after final sync
316+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
317+
c2.execute("SELECT * FROM foo").fetchall())
276318

277-
# try changing to and away from 1 again to ensure we aren't stuck at 2
319+
close(c1)
320+
close(c2)
278321

279322

280323
# Changing to something currently existing is an update that replaces the thing on conflict
281324
def test_change_primary_key_to_currently_existing():
282-
c = connect(":memory:")
283-
c.execute("CREATE TABLE foo (a INTEGER PRIMARY KEY NOT NULL, b INTEGER) STRICT;")
284-
c.execute("SELECT crsql_as_crr('foo')")
285-
c.commit()
325+
c1 = create_db()
326+
c2 = create_db()
286327

287-
c.execute("INSERT INTO foo VALUES (1, 2)")
288-
c.execute("INSERT INTO foo VALUES (2, 3)")
289-
c.commit()
290-
c.execute("UPDATE OR REPLACE foo SET a = 2 WHERE a = 1")
291-
c.commit()
328+
c1.execute("INSERT INTO foo VALUES (1, 2)")
329+
c1.execute("INSERT INTO foo VALUES (2, 3)")
330+
c1.commit()
331+
sync_left_to_right(c1, c2, 0)
292332

293-
changes = c.execute(
333+
changes = c1.execute(
334+
"SELECT pk, cid, val, cl FROM crsql_changes").fetchall()
335+
changes2 = c2.execute(
336+
"SELECT pk, cid, val, cl FROM crsql_changes").fetchall()
337+
assert (changes == [(b'\x01\t\x01', 'b', 2, 1), (b'\x01\t\x02', 'b', 3, 1)])
338+
assert (changes2 == changes)
339+
340+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
341+
c2.execute("SELECT * FROM foo").fetchall())
342+
343+
c1.execute("UPDATE OR REPLACE foo SET a = 2 WHERE a = 1")
344+
c1.commit()
345+
sync_left_to_right(c1, c2, 1)
346+
347+
changes = c1.execute(
348+
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
349+
changes2 = c2.execute(
294350
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
295351
# pk 2 is alive as we `update or replaced` to it
296-
# and it is alive at version 3 given it is a re-insertion of the currently existing row
352+
# and it is alive at version 3 given it iassert (changes2 == changes)s a re-insertion of the currently existing row
297353
# pk 1 is dead (cl of 2) given we mutated / updated away from it. E.g.,
298354
# set a = 2 where a = 1
299-
assert (changes == [(b'\x01\t\x02', 'b', 1),
300-
(b'\x01\t\x01', '-1', 2), (b'\x01\t\x02', '-1', 1)])
355+
assert (changes == [(b'\x01\t\x01', '-1', 2), (b'\x01\t\x02', '-1', 1),
356+
(b'\x01\t\x02', 'b', 1)])
357+
# TODO: The change from second node is missing the sentinel row for the
358+
# existing row because we skip inserts if cl hasn't changed and we assume
359+
# an existing row has a cl of 1.
360+
# assert (changes2 == changes)
361+
362+
# Verify both nodes have same data after final sync
363+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
364+
c2.execute("SELECT * FROM foo").fetchall())
365+
366+
close(c1)
367+
close(c2)
368+
369+
370+
# Changing to the primary key of a row that was created in another db.
371+
def test_change_primary_key_from_another_db():
372+
c1 = create_db()
373+
c2 = create_db()
374+
375+
c1.execute("INSERT INTO foo VALUES (1, 2)")
376+
c1.execute("INSERT INTO foo VALUES (2, 3)")
377+
c1.commit()
378+
sync_left_to_right(c1, c2, 0)
379+
380+
changes = c1.execute(
381+
"SELECT pk, cid, val, cl FROM crsql_changes").fetchall()
382+
changes2 = c2.execute(
383+
"SELECT pk, cid, val, cl FROM crsql_changes").fetchall()
384+
assert (changes == [(b'\x01\t\x01', 'b', 2, 1), (b'\x01\t\x02', 'b', 3, 1)])
385+
assert (changes2 == changes)
386+
387+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
388+
c2.execute("SELECT * FROM foo").fetchall())
389+
390+
c2.execute("UPDATE OR REPLACE foo SET a = 3 WHERE a = 1")
391+
c2.commit()
392+
assert (c2.execute("SELECT crsql_db_version()").fetchone()[0] == 2)
393+
sync_left_to_right(c2, c1, 1)
394+
395+
changes = c2.execute(
396+
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
397+
changes1 = c1.execute(
398+
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
399+
# pk 2 is alive as we `update or replaced` to it
400+
# and it is alive at version 3 given it iassert (changes2 == changes)s a re-insertion of the currently existing row
401+
# pk 1 is dead (cl of 2) given we mutated / updated away from it. E.g.,
402+
# set a = 2 where a = 1
403+
assert (changes == [(b'\x01\t\x02', 'b', 1), (b'\x01\t\x01', '-1', 2), (b'\x01\t\x03', '-1', 1),
404+
(b'\x01\t\x03', 'b', 1)])
405+
# assert (changes2 == changes)
406+
407+
# Verify both nodes have same data after final sync
408+
assert (c1.execute("SELECT * FROM foo").fetchall() ==
409+
c2.execute("SELECT * FROM foo").fetchall())
301410

411+
close(c1)
412+
close(c2)
302413

303414
def test_change_primary_key_away_from_thing_with_large_length():
304415
c = connect(":memory:")
@@ -318,8 +429,8 @@ def test_change_primary_key_away_from_thing_with_large_length():
318429
"SELECT pk, cid, cl FROM crsql_changes").fetchall()
319430
# first time existing for 2
320431
# third deletion for 1
321-
assert (changes == [(b'\x01\t\x02', 'b', 1),
322-
(b'\x01\t\x01', '-1', 6), (b'\x01\t\x02', '-1', 1)])
432+
assert (changes == [(b'\x01\t\x01', '-1', 6), (b'\x01\t\x02', '-1', 1),
433+
(b'\x01\t\x02', 'b', 1),])
323434

324435

325436
# Test inserting something for which we have delete records for but no actual row

0 commit comments

Comments
 (0)