diff --git a/src/query.zig b/src/query.zig index 646d8b6..23d9a79 100644 --- a/src/query.zig +++ b/src/query.zig @@ -2701,3 +2701,158 @@ test "updateWhere keeps a composite index in sync across leading and trailing co try std.testing.expectEqualStrings(exp.name, rows[0].get("name").?.text); } + const everyone = try table.select(null); + defer freeRows(allocator, everyone); + try std.testing.expectEqual(@as(usize, 4), everyone.len); +} + +test "deleteWhere removes every matching row and reports the count" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "users"); + defer table.deinit(); + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + try table.put(.{ .int = 3 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }}); + + const pred = Predicate{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }; + const deleted = try table.deleteWhere(pred); + try std.testing.expectEqual(@as(usize, 2), deleted); + + try std.testing.expectEqual(@as(?Row, null), try table.get(.{ .int = 1 })); + try std.testing.expectEqual(@as(?Row, null), try table.get(.{ .int = 2 })); + + var row3 = (try table.get(.{ .int = 3 })).?; + defer row3.deinit(); + try std.testing.expectEqualStrings("krakow", row3.get("city").?.text); +} + +test "deleteWhere with a null predicate empties the table" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "counters"); + defer table.deinit(); + try table.put(.{ .int = 1 }, &.{.{ .name = "n", .value = .{ .int = 5 } }}); + try table.put(.{ .int = 2 }, &.{.{ .name = "n", .value = .{ .int = 7 } }}); + + const deleted = try table.deleteWhere(null); + try std.testing.expectEqual(@as(usize, 2), deleted); + + const everyone = try table.select(null); + defer freeRows(allocator, everyone); + try std.testing.expectEqual(@as(usize, 0), everyone.len); +} + +test "deleteWhere with no matches deletes nothing and leaves the table untouched" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "users"); + defer table.deinit(); + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + + const deleted = try table.deleteWhere(.{ .eq = .{ .field = "city", .value = .{ .text = "gdansk" } } }); + try std.testing.expectEqual(@as(usize, 0), deleted); + + var row = (try table.get(.{ .int = 1 })).?; + defer row.deinit(); + try std.testing.expectEqualStrings("warsaw", row.get("city").?.text); +} + +test "deleteWhere removes stale secondary index entries so a fresh row can reuse the value" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "users"); + defer table.deinit(); + try table.createIndex("city"); + try table.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + + const deleted = try table.deleteWhere(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + try std.testing.expectEqual(@as(usize, 1), deleted); + + // The old index entry for pk 1 under "warsaw" must be gone, not just the + // row - otherwise a later put of a *different* pk under the same "warsaw" + // value would find a phantom sibling in the index. + const stale = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer freeRows(allocator, stale); + try std.testing.expectEqual(@as(usize, 0), stale.len); + + try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + const fresh = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer freeRows(allocator, fresh); + try std.testing.expectEqual(@as(usize, 1), fresh.len); + try std.testing.expectEqual(@as(i64, 2), fresh[0].get(pkField).?.int); + + const constTable: *const Table = &table; + var plan = try constTable.explain(allocator, .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer plan.deinit(allocator); + try std.testing.expectEqual(PlanKind.index_eq, plan.kind); +} + +test "deleteWhere removes stale composite index entries across matched equality prefix" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "users"); + defer table.deinit(); + try table.createCompositeIndex("city_age", &.{ "city", "age" }); + + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + try table.put(.{ .int = 2 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 31 } }, + }); + try table.put(.{ .int = 3 }, &.{ + .{ .name = "city", .value = .{ .text = "krakow" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + }); + + const preds = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }, + .{ .eq = .{ .field = "age", .value = .{ .int = 30 } } }, + }; + const deleted = try table.deleteWhere(.{ .and_ = &preds }); + try std.testing.expectEqual(@as(usize, 1), deleted); + + const gone = try table.select(.{ .and_ = &preds }); + defer freeRows(allocator, gone); + try std.testing.expectEqual(@as(usize, 0), gone.len); + + // The composite index must still correctly serve the two untouched rows, + // proving the deleted row's entry was surgically removed, not the whole + // (city="warsaw") or (age=30) bucket. + const survivors = [_]struct { pk: i64, city: []const u8, age: i64 }{ + .{ .pk = 2, .city = "warsaw", .age = 31 }, + .{ .pk = 3, .city = "krakow", .age = 30 }, + }; + for (survivors) |exp| { + const survivorPreds = [_]Predicate{ + .{ .eq = .{ .field = "city", .value = .{ .text = exp.city } } }, + .{ .eq = .{ .field = "age", .value = .{ .int = exp.age } } }, + }; + const rows = try table.select(.{ .and_ = &survivorPreds }); + defer freeRows(allocator, rows); + try std.testing.expectEqual(@as(usize, 1), rows.len); + try std.testing.expectEqual(exp.pk, rows[0].get(pkField).?.int); +