Skip to main content

max / synckit

Observe the apply outcome counters and the primary-key bindings ApplyOutcome::unapplied had no caller in the tests, so the derived total could have been any constant. Assert it over a batch that carries a non-zero applied count alongside three filtered, one rejected and two deferred rows, with each part asserted separately, plus the zero case. Cover pk_bindings from both sides: a composite delete keyed off the payload deletes exactly the named row while the wire row id names nothing, a null component of a composite key is rejected instead of binding the partial key and taking every row that shares it, a partial update with a null key is rejected because that path has no row-id fallback, and a payload key overrides the wire row id when the two disagree.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-23 22:15 UTC
Signed with PGP, not checked
Commit: 1296f2a41954b12a5d705cda800b13fbf7b80563
Parent: c9af4da
1 file changed, +158 insertions, -0 deletions
@@ -1250,4 +1250,162 @@
1250 1250 assert_eq!(o.applied, 3);
1251 1251 assert!(o.deferred.is_empty(), "a satisfied FK is not a violation");
1252 1252 }
1253 +
1254 + #[test]
1255 + fn unapplied_totals_the_three_not_applied_kinds() {
1256 + let mut conn = db();
1257 + let o = apply(
1258 + &mut conn,
1259 + &[
1260 + // applied: an ordinary parent row.
1261 + upsert("parent", "p1", json!({"id":"p1","name":"a"})),
1262 + // filtered x3: two excluded cfg keys and one delete on a table
1263 + // that ignores deletes.
1264 + upsert("cfg", "sync_a", json!({"key":"sync_a","value":"1"})),
1265 + upsert("cfg", "sync_b", json!({"key":"sync_b","value":"2"})),
1266 + delete("items", "i1", json!({"id":"i1"})),
1267 + // rejected x1: no payload, so the same bytes always fail.
1268 + ChangeEntry {
1269 + data: None,
1270 + ..upsert("parent", "p2", json!({"id":"p2"}))
1271 + },
1272 + // deferred x2: an unknown table and a missing FK parent.
1273 + upsert("nonexistent", "x", json!({"id":"x"})),
1274 + upsert(
1275 + "child",
1276 + "c1",
1277 + json!({"id":"c1","parent_id":"absent","note":"n"}),
1278 + ),
1279 + ],
1280 + );
1281 +
1282 + assert_eq!(o.applied, 1);
1283 + assert_eq!(o.filtered, 3);
1284 + assert_eq!(o.rejected.len(), 1);
1285 + assert_eq!(o.deferred.len(), 2);
1286 + // The derived total is the sum of exactly those three, and excludes
1287 + // `applied`. Each part is non-zero and distinct, so no constant and no
1288 + // pair of the three adds up to it.
1289 + assert_eq!(o.unapplied(), 6);
1290 + assert_eq!(
1291 + o.unapplied(),
1292 + o.filtered + o.rejected.len() + o.deferred.len()
1293 + );
1294 + }
1295 +
1296 + #[test]
1297 + fn unapplied_is_zero_on_a_clean_apply() {
1298 + let mut conn = db();
1299 + let o = apply(
1300 + &mut conn,
1301 + &[upsert("parent", "p1", json!({"id":"p1","name":"a"}))],
1302 + );
1303 + assert_eq!(o.applied, 1);
1304 + assert_eq!(o.unapplied(), 0, "an applied row is not an unapplied one");
1305 + }
1306 +
1307 + #[test]
1308 + fn a_composite_delete_keys_off_the_payload_not_the_row_id() {
1309 + let mut conn = db();
1310 + apply(
1311 + &mut conn,
1312 + &[
1313 + upsert("tagpair", "ignored", json!({"a":"x","b":"y"})),
1314 + upsert("tagpair", "ignored", json!({"a":"x","b":"z"})),
1315 + ],
1316 + );
1317 + // The wire row id names nothing this table can be keyed by; the payload
1318 + // carries the whole composite key, so exactly one row goes.
1319 + let o = apply(
1320 + &mut conn,
1321 + &[delete("tagpair", "not-a-key", json!({"a":"x","b":"y"}))],
1322 + );
1323 + assert_eq!(o.applied, 1);
1324 + assert!(o.rejected.is_empty());
1325 + let left: Vec<String> = conn
1326 + .prepare("SELECT b FROM tagpair WHERE a='x' ORDER BY b")
1327 + .unwrap()
1328 + .query_map([], |r| r.get(0))
1329 + .unwrap()
1330 + .map(std::result::Result::unwrap)
1331 + .collect();
1332 + assert_eq!(left, vec!["z".to_string()], "the wrong row was deleted");
1333 + }
1334 +
1335 + #[test]
1336 + fn a_null_component_of_a_composite_key_is_rejected_not_bound() {
1337 + let mut conn = db();
1338 + apply(
1339 + &mut conn,
1340 + &[
1341 + upsert("tagpair", "ignored", json!({"a":"x","b":"y"})),
1342 + upsert("tagpair", "ignored", json!({"a":"x","b":"z"})),
1343 + ],
1344 + );
1345 + // `b` is JSON null, so the key cannot be reconstructed. Binding the
1346 + // partial key would delete every row sharing `a`.
1347 + let o = apply(
1348 + &mut conn,
1349 + &[delete("tagpair", "x", json!({"a":"x","b":null}))],
1350 + );
1351 + assert_eq!(o.applied, 0);
1352 + assert_eq!(o.rejected.len(), 1);
1353 + assert_eq!(o.rejected[0].table, "tagpair");
1354 + assert!(o.deferred.is_empty());
1355 + let rows: i64 = conn
1356 + .query_row("SELECT COUNT(*) FROM tagpair", [], |r| r.get(0))
1357 + .unwrap();
1358 + assert_eq!(rows, 2, "a null key component must touch nothing");
1359 + }
1360 +
1361 + #[test]
1362 + fn a_partial_update_with_a_null_key_is_rejected() {
1363 + let mut conn = db();
1364 + conn.execute(
1365 + "INSERT INTO items (id, is_read, is_starred, title) VALUES ('i1', 0, 0, 't')",
1366 + [],
1367 + )
1368 + .unwrap();
1369 + // The partial-update path never falls back to the wire row id, so a null
1370 + // `id` in the payload has no key at all.
1371 + let o = apply(
1372 + &mut conn,
1373 + &[upsert("items", "i1", json!({"id":null,"is_read":1}))],
1374 + );
1375 + assert_eq!(o.applied, 0);
1376 + assert_eq!(o.rejected.len(), 1);
1377 + assert!(o.deferred.is_empty());
1378 + let is_read: i64 = conn
1379 + .query_row("SELECT is_read FROM items WHERE id='i1'", [], |r| r.get(0))
1380 + .unwrap();
1381 + assert_eq!(is_read, 0, "the keyless update must not have landed");
1382 + }
1383 +
1384 + #[test]
1385 + fn a_present_key_in_the_payload_beats_the_wire_row_id() {
1386 + let mut conn = db();
1387 + conn.execute(
1388 + "INSERT INTO items (id, is_read, is_starred, title) VALUES ('i1', 0, 0, 'one')",
1389 + [],
1390 + )
1391 + .unwrap();
1392 + conn.execute(
1393 + "INSERT INTO items (id, is_read, is_starred, title) VALUES ('i2', 0, 0, 'two')",
1394 + [],
1395 + )
1396 + .unwrap();
1397 + let o = apply(
1398 + &mut conn,
1399 + &[upsert("items", "i1", json!({"id":"i2","is_read":1}))],
1400 + );
1401 + assert_eq!(o.applied, 1);
1402 + let read: Vec<i64> = conn
1403 + .prepare("SELECT is_read FROM items ORDER BY id")
1404 + .unwrap()
1405 + .query_map([], |r| r.get(0))
1406 + .unwrap()
1407 + .map(std::result::Result::unwrap)
1408 + .collect();
1409 + assert_eq!(read, vec![0, 1], "the payload key names the row to update");
1410 + }
1253 1411 }