fixturedb

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Jul 31, 2026 License: Apache-2.0 Imports: 10 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

This section is empty.

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 */}
)

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.T, 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`.

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

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
}

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 (*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
	// Directory marks a directory node (`nodes.kind` 0) rather than a leaf (1).
	Directory 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