feat: add stageIndexRemove and openTemp to txn_table

This commit is contained in:
dualfroz committed 2026-09-12 22:13:50 +02:00
1 parent f16fc2ab58
commit ad38d08079
1 file changed
+160
+160
View File
@@ -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();
}