tiny.sql.store
Defined in tiny.sql.
API (7)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/sql/src/root.zig:19
zig
pub const store = @import("store.zig");Source: lib/sql/src/store.zig
zig
const std = @import("std");const simd = @import("simd");const trace = @import("trace.zig");const Bytes = simd.ScalableTag(u8);const Allocator = std.mem.Allocator;const StoreError = error{ InvalidRange, TransactionClosed, TransactionConflict,};pub const Error = Allocator.Error || StoreError;pub const Entry = struct { key: []const u8, value: []const u8, generation: u64,};const Version = struct { key: []u8, value: []u8, generation: u64, deleted: bool = false,};const Mutation = struct { key: []u8, value: []u8 = &.{}, deleted: bool = false, fn deinit(self: *Mutation, allocator: Allocator) void { freeOwned(allocator, self.key); if (!self.deleted) freeOwned(allocator, self.value); self.* = .{ .key = &.{}, .value = &.{}, .deleted = true }; }};pub const Store = struct { allocator: Allocator, versions: std.ArrayList(Version) = .empty, generation: u64 = 0, pub fn init(allocator: Allocator) Store { return .{ .allocator = allocator }; } pub fn deinit(self: *Store) void { for (self.versions.items) |*version| { freeOwned(self.allocator, version.key); if (!version.deleted) freeOwned(self.allocator, version.value); } self.versions.deinit(self.allocator); self.* = .{ .allocator = self.allocator }; } pub fn currentGeneration(self: *const Store) u64 { return self.generation; } pub fn beginRead(self: *const Store) Snapshot { return .{ .store = self, .generation = self.generation }; } pub fn beginWrite(self: *Store) Write { return .{ .store = self, .snapshot_generation = self.generation }; } pub fn get(self: *const Store, key: []const u8) ?[]const u8 { const phase = trace.scope("store.get"); defer phase.end(); return valueFromIndex(self, latestVisibleIndex(self, key, self.generation)); } pub fn getAt(self: *const Store, key: []const u8, generation: u64) ?[]const u8 { return valueFromIndex(self, latestVisibleIndex(self, key, generation)); } pub fn range(self: *const Store, start: ?[]const u8, end: ?[]const u8) Error!Range { return rangeAt(self, start, end, self.generation); } pub fn rangeAt(self: *const Store, start: ?[]const u8, end: ?[]const u8, generation: u64) Error!Range { if (start) |lower| { if (end) |upper| { if (simd.order(Bytes, lower, upper) == .gt) return error.InvalidRange; } } return .{ .store = self, .start = start, .end = end, .generation = generation, .index = if (start) |lower| lowerBoundKey(self.versions.items, lower) else 0, }; } pub fn compact(self: *Store, oldest_visible_generation: u64) void { const phase = trace.scope("store.compact"); defer phase.end(); var read_index: usize = 0; var write_index: usize = 0; while (read_index < self.versions.items.len) { const key = self.versions.items[read_index].key; var group_end = read_index; var has_oldest_version = false; while (group_end < self.versions.items.len and eqlKey(self.versions.items[group_end].key, key)) : (group_end += 1) { const version = self.versions.items[group_end]; if (version.generation == oldest_visible_generation) has_oldest_version = true; } var keep_floor: ?usize = null; var scan = read_index; while (scan < group_end) : (scan += 1) { const version = self.versions.items[scan]; if (version.generation < oldest_visible_generation) { if (keep_floor == null or self.versions.items[keep_floor.?].generation < version.generation) { keep_floor = scan; } } } scan = read_index; while (scan < group_end) : (scan += 1) { const keep = (!has_oldest_version and keep_floor != null and scan == keep_floor.?) or self.versions.items[scan].generation >= oldest_visible_generation; if (keep) { self.versions.items[write_index] = self.versions.items[scan]; write_index += 1; } else { freeOwned(self.allocator, self.versions.items[scan].key); if (!self.versions.items[scan].deleted) freeOwned(self.allocator, self.versions.items[scan].value); } } read_index = group_end; } self.versions.shrinkRetainingCapacity(write_index); }};pub const Snapshot = struct { store: *const Store, generation: u64, pub fn get(self: Snapshot, key: []const u8) ?[]const u8 { const phase = trace.scope("snapshot.get"); defer phase.end(); return self.store.getAt(key, self.generation); } pub fn range(self: Snapshot, start: ?[]const u8, end: ?[]const u8) Error!Range { return self.store.rangeAt(start, end, self.generation); }};pub const Write = struct { store: *Store, snapshot_generation: u64, mutations: std.ArrayList(Mutation) = .empty, closed: bool = false, pub fn deinit(self: *Write) void { for (self.mutations.items) |*mutation| mutation.deinit(self.store.allocator); self.mutations.deinit(self.store.allocator); self.closed = true; } pub fn get(self: *const Write, key: []const u8) Error!?[]const u8 { if (self.closed) return error.TransactionClosed; if (self.localMutation(key)) |mutation| { if (mutation.deleted) return null; return mutation.value; } return self.store.getAt(key, self.snapshot_generation); } pub fn put(self: *Write, key: []const u8, value: []const u8) Error!void { if (self.closed) return error.TransactionClosed; const phase = trace.scope("write.put"); defer phase.end(); try self.setMutation(key, value, false); } pub fn delete(self: *Write, key: []const u8) Error!void { if (self.closed) return error.TransactionClosed; const phase = trace.scope("write.delete"); defer phase.end(); try self.setMutation(key, &.{}, true); } pub fn commit(self: *Write) Error!u64 { if (self.closed) return error.TransactionClosed; const phase = trace.scope("write.commit"); defer phase.end(); if (self.mutations.items.len == 0) { self.closed = true; return self.store.generation; } for (self.mutations.items) |mutation| { if (latestGenerationForKey(self.store, mutation.key)) |generation| { if (generation > self.snapshot_generation) return error.TransactionConflict; } } const next_generation = self.store.generation + 1; try self.store.versions.ensureTotalCapacity(self.store.allocator, self.store.versions.items.len + self.mutations.items.len); for (self.mutations.items) |mutation| { const insert_index = lowerBoundVersion(self.store.versions.items, mutation.key, next_generation); self.store.versions.insertAssumeCapacity(insert_index, .{ .key = mutation.key, .value = mutation.value, .generation = next_generation, .deleted = mutation.deleted, }); } std.debug.assert(versionsAreOrdered(self.store.versions.items)); self.store.generation = next_generation; self.mutations.clearRetainingCapacity(); self.closed = true; trace.progress("write.commit.complete"); return next_generation; } fn setMutation(self: *Write, key: []const u8, value: []const u8, deleted: bool) Error!void { if (self.localMutationIndex(key)) |index| { var mutation = &self.mutations.items[index]; if (!mutation.deleted) freeOwned(self.store.allocator, mutation.value); mutation.value = &.{}; if (!deleted) mutation.value = try self.store.allocator.dupe(u8, value); mutation.deleted = deleted; return; } const owned_key = try self.store.allocator.dupe(u8, key); errdefer freeOwned(self.store.allocator, owned_key); var owned_value: []u8 = &.{}; if (!deleted) owned_value = try self.store.allocator.dupe(u8, value); errdefer if (!deleted) freeOwned(self.store.allocator, owned_value); try self.mutations.append(self.store.allocator, .{ .key = owned_key, .value = owned_value, .deleted = deleted, }); std.mem.sort(Mutation, self.mutations.items, {}, mutationLessThan); } fn localMutation(self: *const Write, key: []const u8) ?Mutation { if (self.localMutationIndex(key)) |index| return self.mutations.items[index]; return null; } fn localMutationIndex(self: *const Write, key: []const u8) ?usize { var low: usize = 0; var high = self.mutations.items.len; while (low < high) { const mid = low + (high - low) / 2; switch (simd.order(Bytes, self.mutations.items[mid].key, key)) { .lt => low = mid + 1, .eq => return mid, .gt => high = mid, } } return null; }};pub const Range = struct { store: *const Store, start: ?[]const u8, end: ?[]const u8, generation: u64, index: usize, pub fn next(self: *Range) ?Entry { const phase = trace.scope("range.next"); defer phase.end(); while (self.index < self.store.versions.items.len) { const key = self.store.versions.items[self.index].key; if (self.start) |lower| { if (simd.order(Bytes, key, lower) == .lt) { self.skipKey(key); continue; } } if (self.end) |upper| { if (simd.order(Bytes, key, upper) != .lt) return null; } var best_index: ?usize = null; while (self.index < self.store.versions.items.len and eqlKey(self.store.versions.items[self.index].key, key)) : (self.index += 1) { const candidate = self.store.versions.items[self.index]; if (candidate.generation <= self.generation) { if (best_index == null or self.store.versions.items[best_index.?].generation < candidate.generation) { best_index = self.index; } } } if (best_index) |index| { const version = self.store.versions.items[index]; if (!version.deleted) { return .{ .key = version.key, .value = version.value, .generation = version.generation, }; } } } return null; } fn skipKey(self: *Range, key: []const u8) void { while (self.index < self.store.versions.items.len and eqlKey(self.store.versions.items[self.index].key, key)) { self.index += 1; } }};fn latestVisibleIndex(store: *const Store, key: []const u8, generation: u64) ?usize { const start = lowerBoundKey(store.versions.items, key); var index = start; var best: ?usize = null; while (index < store.versions.items.len and eqlKey(store.versions.items[index].key, key)) : (index += 1) { const version = store.versions.items[index]; if (version.generation <= generation) { if (best == null or store.versions.items[best.?].generation < version.generation) { best = index; } } } return best;}fn valueFromIndex(store: *const Store, index: ?usize) ?[]const u8 { const found = index orelse return null; const version = store.versions.items[found]; if (version.deleted) return null; return version.value;}fn latestGenerationForKey(self: *const Store, key: []const u8) ?u64 { const start = lowerBoundKey(self.versions.items, key); var index = start; var latest: ?u64 = null; while (index < self.versions.items.len and eqlKey(self.versions.items[index].key, key)) : (index += 1) { latest = self.versions.items[index].generation; } return latest;}fn lowerBoundKey(versions: []const Version, key: []const u8) usize { var low: usize = 0; var high = versions.len; while (low < high) { const mid = low + (high - low) / 2; switch (simd.order(Bytes, versions[mid].key, key)) { .lt => low = mid + 1, .eq, .gt => high = mid, } } return low;}fn lowerBoundVersion(versions: []const Version, key: []const u8, generation: u64) usize { var low: usize = 0; var high = versions.len; while (low < high) { const mid = low + (high - low) / 2; if (versionPrecedesKeyGeneration(versions[mid], key, generation)) { low = mid + 1; } else { high = mid; } } return low;}fn versionLessThan(_: void, left: Version, right: Version) bool { return versionPrecedesKeyGeneration(left, right.key, right.generation);}fn versionPrecedesKeyGeneration(version: Version, key: []const u8, generation: u64) bool { return switch (simd.order(Bytes, version.key, key)) { .lt => true, .gt => false, .eq => version.generation < generation, };}fn versionsAreOrdered(versions: []const Version) bool { if (versions.len < 2) return true; var index: usize = 1; while (index < versions.len) : (index += 1) { if (!versionLessThan({}, versions[index - 1], versions[index])) return false; } return true;}fn versionCount(store: *const Store, key: []const u8) usize { const start = lowerBoundKey(store.versions.items, key); var index = start; var count: usize = 0; while (index < store.versions.items.len and eqlKey(store.versions.items[index].key, key)) : (index += 1) { count += 1; } return count;}fn hasVersion(store: *const Store, key: []const u8, generation: u64) bool { const start = lowerBoundKey(store.versions.items, key); var index = start; while (index < store.versions.items.len and eqlKey(store.versions.items[index].key, key)) : (index += 1) { if (store.versions.items[index].generation == generation) return true; } return false;}fn mutationLessThan(_: void, left: Mutation, right: Mutation) bool { return switch (simd.order(Bytes, left.key, right.key)) { .lt => true, .gt => false, .eq => false, };}fn eqlKey(left: []const u8, right: []const u8) bool { return std.mem.eql(u8, left, right);}fn freeOwned(allocator: Allocator, bytes: []u8) void { if (bytes.len != 0) allocator.free(bytes);}fn putCommitted(store: *Store, key: []const u8, value: []const u8) !u64 { var tx = store.beginWrite(); defer tx.deinit(); try tx.put(key, value); return tx.commit();}fn deleteCommitted(store: *Store, key: []const u8) !u64 { var tx = store.beginWrite(); defer tx.deinit(); try tx.delete(key); return tx.commit();}test "committed writes are visible in key order" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "b", "two"); _ = try putCommitted(&store, "a", "one"); try std.testing.expectEqualStrings("one", store.get("a").?); try std.testing.expectEqualStrings("two", store.get("b").?); var range = try store.range(null, null); const first = range.next().?; const second = range.next().?; try std.testing.expectEqualStrings("a", first.key); try std.testing.expectEqualStrings("b", second.key); try std.testing.expect(range.next() == null);}test "snapshots preserve old values across later commits" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "name", "first"); const snapshot = store.beginRead(); _ = try putCommitted(&store, "name", "second"); try std.testing.expectEqualStrings("first", snapshot.get("name").?); try std.testing.expectEqualStrings("second", store.get("name").?);}test "delete creates a tombstone for new readers" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "k", "v"); const snapshot = store.beginRead(); _ = try deleteCommitted(&store, "k"); try std.testing.expectEqualStrings("v", snapshot.get("k").?); try std.testing.expect(store.get("k") == null);}test "write transactions detect same key conflicts" { var store = Store.init(std.testing.allocator); defer store.deinit(); var left = store.beginWrite(); defer left.deinit(); var right = store.beginWrite(); defer right.deinit(); try left.put("k", "left"); _ = try left.commit(); try right.put("k", "right"); try std.testing.expectError(error.TransactionConflict, right.commit());}test "write transactions allow disjoint commits from the same snapshot" { var store = Store.init(std.testing.allocator); defer store.deinit(); var left = store.beginWrite(); defer left.deinit(); var right = store.beginWrite(); defer right.deinit(); try left.put("a", "left"); try right.put("b", "right"); _ = try left.commit(); _ = try right.commit(); try std.testing.expectEqualStrings("left", store.get("a").?); try std.testing.expectEqualStrings("right", store.get("b").?);}test "range scan honors half open bounds and tombstones" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "a", "1"); _ = try putCommitted(&store, "b", "2"); _ = try putCommitted(&store, "c", "3"); _ = try deleteCommitted(&store, "b"); var range = try store.range("a", "c"); const first = range.next().?; try std.testing.expectEqualStrings("a", first.key); try std.testing.expect(range.next() == null);}test "multi key commits preserve canonical version order" { var store = Store.init(std.testing.allocator); defer store.deinit(); var seed = store.beginWrite(); defer seed.deinit(); try seed.put("b", "old-b"); try seed.put("d", "old-d"); _ = try seed.commit(); var tx = store.beginWrite(); defer tx.deinit(); try tx.put("c", "new-c"); try tx.put("a", "new-a"); try tx.put("b", "new-b"); _ = try tx.commit(); try std.testing.expect(versionsAreOrdered(store.versions.items)); try std.testing.expectEqual(@as(usize, 5), store.versions.items.len); try std.testing.expectEqualStrings("a", store.versions.items[0].key); try std.testing.expectEqual(@as(u64, 2), store.versions.items[0].generation); try std.testing.expectEqualStrings("b", store.versions.items[1].key); try std.testing.expectEqual(@as(u64, 1), store.versions.items[1].generation); try std.testing.expectEqualStrings("b", store.versions.items[2].key); try std.testing.expectEqual(@as(u64, 2), store.versions.items[2].generation); try std.testing.expectEqualStrings("c", store.versions.items[3].key); try std.testing.expectEqual(@as(u64, 2), store.versions.items[3].generation); try std.testing.expectEqualStrings("d", store.versions.items[4].key); try std.testing.expectEqual(@as(u64, 1), store.versions.items[4].generation);}test "compaction drops obsolete floor when oldest generation is present" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "k", "v1"); const snapshot_generation = try putCommitted(&store, "k", "v2"); _ = try putCommitted(&store, "k", "v3"); store.compact(snapshot_generation); try std.testing.expect(versionsAreOrdered(store.versions.items)); try std.testing.expectEqual(@as(usize, 2), versionCount(&store, "k")); try std.testing.expect(!hasVersion(&store, "k", 1)); try std.testing.expectEqualStrings("v2", store.getAt("k", snapshot_generation).?); try std.testing.expectEqualStrings("v3", store.get("k").?);}test "compaction keeps floor when oldest generation falls between versions" { var store = Store.init(std.testing.allocator); defer store.deinit(); _ = try putCommitted(&store, "k", "v1"); _ = try putCommitted(&store, "other", "advance"); _ = try putCommitted(&store, "k", "v3"); store.compact(2); try std.testing.expect(versionsAreOrdered(store.versions.items)); try std.testing.expectEqual(@as(usize, 2), versionCount(&store, "k")); try std.testing.expect(hasVersion(&store, "k", 1)); try std.testing.expect(hasVersion(&store, "k", 3)); try std.testing.expectEqualStrings("v1", store.getAt("k", 2).?); try std.testing.expectEqualStrings("v3", store.get("k").?);}Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 5 |
| Version | 26.7.0 |
| Revision | daab053ee433 |