Skip to documentation
SLOP

tiny.sql.history.refs

Reference 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.

No direct callersNo direct callshistoryrefs
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callstest sourcelib.sql.src.history.refstest: refs advance rejects a hash-val...test sourcelib.sql.src.history.refstest: refs advance rejects a ref befo...history.refs.Snapshotdeinit
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallstest sourcelib.sql.src.history.refstest: refs advance rejects a hash-val...test sourcelib.sql.src.history.refstest: refs advance rejects a ref befo...test sourcelib.sql.src.history.refstest: refs advance rejects metadata a...test sourcelib.sql.src.history.refstest: refs advance resolves stale suf...test sourcelib.sql.src.history.refstest: refs advance streams opaque rec...private sourcelib.sql.src.history.refs.RepairStatedeinitprivate sourcelib.sql.src.history.refs.RepairStatefinishprivate sourcelib.sql.src.history.refs.RepairStateinitprivate sourcelib.sql.src.history.refsapplySuffixprivate sourcelib.sql.src.history.refshasStage+2 morehistory.refsadvance
Static calls · unresolved targets: 1 · external targets: 3.
Called byCallstest sourcelib.sql.src.history.refstest: refs snapshot load rejects a co...test sourcelib.sql.src.history.refstest: refs snapshot load returns null...test sourcelib.sql.src.history.refstest: refs snapshot round-trips entri...history.refsloadExistinghistory.refsload
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallshistory.refsloadtest sourcelib.sql.src.history.refstest: refs advance rejects a hash-val...test sourcelib.sql.src.history.refstest: refs advance rejects a ref befo...test sourcelib.sql.src.history.refstest: refs advance resolves stale suf...private sourcelib.sql.src.history.refsdecodehistory.refspathForhistory.refsloadExisting
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callshistory.refsloadExistinghistory.refsstorehistory.refspathFor
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.sql.src.history.refstest: refs snapshot load rejects a co...test sourcelib.sql.src.history.refstest: refs snapshot round-trips entri...private sourcelib.sql.src.history.refsencodehistory.refspathForhistory.refsstore
Static calls · unresolved targets: 1 · external targets: 4.

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.

Audit

Definitions19
Public names20
Members8
Version26.7.0
Revisiondaab053ee433