max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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 | } |