Skip to documentation
SLOP

tiny.smg.storage.summary

Reference tiny.smg storage summary

Defined in storage.

API (5)

Actions

Public operations.

Types and contracts

Public types and contracts.

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

Source

Called byCallscommand.statusruntest; no linktools.smg.src.conceptstest: concept publication sorts by na...test; no linktools.smg.src.conceptstest: stale concept publication prese...test; no linktools.smg.src.storage.graphtest: matched publication preserves t...test; no linktools.smg.src.storage.graphtest: stale graph publication preserv...+2 morestorage.storeopenReadprivate; no linktools.smg.src.storage.summaryloadStreamingprivate; no linktools.smg.src.storage.summaryreadCachestorage.summaryload
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsconceptspublishprivate; no linktools.smg.src.storage.summarydeinitStreamingGraphprivate; no linktools.smg.src.storage.summarysummarizeCatalogprivate; no linktools.smg.src.storage.summarywriteGraphstorage.summarywriteForHead
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsprivate; no linktools.smg.src.storage.graphfinishPublishTimedtest; no linktools.smg.src.storage.graphtest: matched publication preserves t...private; no linktools.smg.src.storage.summarypublishTestingGraphprivate; no linktools.smg.src.storage.summarydeinitStreamingGraphprivate; no linktools.smg.src.storage.summarysummarizeGraphprivate; no linktools.smg.src.storage.summarywriteGraphstorage.summarywriteGraphForHead
Static calls · unresolved targets: 0 · external targets: 0.

Source: tools/smg/src/storage/root.zig:12

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

Source: tools/smg/src/storage/summary.zig

