tiny.smg.storage.summary
Defined in storage.
API (5)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
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.
tiny.smg.command.status.run[function] attools/smg/src/command/status.zig:12tools.smg.src.concepts.test_concept_publication_sorts_by_name_like_python_storage[function] — test; no exact target attools/smg/src/concepts.zig:884in nearest public ownertiny.smg.conceptstools.smg.src.concepts.test_stale_concept_publication_preserves_the_winner_and_supports_reapply[function] — test; no exact target attools/smg/src/concepts.zig:798in nearest public ownertiny.smg.conceptstools.smg.src.storage.graph.test_matched_publication_preserves_the_graph_and_repairs_summary_and_names[function] — test; no exact target attools/smg/src/storage/graph.zig:582in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.graph.test_stale_graph_publication_preserves_the_winning_generation[function] — test; no exact target attools/smg/src/storage/graph.zig:637in nearest public ownertiny.smg.storage.graphtools.smg.src.storage.summary.test_summary_cache_is_bound_to_the_reader_head_and_stale_reads_do_not_rewrite[function] — test; no exact target attools/smg/src/storage/summary.zig:459in nearest public ownertiny.smg.storage.summarytools.smg.src.storage.summary.test_summary_missing_and_invalid_caches_stream_without_publishing[function] — test; no exact target attools/smg/src/storage/summary.zig:502in nearest public ownertiny.smg.storage.summary
Audit
| Definitions | 6 |
|---|---|
| Public names | 6 |
| Members | 7 |
| Version | 26.7.0 |
| Revision | daab053ee433 |