tiny.sql.history.resolver
Defined in history.
API (16)
Actions
Public operations.
Capacity.deriveResolver.read: The caller owns payload storage and keeps the returned slice alive.Scanner.initScanner.nextWorkspace.activate: Activation borrows all storage before any record is read.Workspace.eachWorkspace.scratch
Types and contracts
Public types and contracts.
CapacityErrorExhaustionLimitsResolverScannedScanner: A file-order scanner retains only one metadata record or stream chunk.StorageWorkspace
Source
Source: lib/sql/src/history/resolver.zig
zig
const std = @import("std");const sql = @import("../root.zig");const identity = @import("identity.zig");const record_mod = @import("record.zig");const version = sql.version;pub const Error = std.Io.File.ReadPositionalError || error{ CapacityExceeded, CapacityOverflow, InvalidHistory, WrongObject,};pub const Limits = struct { metadata_bytes_max: usize = 4 * 1024 * 1024, stream_bytes: usize = 64 * 1024, relation_entries_max: usize = 1024, conflict_entries_max: usize = 1024,};pub const Capacity = struct { bytes: usize, relation_entries: usize, conflict_entries: usize, pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { if (limits.stream_bytes < version.hash_bytes) return error.CapacityOverflow; if (limits.metadata_bytes_max < version.hash_bytes) return error.CapacityOverflow; if (limits.metadata_bytes_max > std.math.maxInt(u32)) { return error.CapacityOverflow; } const bytes = std.math.add( usize, limits.metadata_bytes_max, limits.stream_bytes, ) catch return error.CapacityOverflow; _ = std.math.mul( usize, limits.relation_entries_max, @sizeOf(version.RelationEntry), ) catch return error.CapacityOverflow; _ = std.math.mul( usize, limits.conflict_entries_max, @sizeOf(version.ConflictEntry), ) catch return error.CapacityOverflow; return .{ .bytes = bytes, .relation_entries = limits.relation_entries_max, .conflict_entries = limits.conflict_entries_max, }; }};pub const Storage = struct { bytes: []u8, relations: []version.RelationEntry, conflicts: []version.ConflictEntry,};pub const Exhaustion = struct { name: []const u8, requested: usize, available: usize,};pub const Workspace = struct { metadata: []u8, stream: []u8, relations: []version.RelationEntry, conflicts: []version.ConflictEntry, exhaustion: ?Exhaustion = null, /// Activation borrows all storage before any record is read. pub fn activate(limits: Limits, storage: Storage) Error!Workspace { const capacity = try Capacity.derive(limits); if (storage.bytes.len < capacity.bytes) return error.CapacityExceeded; if (storage.relations.len < capacity.relation_entries) return error.CapacityExceeded; if (storage.conflicts.len < capacity.conflict_entries) return error.CapacityExceeded; return .{ .metadata = storage.bytes[0..limits.metadata_bytes_max], .stream = storage.bytes[limits.metadata_bytes_max..capacity.bytes], .relations = storage.relations[0..limits.relation_entries_max], .conflicts = storage.conflicts[0..limits.conflict_entries_max], }; } pub fn scratch(self: *Workspace) identity.Scratch { return .{ .relations = self.relations, .conflicts = self.conflicts }; } pub fn each( self: *Workspace, kind: record_mod.RecordKind, payload: []const u8, location: identity.Location, context: anytype, comptime accept: fn (@TypeOf(context), identity.Entry) anyerror!void, ) anyerror!void { identity.each(kind, payload, location, self.scratch(), context, accept) catch |err| { if (err != error.KeyScratchExhausted) return err; const count_offset: usize = switch (kind) { .database_root => version.hash_bytes, .conflict_root => 0, else => unreachable, }; if (payload.len -| count_offset < 4) return error.InvalidHistory; const requested = record_mod.readIntU32(payload[count_offset..][0..4]); const available = switch (kind) { .database_root => self.relations.len, .conflict_root => self.conflicts.len, else => unreachable, }; try self.require("key_entries_max", requested, available); unreachable; }; } fn require(self: *Workspace, name: []const u8, requested: usize, available: usize) Error!void { if (requested <= available) return; self.exhaustion = .{ .name = name, .requested = requested, .available = available, }; return error.CapacityExceeded; }};pub const Scanned = struct { kind: record_mod.RecordKind, location: identity.Location, payload: []const u8, end: u64,};/// A file-order scanner retains only one metadata record or stream chunk.pub const Scanner = struct { io: std.Io, file: std.Io.File, workspace: *Workspace, offset: u64, end: u64, chunk_key: version.Hash = undefined, pub fn init( io: std.Io, file: std.Io.File, workspace: *Workspace, start: u64, end: u64, ) Error!Scanner { if (start > end) return error.InvalidHistory; return .{ .io = io, .file = file, .workspace = workspace, .offset = start, .end = end }; } pub fn next(self: *Scanner) Error!?Scanned { if (self.offset == self.end) return null; if (self.end - self.offset < record_mod.record_header_size) return error.InvalidHistory; var header_bytes: [record_mod.record_header_size]u8 = undefined; try readExact(self.io, self.file, &header_bytes, self.offset); const header = try record_mod.Header.decode(&header_bytes); const payload_start = std.math.add( u64, self.offset, record_mod.record_header_size, ) catch return error.InvalidHistory; const record_end = std.math.add( u64, payload_start, header.payload_len, ) catch return error.InvalidHistory; if (record_end > self.end) return error.InvalidHistory; const location = identity.Location{ .record_offset = self.offset, .payload_len = header.payload_len, .envelope_hash = header.envelope_hash, }; var hasher = record_mod.envelopeHasher( @backingInt(header.kind), header.payload_len, ); const payload: []const u8 = if (header.kind == .row_chunk) chunk: { var cursor = payload_start; var copied: usize = 0; while (cursor < record_end) { const count: usize = @intCast(@min(self.workspace.stream.len, record_end - cursor)); try readExact(self.io, self.file, self.workspace.stream[0..count], cursor); if (copied < version.hash_bytes) { const take = @min(version.hash_bytes - copied, count); @memcpy(self.chunk_key[copied..][0..take], self.workspace.stream[0..take]); copied += take; } hasher.update(self.workspace.stream[0..count]); cursor += count; } break :chunk &self.chunk_key; } else metadata: { try self.workspace.require( "metadata_bytes_max", header.payload_len, self.workspace.metadata.len, ); const bytes = self.workspace.metadata[0..header.payload_len]; try readExact(self.io, self.file, bytes, payload_start); hasher.update(bytes); break :metadata bytes; }; var actual: version.Hash = undefined; hasher.final(&actual); if (!version.same(actual, header.envelope_hash)) return error.InvalidHistory; const result = Scanned{ .kind = header.kind, .location = location, .payload = payload, .end = record_end, }; self.offset = record_end; return result; }};pub const Resolver = struct { io: std.Io, file: std.Io.File, indexed_eof: u64, workspace: *Workspace, /// The caller owns payload storage and keeps the returned slice alive. pub fn read( self: *Resolver, key: identity.Key, location: identity.Location, output: []u8, ) Error![]const u8 { const end = std.math.add( u64, location.record_offset, location.encodedLen(), ) catch return error.InvalidHistory; if (end > self.indexed_eof) return error.InvalidHistory; try self.workspace.require("record_payload", location.payload_len, output.len); var header_bytes: [record_mod.record_header_size]u8 = undefined; try readExact(self.io, self.file, &header_bytes, location.record_offset); const header = try record_mod.Header.decode(&header_bytes); if (header.payload_len != location.payload_len) return error.InvalidHistory; if (!version.same(header.envelope_hash, location.envelope_hash)) { return error.InvalidHistory; } try readExact( self.io, self.file, output[0..location.payload_len], location.record_offset + record_mod.record_header_size, ); const payload = output[0..location.payload_len]; if (!version.same( record_mod.recordHash(@backingInt(header.kind), payload), header.envelope_hash, )) return error.InvalidHistory; var found = false; const Context = struct { wanted: identity.Key, position: u32, found: *bool, fn accept(context: *@This(), entry: identity.Entry) Error!void { if (entry.key.kind != context.wanted.kind) return; if (entry.location.node_position != context.position) return; context.found.* = version.same(entry.key.bytes, context.wanted.bytes); } }; var context = Context{ .wanted = key, .position = location.node_position, .found = &found, }; self.workspace.each( header.kind, payload, location, &context, Context.accept, ) catch |err| switch (err) { error.CapacityExceeded => return error.CapacityExceeded, else => return error.InvalidHistory, }; if (!found) return error.WrongObject; return payload; }};fn readExact(io: std.Io, file: std.Io.File, output: []u8, offset: u64) Error!void { var filled: usize = 0; while (filled < output.len) { const position = std.math.add(u64, offset, filled) catch return error.InvalidHistory; const count = try file.readPositionalAll(io, output[filled..], position); if (count == 0) return error.InvalidHistory; filled += count; }}test "history resolver keeps the first typed commit location" { const store_mod = @import("store.zig"); const allocator = std.testing.allocator; const io = std.Options.debug_io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile(io, "objects.history", .{ .read = true }); defer file.close(io); var payload: std.ArrayList(u8) = .empty; defer payload.deinit(allocator); const root = version.emptyHash("resolver-commit"); try record_mod.appendHash(allocator, &payload, root); try record_mod.appendU32(allocator, &payload, 0); const first_end = try store_mod.writeTestingRecord(file, 0, .commit, payload.items); const end = try store_mod.writeTestingRecord(file, first_end, .commit, payload.items); const limits = Limits{ .metadata_bytes_max = 128, .stream_bytes = 64, .relation_entries_max = 2, .conflict_entries_max = 2, }; const capacity = try Capacity.derive(limits); const bytes = try allocator.alloc(u8, capacity.bytes); defer allocator.free(bytes); const relations = try allocator.alloc(version.RelationEntry, capacity.relation_entries); defer allocator.free(relations); const conflicts = try allocator.alloc(version.ConflictEntry, capacity.conflict_entries); defer allocator.free(conflicts); var workspace = try Workspace.activate(limits, .{ .bytes = bytes, .relations = relations, .conflicts = conflicts, }); var scanner = try Scanner.init(io, file, &workspace, 0, end); const first = (try scanner.next()).?; try std.testing.expectEqual(@as(u64, 0), first.location.record_offset); const expected = version.Commit.init(root, &.{}).hash; var captured: ?identity.Entry = null; const Capture = struct { fn accept(target: *?identity.Entry, entry: identity.Entry) !void { target.* = entry; } }; try workspace.each( first.kind, first.payload, first.location, &captured, Capture.accept, ); try std.testing.expect(version.same(expected, captured.?.key.bytes)); const second = (try scanner.next()).?; try std.testing.expectEqual(@as(u64, first_end), second.location.record_offset); try std.testing.expect((try scanner.next()) == null); var resolver = Resolver{ .io = io, .file = file, .indexed_eof = end, .workspace = &workspace }; var output: [64]u8 = undefined; const read = try resolver.read(captured.?.key, captured.?.location, &output); try std.testing.expectEqualSlices(u8, payload.items, read);}test "history resolver typed keys agree with replay across object kinds" { const store_mod = @import("store.zig"); const allocator = std.testing.allocator; const io = std.Options.debug_io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const path = "typed.history"; var relation = try store_mod.testingMerkleRelationRoot( allocator, .{ .branches = 1, .leaves = 2 }, null, "typed", ); defer relation.deinit(); const rows = try store_mod.testingChunkRows(allocator, 300, 42); defer version.freeRelationRows(allocator, rows); const artifact = version.ConflictArtifact.init("metrics", 7, "old", "ours", "theirs"); var database_root: version.DatabaseRoot = undefined; var commit: version.Commit = undefined; { var history = try store_mod.History.open(allocator, tmp.dir, .{ .path = path, .recovery = .reject, }); defer history.deinit(); try history.putRelationRoot(relation); try history.putRelationRows(relation.hash, rows); try history.putConflict(artifact); const conflicts = try history.putConflictRoot(&.{artifact.entry()}); database_root = try version.DatabaseRoot.initSorted(allocator, &.{ .{ .name = "metrics", .hash = relation.hash }, }, conflicts); try history.putDatabaseRoot(database_root); commit = version.Commit.init(database_root.hash, &.{}); try history.putCommit(commit); try history.putRef(.{ .name = "main", .target = commit.hash }); try history.putRef(.{ .name = "temporary", .target = commit.hash }); try history.deleteRef("temporary"); const chunk = history.row_chunks.items[0].payload; const duplicate = try allocator.alloc(u8, chunk.len); defer allocator.free(duplicate); try std.testing.expectEqual( chunk.len, try history.file.?.readPositionalAll(io, duplicate, chunk.offset), ); try history.appendRecord(.row_chunk, duplicate); try history.flushSync(); } defer database_root.deinit(); var replay = try store_mod.History.open(allocator, tmp.dir, .{ .path = path, .recovery = .reject, .read_only = true, }); defer replay.deinit(); const verify_mod = @import("verify.zig"); const verify_limits = verify_mod.Limits{ .metadata_bytes_max = 128 * 1024, .dependencies_max = 4096, .conflict_bytes_max = 4096, .working_bytes_max = 8 * 1024 * 1024, }; const verify_capacity = try verify_mod.Capacity.derive(verify_limits); const verify_bytes = try allocator.alloc(u8, verify_capacity.bytes); defer allocator.free(verify_bytes); var verify_workspace = try verify_mod.Workspace.activate( verify_limits, .{ .bytes = verify_bytes }, ); const report = try verify_mod.verifyFile( &verify_workspace, io, replay.file.?, replay.len(), .{}, ); try std.testing.expect(report.bad == null); try std.testing.expectEqual(replay.len(), report.last_valid_boundary); const limits = Limits{ .metadata_bytes_max = 128 * 1024, .stream_bytes = 4096, .relation_entries_max = 16, .conflict_entries_max = 16, }; const capacity = try Capacity.derive(limits); const bytes = try allocator.alloc(u8, capacity.bytes); defer allocator.free(bytes); const relations = try allocator.alloc(version.RelationEntry, capacity.relation_entries); defer allocator.free(relations); const conflicts = try allocator.alloc(version.ConflictEntry, capacity.conflict_entries); defer allocator.free(conflicts); var workspace = try Workspace.activate(limits, .{ .bytes = bytes, .relations = relations, .conflicts = conflicts, }); var entries: std.ArrayList(identity.Entry) = .empty; defer entries.deinit(allocator); var duplicates: usize = 0; const Collect = struct { allocator: std.mem.Allocator, entries: *std.ArrayList(identity.Entry), duplicates: *usize, fn accept(context: *@This(), entry: identity.Entry) !void { for (context.entries.items) |existing| { if (existing.key.kind != entry.key.kind) continue; if (!version.same(existing.key.bytes, entry.key.bytes)) continue; context.duplicates.* += 1; return; } try context.entries.append(context.allocator, entry); } }; var collect = Collect{ .allocator = allocator, .entries = &entries, .duplicates = &duplicates, }; var scanner = try Scanner.init(io, replay.file.?, &workspace, 0, replay.len()); while (try scanner.next()) |record| { try workspace.each( record.kind, record.payload, record.location, &collect, Collect.accept, ); } try std.testing.expectEqual(@as(usize, 1), duplicates); const replay_count = replay.commits.items.len + replay.conflicts.items.len + replay.database_roots.items.len + replay.relation_roots.items.len + replay.relation_rows.items.len + replay.conflict_roots.items.len + replay.row_chunks.items.len + replay.index_pages.items.len + replay.tree_nodes.items.len + replay.relation_spans.items.len; try std.testing.expectEqual(replay_count, entries.items.len); var resolver = Resolver{ .io = io, .file = replay.file.?, .indexed_eof = replay.len(), .workspace = &workspace, }; const output = try allocator.alloc(u8, 128 * 1024); defer allocator.free(output); for (entries.items) |entry| { const key = entry.key.bytes; const present = switch (entry.key.kind) { .commit => replay.findCommit(key) != null, .conflict => replay.findConflict(key) != null, .database_root => replay.findDatabaseRoot(key) != null, .relation_root => replay.findRelationRoot(key) != null, .relation_rows => replay.hasRelationRows(key), .conflict_root => replay.findConflictRoot(key) != null, .row_chunk => replay.findRowChunk(key) != null, .chunk_index_page => replay.hasIndexPage(key), .tree_node => replay.findTreeNode(key) != null, .relation_spans => replay.lookup.relation_spans.get(key) != null, }; try std.testing.expect(present); _ = try resolver.read(entry.key, entry.location, output); }}Source: lib/sql/src/history/root.zig:6
zig
pub const resolver = @import("resolver.zig");Audit
| Definitions | 17 |
|---|---|
| Public names | 17 |
| Members | 32 |
| Version | 26.7.0 |
| Revision | daab053ee433 |