fixturedb

package
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

  1. 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.

  2. 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>.

  3. 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

View Source
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

View Source
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

func New(t testing.TB, p Producer) *Builder

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

func (b *Builder) ASTNode(id, kind string, in SourceID, span Span, detail ...Detail) *Builder

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

func (b *Builder) Build() (string, *FixtureDB)

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

func (b *Builder) Import(alias, importPath string, in SourceID) *Builder

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

func (b *Builder) LSPHover(symbol, text string) *Builder

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

func (b *Builder) Producer() 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

func (b *Builder) Source(id SourceID, lang, content string) *Builder

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

func (b *Builder) SourceFile(id SourceID, lang, path string) *Builder

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

func Wide(t testing.TB, p Producer, files int) *FixtureDB

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

func (f *FixtureDB) DB() *sql.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.

func (*FixtureDB) DBPath

func (f *FixtureDB) DBPath() string

DBPath implements the dbPathProvider opt-in, which is what enables capnp binding readthrough from the sibling .bindings.capnp log.

func (*FixtureDB) QueryRefs

func (f *FixtureDB) QueryRefs(query string, args ...any) (*sql.Rows, error)

QueryRefs implements RefsQuerier.

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.

func (Producer) String

func (p Producer) String() string

String returns the producer's name, for test failure messages.

type RefsQuerier

type RefsQuerier interface {
	QueryRefs(query string, args ...any) (*sql.Rows, error)
}

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 Span

type Span struct {
	StartByte, EndByte int
	StartRow, StartCol int
	EndRow, EndCol     int
}

Span is a byte-and-row range in a source file, as `_ast` records it.

func Bytes

func Bytes(start, end int) Span

Bytes is the common case: a byte range with rows/cols left at zero.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL