tiny.sql.history.refs
Defined in history.
API (19)
Actions
Public operations.
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
format_versionmagicmax_entriesmax_name_bytesmax_snapshot_bytespath_bytes_maxrepair_metadata_record_bytes_maxsuffix
Source
Source: lib/sql/src/history/refs.zig
zig
const std = @import("std");const simd = @import("simd");const sql = @import("../root.zig");const record_mod = @import("record.zig");const validate_mod = @import("validate.zig");const Bytes = simd.ScalableTag(u8);const version = sql.version;const Allocator = std.mem.Allocator;pub const Error = Allocator.Error;pub const ReadError = Error || std.Io.Dir.ReadFileAllocError;pub const suffix = ".refs";pub const magic: u32 = 0x7473_7266;pub const format_version: u32 = 2;pub const max_snapshot_bytes: usize = 4 * 1024 * 1024;pub const max_entries: u32 = 65_536;pub const max_name_bytes: u32 = 4_096;pub const path_bytes_max: usize = 1_024;pub const repair_metadata_record_bytes_max: usize = max_snapshot_bytes;const repair_stream_bytes: usize = 64 * 1024;const repair_dependencies_max: usize = repair_metadata_record_bytes_max / version.hash_bytes;const repair_conflict_bytes_max: usize = max_snapshot_bytes;const repair_limits = validate_mod.Limits{ .metadata_bytes_max = repair_metadata_record_bytes_max, .dependencies_max = repair_dependencies_max, .conflict_bytes_max = repair_conflict_bytes_max,};const header_size: usize = 4 + 4 + 8 + 4;const entry_fixed_size: usize = 4 + 3 * version.hash_bytes;const digest_size: usize = version.hash_bytes;const write_suffix = ".next";const write_path_bytes_max = path_bytes_max + write_suffix.len;pub const Entry = struct { name: []const u8, head: version.Hash, root: version.Hash, conflicts: version.Hash,};pub const Snapshot = struct { allocator: Allocator, covered_length: u64, entries: []Entry, names: []u8, pub fn deinit(self: *Snapshot) void { self.allocator.free(self.entries); self.allocator.free(self.names); self.* = undefined; } pub fn find(self: *const Snapshot, name: []const u8) ?Entry { std.debug.assert(self.entries.len <= max_entries); for (self.entries) |entry| { if (std.mem.eql(u8, entry.name, name)) return entry; } return null; }};pub fn pathFor(buffer: *[path_bytes_max]u8, history_path: []const u8) ?[]const u8 { if (history_path.len + suffix.len > buffer.len) return null; @memcpy(buffer[0..history_path.len], history_path); @memcpy(buffer[history_path.len..][0..suffix.len], suffix); return buffer[0 .. history_path.len + suffix.len];}pub fn load( allocator: Allocator, io: std.Io, dir: std.Io.Dir, history_path: []const u8,) Error!?Snapshot { return loadExisting(allocator, io, dir, history_path) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, };}pub fn loadExisting( allocator: Allocator, io: std.Io, dir: std.Io.Dir, history_path: []const u8,) ReadError!?Snapshot { var path_buffer: [path_bytes_max]u8 = undefined; const path = pathFor(&path_buffer, history_path) orelse return null; const bytes = dir.readFileAlloc(io, path, allocator, .limited(max_snapshot_bytes)) catch |err| switch (err) { error.StreamTooLong => return null, else => return err, }; defer allocator.free(bytes); return try decode(allocator, bytes);}pub fn store( allocator: Allocator, io: std.Io, dir: std.Io.Dir, history_path: []const u8, covered_length: u64, entries: []const Entry,) (Error || std.Io.File.OpenError || std.Io.File.WritePositionalError || std.Io.Dir.RenameError)!void { std.debug.assert(entries.len <= max_entries); var path_buffer: [path_bytes_max]u8 = undefined; const path = pathFor(&path_buffer, history_path) orelse return; var write_buffer: [write_path_bytes_max]u8 = undefined; @memcpy(write_buffer[0..path.len], path); @memcpy(write_buffer[path.len..][0..write_suffix.len], write_suffix); const write_path = write_buffer[0 .. path.len + write_suffix.len]; const bytes = try encode(allocator, covered_length, entries); defer allocator.free(bytes); { var file = try dir.createFile(io, write_path, .{ .read = true, .truncate = true }); defer file.close(io); try file.writePositionalAll(io, bytes, 0); } try dir.rename(write_path, dir, path, io);}fn encode(allocator: Allocator, covered_length: u64, entries: []const Entry) Error![]u8 { std.debug.assert(entries.len <= max_entries); var total: usize = header_size; for (entries) |entry| { std.debug.assert(entry.name.len <= max_name_bytes); total += entry_fixed_size + entry.name.len; } total += digest_size; std.debug.assert(total <= max_snapshot_bytes); const bytes = try allocator.alloc(u8, total); errdefer allocator.free(bytes); var cursor: usize = 0; writeU32(bytes, &cursor, magic); writeU32(bytes, &cursor, format_version); writeU64(bytes, &cursor, covered_length); writeU32(bytes, &cursor, @intCast(entries.len)); for (entries) |entry| { writeU32(bytes, &cursor, @intCast(entry.name.len)); @memcpy(bytes[cursor..][0..entry.name.len], entry.name); cursor += entry.name.len; @memcpy(bytes[cursor..][0..version.hash_bytes], entry.head[0..]); cursor += version.hash_bytes; @memcpy(bytes[cursor..][0..version.hash_bytes], entry.root[0..]); cursor += version.hash_bytes; @memcpy(bytes[cursor..][0..version.hash_bytes], entry.conflicts[0..]); cursor += version.hash_bytes; } std.debug.assert(cursor + digest_size == total); var digest: version.Hash = undefined; std.crypto.hash.sha2.Sha256.hash(bytes[0..cursor], &digest, .{}); @memcpy(bytes[cursor..][0..digest_size], digest[0..]); return bytes;}fn decode(allocator: Allocator, bytes: []const u8) Error!?Snapshot { if (bytes.len < header_size + digest_size) return null; if (bytes.len > max_snapshot_bytes) return null; const body = bytes[0 .. bytes.len - digest_size]; var digest: version.Hash = undefined; std.crypto.hash.sha2.Sha256.hash(body, &digest, .{}); if (!std.mem.eql(u8, digest[0..], bytes[body.len..])) return null; var cursor: usize = 0; if (readU32(body, &cursor) != magic) return null; if (readU32(body, &cursor) != format_version) return null; const covered_length = readU64(body, &cursor); const entry_count = readU32(body, &cursor); if (entry_count > max_entries) return null; var name_bytes: usize = 0; { var scan = cursor; var index: u32 = 0; while (index < entry_count) : (index += 1) { if (body.len - scan < 4) return null; var peek = scan; const name_len = readU32(body, &peek); if (name_len == 0 or name_len > max_name_bytes) return null; if (body.len - peek < name_len + 3 * version.hash_bytes) return null; name_bytes += name_len; scan = peek + name_len + 3 * version.hash_bytes; } if (scan != body.len) return null; } const entries = try allocator.alloc(Entry, entry_count); errdefer allocator.free(entries); const names = try allocator.alloc(u8, name_bytes); errdefer allocator.free(names); var name_cursor: usize = 0; var index: u32 = 0; while (index < entry_count) : (index += 1) { const name_len = readU32(body, &cursor); const name = names[name_cursor..][0..name_len]; @memcpy(name, body[cursor..][0..name_len]); cursor += name_len; name_cursor += name_len; var head: version.Hash = undefined; @memcpy(head[0..], body[cursor..][0..version.hash_bytes]); cursor += version.hash_bytes; var root: version.Hash = undefined; @memcpy(root[0..], body[cursor..][0..version.hash_bytes]); cursor += version.hash_bytes; var conflicts: version.Hash = undefined; @memcpy(conflicts[0..], body[cursor..][0..version.hash_bytes]); cursor += version.hash_bytes; entries[index] = .{ .name = name, .head = head, .root = root, .conflicts = conflicts, }; } std.debug.assert(cursor == body.len); std.debug.assert(name_cursor == names.len); return .{ .allocator = allocator, .covered_length = covered_length, .entries = entries, .names = names, };}fn writeU32(bytes: []u8, cursor: *usize, value: u32) void { std.mem.writeInt(u32, bytes[cursor.*..][0..4], value, .big); cursor.* += 4;}fn writeU64(bytes: []u8, cursor: *usize, value: u64) void { std.mem.writeInt(u64, bytes[cursor.*..][0..8], value, .big); cursor.* += 8;}fn readU32(bytes: []const u8, cursor: *usize) u32 { const value = std.mem.readInt(u32, bytes[cursor.*..][0..4], .big); cursor.* += 4; return value;}fn readU64(bytes: []const u8, cursor: *usize) u64 { const value = std.mem.readInt(u64, bytes[cursor.*..][0..8], .big); cursor.* += 8; return value;}const RepairStage = enum { current, head, root,};const RepairEntry = struct { name: []u8, head: version.Hash, root: version.Hash, conflicts: version.Hash, stage: RepairStage,};const FastForwardDecision = enum { pending, baseline, target,};const FastForward = struct { id: version.Hash, name: []u8, expected: version.Hash, target: version.Hash, decision: FastForwardDecision = .pending,};const RepairState = struct { allocator: Allocator, entries: std.ArrayList(RepairEntry) = .empty, encoded_bytes: usize = header_size + digest_size, fast_forward: ?FastForward = null, fn init(allocator: Allocator, snapshot: *const Snapshot) !RepairState { var state = RepairState{ .allocator = allocator }; errdefer state.deinit(); try state.entries.ensureTotalCapacity(allocator, snapshot.entries.len); for (snapshot.entries) |entry| { const name = try allocator.dupe(u8, entry.name); state.entries.appendAssumeCapacity(.{ .name = name, .head = entry.head, .root = entry.root, .conflicts = entry.conflicts, .stage = .current, }); state.encoded_bytes += entry_fixed_size + name.len; } std.debug.assert(state.entries.items.len <= max_entries); std.debug.assert(state.encoded_bytes <= max_snapshot_bytes); return state; } fn deinit(self: *RepairState) void { if (self.fast_forward) |pending| self.allocator.free(pending.name); for (self.entries.items) |entry| self.allocator.free(entry.name); self.entries.deinit(self.allocator); self.* = undefined; } fn find(self: *RepairState, name: []const u8) ?usize { std.debug.assert(self.entries.items.len <= max_entries); for (self.entries.items, 0..) |entry, index| { if (std.mem.eql(u8, entry.name, name)) return index; } return null; } fn setRef(self: *RepairState, name: []const u8, head: version.Hash) !void { if (name.len == 0 or name.len > max_name_bytes) return error.InvalidHistory; if (self.find(name)) |index| { if (version.same(self.entries.items[index].head, head)) return; const metadata = self.metadataForHead(head); self.entries.items[index].head = head; if (metadata) |entry| { self.entries.items[index].root = entry.root; self.entries.items[index].conflicts = entry.conflicts; self.entries.items[index].stage = .current; } else { self.entries.items[index].stage = .head; } return; } if (self.entries.items.len >= max_entries) return error.StreamTooLong; const next_bytes = std.math.add( usize, self.encoded_bytes, entry_fixed_size + name.len, ) catch return error.StreamTooLong; if (next_bytes > max_snapshot_bytes) return error.StreamTooLong; const owned_name = try self.allocator.dupe(u8, name); errdefer self.allocator.free(owned_name); try self.entries.append(self.allocator, .{ .name = owned_name, .head = head, .root = undefined, .conflicts = undefined, .stage = .head, }); self.encoded_bytes = next_bytes; } fn metadataForHead(self: *const RepairState, head: version.Hash) ?RepairEntry { std.debug.assert(self.entries.items.len <= max_entries); for (self.entries.items) |entry| { if (entry.stage == .current and version.same(entry.head, head)) return entry; } return null; } fn deleteRef(self: *RepairState, name: []const u8) void { const index = self.find(name) orelse return; const removed = self.entries.orderedRemove(index); self.encoded_bytes -= entry_fixed_size + removed.name.len; self.allocator.free(removed.name); } fn finish(self: *RepairState, covered_length: u64) !Snapshot { if (self.fast_forward != null) return error.InvalidHistory; for (self.entries.items) |entry| { if (entry.stage != .current) return error.InvalidHistory; } const entries = try self.allocator.alloc(Entry, self.entries.items.len); errdefer self.allocator.free(entries); var names_bytes: usize = 0; for (self.entries.items) |entry| names_bytes += entry.name.len; const names = try self.allocator.alloc(u8, names_bytes); errdefer self.allocator.free(names); var cursor: usize = 0; for (self.entries.items, entries) |source, *target| { const name = names[cursor..][0..source.name.len]; @memcpy(name, source.name); cursor += name.len; target.* = .{ .name = name, .head = source.head, .root = source.root, .conflicts = source.conflicts, }; } std.debug.assert(cursor == names.len); return .{ .allocator = self.allocator, .covered_length = covered_length, .entries = entries, .names = names, }; }};const RecordView = struct { kind: record_mod.RecordKind, expected: version.Hash, payload: []const u8,};const ScanMode = enum { refs, commits, roots, fn materializes(self: ScanMode, kind: record_mod.RecordKind) bool { return switch (self) { .refs => switch (kind) { .ref, .ref_delete, .fast_forward_prepare, .fast_forward_commit, .fast_forward_abort, .fast_forward_complete, => true, else => false, }, .commits => kind == .commit, .roots => kind == .database_root, }; }};const RecordScanner = struct { allocator: Allocator, io: std.Io, file: std.Io.File, limit: usize, offset: usize, mode: ScanMode, control: sql.wal.Control, payload: std.ArrayList(u8) = .empty, fn init( allocator: Allocator, io: std.Io, file: std.Io.File, start: usize, end: usize, mode: ScanMode, control: sql.wal.Control, ) RecordScanner { std.debug.assert(start <= end); return .{ .allocator = allocator, .io = io, .file = file, .limit = end, .offset = start, .mode = mode, .control = control, }; } fn deinit(self: *RecordScanner) void { self.payload.deinit(self.allocator); self.* = undefined; } fn next(self: *RecordScanner) !?RecordView { try self.control.check(); if (self.offset == self.limit) return null; if (self.limit - self.offset < record_mod.record_header_size) { return error.InvalidHistory; } var header: [record_mod.record_header_size]u8 = undefined; try self.readExact(&header, self.offset); if (record_mod.readIntU32(header[0..4]) != record_mod.magic or record_mod.readIntU32(header[4..8]) != record_mod.format_version) { return error.InvalidHistory; } const kind_value = record_mod.readIntU32(header[8..12]); const kind = record_mod.recordKind(kind_value) orelse return error.InvalidHistory; const payload_len: usize = record_mod.readIntU32(header[12..16]); if (kind == .row_chunk and payload_len < version.hash_bytes) { return error.InvalidHistory; } const payload_offset = std.math.add( usize, self.offset, record_mod.record_header_size, ) catch return error.InvalidHistory; const payload_end = std.math.add(usize, payload_offset, payload_len) catch return error.InvalidHistory; if (payload_end > self.limit) return error.InvalidHistory; const materialize = self.mode.materializes(kind); if (materialize and payload_len > repair_metadata_record_bytes_max) { return error.StreamTooLong; } self.payload.clearRetainingCapacity(); if (materialize) try self.payload.resize(self.allocator, payload_len); var hasher = std.crypto.hash.sha2.Sha256.init(.{}); record_mod.writeHashU32(&hasher, record_mod.magic); record_mod.writeHashU32(&hasher, record_mod.format_version); record_mod.writeHashU32(&hasher, kind_value); record_mod.writeHashU32(&hasher, @intCast(payload_len)); if (materialize) { try self.readExact(self.payload.items, payload_offset); hasher.update(self.payload.items); } else { var scratch: [repair_stream_bytes]u8 = undefined; var cursor = payload_offset; while (cursor < payload_end) { try self.control.check(); const chunk_len = @min(scratch.len, payload_end - cursor); try self.readExact(scratch[0..chunk_len], cursor); hasher.update(scratch[0..chunk_len]); cursor += chunk_len; } } var actual: version.Hash = undefined; hasher.final(&actual); const expected = header[16..][0..version.hash_bytes].*; if (!version.same(actual, expected)) return error.InvalidHistory; self.offset = payload_end; return .{ .kind = kind, .expected = expected, .payload = self.payload.items, }; } fn readExact(self: *RecordScanner, target: []u8, start: usize) !void { var filled: usize = 0; while (filled < target.len) { try self.control.check(); const count = try self.file.readPositionalAll( self.io, target[filled..], start + filled, ); if (count == 0) return error.InvalidHistory; filled += count; } }};pub fn advance( allocator: Allocator, io: std.Io, dir: std.Io.Dir, history_path: []const u8, snapshot: *const Snapshot, history_length: u64, control: sql.wal.Control,) !Snapshot { try control.check(); if (snapshot.covered_length > history_length) return error.InvalidHistory; const start = std.math.cast(usize, snapshot.covered_length) orelse return error.StreamTooLong; const end = std.math.cast(usize, history_length) orelse return error.StreamTooLong; var file = try dir.openFile(io, history_path, .{}); defer file.close(io); try validate_mod.validateSuffix( allocator, io, file, start, end, snapshot.entries, repair_limits, control, ); var state = try RepairState.init(allocator, snapshot); defer state.deinit(); try applySuffix(&state, allocator, io, file, start, end, control); try resolveHeads(&state, allocator, io, file, start, end, control); if (hasStage(&state, .head) and start != 0) { try resolveHeads(&state, allocator, io, file, 0, start, control); } try resolveRoots(&state, allocator, io, file, start, end, control); if (hasStage(&state, .root) and start != 0) { try resolveRoots(&state, allocator, io, file, 0, start, control); } return try state.finish(history_length);}fn applySuffix( state: *RepairState, allocator: Allocator, io: std.Io, file: std.Io.File, start: usize, end: usize, control: sql.wal.Control,) !void { var scanner = RecordScanner.init(allocator, io, file, start, end, .refs, control); defer scanner.deinit(); while (try scanner.next()) |record| { if (state.fast_forward != null and !fastForwardKind(record.kind)) { return error.InvalidHistory; } switch (record.kind) { .ref => { try applyRef(state, record.payload); }, .ref_delete => try applyRefDelete(state, record.payload), .fast_forward_prepare => try applyFastForwardPrepare(state, record), .fast_forward_commit => try applyFastForwardDecision(state, record.payload, .target), .fast_forward_abort => try applyFastForwardDecision(state, record.payload, .baseline), .fast_forward_complete => try applyFastForwardComplete(state, record.payload), else => {}, } }}fn fastForwardKind(kind: record_mod.RecordKind) bool { return switch (kind) { .fast_forward_prepare, .fast_forward_commit, .fast_forward_abort, .fast_forward_complete, => true, else => false, };}fn applyRef(state: *RepairState, payload: []const u8) !void { var reader = record_mod.PayloadReader.init(payload); const target = try reader.hash(); const name = try reader.readBytes(); try reader.finish(); try state.setRef(name, target);}fn applyRefDelete(state: *RepairState, payload: []const u8) !void { var reader = record_mod.PayloadReader.init(payload); const name = try reader.readBytes(); try reader.finish(); state.deleteRef(name);}fn applyFastForwardPrepare( state: *RepairState, record: RecordView,) !void { if (state.fast_forward != null) return error.InvalidHistory; var reader = record_mod.PayloadReader.init(record.payload); const name = try reader.readBytes(); const expected = try reader.hash(); const target = try reader.hash(); try reader.finish(); const index = state.find(name) orelse return error.InvalidHistory; if (!version.same(state.entries.items[index].head, expected)) return error.InvalidHistory; state.fast_forward = .{ .id = record.expected, .name = try state.allocator.dupe(u8, name), .expected = expected, .target = target, };}fn applyFastForwardDecision( state: *RepairState, payload: []const u8, decision: FastForwardDecision,) !void { var reader = record_mod.PayloadReader.init(payload); const id = try reader.hash(); try reader.finish(); const pending = if (state.fast_forward) |*value| value else return error.InvalidHistory; if (!version.same(pending.id, id) or pending.decision != .pending) { return error.InvalidHistory; } if (decision == .target) try state.setRef(pending.name, pending.target); pending.decision = decision;}fn applyFastForwardComplete(state: *RepairState, payload: []const u8) !void { var reader = record_mod.PayloadReader.init(payload); const id = try reader.hash(); try reader.finish(); const pending = state.fast_forward orelse return error.InvalidHistory; if (!version.same(pending.id, id) or pending.decision == .pending) { return error.InvalidHistory; } const selected = if (pending.decision == .target) pending.target else pending.expected; const index = state.find(pending.name) orelse return error.InvalidHistory; if (!version.same(state.entries.items[index].head, selected)) return error.InvalidHistory; state.allocator.free(pending.name); state.fast_forward = null;}fn hasStage(state: *const RepairState, stage: RepairStage) bool { std.debug.assert(state.entries.items.len <= max_entries); for (state.entries.items) |entry| if (entry.stage == stage) return true; return false;}fn resolveHeads( state: *RepairState, allocator: Allocator, io: std.Io, file: std.Io.File, start: usize, end: usize, control: sql.wal.Control,) !void { if (!hasStage(state, .head) or start == end) return; var scanner = RecordScanner.init(allocator, io, file, start, end, .commits, control); defer scanner.deinit(); while (try scanner.next()) |record| { if (record.kind != .commit) continue; const identity = try commitIdentity(allocator, record.payload); for (state.entries.items) |*entry| { if (entry.stage != .head or !version.same(entry.head, identity.head)) continue; entry.root = identity.root; entry.stage = .root; } if (!hasStage(state, .head)) break; }}const CommitIdentity = struct { head: version.Hash, root: version.Hash,};fn commitIdentity(allocator: Allocator, payload: []const u8) !CommitIdentity { var reader = record_mod.PayloadReader.init(payload); const root = try reader.hash(); const parent_count = try reader.readU32(); const parent_bytes = std.math.mul(usize, parent_count, version.hash_bytes) catch return error.InvalidHistory; if (parent_bytes != reader.remaining()) return error.InvalidHistory; const parents = try allocator.alloc(version.Hash, parent_count); defer allocator.free(parents); for (parents) |*parent| parent.* = try reader.hash(); try reader.finish(); return .{ .head = version.Commit.init(root, parents).hash, .root = root };}fn resolveRoots( state: *RepairState, allocator: Allocator, io: std.Io, file: std.Io.File, start: usize, end: usize, control: sql.wal.Control,) !void { if (!hasStage(state, .root) or start == end) return; var scanner = RecordScanner.init(allocator, io, file, start, end, .roots, control); defer scanner.deinit(); while (try scanner.next()) |record| { if (record.kind != .database_root) continue; const identity = try databaseRootIdentity(allocator, record.payload); for (state.entries.items) |*entry| { if (entry.stage != .root or !version.same(entry.root, identity.root)) continue; entry.conflicts = identity.conflicts; entry.stage = .current; } if (!hasStage(state, .root)) break; }}const DatabaseRootIdentity = struct { root: version.Hash, conflicts: version.Hash,};fn databaseRootIdentity(allocator: Allocator, payload: []const u8) !DatabaseRootIdentity { var reader = record_mod.PayloadReader.init(payload); const conflicts = try reader.hash(); const entry_count = try reader.readU32(); const fixed_bytes = std.math.mul(usize, entry_count, 4 + version.hash_bytes) catch return error.InvalidHistory; if (fixed_bytes > reader.remaining()) return error.InvalidHistory; const entries = try allocator.alloc(version.RelationEntry, entry_count); defer allocator.free(entries); for (entries) |*entry| { entry.* = .{ .name = try reader.readBytes(), .hash = try reader.hash() }; } try reader.finish(); std.mem.sort(version.RelationEntry, entries, {}, relationEntryLessThan); const root = version.DatabaseRoot.init(entries, .{ .hash = conflicts }); return .{ .root = root.hash, .conflicts = conflicts };}fn relationEntryLessThan( _: void, left: version.RelationEntry, right: version.RelationEntry,) bool { return simd.order(Bytes, left.name, right.name) == .lt;}const testing_io = std.Options.debug_io;test "refs snapshot round-trips entries and covered length" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const head = @as([version.hash_bytes]u8, @splat(0xaa)); const root = @as([version.hash_bytes]u8, @splat(0xbb)); const conflicts = @as([version.hash_bytes]u8, @splat(0xcc)); const entries = [_]Entry{ .{ .name = "main", .head = head, .root = root, .conflicts = conflicts }, .{ .name = "feature/wide", .head = root, .root = head, .conflicts = root }, }; try store(std.testing.allocator, testing_io, tmp.dir, "log.history", 12_345, entries[0..]); var loaded = (try load(std.testing.allocator, testing_io, tmp.dir, "log.history")) orelse return error.SnapshotMissing; defer loaded.deinit(); try std.testing.expectEqual(@as(u64, 12_345), loaded.covered_length); try std.testing.expectEqual(@as(usize, 2), loaded.entries.len); const found = loaded.find("main") orelse return error.RefMissing; try std.testing.expectEqualSlices(u8, head[0..], found.head[0..]); try std.testing.expectEqualSlices(u8, root[0..], found.root[0..]); try std.testing.expectEqualSlices(u8, conflicts[0..], found.conflicts[0..]); try std.testing.expect(loaded.find("missing") == null);}test "refs snapshot load rejects a corrupted digest" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const head = @as([version.hash_bytes]u8, @splat(0x11)); const entries = [_]Entry{.{ .name = "main", .head = head, .root = head, .conflicts = head, }}; try store(std.testing.allocator, testing_io, tmp.dir, "log.history", 7, entries[0..]); { var file = try tmp.dir.openFile(testing_io, "log.history" ++ suffix, .{ .mode = .read_write }); defer file.close(testing_io); try file.writePositionalAll(testing_io, "?", 9); } try std.testing.expect((try load(std.testing.allocator, testing_io, tmp.dir, "log.history")) == null);}test "refs snapshot load returns null when the sidecar is missing" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try std.testing.expect((try load(std.testing.allocator, testing_io, tmp.dir, "absent.history")) == null);}test "refs snapshot rejects an empty branch name" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const bytes = try encode(std.testing.allocator, 3, &.{}); defer std.testing.allocator.free(bytes); var snapshot = (try decode(std.testing.allocator, bytes)) orelse return error.SnapshotMissing; defer snapshot.deinit(); try std.testing.expectEqual(@as(usize, 0), snapshot.entries.len); try std.testing.expectEqual(@as(u64, 3), snapshot.covered_length);}test "refs advance resolves stale suffix and pre-boundary targets" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const history_path = "repair.history"; const conflicts = version.ConflictRoot.empty(); const first_root = version.DatabaseRoot.init(&.{}, conflicts); const first_commit = version.Commit.init(first_root.hash, &.{}); const second_root = version.DatabaseRoot.init(&.{.{ .name = "items", .hash = version.emptyHash("repair.items"), }}, conflicts); var second_parents = [_]version.Hash{first_commit.hash}; const second_commit = version.Commit.init(second_root.hash, &second_parents); var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = history_path, .recovery = .reject, }); defer history.deinit(); try history.putDatabaseRoot(first_root); try history.putCommit(first_commit); try history.putRef(.{ .name = "main", .target = first_commit.hash }); var stale = (try loadExisting( std.testing.allocator, testing_io, tmp.dir, history_path, )).?; defer stale.deinit(); try history.putDatabaseRoot(second_root); try history.putCommit(second_commit); try history.putRef(.{ .name = "main", .target = second_commit.hash }); try history.putRef(.{ .name = "archive", .target = first_commit.hash }); const history_stat = try tmp.dir.statFile(testing_io, history_path, .{}); try std.testing.expect(stale.covered_length < history_stat.size); var repaired = try advance( std.testing.allocator, testing_io, tmp.dir, history_path, &stale, history_stat.size, .{}, ); defer repaired.deinit(); try std.testing.expectEqual(history_stat.size, repaired.covered_length); const main = repaired.find("main") orelse return error.RefMissing; try std.testing.expect(version.same(second_commit.hash, main.head)); try std.testing.expect(version.same(second_root.hash, main.root)); try std.testing.expect(version.same(second_root.conflicts, main.conflicts)); const archive = repaired.find("archive") orelse return error.RefMissing; try std.testing.expect(version.same(first_commit.hash, archive.head)); try std.testing.expect(version.same(first_root.hash, archive.root)); try std.testing.expect(version.same(first_root.conflicts, archive.conflicts));}test "refs advance rejects a ref before its target commit" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const history_path = "future-ref.history"; const conflicts = version.ConflictRoot.empty(); const first_root = version.DatabaseRoot.init(&.{}, conflicts); const first_commit = version.Commit.init(first_root.hash, &.{}); const future_root = version.DatabaseRoot.init(&.{.{ .name = "future", .hash = version.emptyHash("future-ref.relation"), }}, conflicts); var parents = [_]version.Hash{first_commit.hash}; const future_commit = version.Commit.init(future_root.hash, &parents); var stale: Snapshot = undefined; { var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = history_path, .recovery = .reject, }); defer history.deinit(); try history.putDatabaseRoot(first_root); try history.putCommit(first_commit); try history.putRef(.{ .name = "main", .target = first_commit.hash }); stale = (try loadExisting( std.testing.allocator, testing_io, tmp.dir, history_path, )).?; try history.putDatabaseRoot(future_root); var payload: std.ArrayList(u8) = .empty; defer payload.deinit(std.testing.allocator); try record_mod.appendHash(std.testing.allocator, &payload, future_commit.hash); try record_mod.appendBytes(std.testing.allocator, &payload, "main"); try history.appendRecord(.ref, payload.items); try history.putCommit(future_commit); } defer stale.deinit(); const history_stat = try tmp.dir.statFile(testing_io, history_path, .{}); try std.testing.expectError( error.InvalidHistory, advance( std.testing.allocator, testing_io, tmp.dir, history_path, &stale, history_stat.size, .{}, ), ); try std.testing.expectError( error.TruncatedHistory, sql.History.open(std.testing.allocator, tmp.dir, .{ .path = history_path, .recovery = .reject, }), );}test "refs advance rejects a hash-valid malformed non-ref suffix" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const history_path = "malformed-conflict.history"; const conflicts = version.ConflictRoot.empty(); const root = version.DatabaseRoot.init(&.{}, conflicts); const commit = version.Commit.init(root.hash, &.{}); var stale: Snapshot = undefined; { var history = try sql.History.open(std.testing.allocator, tmp.dir, .{ .path = history_path, .recovery = .reject, }); defer history.deinit(); try history.putDatabaseRoot(root); try history.putCommit(commit); try history.putRef(.{ .name = "main", .target = commit.hash }); stale = (try loadExisting( std.testing.allocator, testing_io, tmp.dir, history_path, )).?; try history.appendRecord(.conflict, &.{}); } defer stale.deinit(); const refs_before = try tmp.dir.readFileAlloc( testing_io, "malformed-conflict.history.refs", std.testing.allocator, .unlimited, ); defer std.testing.allocator.free(refs_before); const history_stat = try tmp.dir.statFile(testing_io, history_path, .{}); try std.testing.expectError( error.InvalidHistory, advance( std.testing.allocator, testing_io, tmp.dir, history_path, &stale, history_stat.size, .{}, ), ); const refs_after = try tmp.dir.readFileAlloc( testing_io, "malformed-conflict.history.refs", std.testing.allocator, .unlimited, ); defer std.testing.allocator.free(refs_after); try std.testing.expectEqualSlices(u8, refs_before, refs_after); try std.testing.expectError( error.TruncatedHistory, sql.History.open(std.testing.allocator, tmp.dir, .{ .path = history_path, .recovery = .reject, }), );}test "refs advance streams opaque records above the metadata bound" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const history_path = "opaque.history"; const payload = try std.testing.allocator.alloc(u8, repair_metadata_record_bytes_max); defer std.testing.allocator.free(payload); @memset(payload, 0); const kind = record_mod.RecordKind.row_chunk; const expected = record_mod.recordHash(@backingInt(kind), payload); var header: [record_mod.record_header_size]u8 = @splat(0); std.mem.writeInt(u32, header[0..4], record_mod.magic, .big); std.mem.writeInt(u32, header[4..8], record_mod.format_version, .big); std.mem.writeInt(u32, header[8..12], @backingInt(kind), .big); std.mem.writeInt(u32, header[12..16], @intCast(payload.len), .big); @memcpy(header[16..][0..version.hash_bytes], &expected); var file = try tmp.dir.createFile(testing_io, history_path, .{ .read = true }); defer file.close(testing_io); try file.writePositionalAll(testing_io, &header, 0); try file.writePositionalAll(testing_io, payload, header.len); const history_length = header.len + payload.len; var empty = Snapshot{ .allocator = std.testing.allocator, .covered_length = 0, .entries = try std.testing.allocator.alloc(Entry, 0), .names = try std.testing.allocator.alloc(u8, 0), }; defer empty.deinit(); var repaired = try advance( std.testing.allocator, testing_io, tmp.dir, history_path, &empty, history_length, .{}, ); defer repaired.deinit(); try std.testing.expectEqual(@as(usize, 0), repaired.entries.len); try std.testing.expectEqual(@as(u64, history_length), repaired.covered_length);}test "refs advance rejects metadata above the repair bound" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const history_path = "oversized.history"; var header: [record_mod.record_header_size]u8 = @splat(0); std.mem.writeInt(u32, header[0..4], record_mod.magic, .big); std.mem.writeInt(u32, header[4..8], record_mod.format_version, .big); std.mem.writeInt(u32, header[8..12], @backingInt(record_mod.RecordKind.commit), .big); const oversized_payload_bytes = repair_metadata_record_bytes_max + 1; std.mem.writeInt(u32, header[12..16], oversized_payload_bytes, .big); var file = try tmp.dir.createFile(testing_io, history_path, .{ .read = true }); defer file.close(testing_io); try file.writePositionalAll(testing_io, &header, 0); const history_length = record_mod.record_header_size + oversized_payload_bytes; try file.setLength(testing_io, history_length); var empty = Snapshot{ .allocator = std.testing.allocator, .covered_length = 0, .entries = try std.testing.allocator.alloc(Entry, 0), .names = try std.testing.allocator.alloc(u8, 0), }; defer empty.deinit(); try std.testing.expectError( error.StreamTooLong, advance( std.testing.allocator, testing_io, tmp.dir, history_path, &empty, history_length, .{}, ), );}test "refs snapshot rejects the retired format version" { const head = @as([version.hash_bytes]u8, @splat(0x33)); const entries = [_]Entry{.{ .name = "main", .head = head, .root = head, .conflicts = head, }}; const bytes = try encode(std.testing.allocator, 9, &entries); defer std.testing.allocator.free(bytes); std.mem.writeInt(u32, bytes[4..8], 1, .big); std.crypto.hash.sha2.Sha256.hash( bytes[0 .. bytes.len - digest_size], bytes[bytes.len - digest_size ..][0..digest_size], .{}, ); try std.testing.expect((try decode(std.testing.allocator, bytes)) == null);}Source: lib/sql/src/history/root.zig:31
zig
pub const refs = refs_mod;Complete call list for history.refs.advance
7 direct calls.
lib.sql.src.history.refs.RepairState.deinit[method] — private source atlib/sql/src/history/refs.zig:313in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.RepairState.finish[method] — private source atlib/sql/src/history/refs.zig:377in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.RepairState.init[function] — private source atlib/sql/src/history/refs.zig:293in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.applySuffix[function] — private source atlib/sql/src/history/refs.zig:594in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.hasStage[function] — private source atlib/sql/src/history/refs.zig:700in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.resolveHeads[function] — private source atlib/sql/src/history/refs.zig:706in nearest public ownertiny.sql.history.refslib.sql.src.history.refs.resolveRoots[function] — private source atlib/sql/src/history/refs.zig:749in nearest public ownertiny.sql.history.refs
Audit
| Definitions | 19 |
|---|---|
| Public names | 20 |
| Members | 8 |
| Version | 26.7.0 |
| Revision | daab053ee433 |