Skip to documentation
SLOP

tiny.smg.storage.database.lifecycle

Reference tiny.smg storage database lifecycle

Defined in storage.database.

API (12)

Actions

Public operations.

Types and contracts

Public types and contracts.

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

Source

Called byCallsNo direct callsstorage.database.lifecyclecommitIfDirtystorage.database.lifecyclecheckpointIfWalLarge
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callsstorage.database.lifecyclecommitIfDirtyprivate; no linktools.smg.src.storage.database.lifecycleensureSchematest; no linktools.smg.src.storage.database.lifecycletest: smg database rebuilds unreadabl...test; no linktools.smg.src.storage.database.lifecycletest: smg database round-trips nodes ...test; no linktools.smg.src.storage.database.lifecycletest: smg database updates and delete...storage.database.lifecyclecommit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callersstorage.database.lifecyclecheckpointIfWalLargestorage.database.lifecyclecommitstorage.database.lifecyclecommitIfDirty
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallstest; no linktools.smg.src.storage.database.lifecycletest: smg database installs query ind...test; no linktools.smg.src.storage.database.lifecycletest: smg database rebuilds unreadabl...test; no linktools.smg.src.storage.database.lifecycletest: smg database round-trips nodes ...test; no linktools.smg.src.storage.database.lifecycletest: smg database tolerates empty gr...test; no linktools.smg.src.storage.database.lifecycletest: smg database updates and delete...tiny.plucktoplevel.lifecycledeinitprivate; no linktools.smg.src.storage.database.lifecycleensureSchemastorage.database.lifecycleopen
Static calls · unresolved targets: 0 · external targets: 6.
Called byCallsNo direct callersprivate; no linktools.smg.src.storage.database.lifecyclerelationHasIndexstorage.database.lifecyclereaderSchemaReady
Static calls · unresolved targets: 0 · external targets: 0.

Source: tools/smg/src/storage/database/lifecycle.zig

