From ad38d0807906155b292bdd0c26c7ef3727c251fa Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 22:13:50 +0200 Subject: [PATCH] feat: add stageIndexRemove and openTemp to txn_table --- src/txn_table.zig | 160 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/txn_table.zig b/src/txn_table.zig index d85830f..f4f3524 100644 --- a/src/txn_table.zig +++ b/src/txn_table.zig @@ -310,3 +310,163 @@ pub const TableTxn = struct { try self.txn.put(key, pkBytes); } + fn stageIndexRemove(self: *TableTxn, field: []const u8, value: Value, pk: Value) !void { + const key = try idx.indexEntryKey(self.allocator, self.name, field, value, pk); + defer self.allocator.free(key); + try self.txn.delete(key); + } +}; + +fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db { + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + return Db.open(allocator, dbPath, .{}); +} + +test "TableTxn commits multiple rows atomically" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var txn = Transaction.begin(&db); + defer txn.deinit(); + + var t = try TableTxn.open(&txn, allocator, "accounts"); + defer t.deinit(); + + try t.put(.{ .int = 1 }, &.{.{ .name = "balance", .value = .{ .int = 100 } }}); + try t.put(.{ .int = 2 }, &.{.{ .name = "balance", .value = .{ .int = 50 } }}); + + // Nothing visible outside the transaction yet. + const probeKey = try idx.rowKey(allocator, "accounts", .{ .int = 1 }); + defer allocator.free(probeKey); + try std.testing.expect((try db.get(allocator, probeKey)) == null); + + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "accounts"); + defer plain.deinit(); + var row1 = (try plain.get(.{ .int = 1 })).?; + defer row1.deinit(); + var row2 = (try plain.get(.{ .int = 2 })).?; + defer row2.deinit(); + try std.testing.expectEqual(@as(i64, 100), row1.get("balance").?.int); + try std.testing.expectEqual(@as(i64, 50), row2.get("balance").?.int); +} + +test "TableTxn transfer is all-or-nothing on conflict" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var setup = try querymod.Table.open(&db, allocator, "accounts"); + defer setup.deinit(); + try setup.put(.{ .int = 1 }, &.{.{ .name = "balance", .value = .{ .int = 100 } }}); + try setup.put(.{ .int = 2 }, &.{.{ .name = "balance", .value = .{ .int = 50 } }}); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "accounts"); + defer t.deinit(); + + // Move 30 from account 1 to account 2, staged but not yet committed. + var from = (try t.get(.{ .int = 1 })).?; + defer from.deinit(); + var to = (try t.get(.{ .int = 2 })).?; + defer to.deinit(); + try t.put(.{ .int = 1 }, &.{.{ .name = "balance", .value = .{ .int = from.get("balance").?.int - 30 } }}); + try t.put(.{ .int = 2 }, &.{.{ .name = "balance", .value = .{ .int = to.get("balance").?.int + 30 } }}); + + // An external writer touches account 1 before the transfer commits. + { + var external = try querymod.Table.open(&db, allocator, "accounts"); + defer external.deinit(); + try external.put(.{ .int = 1 }, &.{.{ .name = "balance", .value = .{ .int = 999 } }}); + } + + try std.testing.expectError(error.Conflict, txn.commit()); + + // Neither leg of the transfer applied: account 2 must be untouched, + // and account 1 must show the external writer's value, not the stale + // transfer's. + var verify = try querymod.Table.open(&db, allocator, "accounts"); + defer verify.deinit(); + var row1 = (try verify.get(.{ .int = 1 })).?; + defer row1.deinit(); + var row2 = (try verify.get(.{ .int = 2 })).?; + defer row2.deinit(); + try std.testing.expectEqual(@as(i64, 999), row1.get("balance").?.int); + try std.testing.expectEqual(@as(i64, 50), row2.get("balance").?.int); +} + +test "TableTxn keeps a pre-existing index in sync across commit" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var setup = try querymod.Table.open(&db, allocator, "users"); + defer setup.deinit(); + try setup.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + try setup.createIndex("city"); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + try std.testing.expect(t.hasIndex("city")); + + try t.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }}); + try t.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + try txn.commit(); + + var plain = try querymod.Table.open(&db, allocator, "users"); + defer plain.deinit(); + const warsaw = try plain.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } }); + defer querymod.freeRows(allocator, warsaw); + try std.testing.expectEqual(@as(usize, 1), warsaw.len); + try std.testing.expectEqual(@as(i64, 2), warsaw[0].get(pkField).?.int); + + const krakow = try plain.select(.{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } }); + defer querymod.freeRows(allocator, krakow); + try std.testing.expectEqual(@as(usize, 1), krakow.len); + try std.testing.expectEqual(@as(i64, 1), krakow[0].get(pkField).?.int); +} + +test "TableTxn delete removes row and index entries only on commit" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + { + var setup = try querymod.Table.open(&db, allocator, "users"); + defer setup.deinit(); + try setup.createIndex("city"); + try setup.put(.{ .int = 1 }, &.{.{ .name = "city", .value = .{ .text = "warsaw" } }}); + } + + var txn = Transaction.begin(&db); + defer txn.deinit(); + var t = try TableTxn.open(&txn, allocator, "users"); + defer t.deinit(); + try t.delete(.{ .int = 1 }); + + // Not yet committed: still visible outside the transaction. + { + var outside = try querymod.Table.open(&db, allocator, "users"); + defer outside.deinit(); + var stillThere = (try outside.get(.{ .int = 1 })).?; + stillThere.deinit(); + } +