feat: add stageIndexRemove and openTemp to txn_table
This commit is contained in:
1 parent
f16fc2ab58
commit
ad38d08079
1 file changed
+160
@@ -310,3 +310,163 @@ pub const TableTxn = struct {
|
|||||||
try self.txn.put(key, pkBytes);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user