zig
const std = @import("std");const sql = @import("sql");const storage = @import("../root.zig");const limits_mod = @import("../../limits/root.zig");const paths = storage.paths;const rows = storage.rows;pub const Error = rows.Error || sql.lifecycle.Error;const branch_name = "main";const checkpoint_wal_bytes: usize = 4 * 1024 * 1024;const fs_io = std.Options.debug_io;const database_header = sql.wal.Header{    .sequence = 0x534d_4744,    .salt = .{ .first = 0x534d_4744, .second = 0x4752_5048 },};pub const Database = struct {    allocator: std.mem.Allocator,    workspace: *sql.LifecycleWorkspace,    limits: limits_mod.Storage,    file: *sql.FileDatabase,    history: sql.LazyHistory,    connection: sql.Connection,    recovery: sql.LifecycleRecovery,    pub fn deinit(self: *Database) void {        const allocator = self.allocator;        const workspace = self.workspace;        sql.lifecycle.close(self.file, &self.history, &self.connection);        workspace.deallocate(allocator);        allocator.destroy(workspace);        self.* = undefined;    }};pub const Reader = struct {    allocator: std.mem.Allocator,    workspace: *sql.LifecycleWorkspace,    file: *sql.FileReadOnlyDatabase,    catalog: sql.CatalogReader,    head: sql.Hash,    root: sql.Hash,    pub fn deinit(self: *Reader) void {        const allocator = self.allocator;        const workspace = self.workspace;        self.file.deinit();        self.allocator.destroy(self.file);        workspace.deallocate(allocator);        allocator.destroy(workspace);        self.* = undefined;    }};pub const ReaderOpenResult = union(enum) {    ready: Reader,    repair_required: sql.LifecycleReadRepairReason,};pub fn open(    allocator: std.mem.Allocator,    dir: std.Io.Dir,    limits: limits_mod.Storage,) Error!Database {    const workspace = try allocator.create(sql.LifecycleWorkspace);    errdefer allocator.destroy(workspace);    workspace.* = try sql.LifecycleWorkspace.allocate(allocator, .{        .header = database_header,        .max_wal_bytes = limits.wal_recovery_limit_bytes,        .path_storage = sql.file.PathStorage.Limits.forDirect(.{            .database = paths.database_file_name,            .wal = paths.wal_file_name,        }),    });    errdefer workspace.deallocate(allocator);    var lifecycle = try sql.lifecycle.open(allocator, workspace, dir, .{        .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name },        .history_path = paths.history_file_name,        .branch = branch_name,        .header = database_header,        .max_wal_bytes = limits.wal_recovery_limit_bytes,    });    errdefer lifecycle.deinit();    var database = Database{        .allocator = lifecycle.allocator,        .workspace = workspace,        .limits = limits,        .file = lifecycle.file,        .history = lifecycle.history,        .connection = lifecycle.connection,        .recovery = lifecycle.recovery,    };    try ensureSchema(&database);    return database;}pub fn openReader(    allocator: std.mem.Allocator,    dir: std.Io.Dir,    limits: limits_mod.Storage,) Error!ReaderOpenResult {    const workspace = try allocator.create(sql.LifecycleWorkspace);    errdefer allocator.destroy(workspace);    workspace.* = try sql.LifecycleWorkspace.allocate(allocator, .{        .header = database_header,        .max_wal_bytes = limits.wal_recovery_limit_bytes,        .path_storage = sql.file.PathStorage.Limits.forDirect(.{            .database = paths.database_file_name,            .wal = paths.wal_file_name,        }),    });    errdefer workspace.deallocate(allocator);    const opened = switch (try sql.lifecycle.openReadOnly(allocator, workspace, dir, .{        .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name },        .history_path = paths.history_file_name,        .branch = branch_name,        .header = database_header,        .max_wal_bytes = limits.wal_recovery_limit_bytes,    })) {        .ready => |ready| ready,        .repair_required => |reason| {            workspace.deallocate(allocator);            allocator.destroy(workspace);            return .{ .repair_required = reason };        },    };    return .{ .ready = .{        .allocator = opened.allocator,        .workspace = workspace,        .file = opened.file,        .catalog = opened.catalog,        .head = opened.head,        .root = opened.root,    } };}pub fn readerSchemaReady(reader: *Reader) Error!bool {    return try relationHasIndex(&reader.catalog, reader.allocator, rows.nodes_relation, rows.node_name_index) and        try relationHasIndex(&reader.catalog, reader.allocator, rows.edges_relation, rows.edge_source_index) and        try relationHasIndex(&reader.catalog, reader.allocator, rows.edges_relation, rows.edge_target_index) and        try relationHasIndex(&reader.catalog, reader.allocator, rows.concept_documents_relation, rows.concept_name_index);}pub fn commitIfDirty(opened: *Database) Error!void {    if (!(try opened.connection.checkout()).working.dirty()) return;    _ = try commit(opened);    try checkpointIfWalLarge(opened);}pub fn commit(opened: *Database) Error!sql.Hash {    return try sql.lifecycle.commit(&opened.connection, &opened.history, opened.file);}pub fn checkpointIfWalLarge(opened: *Database) Error!void {    try sql.lifecycle.checkpointIfWalLarge(opened.file, database_header, checkpoint_wal_bytes);}fn checkpoint(opened: *Database) Error!void {    try sql.lifecycle.checkpoint(opened.file, database_header);}fn ensureSchema(opened: *Database) Error!void {    const statements = [_]struct { name: []const u8, source: []const u8 }{        .{ .name = rows.nodes_relation, .source = "CREATE TABLE " ++ rows.nodes_relation ++ " (" ++ rows.node_columns ++ ")" },        .{ .name = rows.edges_relation, .source = "CREATE TABLE " ++ rows.edges_relation ++ " (" ++ rows.edge_columns ++ ")" },        .{ .name = rows.concept_documents_relation, .source = "CREATE TABLE " ++ rows.concept_documents_relation ++ " (" ++ rows.concept_document_columns ++ ")" },    };    var names = try opened.connection.catalog.relationNames(opened.allocator);    defer names.deinit();    var created = false;    for (statements) |statement| {        var exists = false;        for (names.names) |name| {            if (std.mem.eql(u8, name, statement.name)) {                exists = true;                break;            }        }        if (exists) continue;        var result = try opened.connection.execute(opened.allocator, statement.source, .{ .durability = .buffered });        result.deinit(opened.allocator);        created = true;    }    if (created) {        _ = try commit(opened);        try checkpoint(opened);    }    const indexes = [_]struct { relation: []const u8, name: []const u8, columns: []const u8 }{        .{ .relation = rows.nodes_relation, .name = rows.node_name_index, .columns = "name" },        .{ .relation = rows.edges_relation, .name = rows.edge_source_index, .columns = "source" },        .{ .relation = rows.edges_relation, .name = rows.edge_target_index, .columns = "target" },        .{ .relation = rows.concept_documents_relation, .name = rows.concept_name_index, .columns = "name" },    };    for (indexes) |index| {        if (try relationHasIndex(&opened.connection.catalog, opened.allocator, index.relation, index.name)) continue;        const source = try std.fmt.allocPrint(opened.allocator, "CREATE INDEX {s} ON {s} ({s})", .{ index.name, index.relation, index.columns });        defer opened.allocator.free(source);        var result = try opened.connection.execute(opened.allocator, source, .{ .durability = .buffered });        result.deinit(opened.allocator);        _ = try commit(opened);        try checkpoint(opened);    }}fn relationHasIndex(catalog: anytype, allocator: std.mem.Allocator, relation: []const u8, index: []const u8) Error!bool {    var handle = catalog.openRelation(allocator, relation) catch |err| switch (err) {        error.RelationNotFound => return false,        else => return err,    };    defer handle.deinit();    for (handle.index_definitions) |definition| {        if (std.mem.eql(u8, definition.name, index)) return true;    }    return false;}test "smg database round-trips nodes and edges with history" {    const database = @import("root.zig");    const edits = database.edits;    const read = database.read;    const model = @import("../../root.zig").model;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const metadata = [_]model.Pair{        .{ .key = "source", .value = "scan" },        .{ .key = "metrics", .value = "{\"cyclomatic_complexity\":2}", .json = true },    };    const node = model.Node{        .name = "app.main",        .type = model.NodeType.function,        .file = "src/app.zig",        .line = 3,        .end_line = 9,        .docstring = "doc body",        .metadata = &metadata,    };    const edge = model.Edge{        .source = "app",        .target = "app.main",        .rel = model.RelType.contains,    };    var first_head: sql.Hash = undefined;    {        var db = try open(std.testing.allocator, tmp.dir, testingLimits());        defer db.deinit();        try edits.apply(&db, .{            .node_puts = &.{.{ .rowid = 1, .node = node }},            .edge_puts = &.{.{ .rowid = 1, .edge = edge }},        });        try std.testing.expect((try db.connection.checkout()).working.dirty());        first_head = try commit(&db);        try std.testing.expect(!(try db.connection.checkout()).working.dirty());    }    var db = try open(std.testing.allocator, tmp.dir, testingLimits());    defer db.deinit();    try std.testing.expect(!(try db.connection.checkout()).working.dirty());    try std.testing.expect(sql.version.same(first_head, (try db.connection.checkout()).head));    {        var node_handle = try db.connection.catalog.openRelation(std.testing.allocator, rows.nodes_relation);        defer node_handle.deinit();        try std.testing.expectEqual(@as(usize, 1), node_handle.index_definitions.len);        try std.testing.expectEqualStrings(rows.node_name_index, node_handle.index_definitions[0].name);    }    {        var edge_handle = try db.connection.catalog.openRelation(std.testing.allocator, rows.edges_relation);        defer edge_handle.deinit();        try std.testing.expectEqual(@as(usize, 2), edge_handle.index_definitions.len);        try std.testing.expectEqualStrings(rows.edge_source_index, edge_handle.index_definitions[0].name);        try std.testing.expectEqualStrings(rows.edge_target_index, edge_handle.index_definitions[1].name);    }    const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator);    defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes);    try std.testing.expectEqual(@as(usize, 1), nodes.len);    try std.testing.expectEqual(@as(i64, 1), nodes[0].rowid);    try std.testing.expectEqualStrings("app.main", nodes[0].node.name);    try std.testing.expectEqual(@as(?i64, 3), nodes[0].node.line);    try std.testing.expectEqual(@as(?i64, 9), nodes[0].node.end_line);    try std.testing.expectEqualStrings("doc body", nodes[0].node.docstring.?);    try std.testing.expectEqualStrings("scan", model.pairValue(nodes[0].node.metadata, "source").?);    try std.testing.expectEqualStrings("{\"cyclomatic_complexity\":2}", model.pairValue(nodes[0].node.metadata, "metrics").?);    try std.testing.expect(nodes[0].node.metadata[1].json);    const edges = try read.databaseEdges(&db, std.testing.allocator, std.testing.allocator);    defer rows.freeEdgeRowSlice(std.testing.allocator, std.testing.allocator, edges);    try std.testing.expectEqual(@as(usize, 1), edges.len);    try std.testing.expectEqualStrings("app", edges[0].edge.source);    try std.testing.expectEqualStrings("app.main", edges[0].edge.target);    try std.testing.expectEqual(@as(usize, 0), edges[0].edge.metadata.len);}test "smg database updates and deletes survive checkpointed reopen" {    const database = @import("root.zig");    const edits = database.edits;    const read = database.read;    const model = @import("../../root.zig").model;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const kept = model.Node{ .name = "app.kept", .type = model.NodeType.function };    const dropped = model.Node{ .name = "app.dropped", .type = model.NodeType.function };    {        var db = try open(std.testing.allocator, tmp.dir, testingLimits());        defer db.deinit();        try edits.apply(&db, .{            .node_puts = &.{ .{ .rowid = 1, .node = kept }, .{ .rowid = 2, .node = dropped } },        });        _ = try commit(&db);        var renamed = kept;        renamed.line = 42;        try edits.apply(&db, .{            .node_puts = &.{.{ .rowid = 1, .node = renamed }},            .node_deletes = &.{2},        });        _ = try commit(&db);        try checkpoint(&db);    }    var db = try open(std.testing.allocator, tmp.dir, testingLimits());    defer db.deinit();    const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator);    defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes);    try std.testing.expectEqual(@as(usize, 1), nodes.len);    try std.testing.expectEqualStrings("app.kept", nodes[0].node.name);    try std.testing.expectEqual(@as(?i64, 42), nodes[0].node.line);}test "smg database rebuilds unreadable history from live state" {    const database = @import("root.zig");    const edits = database.edits;    const read = database.read;    const model = @import("../../root.zig").model;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const node = model.Node{ .name = "app.rebuild", .type = model.NodeType.function };    {        var db = try open(std.testing.allocator, tmp.dir, testingLimits());        defer db.deinit();        try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 1, .node = node }} });        _ = try commit(&db);        try checkpoint(&db);    }    {        var file = try tmp.dir.createFile(fs_io, paths.history_file_name, .{ .read = true, .truncate = false });        var stale_version: [4]u8 = undefined;        std.mem.writeInt(u32, stale_version[0..], 1, .big);        try file.writePositionalAll(fs_io, stale_version[0..], 4);        file.close(fs_io);    }    var db = try open(std.testing.allocator, tmp.dir, testingLimits());    defer db.deinit();    try std.testing.expect(!(try db.connection.checkout()).working.dirty());    const nodes = try read.databaseNodes(&db, std.testing.allocator, std.testing.allocator);    defer rows.freeNodeRowSlice(std.testing.allocator, std.testing.allocator, nodes);    try std.testing.expectEqual(@as(usize, 1), nodes.len);    try std.testing.expectEqualStrings("app.rebuild", nodes[0].node.name);    const next = model.Node{ .name = "app.next", .type = model.NodeType.function };    try edits.apply(&db, .{ .node_puts = &.{.{ .rowid = 2, .node = next }} });    _ = try commit(&db);}test "smg database tolerates empty graph edits" {    const database = @import("root.zig");    const edits = database.edits;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var db = try open(std.testing.allocator, tmp.dir, testingLimits());    defer db.deinit();    try edits.apply(&db, .{});    try std.testing.expect(!(try db.connection.checkout()).working.dirty());}test "smg database installs query indexes on existing unindexed relations" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    {        var raw = try sql.lifecycle.openForTesting(std.testing.allocator, tmp.dir, .{            .paths = .{ .database = paths.database_file_name, .wal = paths.wal_file_name },            .history_path = paths.history_file_name,            .branch = branch_name,            .header = database_header,            .max_wal_bytes = testingLimits().wal_recovery_limit_bytes,        });        defer raw.deinit();        var nodes_result = try raw.connection.execute(std.testing.allocator, "CREATE TABLE nodes (name, type, file, line, end_line, docstring, metadata)", .{ .durability = .buffered });        nodes_result.deinit(std.testing.allocator);        var edges_result = try raw.connection.execute(std.testing.allocator, "CREATE TABLE edges (source, rel, target, metadata)", .{ .durability = .buffered });        edges_result.deinit(std.testing.allocator);        _ = try sql.lifecycle.commit(&raw.connection, &raw.history, raw.file);    }    var upgraded = try open(std.testing.allocator, tmp.dir, testingLimits());    defer upgraded.deinit();    try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.nodes_relation, rows.node_name_index));    try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.edges_relation, rows.edge_source_index));    try std.testing.expect(try relationHasIndex(&upgraded.connection.catalog, std.testing.allocator, rows.edges_relation, rows.edge_target_index));}fn testingLimits() limits_mod.Storage {    return @import("../../root.zig").default_limits.storage;}

Source: tools/smg/src/storage/database/root.zig:2

zig
pub const lifecycle = @import("lifecycle.zig");

Audit

Definitions13
Public names25
Members15
Version26.7.0
Revisiondaab053ee433