Documentation
¶
Overview ¶
Package fixturedb builds SQLite test fixtures whose TABLE SHAPE is the shape a real producer writes, and removes a test's ability to state a schema at all.
Why this exists ¶
Before this package, ~34 test files hand-wrote `CREATE TABLE node_refs (...)`, in ten mutually incompatible spellings, and zero of them reproduced ley-line's real shape. That was not a cosmetic problem. `ensureCanonicalViews` PROBES for columns and emits a structurally DIFFERENT `v_refs` / `v_defs` body per combination — most sharply:
refsReferrerExpr := "node_id"
if hasRefsContainer {
refsReferrerExpr = "COALESCE(NULLIF(container_node_id, ''), node_id)"
}
So a fixture's `CREATE TABLE` literal was a HIDDEN TEST PARAMETER selecting which SQL the rule under test actually executed. A rule tested against a two-column node_refs exercised the degraded arm while production exercised the full arm, and neither side noticed.
Compounding it, `node_refs.node_id` has two incompatible meanings: the mache/tree-sitter projection puts the ENCLOSING CONSTRUCT there; ley-line puts the CALL-SITE LEAF there and the enclosing def in `container_node_id`. A fixture picked a meaning implicitly, by what it INSERTed, with nowhere in the test saying which.
This is the same defect class as mache-e3d9bb (`Node.Children` had two accessors that silently disagreed; the fix was removing the second way to ask), one layer down. The fix shape is the same: remove the second way to state a schema.
The three properties that carry the weight ¶
NO method on Builder accepts SQL, DDL, a column name, or a table name. That absence IS the design. The DDL in schema.go is unexported and there is no escape hatch.
Builder.Ref takes `from` and `at` as SEPARATE TYPED PARAMETERS, so the two meanings of node_id become distinct arguments instead of an undeclared convention. Leyline writes node_id=<at>, container_node_id=<from>; Standalone writes node_id=<from> and structurally cannot express <at>.
Builder.Build returns a fixture already wired through the canonical view installer, so a test cannot forget the install (a real past problem — see the note in cmd/smell_refs_views.go about "the 26 tests that build fixtures by hand").
Provenance ¶
The Leyline DDL is DERIVED, not hand-written: see schema_leyline.go and the conformance test that re-derives it from the pinned binary and fails on drift. The Standalone DDL is likewise conformance-tested against what ingest.NewSQLiteWriter actually creates.
Index ¶
- Constants
- Variables
- func RegisterViewInstaller(install func(RefsQuerier) error)
- type Builder
- func (b *Builder) ASTNode(id, kind string, in SourceID, span Span, detail ...Detail) *Builder
- func (b *Builder) Build() (string, *FixtureDB)
- func (b *Builder) Construct(id ConstructID, where ...Where) *Builder
- func (b *Builder) Def(token string, in ConstructID, kind CanonicalKind, detail ...Detail) *Builder
- func (b *Builder) Import(alias, importPath string, in SourceID) *Builder
- func (b *Builder) InjectedRef(token string, from ConstructID, inner string) *Builder
- func (b *Builder) LSPDef(token string, in ConstructID, uri string, ...) *Builder
- func (b *Builder) LSPHover(symbol, text string) *Builder
- func (b *Builder) LSPSymbol(at ConstructID, symbolKind, diagnostics string) *Builder
- func (b *Builder) Parsed(id ConstructID) *Builder
- func (b *Builder) Producer() Producer
- func (b *Builder) Ref(token string, from ConstructID, at SiteID, qualifier string, detail ...Detail) *Builder
- func (b *Builder) Source(id SourceID, lang, content string) *Builder
- func (b *Builder) SourceFile(id SourceID, lang, path string) *Builder
- type CanonicalKind
- type ConstructID
- type Detail
- type FixtureDB
- type Producer
- type RefsQuerier
- type SiteID
- type SourceID
- type Span
- type Where
Constants ¶
const WideDeepID = "pkg/f0000.go/function_declaration_1/block"
WideDeepID is a construct every Wide corpus holds, whatever its width, at the same depth: the block inside the second function of the first file.
Variables ¶
var ( // Leyline is the ley-line-open parse output: node_refs carries // source_id / container_node_id / qualifier / node_hash and has NO // primary key, so duplicate (token, node_id) rows SURVIVE. node_id is // the call-SITE leaf; the enclosing definition is container_node_id. // This is the shape production reads. Leyline = Producer{/* contains filtered or unexported fields */} // Standalone is mache's own schema projection (internal/ingest.SQLiteWriter): // node_refs is (token, node_id) with PRIMARY KEY (token, node_id) WITHOUT // ROWID, so duplicate rows are DEDUPED. node_id is the ENCLOSING CONSTRUCT; // there is no place to put a call site or a qualifier. // // The dedupe is not incidental. Any COUNT/AVG-over-v_refs rule measures a // different quantity here than on [Leyline] — the fan_out_skew class // (mache-50e939). Standalone = Producer{/* contains filtered or unexported fields */} // LeylineV6 is ley-line-open's projection-v6 (v0.20.x): integer nids // (file_id<<24 | preorder ordinal, directories -dir_id), interned names and // kinds, and paths derived by LLO's v_node_path rather than stored. A // spec's construct ids are reproduced as v_node_path, so the same spec can // be read from either shape; it must therefore name syntax nodes as // leyline does, suffixing "_k" only on a kind that repeats among siblings. LeylineV6 = Producer{/* contains filtered or unexported fields */} )
Functions ¶
func RegisterViewInstaller ¶
func RegisterViewInstaller(install func(RefsQuerier) error)
RegisterViewInstaller records the canonical-view installer for this test binary. Call it from an init() in the package under test; calling it twice is a programming error and panics rather than silently taking the last write.
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder accumulates a fixture's CONTENT. It has no method that accepts SQL, DDL, a column name or a table name — that absence is the design, not an oversight, and adding such a method re-opens the defect this package closed.
Every method returns the receiver so calls chain.
func New ¶
New starts a fixture for the given producer. p is MANDATORY and must be one of Leyline / Standalone; the zero Producer fails the test immediately, which is how "a fixture cannot exist without naming its producer" is enforced at the one place a fixture can be created.
func (*Builder) ASTNode ¶
ASTNode declares one `_ast` row: a parse-tree node of tree-sitter kind `kind` spanning `span` in source `in`.
It also declares the node's `nodes` row, because ley-line writes one for every parse-tree node it keeps and the projection's file index JOINs the two — an `_ast` row without its `nodes` row is a node the walker cannot see. A leaf's token lands in `nodes.record` too, as ley-line writes it.
On Leyline the node's position under its parent goes to `node_child`: the parent is the ASTNode whose id is this id's directory, children are ordered by start byte, and Detail.Field names the field. Every intermediate node a selector walks through must therefore be declared — as in a real parse.
func (*Builder) Build ¶
Build materialises the fixture and returns its path alongside it.
The connection is capped at ONE so the TEMP views survive across queries — TEMP objects are per-connection, and a fixture whose pool hands out a second connection loses v_defs / v_refs / v_test_nodes non-deterministically. Getting that wrong was a per-fixture coin flip before this package; now it is settled in one place.
The canonical views are installed here, not by the caller, so a test cannot forget them.
func (*Builder) Construct ¶
func (b *Builder) Construct(id ConstructID, where ...Where) *Builder
Construct declares an enclosing definition and its `nodes` row. Declaring the same id twice fills in whatever Where adds rather than duplicating the construct, so a fixture can name a construct before it knows its source.
func (*Builder) Def ¶
func (b *Builder) Def(token string, in ConstructID, kind CanonicalKind, detail ...Detail) *Builder
Def declares that `token` is DEFINED by construct `in`, with κ kind `kind`.
On Leyline this writes token / node_id / source_id / container_node_id / canonical_kind / node_hash. On Standalone it writes token and node_id and DROPS the rest, because the mache projection has nowhere to put them — which is the point: the same test source now produces the shape it claims.
func (*Builder) Import ¶
Import declares one `_imports` row: `alias` bound to module `importPath` inside source `in`. Only Leyline has this table; on Standalone the call is dropped.
func (*Builder) InjectedRef ¶ added in v0.22.0
func (b *Builder) InjectedRef(token string, from ConstructID, inner string) *Builder
InjectedRef declares that token is referenced from inside a language injected into construct from: a code span in markdown, SQL in a Go string. leyline indexes such an occurrence but stores no node for it (ley-line-open-260fcf). On projection-v4 its node_id is from's path, then "#inj0/", then inner, the injected tree's path, and only node_refs names it; on v6 it is a nid in from's file past every node the file stores.
func (*Builder) LSPDef ¶
func (b *Builder) LSPDef(token string, in ConstructID, uri string, startLine, startCol, endLine, endCol int) *Builder
LSPDef declares one `_lsp_defs` row — a binding-fidelity definition, which ensureCanonicalViews unions into v_defs when `_lsp_defs.def_token` exists. The table is LSP-enrichment output, not a producer table, so it is available on both producers.
func (*Builder) LSPHover ¶ added in v0.22.0
LSPHover declares one `_lsp_hover` row. `leyline lsp` keys hover by the symbol's standalone address, "symbols/" plus its document-symbol path (symbols/Store/Add), not by an AST node. That address is a chain of nodes outside every file: on projection-v6 each link is a directory, interned the way LLO's standalone_chain interns it; on v4 each is a nodes row whose id is the path.
func (*Builder) LSPSymbol ¶ added in v0.22.0
func (b *Builder) LSPSymbol(at ConstructID, symbolKind, diagnostics string) *Builder
LSPSymbol declares one `_lsp` row: a document symbol merged onto the AST node its name is in, which on projection-v6 the spec must declare, since v6 keys the row by that node's nid. diagnostics is the JSON array `leyline lsp` stores for the symbol's range, or "" for none.
func (*Builder) Parsed ¶ added in v0.22.0
func (b *Builder) Parsed(id ConstructID) *Builder
Parsed declares id the way a parse writes it: the node, and every syntax node from its source file down to it, each a nodes row with an _ast row of the kind its segment names (the raw kind, less a "_k" suffix). A parse never writes a syntax node without its ancestors or its kind, and a rule that reads ancestry (which kind of construct is this inside?) needs both. Nodes already declared through ASTNode keep their spans; the rest get an empty one. The source file itself is declared too, as Go's root (source_file).
func (*Builder) Producer ¶
Producer reports which producer shape this fixture reproduces, so a test can assert on it or branch its expectations explicitly rather than by accident.
func (*Builder) Ref ¶
func (b *Builder) Ref(token string, from ConstructID, at SiteID, qualifier string, detail ...Detail) *Builder
Ref declares that `token` is REFERENCED from inside construct `from`, at occurrence `at`, through selector `qualifier`.
`from` and `at` are separate parameters because `node_refs.node_id` means two incompatible things:
- Leyline writes node_id = at (the call-SITE leaf), container_node_id = from (the enclosing def), plus source_id / qualifier / node_hash.
- Standalone writes node_id = from (the enclosing CONSTRUCT) and has no column for at or qualifier at all.
An empty `at` means "an unnamed occurrence": the builder synthesises a unique ley-line-shaped call-site leaf under `from`. An empty `qualifier` is stored as NULL, which is what ley-line writes for a bare (unqualified) call.
func (*Builder) Source ¶
Source declares a source file. lang may be empty when the test does not care; content may be empty, in which case `_source.content` is NULL — which is what ley-line v0.13.0 writes (it stores bytes in source_blobs and only a path here). Pass content when the test is about snippet extraction.
Re-declaring a source fills in whichever of lang/content was previously empty rather than replacing it, so ASTNode's implicit declaration never clobbers an explicit one.
func (*Builder) SourceFile ¶ added in v0.22.0
SourceFile declares a source file by PATH — `_source.content` NULL and `_source.path` pointing at a real file, which is what ley-line writes by default and what the projection reads from disk when the row carries no bytes. Re-declaring a source this way overrides the synthetic path a prior Builder.Source or Builder.ASTNode gave it, so the two can be stated in either order.
type CanonicalKind ¶
type CanonicalKind string
CanonicalKind is ley-line's closed κ vocabulary, written to `node_defs.canonical_kind`. It normalises per-language tree-sitter kinds (function_declaration / function_item / function_definition) to one value.
Standalone has no column for it; a kind passed to a Standalone fixture is dropped, exactly as the mache projection drops it.
const ( NoKind CanonicalKind = "" Function CanonicalKind = "function" Method CanonicalKind = "method" Type CanonicalKind = "type" Module CanonicalKind = "module" Constant CanonicalKind = "constant" Variable CanonicalKind = "variable" Field CanonicalKind = "field" Interface CanonicalKind = "interface" )
The κ values ley-line emits. NoKind is for defs whose kind ley-line leaves NULL.
type ConstructID ¶
type ConstructID string
ConstructID identifies an enclosing DEFINITION — a function, method, type or module. On both producers this is what lands in `node_defs.node_id`.
type Detail ¶
type Detail struct {
// Container is the construct a DEFINITION is nested inside —
// `node_defs.container_node_id`, which ley-line populates for a method in an
// impl/type block and leaves NULL for a top-level item.
Container ConstructID
// Subtree labels a deduped merkle subtree. Occurrences carrying the same
// label share one node_hash; an empty label means "unique to this
// occurrence".
//
// It is a NAME, not bytes. node_hash is one-to-many by construction (one
// content row, many occurrences), and a fan-out test needs to say "these
// are the same subtree" without any test ever writing a hash literal.
Subtree string
// Token is an `_ast` node's token text, which the builder stores in
// `node_content` and points at from `_ast.node_hash` — the same indirection
// ley-line uses, and the one v_test_nodes' attribute detection walks.
Token string
// Field is the tree-sitter field an `_ast` node sits under in its parent
// — `receiver`, `name`, `type` — which lands in `node_child.field` on the
// parent's child list. Empty means the node has no field, which is what
// ley-line writes for a `type_spec` under its `type_declaration` or a
// `parameter_declaration` under its list. A selector's `field:` labels
// are matched against this, so a fixture for a labelled selector must
// state the fields the real parse would carry.
Field string
}
Detail states the optional facts about a definition, a reference or an `_ast` row. Fields that do not apply to the row kind are ignored, and every field is dropped on Standalone when the mache projection has no column for it.
type FixtureDB ¶
type FixtureDB struct {
// contains filtered or unexported fields
}
FixtureDB is a built fixture: a real on-disk SQLite database whose shape is the producer's, with the canonical views already installed.
It satisfies RefsQuerier and cmd's dbPathProvider, so it drops directly into the production call path.
func Wide ¶ added in v0.22.0
Wide builds a corpus of files source files under pkg/, each with three functions (so the kind repeats and is suffixed) and a block inside the second. Every file has the same shape, so two Wide corpora differ only in size: a lookup of WideDeepID is the same lookup in both, which is what a gate on how cost GROWS needs.
func (*FixtureDB) DB ¶
DB exposes the connection so a test can assert on rows directly.
It is NOT an escape hatch for DDL: the fixture's shape is already fixed by the producer, and internal/lint's LLO boundary rule fails any test that writes an LLO-owned table.
type Producer ¶
type Producer struct {
// contains filtered or unexported fields
}
Producer names the tool whose table shape a fixture reproduces.
The set is CLOSED: the only values are Leyline and Standalone. The single field is unexported, so the zero value is invalid and a caller outside this package cannot construct a third. A fixture therefore cannot exist without naming the producer it models — which is precisely what the 34 hand-written fixtures never did.
type RefsQuerier ¶
RefsQuerier is the query surface a fixture presents. It is deliberately the same shape as cmd's refsQuerier so a fixture drops straight into the production call path.
type SiteID ¶
type SiteID string
SiteID identifies one distinct OCCURRENCE inside a construct: a single call expression, not the function containing it. The empty SiteID means "an unnamed occurrence" and the builder synthesises a unique leaf path for it.
SiteID exists as its own type because `node_refs.node_id` means a site on Leyline and a construct on Standalone. Making them two parameters of two types is the whole reason a migrated test can no longer be ambiguous about which producer it modelled.
type SourceID ¶
type SourceID string
SourceID is a source file's ley-line identity: its path RELATIVE to the parse root ("pkg/orphan.go"), never an absolute path and never a bare basename.
type Where ¶
type Where struct {
// Source pins the construct's source file instead of inferring it.
Source SourceID
// Name sets `nodes.name`. Defaults to the id's last path segment.
Name string
// Parent sets `nodes.parent_id`.
Parent ConstructID
// Leaf marks a leaf node: `nodes.kind` 0, which mache reads as
// graph.NodeKindFile, a token with no children whose `record` is its text.
// The default is 1 (graph.NodeKindDir), what ley-line writes for any node
// with children: directories, files and most syntax nodes.
Leaf bool
}
Where states where a construct lives. The zero value means "infer it", which is the common case: Builder.Construct derives the source from the id's longest leading path prefix whose last segment has a file extension — "pkg/served_test.go/function_declaration_0" infers "pkg/served_test.go", which is exactly how ley-line composes these ids.