Skip to documentation
SLOP

tiny.smg.search

Reference tiny.smg search

Defined in tiny.smg.

API (13)

Actions

Public operations.

Types and contracts

Public types and contracts.

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

Source

Called byCallsNo direct callsprivate; no linktools.smg.src.searchcacheMatchesSourceprivate; no linktools.smg.src.searchreplaceCacheFilestest; no linktools.smg.src.searchtest: matched graph publication rebui...test; no linktools.smg.src.searchtest: rebuild and query native search...searchdbPath
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest; no linktools.smg.src.searchtest: rebuild and query native search...searchlocation
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallssearchsearchStoredtest; no linktools.smg.src.searchtest: normalize identifier queries fo...private; no linktools.smg.src.searchidentifierShapedsearchsplitIdentifiersearchnormalizeQuery
Static calls · unresolved targets: 0 · external targets: 7.
Called byCallstest; no linktools.smg.src.searchtest: rebuild and query native search...private; no linktools.smg.src.searchrebuildStoredFromDatabasestorage.storeopensearchrebuildStored
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsprivate; no linktools.smg.src.command.sessiontestSearchStoredtest; 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.searchtest: matched graph publication rebui...test; no linktools.smg.src.searchtest: rebuild and query native search...+3 moreprivate; no linktools.smg.src.searchcacheWorkspaceprivate; no linktools.smg.src.searchinvalidQuerysearchnormalizeQueryprivate; no linktools.smg.src.searchopenCacheReadOnlyprivate; no linktools.smg.src.searchquerySearchIndex+5 moresearchsearchStored
Static calls · unresolved targets: 1 · external targets: 7.
Called byCallsNo direct callstest; no linktools.smg.src.searchtest: rebuild and query native search...searchsnippet
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallssearchnormalizeQuerytest; no linktools.smg.src.searchtest: split identifiers like python s...private; no linktools.smg.src.searchidentifierTokensIntosearchsplitIdentifier
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsconceptspublishprivate; no linktools.smg.src.searchreplaceAndSyncForTestprivate; no linktools.smg.src.searchsyncStoredForTestprivate; no linktools.smg.src.storage.graphfinishPublishTimedprivate; no linktools.smg.src.searchcacheMatchesSourceprivate; no linktools.smg.src.searchrebuildStoredFromDatabaseprivate; no linktools.smg.src.searchsourceForHeadprivate; no linktools.smg.src.searchupdateStoredFromChangesstorage.namesmatchesHeadstorage.nameswriteForHeadsearchsyncStoredFromDatabase
Static calls · unresolved targets: 0 · external targets: 0.

Source: tools/smg/src/root.zig:31

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

Source: tools/smg/src/search.zig

zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const sql = @import("sql");const sys = @import("sys");const graph_mod = @import("graph.zig");const limits_mod = @import("limits/root.zig");const model = @import("model.zig");const storage = @import("storage/root.zig");const fs_io = std.Options.debug_io;pub const Error = error{    InvalidSearchQuery,    SearchCacheRepairRequired,};const db_name = "search.sql";const wal_name = "search.sql.wal";const temp_db_name = "search.sql.tmp";const temp_wal_name = "search.sql.tmp.wal";const cache_schema: u64 = 3;const search_field_names = [_][]const u8{ "name_tokens", "docstring" };pub const SyncKind = enum {    matched,    incremental,    rebuilt,};pub const Hit = struct {    rank: i64,    name: []const u8,    kind: []const u8,    file: ?[]const u8,    line_start: ?i64,    line_end: ?i64,    docstring: ?[]const u8,    score: f64,};pub const Result = struct {    hits: []const Hit,    total: i64,};pub const DocumentPlan = struct {    pub const Limits = struct {        documents: usize,        text_bytes: usize,    };    pub const Capacity = struct {        documents: usize,        text_bytes: usize,        bytes: usize,        pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {            const document_bytes = try alloc_phase.capacity.mul(                usize,                limits.documents,                @sizeOf(sql.SearchDocument),            );            return .{                .documents = limits.documents,                .text_bytes = limits.text_bytes,                .bytes = try alloc_phase.capacity.add(                    usize,                    document_bytes,                    limits.text_bytes,                ),            };        }    };    pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "smg.search_document_plan",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "one_search_document_record_per_persisted_node_row",                        .lifetime = .steady,                        .detail = "one search document record per persisted node row",                    },                    .{                        .id = "one_contiguous_bounded_search_text_region_admitted_bf91a1d01c82",                        .lifetime = .steady,                        .detail = "one contiguous bounded search-text region admitted from exact row views",                    },                },                .excluded = &.{                    "persisted graph relation scans and borrowed row bytes",                    "graph database pager catalog and operating-system read storage",                    "SQL search postings keys lengths write staging WAL and cache database",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(Limits, "documents", "documents"),                    alloc_phase.capacity.bindInput(Limits, "text_bytes", "text_bytes"),                },                .type_selectors = &.{                    alloc_phase.capacity.bindType(sql.SearchDocument, "search_document"),                },                .nodes = &.{                    .{ .input = 0 },                    .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },                    .{ .input = 1 },                    .{ .add = .{ .left = 1, .right = 2 } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .upper_bound,                    .expression = 3,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "checked record and rendered-text arithmetic plus exact acquisitions reject overflow or OOM before the second relation pass",            },            .risks = .{                .transitive = .{                    .status = .open,                    .detail = "identifier rendering and row views are sealed in the witness but lack a transitive allocation-closure certificate",                },                .foreign = .{                    .status = .excluded,                    .detail = "graph and search database effects occur outside the plan-owned slices",                },            },            .obligations = &.{                .{ .key = "smg_search_document_plan_capacity_capacity_model", .role = .capacity_model },                .{ .key = "smg_search_document_plan_capacity_overload", .role = .overload },                .{ .key = "smg_search_document_plan_oom", .role = .overload },                .{ .key = "smg_search_document_plan_sealed", .role = .transitive_risk },                .{ .key = "smg_search_document_plan_integration", .role = .foreign_risk },            },        },        .bindings = .{            .owner = @This(),            .seal = .{                .family = alloc_phase.capacity.selector(@This().activate),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },            .teardown = .{                .family = alloc_phase.capacity.selector(@This().deinit),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },        },    };    phase: alloc_phase.capacity.Phase,    capacity: Capacity,    documents: []sql.SearchDocument,    text: []u8,    documents_filled: usize,    text_filled: usize,    pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!DocumentPlan {        const capacity = try Capacity.derive(limits);        const documents = try documentAllocSlice(sql.SearchDocument, allocator, capacity.documents);        errdefer documentFreeSlice(sql.SearchDocument, allocator, documents);        return .{            .phase = .initialization,            .capacity = capacity,            .documents = documents,            .text = try documentAllocSlice(u8, allocator, capacity.text_bytes),            .documents_filled = 0,            .text_filled = 0,        };    }    pub fn append(self: *DocumentPlan, rowid: i64, name: []const u8, docstring: ?[]const u8) error{CapacityOverflow}!void {        std.debug.assert(self.phase == .initialization);        if (self.documents_filled == self.documents.len) return error.CapacityOverflow;        const text_bytes = try nodeSearchTextBytes(name, docstring);        const text_end = std.math.add(usize, self.text_filled, text_bytes) catch return error.CapacityOverflow;        if (text_end > self.text.len) return error.CapacityOverflow;        const destination = self.text[self.text_filled..text_end];        fillNodeSearchText(destination, name, docstring);        self.documents[self.documents_filled] = .{ .rowid = rowid, .text = destination };        self.documents_filled += 1;        self.text_filled = text_end;    }    pub fn activate(self: *DocumentPlan) void {        std.debug.assert(self.phase == .initialization);        std.debug.assert(self.documents_filled == self.documents.len);        std.debug.assert(self.text_filled == self.text.len);        self.phase = .steady;    }    pub fn deinit(self: *DocumentPlan, allocator: std.mem.Allocator) void {        std.debug.assert(self.phase != .teardown);        self.phase = .teardown;        documentFreeSlice(sql.SearchDocument, allocator, self.documents);        documentFreeSlice(u8, allocator, self.text);        self.* = undefined;    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(DocumentPlan);}const StoredDocuments = struct {    plan: DocumentPlan,    head: sql.Hash,};const DocumentChanges = struct {    added: std.ArrayList(sql.SearchDocument) = .empty,    updated: std.ArrayList(sql.SearchDocument) = .empty,    fn deinit(self: *DocumentChanges, allocator: std.mem.Allocator) void {        self.added.deinit(allocator);        self.updated.deinit(allocator);        self.* = undefined;    }};fn changedDocuments(    allocator: std.mem.Allocator,    node_puts: []const storage.rows.NodePut,) !DocumentPlan {    var text_bytes: usize = 0;    for (node_puts) |put| {        text_bytes = std.math.add(            usize,            text_bytes,            try nodeSearchTextBytes(put.node.name, put.node.docstring),        ) catch return error.CapacityOverflow;    }    var plan = try DocumentPlan.init(allocator, .{        .documents = node_puts.len,        .text_bytes = text_bytes,    });    errdefer plan.deinit(allocator);    for (node_puts) |put| {        try plan.append(put.rowid, put.node.name, put.node.docstring);    }    plan.activate();    return plan;}pub fn rebuildStored(    allocator: std.mem.Allocator,    root: []const u8,    limits: limits_mod.Limits,) !usize {    var opened = try storage.store.open(allocator, root, limits.storage);    defer opened.close();    const head = (try opened.database.connection.checkout()).head;    return try rebuildStoredFromDatabase(        allocator,        root,        &opened.database,        head,        limits.search,    );}pub fn syncStoredFromDatabase(    allocator: std.mem.Allocator,    root: []const u8,    database: *storage.database.Database,    previous_head: sql.Hash,    head: sql.Hash,    node_puts: []const storage.rows.NodePut,    node_deletes: []const i64,    limits: limits_mod.Search,) !SyncKind {    if (node_puts.len == 0 and node_deletes.len == 0 and        try cacheMatchesSource(allocator, root, sourceForHead(head)))    {        if (!(try storage.names.matchesHead(allocator, root, head))) {            try storage.names.writeForHead(allocator, root, database, head);        }        return .matched;    }    if (try updateStoredFromChanges(        allocator,        root,        previous_head,        head,        node_puts,        node_deletes,        limits,    )) {        try storage.names.writeForHead(allocator, root, database, head);        return .incremental;    }    _ = try rebuildStoredFromDatabase(allocator, root, database, head, limits);    return .rebuilt;}fn updateStoredFromChanges(    allocator: std.mem.Allocator,    root: []const u8,    previous_head: sql.Hash,    head: sql.Hash,    node_puts: []const storage.rows.NodePut,    node_deletes: []const i64,    limits: limits_mod.Search,) !bool {    if (node_deletes.len > limits.incremental_delete_count) return false;    var documents = try changedDocuments(allocator, node_puts);    defer documents.deinit(allocator);    if (documents.documents.len > limits.rebuild_batch_documents or        documents.text.len > limits.rebuild_batch_text_bytes)    {        return false;    }    var workspace = try cacheWorkspace(        allocator,        sql.file.default_max_wal_bytes,    );    defer workspace.deallocate(allocator);    var cache_database = openCache(allocator, &workspace, root) catch |err| {        if (err == error.FileNotFound) return false;        if (searchCacheReadFailure(err)) return false;        return err;    };    defer cache_database.deinit();    var index = sql.Search.open(&cache_database, .{        .index_prefixes = false,        .field_names = &search_field_names,    }) catch |err| {        if (searchCacheReadFailure(err)) return false;        return err;    };    const source_matches = index.sourceMatches(        allocator,        sourceForHead(previous_head),    ) catch |err| {        if (incrementalRepairRequired(err)) return false;        return err;    };    if (!source_matches) return false;    var changes = (try classifyChangedDocuments(        allocator,        &index,        documents.documents,    )) orelse return false;    defer changes.deinit(allocator);    if (!(try applyDocumentChanges(        allocator,        &index,        changes,        node_deletes,    ))) return false;    return try advanceIncrementalCache(        allocator,        &cache_database,        &index,        previous_head,        head,    );}fn classifyChangedDocuments(    allocator: std.mem.Allocator,    index: *sql.Search,    documents: []const sql.SearchDocument,) !?DocumentChanges {    var changes: DocumentChanges = .{};    errdefer changes.deinit(allocator);    try changes.added.ensureTotalCapacityPrecise(allocator, documents.len);    try changes.updated.ensureTotalCapacityPrecise(allocator, documents.len);    for (documents) |document| {        const previous = index.getText(allocator, document.rowid) catch |err| {            if (incrementalRepairRequired(err)) {                changes.deinit(allocator);                return null;            }            return err;        };        if (previous) |text| {            defer allocator.free(text);            if (!std.mem.eql(u8, text, document.text)) {                changes.updated.appendAssumeCapacity(document);            }        } else {            changes.added.appendAssumeCapacity(document);        }    }    return changes;}fn applyDocumentChanges(    allocator: std.mem.Allocator,    index: *sql.Search,    changes: DocumentChanges,    node_deletes: []const i64,) !bool {    for (node_deletes) |rowid| {        _ = index.delete(allocator, rowid, .{ .durability = .buffered }) catch |err| {            if (incrementalRepairRequired(err)) return false;            return err;        };    }    if (changes.added.items.len != 0) {        _ = index.loadAllNew(            allocator,            changes.added.items,            .{ .durability = .buffered },        ) catch |err| {            if (incrementalRepairRequired(err)) return false;            return err;        };    }    if (changes.updated.items.len != 0) {        _ = index.putAll(            allocator,            changes.updated.items,            .{ .durability = .buffered },        ) catch |err| {            if (incrementalRepairRequired(err)) return false;            return err;        };    }    return true;}fn advanceIncrementalCache(    allocator: std.mem.Allocator,    cache_database: *sql.FileDatabase,    index: *sql.Search,    previous_head: sql.Hash,    head: sql.Hash,) !bool {    const advanced = index.advanceSource(        allocator,        sourceForHead(previous_head),        sourceForHead(head),        .{ .durability = .buffered },    ) catch |err| {        if (incrementalRepairRequired(err)) return false;        return err;    };    if (advanced == null) return false;    try cache_database.syncWal();    _ = try cache_database.checkpoint(.{ .restart_header = cacheHeader(2) });    return true;}fn incrementalRepairRequired(err: anyerror) bool {    if (searchCacheReadFailure(err)) return true;    return switch (err) {        error.KeyNotFound,        error.SearchIndexRebuildRequired,        error.TransactionTooLarge,        => true,        else => false,    };}fn rebuildStoredFromDatabase(    allocator: std.mem.Allocator,    root: []const u8,    database: *storage.database.Database,    head: sql.Hash,    limits: limits_mod.Search,) !usize {    var stored = try storedDocuments(allocator, database, head);    defer stored.plan.deinit(allocator);    try rebuildDocuments(        allocator,        root,        stored.plan.documents,        sourceForHead(stored.head),        limits,    );    try storage.names.writeForHead(allocator, root, database, stored.head);    return stored.plan.documents.len;}fn storedDocuments(allocator: std.mem.Allocator, database: *storage.database.Database, head: sql.Hash) !StoredDocuments {    const limits = try storedDocumentLimits(allocator, database);    var plan = try DocumentPlan.init(allocator, limits);    errdefer plan.deinit(allocator);    try fillStoredDocuments(allocator, database, &plan);    plan.activate();    return .{        .plan = plan,        .head = head,    };}fn storedDocumentLimits(allocator: std.mem.Allocator, database: *storage.database.Database) !DocumentPlan.Limits {    var handle = database.connection.catalog.openRelation(allocator, storage.rows.nodes_relation) catch |err| switch (err) {        error.RelationNotFound => return .{ .documents = 0, .text_bytes = 0 },        else => return err,    };    defer handle.deinit();    var scan: sql.TableScan = undefined;    try handle.relation.scan(&scan, allocator, null, null);    defer scan.deinit();    var documents: usize = 0;    var text_bytes: usize = 0;    while (try scan.next()) |entry| {        const name = try storage.rows.nodeNameView(entry.bytes);        const docstring = try storage.rows.nodeDocstringView(entry.bytes);        text_bytes = std.math.add(usize, text_bytes, try nodeSearchTextBytes(name, docstring)) catch return error.CapacityOverflow;        documents = std.math.add(usize, documents, 1) catch return error.CapacityOverflow;    }    const limits = DocumentPlan.Limits{        .documents = documents,        .text_bytes = text_bytes,    };    _ = try DocumentPlan.Capacity.derive(limits);    return limits;}fn fillStoredDocuments(allocator: std.mem.Allocator, database: *storage.database.Database, plan: *DocumentPlan) !void {    var handle = database.connection.catalog.openRelation(allocator, storage.rows.nodes_relation) catch |err| switch (err) {        error.RelationNotFound => if (plan.documents.len == 0) return else return error.MalformedRow,        else => return err,    };    defer handle.deinit();    var scan: sql.TableScan = undefined;    try handle.relation.scan(&scan, allocator, null, null);    defer scan.deinit();    while (try scan.next()) |entry| {        const name = try storage.rows.nodeNameView(entry.bytes);        const docstring = try storage.rows.nodeDocstringView(entry.bytes);        try plan.append(entry.rowid, name, docstring);    }    if (plan.documents_filled != plan.documents.len or plan.text_filled != plan.text.len) return error.MalformedRow;}fn rebuildDocuments(    allocator: std.mem.Allocator,    root: []const u8,    documents: []const sql.SearchDocument,    source: sql.SearchSource,    limits: limits_mod.Search,) !void {    const dir_path = try cacheDir(allocator, root);    defer allocator.free(dir_path);    try sys.fs.createDirPath(dir_path);    try deleteTempCacheFiles(allocator, root);    errdefer deleteTempCacheFiles(allocator, root) catch {};    var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{});    defer dir.close(fs_io);    var workspace = try cacheWorkspace(allocator, limits.rebuild_wal_bytes);    defer workspace.deallocate(allocator);    {        var cache_database = try sql.FileDatabase.open(allocator, &workspace, dir, .{            .paths = .{ .database = temp_db_name, .wal = temp_wal_name },            .header = cacheHeader(1),            .max_wal_bytes = limits.rebuild_wal_bytes,        });        defer cache_database.deinit();        var cache = try sql.Search.open(&cache_database, .{ .index_prefixes = false, .field_names = &search_field_names });        try loadDocumentBatches(&cache_database, &cache, allocator, documents, source, .{            .documents = limits.rebuild_batch_documents,            .text_bytes = limits.rebuild_batch_text_bytes,        });    }    try replaceCacheFiles(allocator, root);}const DocumentBatchLimits = struct {    documents: usize,    text_bytes: usize,};fn loadDocumentBatches(    database: *sql.FileDatabase,    cache: *sql.Search,    allocator: std.mem.Allocator,    documents: []const sql.SearchDocument,    source: sql.SearchSource,    limits: DocumentBatchLimits,) !void {    if (documents.len == 0) {        _ = try cache.loadAllForSource(allocator, documents, source, .{ .durability = .buffered });        try database.syncWal();        _ = try database.checkpoint(.{ .restart_header = cacheHeader(2) });        return;    }    var start: usize = 0;    var first = true;    while (start < documents.len) {        var end = documentBatchEnd(documents, start, limits);        while (true) {            loadDocumentBatch(cache, allocator, documents[start..end], source, first) catch |err| switch (err) {                error.TransactionTooLarge => {                    if (end - start == 1) return err;                    end = start + (end - start) / 2;                    continue;                },                else => return err,            };            break;        }        const final = end == documents.len;        if (final) try database.syncWal();        _ = try database.checkpoint(.{ .restart_header = cacheHeader(if (final) 2 else 1) });        start = end;        first = false;    }}fn loadDocumentBatch(    cache: *sql.Search,    allocator: std.mem.Allocator,    documents: []const sql.SearchDocument,    source: sql.SearchSource,    first: bool,) !void {    if (first) {        _ = try cache.loadAllForSource(allocator, documents, source, .{ .durability = .buffered });    } else {        _ = try cache.loadAllNew(allocator, documents, .{ .durability = .buffered });    }}fn documentBatchEnd(documents: []const sql.SearchDocument, start: usize, limits: DocumentBatchLimits) usize {    std.debug.assert(limits.documents > 0);    std.debug.assert(limits.text_bytes >= sql.SearchMaxDocumentBytes);    std.debug.assert(start < documents.len);    var end = start;    var text_bytes: usize = 0;    while (end < documents.len and end - start < limits.documents) : (end += 1) {        const document_bytes = @min(documents[end].text.len, sql.SearchMaxDocumentBytes);        if (document_bytes > limits.text_bytes - text_bytes) break;        text_bytes += document_bytes;    }    std.debug.assert(end > start);    return end;}fn sourceForHead(head: sql.Hash) sql.SearchSource {    return .{ .schema = cache_schema, .head = head };}pub fn searchStored(    allocator: std.mem.Allocator,    root: []const u8,    raw_query: []const u8,    kind: ?[]const u8,    limit: i64,    limits: limits_mod.Storage,) !Result {    const normalized = try normalizeQuery(allocator, raw_query);    defer allocator.free(normalized);    if (invalidQuery(normalized)) return Error.InvalidSearchQuery;    var opened = try storage.store.openRead(allocator, root, limits);    defer opened.close();    var workspace = try cacheWorkspace(        allocator,        sql.file.default_max_wal_bytes,    );    defer workspace.deallocate(allocator);    var database = try openCacheReadOnly(allocator, &workspace, root);    defer database.deinit();    const index = sql.search.Reader.open(database.snapshot(), .{        .index_prefixes = false,        .field_names = &search_field_names,    }) catch |err| {        if (searchCacheReadFailure(err)) return Error.SearchCacheRepairRequired;        return err;    };    const source_matches = index.sourceMatches(allocator, sourceForHead(opened.reader.head)) catch |err| {        if (searchCacheReadFailure(err)) return Error.SearchCacheRepairRequired;        return err;    };    if (!source_matches) return Error.SearchCacheRepairRequired;    const index_limit = searchIndexLimit(kind, limit);    var matched = querySearchIndex(&index, allocator, normalized, index_limit) catch |err| {        if (searchCacheReadFailure(err)) return Error.SearchCacheRepairRequired;        return err;    };    defer matched.deinit();    return try selectStoredMatches(allocator, &opened.reader, matched.hits, matched.total, kind, limit);}fn searchCacheReadFailure(err: anyerror) bool {    return switch (err) {        error.ColumnOutOfBounds,        error.InvalidChecksum,        error.InvalidDatabaseFile,        error.InvalidKey,        error.InvalidPage,        error.InvalidPageId,        error.InvalidRange,        error.InvalidRecord,        error.InvalidRow,        error.InvalidSearchIndex,        error.InvalidWal,        error.OutputTooSmall,        error.SecondaryIndexCorrupt,        error.TreeIdentityMissing,        error.TreeSpaceMismatch,        error.TreeTooDeep,        error.UnsupportedPageSize,        => true,        else => false,    };}fn searchIndexLimit(kind: ?[]const u8, limit: i64) usize {    if (kind != null or limit <= 0) return 0;    return @intCast(limit);}fn querySearchIndex(index: anytype, allocator: std.mem.Allocator, query: []const u8, limit: usize) !sql.SearchResults {    if (limit == 0) return index.query(allocator, query, 0) catch |err| switch (err) {        error.InvalidQuery => Error.InvalidSearchQuery,        else => return err,    };    return index.queryScoreBand(allocator, query, limit) catch |err| switch (err) {        error.InvalidQuery => Error.InvalidSearchQuery,        else => return err,    };}pub fn dbPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, db_name });}pub fn splitIdentifier(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {    const empty = @constCast((&[_]u8{})[0..]);    const bytes = try identifierTokensInto(empty, name);    const out = try allocator.alloc(u8, bytes);    errdefer allocator.free(out);    const filled = try identifierTokensInto(out, name);    std.debug.assert(filled == out.len);    return out;}const IdentifierSink = struct {    out: []u8,    produced: usize = 0,    last_space: bool = false,    fn append(self: *IdentifierSink, byte: u8) error{CapacityOverflow}!void {        if (self.produced == std.math.maxInt(usize)) return error.CapacityOverflow;        if (self.produced < self.out.len) self.out[self.produced] = byte;        self.produced += 1;        self.last_space = byte == ' ';    }    fn appendSpace(self: *IdentifierSink) error{CapacityOverflow}!void {        if (self.produced == 0 or self.last_space) return;        try self.append(' ');    }    fn finish(self: *IdentifierSink) usize {        if (self.produced != 0 and self.last_space) self.produced -= 1;        return self.produced;    }};fn identifierTokensInto(out: []u8, name: []const u8) error{CapacityOverflow}!usize {    var sink = IdentifierSink{ .out = out };    var previous: ?u8 = null;    for (name, 0..) |byte, index| {        if (byte == '.' or byte == '_') {            try sink.appendSpace();            previous = ' ';            continue;        }        const next = if (index + 1 < name.len) name[index + 1] else 0;        if (previous) |prev| {            if (needsCamelSpace(prev, byte, next)) try sink.appendSpace();        }        try sink.append(std.ascii.toLower(byte));        previous = byte;    }    return sink.finish();}pub fn normalizeQuery(allocator: std.mem.Allocator, raw: []const u8) ![]const u8 {    const trimmed = std.mem.trim(u8, raw, " \t\r\n");    if (trimmed.len == 0) return try allocator.dupe(u8, trimmed);    if (!identifierShaped(trimmed)) return try allocator.dupe(u8, trimmed);    var words = std.mem.splitAny(u8, trimmed, " \t\r\n");    while (words.next()) |word| {        if (word.len == 0) continue;        if (std.mem.eql(u8, word, "AND") or std.mem.eql(u8, word, "OR") or std.mem.eql(u8, word, "NOT") or std.mem.eql(u8, word, "NEAR")) return try allocator.dupe(u8, trimmed);    }    const split = try splitIdentifier(allocator, trimmed);    defer allocator.free(split);    if (split.len == 0) return try allocator.dupe(u8, trimmed);    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    var parts = std.mem.splitScalar(u8, split, ' ');    var first = true;    while (parts.next()) |part| {        if (part.len == 0) continue;        if (!first) try out.writer.writeAll(" AND ");        first = false;        try out.writer.writeAll(part);    }    if (first) return try allocator.dupe(u8, trimmed);    return try out.toOwnedSlice();}pub fn location(allocator: std.mem.Allocator, hit: Hit) ![]const u8 {    if (hit.file) |file| {        if (hit.line_start) |line| return try std.fmt.allocPrint(allocator, "{s}:{d}", .{ file, line });        return file;    }    return "-";}pub fn snippet(hit: Hit) []const u8 {    const doc = hit.docstring orelse return "-";    if (doc.len == 0) return "-";    const first_line = if (std.mem.indexOfScalar(u8, doc, '\n')) |index| doc[0..index] else doc;    const trimmed = std.mem.trim(u8, first_line, " \t\r\n");    return trimmed[0..@min(trimmed.len, 80)];}fn cacheDir(allocator: std.mem.Allocator, root: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name });}fn walPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, wal_name });}fn tempDbPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, temp_db_name });}fn tempWalPath(allocator: std.mem.Allocator, root: []const u8) ![]const u8 {    return try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name, temp_wal_name });}fn cacheMatchesSource(allocator: std.mem.Allocator, root: []const u8, source: sql.SearchSource) !bool {    const path = try dbPath(allocator, root);    defer allocator.free(path);    if (!sys.fs.exists(path)) return false;    var workspace = try cacheWorkspace(        allocator,        sql.file.default_max_wal_bytes,    );    defer workspace.deallocate(allocator);    var database = openCache(allocator, &workspace, root) catch |err| {        if (searchCacheReadFailure(err)) return false;        return err;    };    defer database.deinit();    const index = sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names }) catch |err| {        if (searchCacheReadFailure(err)) return false;        return err;    };    return index.sourceMatches(allocator, source) catch |err| {        if (searchCacheReadFailure(err)) return false;        return err;    };}fn deleteTempCacheFiles(allocator: std.mem.Allocator, root: []const u8) !void {    const database_path = try tempDbPath(allocator, root);    defer allocator.free(database_path);    const wal_path = try tempWalPath(allocator, root);    defer allocator.free(wal_path);    try deleteOptionalFile(database_path);    try deleteOptionalFile(wal_path);}fn deleteOptionalFile(path: []const u8) !void {    sys.fs.deleteFile(path) catch |err| switch (err) {        error.FileNotFound => {},        else => return err,    };}fn replaceCacheFiles(allocator: std.mem.Allocator, root: []const u8) !void {    const wal_path = try walPath(allocator, root);    defer allocator.free(wal_path);    const temporary_database_path = try tempDbPath(allocator, root);    defer allocator.free(temporary_database_path);    const database_path = try dbPath(allocator, root);    defer allocator.free(database_path);    const temporary_wal_path = try tempWalPath(allocator, root);    defer allocator.free(temporary_wal_path);    try deleteOptionalFile(wal_path);    try sys.fs.rename(temporary_database_path, database_path);    try deleteOptionalFile(wal_path);    try sys.fs.rename(temporary_wal_path, wal_path);}fn cacheWorkspace(    allocator: std.mem.Allocator,    max_wal_bytes: usize,) !sql.FileDatabase.Workspace {    return try sql.FileDatabase.Workspace.allocate(        allocator,        cacheWorkspaceLimits(max_wal_bytes),    );}fn cacheWorkspaceLimits(max_wal_bytes: usize) sql.FileDatabase.Workspace.Limits {    return .{        .header = cacheHeader(1),        .max_wal_bytes = max_wal_bytes,        .path_storage = .{            .database_bytes = @max(db_name.len, temp_db_name.len),            .wal_bytes = @max(wal_name.len, temp_wal_name.len),        },    };}test "search cache workspace path capacity spans current and rebuild names" {    const limits = cacheWorkspaceLimits(sql.file.default_max_wal_bytes).path_storage;    try std.testing.expectEqual(        @max(db_name.len, temp_db_name.len),        limits.database_bytes,    );    try std.testing.expectEqual(        @max(wal_name.len, temp_wal_name.len),        limits.wal_bytes,    );}fn openCache(    allocator: std.mem.Allocator,    workspace: *sql.FileDatabase.Workspace,    root: []const u8,) !sql.FileDatabase {    const dir_path = try cacheDir(allocator, root);    defer allocator.free(dir_path);    var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{});    defer dir.close(fs_io);    return try sql.FileDatabase.open(allocator, workspace, dir, .{        .paths = .{ .database = db_name, .wal = wal_name },        .header = cacheHeader(3),        .read_cache_capacity = 16,    });}fn openCacheForTesting(    allocator: std.mem.Allocator,    root: []const u8,) !sql.FileDatabase {    const dir_path = try cacheDir(allocator, root);    defer allocator.free(dir_path);    var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{});    defer dir.close(fs_io);    return try sql.FileDatabase.openForTesting(allocator, dir, .{        .paths = .{ .database = db_name, .wal = wal_name },        .header = cacheHeader(3),        .read_cache_capacity = 16,    });}fn openCacheReadOnly(    allocator: std.mem.Allocator,    workspace: *sql.FileDatabase.Workspace,    root: []const u8,) !sql.FileReadOnlyDatabase {    const dir_path = try cacheDir(allocator, root);    defer allocator.free(dir_path);    var dir = try std.Io.Dir.openDirAbsolute(fs_io, dir_path, .{});    defer dir.close(fs_io);    const opened = try sql.FileReadOnlyDatabase.openExisting(allocator, workspace, dir, .{        .paths = .{ .database = db_name, .wal = wal_name },        .header = cacheHeader(3),        .read_cache_capacity = 16,    });    return switch (opened) {        .ready => |ready| ready,        .repair_required => Error.SearchCacheRepairRequired,    };}fn cacheHeader(sequence: u32) sql.wal.Header {    return .{        .sequence = sequence,        .salt = .{ .first = 0x534d_4702 + sequence, .second = 0x5155_4553 + sequence },    };}fn nodeSearchTextBytes(name: []const u8, docstring: ?[]const u8) error{CapacityOverflow}!usize {    const token_bytes = try identifierTokensInto(@constCast((&[_]u8{})[0..]), name);    const doc = docstring orelse return @min(token_bytes, sql.SearchMaxDocumentBytes);    if (doc.len == 0) return @min(token_bytes, sql.SearchMaxDocumentBytes);    const separator_end = std.math.add(usize, token_bytes, 1) catch return error.CapacityOverflow;    const full_bytes = std.math.add(usize, separator_end, doc.len) catch return error.CapacityOverflow;    return @min(full_bytes, sql.SearchMaxDocumentBytes);}fn fillNodeSearchText(out: []u8, name: []const u8, docstring: ?[]const u8) void {    const token_bytes = identifierTokensInto(@constCast((&[_]u8{})[0..]), name) catch unreachable;    const rendered_bytes = identifierTokensInto(out[0..@min(out.len, token_bytes)], name) catch unreachable;    std.debug.assert(rendered_bytes == token_bytes);    const doc = docstring orelse {        std.debug.assert(out.len == @min(token_bytes, sql.SearchMaxDocumentBytes));        return;    };    if (doc.len == 0) {        std.debug.assert(out.len == @min(token_bytes, sql.SearchMaxDocumentBytes));        return;    }    if (token_bytes >= out.len) return;    out[token_bytes] = sql.SearchFieldSeparator;    const doc_start = token_bytes + 1;    @memcpy(out[doc_start..], doc[0 .. out.len - doc_start]);}const SelectedHit = struct {    node: *const model.Node,    score: usize,};fn selectStoredMatches(allocator: std.mem.Allocator, reader: *storage.database.Reader, hits: []const sql.SearchHit, matched_total: usize, kind: ?[]const u8, limit: i64) !Result {    const candidate_hits = storedCandidateHits(hits, kind, limit);    if (candidate_hits.len == 0) return .{ .hits = &.{}, .total = if (kind == null) @intCast(matched_total) else 0 };    const rowids = try hitRowids(allocator, candidate_hits);    defer if (rowids.len != 0) allocator.free(rowids);    const rows = try storage.nodes.rowsByIdFromReader(allocator, reader, rowids);    defer storage.nodes.freeRows(allocator, rows);    var selected: std.ArrayList(SelectedHit) = .empty;    defer selected.deinit(allocator);    const max_hits: usize = if (limit > 0) @intCast(limit) else 0;    const capped = max_hits != 0;    var total: i64 = if (kind == null) @intCast(matched_total) else 0;    for (candidate_hits) |hit| {        const node = nodePointerForStoredRow(rows, hit.rowid) orelse continue;        if (kind != null and !std.mem.eql(u8, node.type, kind.?)) continue;        if (kind != null) total += 1;        const selected_hit: SelectedHit = .{ .node = node, .score = hit.score };        if (!capped or selected.items.len < max_hits) {            try selected.append(allocator, selected_hit);            if (capped and selected.items.len == max_hits) std.mem.sort(SelectedHit, selected.items, {}, selectedHitLess);            continue;        }        if (!selectedHitLess({}, selected_hit, selected.items[selected.items.len - 1])) continue;        selected.items[selected.items.len - 1] = selected_hit;        restoreSelectedOrder(selected.items);    }    std.mem.sort(SelectedHit, selected.items, {}, selectedHitLess);    const out = try allocator.alloc(Hit, selected.items.len);    var filled: usize = 0;    errdefer {        freeHits(allocator, out[0..filled]);        allocator.free(out);    }    for (selected.items, 0..) |selected_hit, index| {        out[index] = try hitFromNode(allocator, selected_hit.node.*, selected_hit.score);        out[index].rank = @intCast(index + 1);        filled += 1;    }    return .{ .hits = out, .total = total };}fn storedCandidateHits(hits: []const sql.SearchHit, kind: ?[]const u8, limit: i64) []const sql.SearchHit {    if (kind != null or limit <= 0) return hits;    const max_hits: usize = @intCast(limit);    if (hits.len <= max_hits) return hits;    const cutoff = hits[max_hits - 1].score;    var count = max_hits;    while (count < hits.len and hits[count].score >= cutoff) count += 1;    return hits[0..count];}fn hitRowids(allocator: std.mem.Allocator, hits: []const sql.SearchHit) ![]i64 {    if (hits.len == 0) return &.{};    const rowids = try allocator.alloc(i64, hits.len);    for (hits, 0..) |hit, index| rowids[index] = hit.rowid;    return rowids;}fn nodePointerForStoredRow(rows: []const storage.nodes.NodeRow, rowid: i64) ?*const model.Node {    var low: usize = 0;    var high = rows.len;    while (low < high) {        const mid = low + (high - low) / 2;        if (rows[mid].rowid == rowid) return &rows[mid].node;        if (rows[mid].rowid < rowid) {            low = mid + 1;        } else {            high = mid;        }    }    return null;}fn restoreSelectedOrder(hits: []SelectedHit) void {    var index = hits.len - 1;    while (index > 0 and selectedHitLess({}, hits[index], hits[index - 1])) : (index -= 1) {        std.mem.swap(SelectedHit, &hits[index], &hits[index - 1]);    }}fn selectedHitLess(_: void, left: SelectedHit, right: SelectedHit) bool {    if (left.score != right.score) return left.score > right.score;    return std.mem.lessThan(u8, left.node.name, right.node.name);}fn hitFromNode(allocator: std.mem.Allocator, node: model.Node, score: usize) !Hit {    const name = try allocator.dupe(u8, node.name);    errdefer allocator.free(name);    const kind = try allocator.dupe(u8, node.type);    errdefer allocator.free(kind);    const file = try dupOptional(allocator, node.file);    errdefer if (file) |value| allocator.free(value);    const docstring = try dupOptional(allocator, node.docstring);    errdefer if (docstring) |value| allocator.free(value);    return .{        .rank = 0,        .name = name,        .kind = kind,        .file = file,        .line_start = node.line,        .line_end = node.end_line,        .docstring = docstring,        .score = @floatFromInt(score),    };}fn freeHits(allocator: std.mem.Allocator, hits: []const Hit) void {    for (hits) |hit| {        allocator.free(hit.name);        allocator.free(hit.kind);        if (hit.file) |file| allocator.free(file);        if (hit.docstring) |docstring| allocator.free(docstring);    }}fn dupOptional(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 {    return if (value) |text| try allocator.dupe(u8, text) else null;}fn invalidQuery(query: []const u8) bool {    const trimmed = std.mem.trim(u8, query, " \t\r\n");    if (trimmed.len == 0) return true;    var offset: usize = 0;    var saw_operand = false;    var expects_operand = true;    while (nextQueryPart(trimmed, &offset)) |part| {        if (!part.quoted and booleanOperator(part.text)) {            if (expects_operand or !saw_operand) return true;            expects_operand = true;            continue;        }        saw_operand = true;        expects_operand = false;    }    return !saw_operand or expects_operand;}const QueryPart = struct {    text: []const u8,    quoted: bool,};fn nextQueryPart(source: []const u8, offset: *usize) ?QueryPart {    while (offset.* < source.len and std.ascii.isWhitespace(source[offset.*])) offset.* += 1;    if (offset.* >= source.len) return null;    if (source[offset.*] == '"') {        const start = offset.* + 1;        var end = start;        while (end < source.len and source[end] != '"') end += 1;        offset.* = if (end < source.len) end + 1 else end;        return .{ .text = source[start..end], .quoted = true };    }    const start = offset.*;    while (offset.* < source.len and !std.ascii.isWhitespace(source[offset.*])) offset.* += 1;    return .{ .text = source[start..offset.*], .quoted = false };}fn booleanOperator(text_value: []const u8) bool {    return std.mem.eql(u8, text_value, "AND") or std.mem.eql(u8, text_value, "OR") or std.mem.eql(u8, text_value, "NOT");}fn identifierShaped(value: []const u8) bool {    for (value) |byte| {        if (std.ascii.isAlphanumeric(byte) or byte == '.' or byte == '_' or byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n') continue;        return false;    }    return true;}fn needsCamelSpace(previous: u8, current: u8, next: u8) bool {    if ((std.ascii.isLower(previous) or std.ascii.isDigit(previous)) and std.ascii.isUpper(current)) return true;    if (std.ascii.isUpper(previous) and std.ascii.isUpper(current) and next != 0 and std.ascii.isLower(next)) return true;    return false;}fn documentAllocSlice(comptime T: type, allocator: std.mem.Allocator, count: usize) std.mem.Allocator.Error![]T {    if (count == 0) return @constCast((&[_]T{})[0..]);    return try allocator.alloc(T, count);}fn documentFreeSlice(comptime T: type, allocator: std.mem.Allocator, values: []T) void {    if (values.len != 0) allocator.free(values);}fn modelDocumentPlanBytes(limits: DocumentPlan.Limits) ?usize {    if (limits.documents > std.math.maxInt(usize) / @sizeOf(sql.SearchDocument)) return null;    const document_bytes = limits.documents * @sizeOf(sql.SearchDocument);    if (document_bytes > std.math.maxInt(usize) - limits.text_bytes) return null;    return document_bytes + limits.text_bytes;}test "search document plan capacity matches an independent bounded text model" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(DocumentPlan, "smg_search_document_plan_capacity_capacity_model"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(DocumentPlan, "smg_search_document_plan_capacity_overload"),            null,            null,            null,            null,            null,            null,        );    }    for (0..64) |documents| {        const limits = DocumentPlan.Limits{            .documents = documents,            .text_bytes = documents * 19,        };        const capacity = try DocumentPlan.Capacity.derive(limits);        try std.testing.expectEqual(modelDocumentPlanBytes(limits).?, capacity.bytes);    }    const overflow = DocumentPlan.Limits{        .documents = std.math.maxInt(usize),        .text_bytes = 1,    };    try std.testing.expect(modelDocumentPlanBytes(overflow) == null);    try std.testing.expectError(error.CapacityOverflow, DocumentPlan.Capacity.derive(overflow));    var long_doc: [sql.SearchMaxDocumentBytes + 64]u8 = undefined;    @memset(&long_doc, 'x');    try std.testing.expectEqual(        @as(usize, sql.SearchMaxDocumentBytes),        try nodeSearchTextBytes("app.main", &long_doc),    );}fn checkDocumentPlanInitAllocationFailures(allocator: std.mem.Allocator) !void {    var plan = try DocumentPlan.init(allocator, .{ .documents = 3, .text_bytes = 64 });    plan.deinit(allocator);}test "search document plan initialization cleans allocation failure and retries" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(DocumentPlan, "smg_search_document_plan_oom"),            null,            null,            null,            null,            null,            null,        );    }    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkDocumentPlanInitAllocationFailures,        .{},    );    const expected = "app main" ++ [_]u8{sql.SearchFieldSeparator} ++ "entry";    var plan = try DocumentPlan.init(std.testing.allocator, .{ .documents = 1, .text_bytes = expected.len });    defer plan.deinit(std.testing.allocator);    try plan.append(7, "app.main", "entry");    plan.activate();    try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, plan.phase);}test "search document plan fills exact records and bounded text while sealed" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(DocumentPlan, "smg_search_document_plan_sealed"),            null,            null,            null,            null,            null,            null,        );    }    const expected = [_][]const u8{        "",        "app main",        "parse html document" ++ [_]u8{sql.SearchFieldSeparator} ++ "Read markup",    };    const limits = DocumentPlan.Limits{        .documents = expected.len,        .text_bytes = expected[0].len + expected[1].len + expected[2].len,    };    var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);    var plan = try DocumentPlan.init(phase_allocator.initializationAllocator(), limits);    defer {        if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();        if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();        plan.deinit(phase_allocator.teardownAllocator());        phase_allocator.deinit();    }    const documents_pointer = plan.documents.ptr;    const text_pointer = plan.text.ptr;    phase_allocator.seal();    try plan.append(3, "", null);    try plan.append(5, "app.main", null);    try plan.append(9, "parseHTMLDocument", "Read markup");    plan.activate();    for (expected, plan.documents) |text_value, document| {        try std.testing.expectEqualStrings(text_value, document.text);    }    try std.testing.expectEqual(@as(i64, 3), plan.documents[0].rowid);    try std.testing.expectEqual(@as(i64, 5), plan.documents[1].rowid);    try std.testing.expectEqual(@as(i64, 9), plan.documents[2].rowid);    try std.testing.expect(plan.documents[1].text.ptr == plan.text[expected[0].len..].ptr);    try std.testing.expectEqual(documents_pointer, plan.documents.ptr);    try std.testing.expectEqual(text_pointer, plan.text.ptr);    try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());}test "stored search document plan preserves rowids and rendered fields without node snapshots" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(DocumentPlan, "smg_search_document_plan_integration"),            null,            null,            null,            null,            null,            null,        );    }    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-search-document-plan-{x}", .{@as(u64, @intCast(@max(0, sys.time.realMilliTimestamp())))});    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    _ = try storage.initProject(allocator, root, testingLimits().storage);    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{        .name = "z.item",        .type = model.NodeType.function,        .docstring = "Last entry",        .metadata = try model.sourcePair(allocator, "scan"),    });    try graph_mod.addNode(&graph, .{        .name = "a.HTTPServer",        .type = model.NodeType.class,        .docstring = "Serve requests",        .metadata = try model.sourcePair(allocator, "manual"),    });    var opened = try storage.store.open(allocator, root, testingLimits().storage);    defer opened.close();    const saved = try storage.graph.replace(allocator, &opened.database, graph);    var stored = try storedDocuments(std.testing.allocator, &opened.database, saved);    defer stored.plan.deinit(std.testing.allocator);    try std.testing.expect(std.mem.eql(u8, &saved, &stored.head));    try std.testing.expectEqual(@as(usize, 2), stored.plan.documents.len);    try std.testing.expectEqual(@as(i64, 1), stored.plan.documents[0].rowid);    try std.testing.expectEqualStrings(        "a http server" ++ [_]u8{sql.SearchFieldSeparator} ++ "Serve requests",        stored.plan.documents[0].text,    );    try std.testing.expectEqual(@as(i64, 2), stored.plan.documents[1].rowid);    try std.testing.expectEqualStrings(        "z item" ++ [_]u8{sql.SearchFieldSeparator} ++ "Last entry",        stored.plan.documents[1].text,    );}test "search document batches cap records and indexed text" {    const large_text = @as([(sql.SearchMaxDocumentBytes / 2 + 1)]u8, @splat('x'));    const documents = [_]sql.SearchDocument{        .{ .rowid = 1, .text = &large_text },        .{ .rowid = 2, .text = &large_text },        .{ .rowid = 3, .text = "tail" },        .{ .rowid = 4, .text = "last" },    };    const text_limited = DocumentBatchLimits{        .documents = documents.len,        .text_bytes = sql.SearchMaxDocumentBytes,    };    try std.testing.expectEqual(@as(usize, 1), documentBatchEnd(&documents, 0, text_limited));    try std.testing.expectEqual(@as(usize, 4), documentBatchEnd(&documents, 1, text_limited));    const record_limited = DocumentBatchLimits{        .documents = 2,        .text_bytes = testingLimits().search.rebuild_batch_text_bytes,    };    try std.testing.expectEqual(@as(usize, 2), documentBatchEnd(&documents, 0, record_limited));    try std.testing.expectEqual(@as(usize, 4), documentBatchEnd(&documents, 2, record_limited));}test "search document batches preserve source and postings across reopen" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const source = sql.SearchSource{        .schema = cache_schema,        .head = @as([32]u8, @splat(7)),    };    const documents = [_]sql.SearchDocument{        .{ .rowid = 1, .text = "shared alpha" },        .{ .rowid = 2, .text = "shared beta" },        .{ .rowid = 3, .text = "shared gamma" },        .{ .rowid = 4, .text = "shared delta" },        .{ .rowid = 5, .text = "shared epsilon" },    };    {        var database = try sql.FileDatabase.openForTesting(std.testing.allocator, tmp.dir, .{            .paths = .{ .database = temp_db_name, .wal = temp_wal_name },            .header = cacheHeader(1),            .max_wal_bytes = testingLimits().search.rebuild_wal_bytes,        });        defer database.deinit();        var cache = try sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names });        try loadDocumentBatches(&database, &cache, std.testing.allocator, &.{}, source, .{            .documents = 2,            .text_bytes = sql.SearchMaxDocumentBytes,        });        try std.testing.expect(try cache.sourceMatches(std.testing.allocator, source));        try loadDocumentBatches(&database, &cache, std.testing.allocator, &documents, source, .{            .documents = 2,            .text_bytes = sql.SearchMaxDocumentBytes,        });        try std.testing.expect(try cache.sourceMatches(std.testing.allocator, source));    }    var reopened = try sql.FileDatabase.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = temp_db_name, .wal = temp_wal_name },        .header = cacheHeader(3),        .max_wal_bytes = testingLimits().search.rebuild_wal_bytes,    });    defer reopened.deinit();    const cache = try sql.Search.open(&reopened, .{ .index_prefixes = false, .field_names = &search_field_names });    try std.testing.expect(try cache.sourceMatches(std.testing.allocator, source));    var shared = try cache.query(std.testing.allocator, "shared", 0);    defer shared.deinit();    try std.testing.expectEqual(documents.len, shared.total);    for (shared.hits, 0..) |hit, index| try std.testing.expectEqual(documents[index].rowid, hit.rowid);    var epsilon = try cache.query(std.testing.allocator, "epsilon", 0);    defer epsilon.deinit();    try std.testing.expectEqual(@as(usize, 1), epsilon.total);    try std.testing.expectEqual(@as(i64, 5), epsilon.hits[0].rowid);}test "search document batches retry within the WAL capacity" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var text: [sql.SearchMaxDocumentBytes]u8 = undefined;    for (&text, 0..) |*byte, index| byte.* = "alpha "[index % "alpha ".len];    var documents: [8]sql.SearchDocument = undefined;    for (&documents, 0..) |*document, index| document.* = .{        .rowid = @intCast(index + 1),        .text = &text,    };    const source = sql.SearchSource{        .schema = cache_schema,        .head = @as([32]u8, @splat(9)),    };    var database = try sql.FileDatabase.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = temp_db_name, .wal = temp_wal_name },        .header = cacheHeader(1),        .max_wal_bytes = 256 * 1024,    });    defer database.deinit();    var cache = try sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names });    try std.testing.expectError(        error.TransactionTooLarge,        cache.loadAllForSource(std.testing.allocator, &documents, source, .{ .durability = .buffered }),    );    try loadDocumentBatches(&database, &cache, std.testing.allocator, &documents, source, .{        .documents = documents.len,        .text_bytes = documents.len * sql.SearchMaxDocumentBytes,    });    try std.testing.expect(try cache.sourceMatches(std.testing.allocator, source));    var alpha = try cache.query(std.testing.allocator, "alpha", 0);    defer alpha.deinit();    try std.testing.expectEqual(documents.len, alpha.total);}test "split identifiers like python search schema" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    try std.testing.expectEqualStrings("smg cli helpers truncate", try splitIdentifier(allocator, "smg.cli.helpers._truncate"));    try std.testing.expectEqualStrings("parse html document", try splitIdentifier(allocator, "parseHTMLDocument"));    try std.testing.expectEqualStrings("smg graph sem graph analyze hot", try splitIdentifier(allocator, "smg.graph.SemGraph.analyze_hot"));}test "normalize identifier queries for fts" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    try std.testing.expectEqualStrings("smg AND cli AND helpers AND truncate", try normalizeQuery(allocator, "smg.cli.helpers._truncate"));    try std.testing.expectEqualStrings("foo OR", try normalizeQuery(allocator, "foo OR"));    try std.testing.expectEqualStrings("foo AND or AND bar", try normalizeQuery(allocator, "foo or bar"));}test "reject malformed search queries like python sqlite fts" {    try std.testing.expect(invalidQuery(""));    try std.testing.expect(invalidQuery("foo OR"));    try std.testing.expect(invalidQuery("foo AND"));    try std.testing.expect(invalidQuery("foo NOT"));    try std.testing.expect(invalidQuery("OR foo"));    try std.testing.expect(invalidQuery("NOT foo"));    try std.testing.expect(invalidQuery("foo AND NOT bar"));    try std.testing.expect(!invalidQuery("foo OR bar"));    try std.testing.expect(!invalidQuery("foo AND bar"));    try std.testing.expect(!invalidQuery("foo NOT bar"));    try std.testing.expect(!invalidQuery("NEAR"));    try std.testing.expect(!invalidQuery("foo AND or AND bar"));}test "sync incrementally applies graph changes and advances the source head" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-search-sync-test-{x}", .{@as(u64, @intCast(@max(0, sys.time.realMilliTimestamp())))});    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name }));    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{        .name = "sync.target",        .type = model.NodeType.function,        .file = "src/sync.py",        .line = 3,        .docstring = "sync probe",    });    const saved = try replaceAndSyncForTest(allocator, graph, root);    try std.testing.expectEqual(SyncKind.rebuilt, saved.kind);    {        var database = try openCacheForTesting(allocator, root);        defer database.deinit();        const index = try sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names });        try std.testing.expect(try index.sourceMatches(allocator, sourceForHead(saved.head)));    }    try std.testing.expectEqual(        SyncKind.matched,        try syncStoredForTest(allocator, root, saved.head),    );    try graph_mod.addNode(&graph, .{        .name = "sync.other",        .type = model.NodeType.function,        .file = "src/sync.py",        .line = 9,        .docstring = "second probe",    });    const changed = try replaceAndSyncForTest(allocator, graph, root);    try std.testing.expectEqual(SyncKind.incremental, changed.kind);    try std.testing.expect(!std.mem.eql(u8, &saved.head, &changed.head));    {        var database = try openCacheForTesting(allocator, root);        defer database.deinit();        const index = try sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names });        try std.testing.expect(try index.sourceMatches(allocator, sourceForHead(changed.head)));    }    const result = try searchStored(        allocator,        root,        "sync.other",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), result.total);    const other_index = graph.node_index.get("sync.other").?;    graph.nodes.items[other_index].line = 10;    const metadata = try replaceAndSyncForTest(allocator, graph, root);    try std.testing.expectEqual(SyncKind.incremental, metadata.kind);    const retained = try searchStored(        allocator,        root,        "second probe",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), retained.total);    graph.nodes.items[other_index].docstring = "replacement marker";    const updated = try replaceAndSyncForTest(allocator, graph, root);    try std.testing.expectEqual(SyncKind.incremental, updated.kind);    const replacement = try searchStored(        allocator,        root,        "replacement marker",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), replacement.total);    const retired = try searchStored(        allocator,        root,        "second probe",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 0), retired.total);    try graph_mod.removeNodes(&graph, &.{"sync.other"});    const removed = try replaceAndSyncForTest(allocator, graph, root);    try std.testing.expectEqual(SyncKind.incremental, removed.kind);    const absent = try searchStored(        allocator,        root,        "replacement marker",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 0), absent.total);}test "stored search rejects a mismatched cache source without rebuilding" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-search-source-test-{x}", .{@as(u64, @intCast(@max(0, sys.time.realMilliTimestamp())))});    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name }));    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{        .name = "source.target",        .type = model.NodeType.function,        .docstring = "source probe",    });    const head = try replaceGraphForTest(allocator, graph, root);    _ = try syncStoredForTest(allocator, root, head);    const expected = sourceForHead(head);    const mismatched = sql.SearchSource{ .schema = cache_schema + 1, .head = head };    {        var database = try openCacheForTesting(allocator, root);        defer database.deinit();        var index = try sql.Search.open(&database, .{            .index_prefixes = false,            .field_names = &search_field_names,        });        try std.testing.expect((try index.advanceSource(            allocator,            expected,            mismatched,            .{ .durability = .synced },        )) != null);    }    try std.testing.expectError(        Error.SearchCacheRepairRequired,        searchStored(            allocator,            root,            "source",            null,            10,            testingLimits().storage,        ),    );    var database = try openCacheForTesting(allocator, root);    defer database.deinit();    const index = try sql.Search.open(&database, .{        .index_prefixes = false,        .field_names = &search_field_names,    });    try std.testing.expect(try index.sourceMatches(allocator, mismatched));}test "matched graph publication rebuilds a malformed search cache" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-search-matched-repair-{x}", .{        @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))),    });    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    _ = try storage.initProject(allocator, root, testingLimits().storage);    const source = try storage.graph.loadSnapshot(        allocator,        allocator,        root,        testingLimits().storage,    );    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{        .name = "repair.target",        .type = model.NodeType.function,        .docstring = "matched search repair",    });    const published = try storage.graph.publishTimed(        allocator,        root,        graph,        source.head,        testingLimits(),    );    try sys.fs.writeFile(try dbPath(allocator, root), "invalid search cache");    sys.fs.deleteFile(try walPath(allocator, root)) catch |err| switch (err) {        error.FileNotFound => {},        else => return err,    };    const repaired = try storage.graph.publishMatchedTimed(        allocator,        root,        graph,        published.head,        testingLimits(),    );    try std.testing.expect(sql.version.same(published.head, repaired.head));    try std.testing.expectEqual(@as(u64, 0), repaired.graph_ns);    try std.testing.expectEqual(SyncKind.rebuilt, repaired.index_kind);    try std.testing.expectEqual(@as(usize, 0), repaired.node_puts);    try std.testing.expectEqual(@as(usize, 0), repaired.node_deletes);    const result = try searchStored(        allocator,        root,        "matched search repair",        null,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), result.total);    try std.testing.expect(try storage.names.matchesHead(allocator, root, published.head));}test "rebuild and query native search index" {    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena.deinit();    const allocator = arena.allocator();    const root = try std.fmt.allocPrint(allocator, "/tmp/smg-search-test-{x}", .{@as(u64, @intCast(@max(0, sys.time.realMilliTimestamp())))});    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(try std.fs.path.join(allocator, &.{ root, storage.paths.smg_dir_name }));    var graph = graph_mod.init(allocator);    try graph_mod.addNode(&graph, .{        .name = "smg.cli.helpers._truncate",        .type = model.NodeType.function,        .file = "src/smg/cli/_helpers.py",        .line = 42,        .docstring = "Trim a string to width",    });    try graph_mod.addNode(&graph, .{        .name = "smg.graph.SemGraph",        .type = model.NodeType.class,        .file = "src/smg/graph.py",        .line = 10,        .docstring = "Core graph structure",    });    try graph_mod.addNode(&graph, .{        .name = "aaa.truncate",        .type = model.NodeType.function,        .file = "src/smg/aaa.py",        .line = 7,        .docstring = "Truncate helper",    });    try graph_mod.addNode(&graph, .{        .name = "zzz.truncate",        .type = model.NodeType.class,        .file = "src/smg/zzz.py",        .line = 8,        .docstring = "Truncate class",    });    const saved = try replaceGraphForTest(allocator, graph, root);    try std.testing.expectEqual(        @as(usize, 4),        try rebuildStored(allocator, root, testingLimits()),    );    try std.testing.expect(sys.fs.exists(try dbPath(allocator, root)));    {        var database = try openCacheForTesting(allocator, root);        defer database.deinit();        const index = try sql.Search.open(&database, .{ .index_prefixes = false, .field_names = &search_field_names });        try std.testing.expect(try index.sourceMatches(allocator, sourceForHead(saved)));    }    const result = try searchStored(        allocator,        root,        "smg.cli.helpers._truncate",        model.NodeType.function,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), result.total);    try std.testing.expectEqualStrings("smg.cli.helpers._truncate", result.hits[0].name);    try std.testing.expectEqualStrings("src/smg/cli/_helpers.py:42", try location(allocator, result.hits[0]));    try std.testing.expectEqualStrings("Trim a string to width", snippet(result.hits[0]));    const ranked = try searchStored(        allocator,        root,        "truncate",        null,        2,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 3), ranked.total);    try std.testing.expectEqual(@as(usize, 2), ranked.hits.len);    try std.testing.expectEqualStrings("aaa.truncate", ranked.hits[0].name);    try std.testing.expectEqual(@as(i64, 1), ranked.hits[0].rank);    try std.testing.expectEqualStrings("zzz.truncate", ranked.hits[1].name);    try std.testing.expectEqual(@as(i64, 2), ranked.hits[1].rank);    const filtered = try searchStored(        allocator,        root,        "truncate",        model.NodeType.class,        10,        testingLimits().storage,    );    try std.testing.expectEqual(@as(i64, 1), filtered.total);    try std.testing.expectEqualStrings("zzz.truncate", filtered.hits[0].name);}fn syncStoredForTest(    allocator: std.mem.Allocator,    root: []const u8,    head: sql.Hash,) !SyncKind {    var opened = try storage.store.open(allocator, root, testingLimits().storage);    defer opened.close();    return try syncStoredFromDatabase(        allocator,        root,        &opened.database,        head,        head,        &.{},        &.{},        testingLimits().search,    );}const TestSync = struct {    head: sql.Hash,    kind: SyncKind,};fn replaceAndSyncForTest(    allocator: std.mem.Allocator,    graph: graph_mod.Graph,    root: []const u8,) !TestSync {    var opened = try storage.store.open(allocator, root, testingLimits().storage);    defer opened.close();    const previous_head = (try opened.database.connection.checkout()).head;    var replaced = try storage.graph.replaceObserved(        allocator,        &opened.database,        graph,    );    defer replaced.deinit(allocator);    return .{        .head = replaced.head,        .kind = try syncStoredFromDatabase(            allocator,            root,            &opened.database,            previous_head,            replaced.head,            replaced.changes.node_puts,            replaced.changes.node_deletes,            testingLimits().search,        ),    };}fn replaceGraphForTest(allocator: std.mem.Allocator, graph: graph_mod.Graph, root: []const u8) !sql.Hash {    var opened = try storage.store.open(allocator, root, testingLimits().storage);    defer opened.close();    return try storage.graph.replace(allocator, &opened.database, graph);}fn testingLimits() limits_mod.Limits {    return @import("root.zig").default_limits;}

Complete caller list for search.searchStored

8 direct callers.

Complete call list for search.searchStored

10 direct calls.

Audit

Definitions13
Public names13
Members15
Version26.7.0
Revisiondaab053ee433