@@ -749,23 +749,21 @@ fn prepareCurrentTableQuery(cursor: *ChangesCursor, db: ?*vtab.sqlite3) c_int {
749749 return vtab .SQLITE_OK ;
750750}
751751
752- /// Check if the current row is a sentinel row (col_name = '-1')
753- /// Sentinel rows with EVEN col_version are tombstones (deleted rows) and MUST be included
754- /// for sync to work correctly. Only odd col_version sentinels (row-creation markers) are filtered.
755- /// Reference: CR-SQLite uses even col_version = deleted, odd = live
752+ /// Check if the current row is a sentinel row (col_name = '-1') that should be skipped.
753+ ///
754+ /// Sentinel rows (cid = '-1') are emitted ONLY for PK-only tables to track row existence.
755+ /// For tables with non-PK columns, no sentinels are emitted on INSERT (column changes suffice).
756+ ///
757+ /// Tombstone sentinels (even col_version) are always included - they represent deleted rows.
758+ /// Live sentinels (odd col_version) for PK-only tables are included - they're the only change.
759+ ///
760+ /// Since we now only emit sentinels for PK-only tables, we should never skip them.
761+ /// The only sentinels to skip are tombstone markers during resurrection (not applicable here).
756762fn isSentinelRow (stmt : ? * api.sqlite3_stmt ) bool {
757- const col_name_ptr = columnTextFromStmt (stmt , 1 );
758- if (col_name_ptr ) | cn | {
759- const col_name_slice = std .mem .span (cn );
760- if (std .mem .eql (u8 , col_name_slice , "-1" )) {
761- // This is a sentinel row - check col_version to determine if it's a tombstone
762- const col_version = columnInt64FromStmt (stmt , 2 );
763- // Even col_version = deleted (tombstone) -> include in crsql_changes
764- // Odd col_version = live (row-creation marker) -> exclude from crsql_changes
765- // We return true (skip) only for odd (live) sentinels
766- return @mod (col_version , 2 ) == 1 ;
767- }
768- }
763+ _ = stmt ;
764+ // Now that INSERT triggers only emit sentinels for PK-only tables,
765+ // all sentinels in the clock table should be included in crsql_changes.
766+ // Tombstones (even col_version) and live sentinels (odd col_version) are both valid changes.
769767 return false ;
770768}
771769
@@ -1096,26 +1094,32 @@ fn fetchColumnValue(db: ?*vtab.sqlite3, table_name: []const u8, col_name: []cons
10961094}
10971095
10981096/// Fetch the causal length from the sentinel row (col_name = '-1')
1097+ /// For tables without a sentinel (non-PK-only tables), returns 1 if the row has any clock entries
1098+ /// (meaning the row exists - odd cl = live).
10991099fn fetchCausalLength (db : ? * vtab.sqlite3 , table_name : []const u8 , pk : i64 ) i64 {
11001100 var sql_buf : [512 ]u8 = undefined ;
11011101 const sql = std .fmt .bufPrintZ (& sql_buf , "SELECT col_version FROM \" {s}__crsql_clock\" WHERE pk = ? AND col_name = '-1'" , .{table_name }) catch {
1102- return 0 ;
1102+ return 1 ; // Default to 1 (live) on error
11031103 };
11041104
11051105 var stmt : ? * api.sqlite3_stmt = null ;
11061106 if (prepareV2 (toApiDb (db ), sql , -1 , & stmt , null ) != vtab .SQLITE_OK ) {
1107- return 0 ;
1107+ return 1 ; // Default to 1 (live) on error
11081108 }
11091109 defer _ = finalizeStmt (stmt );
11101110
11111111 if (api .bind_int64 (stmt , 1 , pk ) != vtab .SQLITE_OK ) {
1112- return 0 ;
1112+ return 1 ; // Default to 1 (live) on error
11131113 }
11141114
11151115 if (stepStmt (stmt ) == vtab .SQLITE_ROW ) {
11161116 return columnInt64FromStmt (stmt , 0 );
11171117 }
1118- return 0 ;
1118+
1119+ // No sentinel row - for non-PK-only tables, this is normal.
1120+ // If we're querying cl for a row that has clock entries, the row must exist (cl = 1, odd = live).
1121+ // The caller is iterating over clock entries, so we know the row exists.
1122+ return 1 ;
11191123}
11201124
11211125/// xColumn - Return column value
@@ -1520,10 +1524,47 @@ fn changesUpdate(
15201524 // Handle sentinel operations (cid = "-1") for non-existent rows
15211525 const is_sentinel_for_new = std .mem .eql (u8 , cid_slice , "-1" );
15221526 if (is_sentinel_for_new ) {
1523- // Sentinel for non-existent row - this is a tombstone/delete marker
1524- // We need to create just the clock entry without a base table row
1525- // For now, skip - the row doesn't exist and we're being told to delete it
1526- log .debug ("changesUpdate: sentinel for non-existent row, skipping" , .{});
1527+ // Sentinel with even cl = tombstone for non-existent row (skip)
1528+ // Sentinel with odd cl = row creation marker (need to create row for PK-only tables)
1529+ const is_tombstone = @mod (cl , 2 ) == 0 ;
1530+ if (is_tombstone ) {
1531+ // Tombstone for non-existent row - nothing to delete
1532+ log .debug ("changesUpdate: tombstone sentinel for non-existent row, skipping" , .{});
1533+ pRowid .* = 0 ;
1534+ return vtab .SQLITE_OK ;
1535+ }
1536+ // Live sentinel (odd cl) - this is a PK-only table row creation
1537+ // We need to create the row with just the PK columns
1538+ log .debug ("changesUpdate: live sentinel for PK-only table, creating row" , .{});
1539+
1540+ // For PK-only tables, we insert a row with just the PK values (extracted from pk blob)
1541+ // The pk blob contains the packed PK column values
1542+ const base_rowid = merge_insert .insertPkOnlyRow (api_db , table_slice , pk_ptr , @intCast (pk_len )) catch {
1543+ log .debug ("changesUpdate: insertPkOnlyRow failed" , .{});
1544+ return vtab .SQLITE_ERROR ;
1545+ };
1546+
1547+ // Insert into __crsql_pks table and get the pk (auto-increment key)
1548+ const pks_pk = merge_insert .insertIntoPksTableAndGetPk (api_db , table_slice , base_rowid , pk_ptr , @intCast (pk_len )) catch {
1549+ log .debug ("changesUpdate: insertIntoPksTableAndGetPk for PK-only failed" , .{});
1550+ return vtab .SQLITE_ERROR ;
1551+ };
1552+
1553+ // Insert sentinel clock entry using pks_pk (NOT base_rowid)
1554+ const site_id_ptr_sentinel : ? [* ]const u8 = @ptrCast (site_id_blob );
1555+ if (merge_stmts ) | stmts | {
1556+ merge_insert .setWinnerClockCached (stmts , pks_pk , "-1" , col_version , db_version , site_id_ptr_sentinel , @intCast (site_id_len ), seq ) catch {
1557+ log .debug ("changesUpdate: setWinnerClockCached for PK-only sentinel failed" , .{});
1558+ return vtab .SQLITE_ERROR ;
1559+ };
1560+ } else {
1561+ merge_insert .setWinnerClock (api_db , table_slice , pks_pk , "-1" , col_version , db_version , site_id_ptr_sentinel , @intCast (site_id_len ), seq ) catch {
1562+ log .debug ("changesUpdate: setWinnerClock for PK-only sentinel failed" , .{});
1563+ return vtab .SQLITE_ERROR ;
1564+ };
1565+ }
1566+
1567+ rows_impacted .incrementRowsImpacted ();
15271568 pRowid .* = 0 ;
15281569 return vtab .SQLITE_OK ;
15291570 }
@@ -1534,47 +1575,41 @@ fn changesUpdate(
15341575 // Get the value from argv[5] (column 3: val)
15351576 const insert_value = toApiValue (argv [5 ]);
15361577
1537- // Step 1a: Insert into base table
1538- const new_pk = merge_insert .insertIntoBaseTable (api_db , table_slice , cid_slice , insert_value , pk_ptr , @intCast (pk_len )) catch {
1578+ // Step 1a: Insert into base table (returns the base table rowid)
1579+ const base_rowid = merge_insert .insertIntoBaseTable (api_db , table_slice , cid_slice , insert_value , pk_ptr , @intCast (pk_len )) catch {
15391580 log .debug ("changesUpdate: insertIntoBaseTable failed" , .{});
15401581 return vtab .SQLITE_ERROR ;
15411582 };
15421583
1543- // Step 1b: Insert into __crsql_pks table (use cached if available)
1544- if (merge_stmts ) | stmts | {
1545- merge_insert .insertIntoPksTableCached (stmts , new_pk , pk_ptr , @intCast (pk_len )) catch {
1546- log .debug ("changesUpdate: insertIntoPksTableCached failed" , .{});
1547- return vtab .SQLITE_ERROR ;
1548- };
1549- } else {
1550- merge_insert .insertIntoPksTable (api_db , table_slice , new_pk , pk_ptr , @intCast (pk_len )) catch {
1551- log .debug ("changesUpdate: insertIntoPksTable failed" , .{});
1552- return vtab .SQLITE_ERROR ;
1553- };
1554- }
1584+ // Step 1b: Insert into __crsql_pks table and get the pk (auto-increment key)
1585+ // The pk is what we use for clock table entries, NOT the base table rowid
1586+ const pks_pk = merge_insert .insertIntoPksTableAndGetPk (api_db , table_slice , base_rowid , pk_ptr , @intCast (pk_len )) catch {
1587+ log .debug ("changesUpdate: insertIntoPksTableAndGetPk failed" , .{});
1588+ return vtab .SQLITE_ERROR ;
1589+ };
15551590
1556- // Step 1c: Insert clock entry for the column (use cached if available )
1591+ // Step 1c: Insert clock entry for the column using pks_pk (NOT base_rowid )
15571592 const site_id_ptr_insert : ? [* ]const u8 = @ptrCast (site_id_blob );
15581593 if (merge_stmts ) | stmts | {
1559- merge_insert .setWinnerClockCached (stmts , new_pk , cid_slice , col_version , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
1594+ merge_insert .setWinnerClockCached (stmts , pks_pk , cid_slice , col_version , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
15601595 log .debug ("changesUpdate: setWinnerClockCached for new row failed" , .{});
15611596 return vtab .SQLITE_ERROR ;
15621597 };
15631598 } else {
1564- merge_insert .setWinnerClock (api_db , table_slice , new_pk , cid_slice , col_version , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
1599+ merge_insert .setWinnerClock (api_db , table_slice , pks_pk , cid_slice , col_version , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
15651600 log .debug ("changesUpdate: setWinnerClock for new row failed" , .{});
15661601 return vtab .SQLITE_ERROR ;
15671602 };
15681603 }
15691604
1570- // Step 1d: Insert sentinel clock entry with the incoming cl (use cached if available)
1605+ // Step 1d: Insert sentinel clock entry with the incoming cl using pks_pk
15711606 if (merge_stmts ) | stmts | {
1572- merge_insert .setWinnerClockCached (stmts , new_pk , "-1" , cl , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
1607+ merge_insert .setWinnerClockCached (stmts , pks_pk , "-1" , cl , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
15731608 log .debug ("changesUpdate: setWinnerClockCached for sentinel failed" , .{});
15741609 return vtab .SQLITE_ERROR ;
15751610 };
15761611 } else {
1577- merge_insert .setWinnerClock (api_db , table_slice , new_pk , "-1" , cl , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
1612+ merge_insert .setWinnerClock (api_db , table_slice , pks_pk , "-1" , cl , db_version , site_id_ptr_insert , @intCast (site_id_len ), seq ) catch {
15781613 log .debug ("changesUpdate: setWinnerClock for sentinel failed" , .{});
15791614 return vtab .SQLITE_ERROR ;
15801615 };
0 commit comments