zig
const std = @import("std");const pretty_json = @import("pretty").json;const sql = @import("sql");const sys = @import("sys");const smg = @import("../root.zig");const storage = smg.storage;const database = storage.database;const rows = storage.rows;const store = storage.store;const graph_mod = smg.graph;const model = smg.model;const cache_name = "summary.json";const temp_cache_name = "summary.json.tmp";const schema = "tiny.smg.storage-summary/v2";pub const Count = struct {    name: []const u8,    count: usize,    first: []const u8,};pub const Graph = struct {    nodes: usize,    edges: usize,    node_types: []const Count,    rel_types: []const Count,};pub fn load(allocator: std.mem.Allocator, root: []const u8, limits: smg.StorageLimits) !Graph {    var opened = try store.openRead(allocator, root, limits);    defer opened.close();    return readCache(allocator, root, opened.reader.head, limits) catch        try loadStreaming(allocator, &opened.reader);}pub fn writeForHead(    allocator: std.mem.Allocator,    root: []const u8,    opened: *database.Database,    head: sql.Hash,) !void {    const checkout = try opened.connection.checkout();    std.debug.assert(!checkout.working.dirty());    std.debug.assert(sql.version.same(checkout.head, head));    var graph_summary = try summarizeCatalog(        allocator,        &opened.connection.catalog,        opened.allocator,    );    defer deinitStreamingGraph(allocator, &graph_summary);    try writeGraph(allocator, root, head, graph_summary);}pub fn writeGraphForHead(    allocator: std.mem.Allocator,    root: []const u8,    graph: graph_mod.Graph,    head: sql.Hash,) !void {    var graph_summary = try summarizeGraph(allocator, graph);    defer deinitStreamingGraph(allocator, &graph_summary);    try writeGraph(allocator, root, head, graph_summary);}fn loadStreaming(allocator: std.mem.Allocator, reader: *database.Reader) !Graph {    return try summarizeCatalog(allocator, &reader.catalog, reader.allocator);}fn summarizeCatalog(    allocator: std.mem.Allocator,    catalog: anytype,    scratch: std.mem.Allocator,) !Graph {    const nodes = try relationEntryCount(catalog, scratch, rows.nodes_relation);    const edges = try relationEntryCount(catalog, scratch, rows.edges_relation);    const node_types = try nodeCounts(allocator, catalog, scratch);    errdefer deinitCountSlice(allocator, node_types);    const rel_types = try edgeCounts(allocator, catalog, scratch);    return .{        .nodes = nodes,        .edges = edges,        .node_types = node_types,        .rel_types = rel_types,    };}fn summarizeGraph(    allocator: std.mem.Allocator,    graph: graph_mod.Graph,) !Graph {    var node_types: std.ArrayList(Count) = .empty;    errdefer deinitCounts(allocator, &node_types);    for (graph.nodes.items) |node| {        try record(allocator, &node_types, node.type, node.name);    }    std.mem.sort(Count, node_types.items, {}, firstLess);    var rel_types: std.ArrayList(Count) = .empty;    errdefer deinitCounts(allocator, &rel_types);    var key: std.ArrayList(u8) = .empty;    defer key.deinit(allocator);    for (graph.edges.items) |edge| {        key.clearRetainingCapacity();        try edgeKey(&key, allocator, edge.source, edge.rel, edge.target);        try record(allocator, &rel_types, edge.rel, key.items);    }    std.mem.sort(Count, rel_types.items, {}, firstLess);    const owned_node_types = try node_types.toOwnedSlice(allocator);    errdefer deinitCountSlice(allocator, owned_node_types);    const owned_rel_types = try rel_types.toOwnedSlice(allocator);    return .{        .nodes = graph.nodes.items.len,        .edges = graph.edges.items.len,        .node_types = owned_node_types,        .rel_types = owned_rel_types,    };}fn deinitStreamingGraph(allocator: std.mem.Allocator, graph: *Graph) void {    deinitCountSlice(allocator, graph.node_types);    deinitCountSlice(allocator, graph.rel_types);    graph.* = undefined;}fn deinitCountSlice(allocator: std.mem.Allocator, counts: []const Count) void {    for (counts) |count| {        allocator.free(count.name);        allocator.free(count.first);    }    allocator.free(counts);}fn relationEntryCount(catalog: anytype, scratch: std.mem.Allocator, relation: []const u8) !usize {    var handle = try catalog.openRelation(scratch, relation);    defer handle.deinit();    return try handle.relation.table.count();}fn nodeCounts(    allocator: std.mem.Allocator,    catalog: anytype,    scratch: std.mem.Allocator,) ![]Count {    var handle = try catalog.openRelation(scratch, rows.nodes_relation);    defer handle.deinit();    var scan: sql.TableScan = undefined;    try handle.relation.scan(&scan, scratch, null, null);    defer scan.deinit();    var counts: std.ArrayList(Count) = .empty;    errdefer deinitCounts(allocator, &counts);    while (try scan.next()) |entry| {        try record(            allocator,            &counts,            try rows.nodeTypeView(entry.bytes),            try rows.nodeNameView(entry.bytes),        );    }    std.mem.sort(Count, counts.items, {}, firstLess);    return try counts.toOwnedSlice(allocator);}fn edgeCounts(    allocator: std.mem.Allocator,    catalog: anytype,    scratch: std.mem.Allocator,) ![]Count {    var handle = try catalog.openRelation(scratch, rows.edges_relation);    defer handle.deinit();    var scan: sql.TableScan = undefined;    try handle.relation.scan(&scan, scratch, null, null);    defer scan.deinit();    var counts: std.ArrayList(Count) = .empty;    errdefer deinitCounts(allocator, &counts);    var key: std.ArrayList(u8) = .empty;    defer key.deinit(allocator);    while (try scan.next()) |entry| {        const source = try rows.edgeSourceView(entry.bytes);        const rel = try rows.edgeRelView(entry.bytes);        const target = try rows.edgeTargetView(entry.bytes);        key.clearRetainingCapacity();        try edgeKey(&key, allocator, source, rel, target);        try record(allocator, &counts, rel, key.items);    }    std.mem.sort(Count, counts.items, {}, firstLess);    return try counts.toOwnedSlice(allocator);}fn edgeKey(    key: *std.ArrayList(u8),    allocator: std.mem.Allocator,    source: []const u8,    rel: []const u8,    target: []const u8,) !void {    try key.appendSlice(allocator, source);    try key.append(allocator, 0);    try key.appendSlice(allocator, rel);    try key.append(allocator, 0);    try key.appendSlice(allocator, target);}fn record(    allocator: std.mem.Allocator,    counts: *std.ArrayList(Count),    name: []const u8,    first: []const u8,) !void {    for (counts.items) |*count| {        if (!std.mem.eql(u8, count.name, name)) continue;        count.count += 1;        if (std.mem.lessThan(u8, first, count.first)) {            const replacement = try allocator.dupe(u8, first);            allocator.free(count.first);            count.first = replacement;        }        return;    }    const owned_name = try allocator.dupe(u8, name);    errdefer allocator.free(owned_name);    const owned_first = try allocator.dupe(u8, first);    errdefer allocator.free(owned_first);    try counts.append(allocator, .{        .name = owned_name,        .count = 1,        .first = owned_first,    });}fn deinitCounts(allocator: std.mem.Allocator, counts: *std.ArrayList(Count)) void {    for (counts.items) |count| {        allocator.free(count.name);        allocator.free(count.first);    }    counts.deinit(allocator);}fn firstLess(_: void, left: Count, right: Count) bool {    return std.mem.lessThan(u8, left.first, right.first);}const CachedCount = struct {    name: []const u8,    count: usize,};const CachedGraph = struct {    schema: []const u8,    head: []const u8,    nodes: usize,    edges: usize,    node_types: []const CachedCount,    rel_types: []const CachedCount,};fn readCache(    allocator: std.mem.Allocator,    root: []const u8,    head: sql.Hash,    limits: smg.StorageLimits,) !Graph {    const path = try cachePath(allocator, root, cache_name);    defer allocator.free(path);    const raw = try sys.fs.readFileAlloc(allocator, path, limits.max_summary_bytes);    defer allocator.free(raw);    var parsed = try std.json.parseFromSlice(CachedGraph, allocator, raw, .{});    defer parsed.deinit();    const cached = parsed.value;    if (!std.mem.eql(u8, cached.schema, schema)) return error.InvalidSummarySchema;    if (!cacheHeadMatches(cached.head, head)) return error.StaleSummary;    const node_types = try cachedCounts(allocator, cached.node_types);    errdefer deinitCountSlice(allocator, node_types);    return .{        .nodes = cached.nodes,        .edges = cached.edges,        .node_types = node_types,        .rel_types = try cachedCounts(allocator, cached.rel_types),    };}fn cacheHeadMatches(cached: []const u8, head: sql.Hash) bool {    const expected = std.fmt.bytesToHex(head, .lower);    return std.mem.eql(u8, cached, expected[0..]);}fn cachedCounts(allocator: std.mem.Allocator, cached: []const CachedCount) ![]Count {    const counts = try allocator.alloc(Count, cached.len);    var initialized: usize = 0;    errdefer {        for (counts[0..initialized]) |count| {            allocator.free(count.name);            allocator.free(count.first);        }        allocator.free(counts);    }    for (cached, counts) |source, *target| {        const name = try allocator.dupe(u8, source.name);        const first = allocator.dupe(u8, source.name) catch |err| {            allocator.free(name);            return err;        };        target.* = .{ .name = name, .count = source.count, .first = first };        initialized += 1;    }    return counts;}fn writeGraph(    allocator: std.mem.Allocator,    root: []const u8,    head: sql.Hash,    graph_summary: Graph,) !void {    var out: std.Io.Writer.Allocating = .init(allocator);    defer out.deinit();    var writer = pretty_json.Writer.init(&out.writer, .minified);    try writer.beginObject();    try writer.objectField("schema");    try writer.write(schema);    try writer.objectField("head");    try writer.hexString(head[0..]);    try writer.objectField("nodes");    try writer.write(graph_summary.nodes);    try writer.objectField("edges");    try writer.write(graph_summary.edges);    try writer.objectField("node_types");    try writeCounts(&writer, graph_summary.node_types);    try writer.objectField("rel_types");    try writeCounts(&writer, graph_summary.rel_types);    try writer.endObject();    try out.writer.writeByte('\n');    const temporary = try cachePath(allocator, root, temp_cache_name);    defer allocator.free(temporary);    const destination = try cachePath(allocator, root, cache_name);    defer allocator.free(destination);    try sys.fs.writeFile(temporary, out.written());    try sys.fs.rename(temporary, destination);}fn writeCounts(writer: *pretty_json.Writer, counts: []const Count) !void {    try writer.beginArray();    for (counts) |count| {        try writer.beginObject();        try writer.objectField("name");        try writer.write(count.name);        try writer.objectField("count");        try writer.write(count.count);        try writer.endObject();    }    try writer.endArray();}fn cachePath(allocator: std.mem.Allocator, root: []const u8, name: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, name });}fn checkRecordAllocationFailures(allocator: std.mem.Allocator) !void {    var counts: std.ArrayList(Count) = .empty;    defer deinitCounts(allocator, &counts);    try record(allocator, &counts, "function", "z.last");    try record(allocator, &counts, "function", "a.first");    try record(allocator, &counts, "type", "m.middle");}const TestingSummary = enum {    write,    omit,};fn publishTestingGraph(    allocator: std.mem.Allocator,    root: []const u8,    graph: graph_mod.Graph,    summary: TestingSummary,) !sql.Hash {    var publisher = try store.beginRepairPublish(        allocator,        root,        smg.default_limits.storage,    );    defer publisher.close();    try storage.sync.replaceGraph(allocator, &publisher.store.database, graph);    const head = (try publisher.store.database.connection.checkout()).head;    if (summary == .write) {        try writeGraphForHead(            allocator,            root,            graph,            head,        );    }    try publisher.finish();    return head;}fn testingRoot(allocator: std.mem.Allocator, offset: i64) ![]const u8 {    const now = @max(0, sys.time.realMilliTimestamp() + offset);    return try std.fmt.allocPrint(allocator, "/tmp/smg-storage-summary-test-{x}", .{        @as(u64, @intCast(now)),    });}test "summary counts clean allocation failure and replacement" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkRecordAllocationFailures,        .{},    );    var counts: std.ArrayList(Count) = .empty;    defer deinitCounts(std.testing.allocator, &counts);    try record(std.testing.allocator, &counts, "function", "z.last");    try record(std.testing.allocator, &counts, "function", "a.first");    try std.testing.expectEqual(@as(usize, 1), counts.items.len);    try std.testing.expectEqual(@as(usize, 2), counts.items[0].count);    try std.testing.expectEqualStrings("a.first", counts.items[0].first);}test "in-memory summary matches the stored graph catalog" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{ .name = "b", .type = model.NodeType.function });    try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module });    try graph_mod.addEdge(&graph, .{        .source = "a",        .target = "b",        .rel = model.RelType.contains,    });    var temporary = std.testing.tmpDir(.{});    defer temporary.cleanup();    var opened = try database.open(allocator, temporary.dir, smg.default_limits.storage);    defer opened.deinit();    try storage.sync.replaceGraph(allocator, &opened, graph);    var direct = try summarizeGraph(allocator, graph);    defer deinitStreamingGraph(allocator, &direct);    var stored = try summarizeCatalog(        allocator,        &opened.connection.catalog,        opened.allocator,    );    defer deinitStreamingGraph(allocator, &stored);    try std.testing.expectEqual(direct.nodes, stored.nodes);    try std.testing.expectEqual(direct.edges, stored.edges);    try expectCountsEqual(direct.node_types, stored.node_types);    try expectCountsEqual(direct.rel_types, stored.rel_types);}fn expectCountsEqual(left: []const Count, right: []const Count) !void {    try std.testing.expectEqual(left.len, right.len);    for (left, right) |a, b| {        try std.testing.expectEqualStrings(a.name, b.name);        try std.testing.expectEqual(a.count, b.count);        try std.testing.expectEqualStrings(a.first, b.first);    }}test "summary cache is bound to the reader head and stale reads do not rewrite" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try testingRoot(allocator, 0);    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    var graph = graph_mod.init(allocator);    defer graph_mod.deinit(&graph);    try graph_mod.addNode(&graph, .{ .name = "a", .type = model.NodeType.module });    const first_head = try publishTestingGraph(allocator, root, graph, .write);    const path = try cachePath(allocator, root, cache_name);    const cached = try sys.fs.readFileAlloc(allocator, path, 1024 * 1024);    const head_text = std.fmt.bytesToHex(first_head, .lower);    try std.testing.expect(std.mem.indexOf(u8, cached, head_text[0..]) != null);    try std.testing.expectEqual(        @as(usize, 1),        (try load(allocator, root, smg.default_limits.storage)).nodes,    );    try writeGraph(allocator, root, first_head, .{        .nodes = 99,        .edges = 0,        .node_types = &.{},        .rel_types = &.{},    });    try std.testing.expectEqual(        @as(usize, 99),        (try load(allocator, root, smg.default_limits.storage)).nodes,    );    const exact_head_cache = try sys.fs.readFileAlloc(allocator, path, 1024 * 1024);    try graph_mod.addNode(&graph, .{ .name = "b", .type = model.NodeType.function });    const second_head = try publishTestingGraph(allocator, root, graph, .omit);    try std.testing.expect(!sql.version.same(first_head, second_head));    try std.testing.expectEqual(        @as(usize, 2),        (try load(allocator, root, smg.default_limits.storage)).nodes,    );    const unchanged = try sys.fs.readFileAlloc(allocator, path, 1024 * 1024);    try std.testing.expectEqualSlices(u8, exact_head_cache, unchanged);}test "summary missing and invalid caches stream without publishing" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try testingRoot(allocator, 1);    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    var graph = graph_mod.init(allocator);    defer graph_mod.deinit(&graph);    try graph_mod.addNode(&graph, .{ .name = "only", .type = model.NodeType.module });    _ = try publishTestingGraph(allocator, root, graph, .omit);    const path = try cachePath(allocator, root, cache_name);    try std.testing.expect(!sys.fs.exists(path));    try std.testing.expectEqual(        @as(usize, 1),        (try load(allocator, root, smg.default_limits.storage)).nodes,    );    try std.testing.expect(!sys.fs.exists(path));    try sys.fs.writeFile(path, "invalid summary");    try std.testing.expectEqual(        @as(usize, 1),        (try load(allocator, root, smg.default_limits.storage)).nodes,    );    const unchanged = try sys.fs.readFileAlloc(allocator, path, 1024);    try std.testing.expectEqualStrings("invalid summary", unchanged);}

Complete caller list for storage.summary.load

7 direct callers.

Audit

Definitions6
Public names6
Members7
Version26.7.0
Revisiondaab053ee433