storetest

package
v0.0.0-...-46e3389 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package storetest provides a conformance test suite for store.Store implementations. Each implementation wires the suite via a Factory function that returns a fresh, empty store.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FuzzAttachmentKeyCollision

func FuzzAttachmentKeyCollision(f *testing.F, factory FuzzFactory)

FuzzAttachmentKeyCollision verifies that attachment key construction rejects invalid property names and round-trips valid ones.

func FuzzCloneNestedValues

func FuzzCloneNestedValues(f *testing.F, factory FuzzFactory)

FuzzCloneNestedValues verifies deep clone semantics for nested property values.

func FuzzConcurrentOps

func FuzzConcurrentOps(f *testing.F, factory FuzzFactory)

FuzzConcurrentOps verifies the store is safe under concurrent access.

func FuzzPropertyValuesTypeZoo

func FuzzPropertyValuesTypeZoo(f *testing.F, factory FuzzFactory)

FuzzPropertyValuesTypeZoo verifies that every accepted property value, of any type, reads back equal to what was written. The name predates the removal of PropertyValues (TKT-KQXVF7); it is kept so the backends' fuzz corpora keep their directories.

The round-trip half applies the same directional oracle as createEntityOrSkip: anything storeutil.ValidateProperties rejects, the store MUST reject; anything it accepts MUST come back byte-for-byte. It exists because both halves have failed silently before. BUG-B1RA3J: a value fsstore wrote as a block scalar that read back as "" with no error. BUG-X7ICNM: invalid UTF-8 that pgstore "stored" as U+FFFD and reported success. A target that only checked for an absent error passed over both.

func FuzzRelationKeyCollision

func FuzzRelationKeyCollision(f *testing.F, factory FuzzFactory)

FuzzRelationKeyCollision verifies that relation key construction never causes collisions or round-trip failures.

func FuzzRenameKeyCollapse

func FuzzRenameKeyCollapse(f *testing.F, factory FuzzFactory)

FuzzRenameKeyCollapse verifies rename never loses relations.

func RunAddressTests

func RunAddressTests(t *testing.T, f Factory)

RunAddressTests pins how every backend answers an entity.Ref (TKT-KQXVF7). A row is addressed by (id, face). A ref that names no row is ErrNotFound, whether the row is absent or the ref is malformed: a malformed face can never be declared, so it names nothing, and a backend must not map it onto some other row, a path, or a SQL error.

func RunAll

func RunAll(t *testing.T, f Factory, sf SearchFactory, vsf VisibleSearchFactory, caps Capabilities)

RunAll runs the full conformance suite. The optional SearchFactory is used for search tests; if nil, search tests are skipped. The optional VisibleSearchFactory is used for ACL-scoped search tests; if nil, they are skipped. Capabilities gates optional feature tests (e.g. attachments).

func RunAttachmentTests

func RunAttachmentTests(t *testing.T, f Factory)

RunAttachmentTests runs attachment conformance tests.

func RunBulkMigrateTests

func RunBulkMigrateTests(t *testing.T, f Factory)

RunBulkMigrateTests is the conformance suite for bulk migration rewrites (TKT-HH7PKJ).

Every backend must pass these, whether it implements store.BulkMigrator natively (pgstore and sqlitestore rewrite the table in one statement) or is served by the generic fallback. store.SwapRelationEndpoints picks the path and the OBSERVABLE contract is identical either way — that equivalence is the whole point of the seam, and the reason this suite is not gated on a capability flag.

Keeping both paths under ONE contract is deliberate. A native fast path and a hand-written loop are exactly the pair that drifts: BUG-TMGWIN happened because detection and remediation were maintained as two lists, each looking complete alone.

func RunCASTests

func RunCASTests(t *testing.T, f Factory)

RunCASTests runs the compare-and-swap conformance suite for store.EntityWriter.UpdateEntityIf (TKT-34XS2R).

This is part of RunAll rather than an opt-in section, deliberately. CAS is the primitive callers reach for INSTEAD of a process-local mutex, so a backend that accepts a stale expected-version is not merely missing a feature — it silently returns success for a write that lost a race, which is the exact failure the caller adopted CAS to avoid. There is no "degrade gracefully" story here, so there is no capability flag.

func RunEndpointMatchTests

func RunEndpointMatchTests(t *testing.T, f Factory)

RunEndpointMatchTests is the conformance suite for store.RelationPredicate.EndpointMatch — filtering the candidate set by the TYPE and PROPERTIES of the entity on the far side of a relation, rather than by endpoint id (TKT-RELTRV).

The scenarios that matter to a backend author, beyond the happy path:

  • a dangling edge (endpoint row absent) is NOT a match, so a pushed-down JOIN and a Go-side lookup agree
  • EndpointMatch composes with Endpoints as a CONJUNCTION, not a widening
  • Negate inverts the whole predicate including the endpoint filter
  • a chained hop resolves against the endpoint, not the original candidate

Run via RunAll.

func RunEntityTests

func RunEntityTests(t *testing.T, f Factory)

RunEntityTests runs entity CRUD conformance tests.

func RunFaceSelectionTests

func RunFaceSelectionTests(t *testing.T, f Factory)

RunFaceSelectionTests is the face-selection conformance suite (TKT-KQXVF7): the zero selection is refused by every read, AtFaces is a set (empty matches nothing), AllFaces pages resume inside a family, FaceIn composes with every mode, and an endpoint match reads its endpoint at its own selection or the query's (design A6).

The seed is one faced type with every family shape: all three faces (DOC-1), a named face only (DOC-2) and the default face only (DOC-3), plus an unfaced type whose identity edges point at them.

func RunFacedVisibleSearchTests

func RunFacedVisibleSearchTests(t *testing.T, vsf VisibleSearchFactory)

RunFacedVisibleSearchTests pins the per-face half of the scope (TKT-7IZHP0): the verdict and the face allowlist trim each entity's candidate faces BEFORE the world ranks them. A denied prime falls through to a readable face, as on lists and the single-entity read, and a face the scope denies is never matched or served.

The world prefers draft and falls back to published. POL-1 holds both faces, POL-2 only a draft, and the term "sketch" appears only in POL-1's draft.

func RunFamilyReplaceTests

func RunFamilyReplaceTests(t *testing.T, f Factory, caps Capabilities)

RunFamilyReplaceTests pins store.FamilyReplacer: one validation rule set for every backend, a face swap that never trips the family key, and a delete event for every coordinate the conversion empties.

func RunFamilyTests

func RunFamilyTests(t *testing.T, f Factory)

RunFamilyTests pins the family operations: DeleteFamily, DeleteFace and RenameFamily on a family stored only at named faces.

func RunFieldScopeSearchTests

func RunFieldScopeSearchTests(t *testing.T, vsf VisibleSearchFactory)

RunFieldScopeSearchTests pins search.TypeScope.Fields, the closure of the match-on-hidden-field oracle: a hit whose text matched ONLY properties the reader may not see is dropped, while a hit that matched a visible property, an ungated property, the id or the content survives. The limit counts the surviving hits.

The fixture's `code` value ("zeta777") appears in no title, so a search for it can only match through that property.

func RunFreshnessTests

func RunFreshnessTests(t *testing.T, f Factory)

RunFreshnessTests exercises store.Freshness, the mandatory member of store.Store that had no conformance coverage at all until TKT-8TJ2WN.

It is worth pinning because the three shipped implementations are genuinely different — fsstore takes the max of four filesystem mtimes, memstore scans two maps, pgstore runs SQL max() over a UNION ALL — and because a wrong answer degrades silently: appbuild compares this against the search index's stored timestamp to decide whether to rebuild, so a store that under-reports serves stale search results with no error anywhere.

The assertions are deliberately coarse (monotonicity and coverage, not exact values). A store whose timestamps come from the filesystem or the database clock cannot promise more than that, and demanding equality with a test-observed wall clock would fail those backends for no benefit.

func RunGraphDifferential

func RunGraphDifferential(t *testing.T, f Factory)

RunGraphDifferential compares a backend's native graph queries with graphquerynaive, the behavioral reference, on ONE seeded store. The seed mixes every value shape a property can hold (strings, the empty string, JSON null, absent keys, lists, the empty list, integers, booleans, a real), awkward property names, faces, state-tailed edges, inheritance chains with a cycle, and dangling-free endpoints; the queries are generated from fixed seeds so a failure reproduces.

The reference runs graphquerynaive over a memstore seeded identically, not over the store under test: graphquerynaive reads through ListEntities, and a SQL backend's ListEntities shares its world ranking with its graph queries, so a ranking defect would otherwise agree with itself. A backend that delegates to graphquerynaive exercises only its ListEntities here.

Property names a SQL backend cannot render send the whole query to its naive fallback, so the generator uses one rarely: most queries must take the SQL path to test anything.

Two answers are deliberately NOT compared, because they are documented divergences rather than defects: the GraphCount total under a world with Any (BUG-OQQE8D), and sort keys over list or real values, which Go and SQL render differently (the generator never orders by those properties).

func RunGraphHeaderTests

func RunGraphHeaderTests(t *testing.T, f Factory)

RunGraphHeaderTests pins the content-free projection of GraphQuery (TKT-1U8XYN): for a type-plus-property query and a relation-predicate query, store.GraphQueryHeaders yields exactly HeaderOf(row) for every row GraphQuery yields, in the same order — whether the backend implements store.GraphHeaderQueryer natively or through the generic fallback.

func RunGraphPagingTests

func RunGraphPagingTests(t *testing.T, f Factory)

RunGraphPagingTests pins GraphQuery.OrderBy/Limit/Offset (TKT-1U8XYN): byte-wise ordering on the property's string form, a row without the property sorting as the largest value, id as tiebreak; a Limit/Offset window equal to slicing the full ordered result; GraphCount unaffected by paging; headers paging identically.

func RunGraphPositionTests

func RunGraphPositionTests(t *testing.T, f Factory)

RunGraphPositionTests pins store.GraphPosition to the one definition it can have: for every matched id, the position equals what the ordered GraphQuery result says (TKT-U9DYW4). Stating it as a differential rather than as expected literals is the point — a backend answers a position natively (pgstore, one window-function statement) precisely where it also orders natively, and the two must not drift.

func RunGraphQueryTests

func RunGraphQueryTests(t *testing.T, f Factory)

RunGraphQueryTests is the conformance suite for store.GraphQueryer. Pins the contract every backend implementation must honor — regardless of whether it delegates to graphquerynaive or pushes the query into the underlying engine. New implementations get the same expectations for free.

Each subtest seeds a small graph, runs a GraphQuery, and asserts on the (id-sorted) result set. The scenarios cover:

  • direct inbound match (no transitive expansion)
  • direct outbound match
  • InheritThrough endpoint-side expansion (groups-shaped)
  • EntityInheritThrough entity-side expansion (containment-shaped)
  • both expansions composed
  • OfTypes filter
  • cycle / self-loop termination
  • depth-cap truncation
  • GraphCount returning (matched, total)

Run via RunAll alongside the other conformance suites.

func RunHeaderTests

func RunHeaderTests(t *testing.T, f Factory)

RunHeaderTests runs conformance tests for content-free header listing (TKT-1ESTYJ).

Every backend must pass these, whether it implements store.HeaderReader natively or is served by the generic fallback: store.ListEntityHeaders picks the path, and the OBSERVABLE contract is identical either way. That equivalence is the point — a caller must never have to know which it got.

func RunIteratorNestingTests

func RunIteratorNestingTests(t *testing.T, f Factory, vsf VisibleSearchFactory)

RunIteratorNestingTests pins that a caller may make store calls inside the body of any store iteration (BUG-9TGOH1). Visibility redaction does exactly that for every row. A backend that holds a pooled connection across yield needs a second connection per concurrent iteration, and deadlocks once the iterations outnumber the pool. On a Tx view, the same backend would issue the nested statement on a connection whose result is still being read.

vsf may be nil; the visible-search cases are then skipped.

func RunMatchingFacesTests

func RunMatchingFacesTests(t *testing.T, f Factory)

RunMatchingFacesTests pins store.GraphQueryer.MatchingFaces, the per-face row verdict the ACL read gate runs (acl.Request.ReadableFacesMany). The gate asks it under AllFaces with the grant's face allowlist, so a backend that answered per id instead of per face row, or dropped a branch's face set, would widen or narrow every bare-id read at once.

func RunMentionRollbackTests

func RunMentionRollbackTests(t *testing.T, f Factory)

RunMentionRollbackTests pins that a rolled-back write leaves no index entry. Run by backends with the strong Tx contract (see Capabilities.TxRollback).

func RunMentionTests

func RunMentionTests(t *testing.T, f Factory)

RunMentionTests is the conformance suite for store.MentionIndex (TKT-Q6AKUN). Every in-tree backend implements the capability, so a store that does not is a failure, not a skip.

func RunObserverTests

func RunObserverTests(t *testing.T, f ObserverFactory)

RunObserverTests pins the observer contract every backend must honor (store.EntityObserver + store.FaceObserver). It lives HERE, not in each backend's own tests, because the contract once drifted exactly that way: memstore and fsstore pinned per-face announcements while pgstore pinned the pre-worlds skip, and each backend's tests were green.

func RunPaginationTests

func RunPaginationTests(t *testing.T, f Factory)

RunPaginationTests runs conformance tests for ListEntitiesPage and ListRelationsPage. These verify the cursor contract, limit semantics, and the interaction with query filters — properties the dataentry handlers (and any other paginating caller) depend on.

func RunQueryTests

func RunQueryTests(t *testing.T, f Factory)

RunQueryTests runs entity query conformance tests.

func RunRelatedTests

func RunRelatedTests(t *testing.T, f Factory)

RunRelatedTests is the conformance suite for store.GraphQuery.Related, the caller-supplied conjunction of directed relation predicates (TKT-XKCNCL).

The scenarios that matter to a backend author:

  • Incoming reads the entity's incoming edges and false its outgoing ones, exactly as HasInbound and HasOutbound do
  • entries are ANDed with each other and with HasInbound: an entry never replaces or widens the slot the ACL read gate occupies
  • Negate, paging and the scoped count honor every entry
  • the EndpointMatch shape bound applies to entries too

Run via RunAll.

func RunRelationTests

func RunRelationTests(t *testing.T, f Factory)

RunRelationTests runs relation CRUD conformance tests.

func RunSearchTests

func RunSearchTests(t *testing.T, sf SearchFactory)

RunSearchTests runs search and filter conformance tests.

func RunSoftDeleteTests

func RunSoftDeleteTests(t *testing.T, f Factory, sf SearchFactory, attachments bool)

RunSoftDeleteTests is the conformance suite for store.SoftDeleter. It is the proof that a marked entity is hidden from every read path, since a read method that forgets the mark shows a deleted entity with no error. sf is optional; with it, the search index is checked too.

func RunStateTests

func RunStateTests(t *testing.T, f Factory)

RunStateTests is the content-states conformance suite (TKT-DOFYR1): addressing by (id, face), the write-path row-family invariants, default-vs-AllFaces query semantics, relation tail faces in both match implementations, and the delete/rename cascades over a state family. These cases define the contract BEFORE the second backend implements — they are what keep storeutil.MatchRelation and pgstore's relationWhere from drifting.

func RunSweepAttributionTests

func RunSweepAttributionTests(t *testing.T, f Factory, sweepNow func(t *testing.T, s store.Store))

RunSweepAttributionTests pins who a swept create/update version is attributed to. RunVersionTests drives capture through the synchronous writer and so cannot see this; the sweep is the only path that records creates and updates, so the attribution it stamps is what history shows for almost every edit.

The contract (TKT-ZIRMGM): a write carrying a store.Attribution is credited to that editor, and a write without one is credited to the version-sweep system principal, never to a guessed or stale identity.

sweepNow must run one tick that treats every row as settled.

func RunSweepBacklogTests

func RunSweepBacklogTests(t *testing.T, f Factory, sweepNow func(t *testing.T, s store.Store))

RunSweepBacklogTests pins that the sweep captures every change when more rows are settled than one tick processes (BUG-1DWMYO).

The sweep dedups in Go, after its candidate query. A row the query selects and the dedup skips is selected again on every tick, so once a batch of them exists the rows behind them are never reached: not slowly, never. The cases below are the ways such rows arose: settled rows that were already captured, numbers whose stored form differs from their hashed form, and rows saved unchanged after a force-live purge.

sweepNow must run one tick that treats every row as settled, with a batch smaller than sweepBacklogRows.

func RunSweepOriginTests

func RunSweepOriginTests(t *testing.T, f Factory, sweepNow func(t *testing.T, s store.Store))

RunSweepOriginTests pins the provenance a swept create/update version carries. The sweep captures from the live row, so a backend must keep the write's store.Origin on the row for the version to name it (BUG-YC5Z07).

sweepNow must run one tick that treats every row as settled.

func RunTxAbnormalExitTests

func RunTxAbnormalExitTests(t *testing.T, f Factory)

TxDurabilityFactory builds a store for the abnormal-exit cases below. It is separate from Factory because these cases need a store that OUTLIVES the transaction under test — a factory that registers t.Cleanup teardown is fine, but the store must still be usable after fn has panicked.

The cases exist because RunTxRollbackTests only ever returns an ERROR from fn. A backend can pass it while leaving the store permanently unusable after a panic or a cancelled commit, which is exactly what happened to sqlitestore: abandoning an open transaction on a pooled connection poisoned every subsequent transaction AND made the uncommitted write durable. The suite could not see it, so the bug shipped to review.

func RunTxObserverIsolationTests

func RunTxObserverIsolationTests(t *testing.T, newStore func(t *testing.T, o store.EntityObserver) store.Store)

RunTxObserverIsolationTests asserts observers do not witness rolled-back writes.

Separate from RunTxRollbackTests because it needs a store with an observer attached, which Factory cannot express. Worth its own entry point: observers are how derived state (search indexes) stays correct, so an observer firing for a write that never committed leaves the index holding a phantom entity — and nothing self-heals until a full reindex.

func RunTxRollbackTests

func RunTxRollbackTests(t *testing.T, f Factory)

RunTxRollbackTests exercises the stronger Tx guarantees of backends with real rollback (pgstore): an error from fn discards the transaction's writes, delivers no events, and post-commit delivery works. fsstore/memstore deliberately do NOT meet these (DEC-8UIL0 reduced single-user guarantees) and must not run this suite.

func RunTxStressTest

func RunTxStressTest(t *testing.T, f Factory)

RunTxStressTest soaks a store under sustained mixed concurrent load — Tx read-modify-write counters, multi-write transactions with injected failures, nested transactions, plain writes, reads, and a draining subscriber — with a progress watchdog that converts a deadlock into a failing test with a full goroutine dump (Go's runtime deadlock panic only fires when ALL goroutines block, which a partial deadlock never triggers).

End-state invariants, valid for every backend regardless of rollback support:

  • No lost updates: the counter equals the number of Tx increments that returned success.
  • Pair atomicity: each two-entity transaction either left both entities or neither (the failure is injected only after both writes, so this holds under rollback AND under the fs/mem no-rollback contract).

Duration: 2s default; set RELA_STRESS_SECONDS for longer local runs.

func RunTxTests

func RunTxTests(t *testing.T, f Factory)

RunTxTests exercises the store.Transactor contract every backend must meet: serialization, view re-entrancy, read-your-writes, error propagation, and nested-Tx joining. Rollback and post-commit event delivery are backend-dependent — see RunTxRollbackTests.

func RunValidationTests

func RunValidationTests(t *testing.T, f Factory)

RunValidationTests runs input validation conformance tests.

func RunVersionTagTests

func RunVersionTagTests(t *testing.T, f Factory, sweepNow func(t *testing.T, s store.Store))

RunVersionTagTests pins the store.VersionTagger contract (TKT-VO6VG9): capture-now, moving and removing a tag, which lifecycle a tag belongs to across rename, delete and re-creation, per-face scope, the purge refusal, the compare-and-set precondition, the in-transaction refusal and concurrent moves.

sweepNow must run one sweep tick that treats every row as settled. Deletes and renames are captured synchronously above the store, so the suite writes those version rows itself, as entitymanager would.

func RunVersionTests

func RunVersionTests(t *testing.T, f Factory)

RunVersionTests is the conformance suite for content versioning — the optional capability bundle a backend advertises via store.VersionService.

Why this lives here rather than beside an implementation

pgstore had versioning to itself for a long time, and its guarantees were pinned only by tests inside pgstore. That is fine for one implementation and actively misleading for two: the second backend's tests describe whatever it happens to do, the first backend's describe whatever IT happens to do, and nothing anywhere states what a caller may RELY on. The consumers (internal/dataentry's history handlers) type-assert the interface and cannot see which backend answered, so the contract has to be the thing under test.

Extracted when sqlitestore became the second implementation (TKT-4NU9ZD), following the precedent set by Capabilities.TxRollback in TKT-8TJ2WN: the backend DECLARES the tier and the suite runs, so a backend that has the capability and forgets to say so gets no silent pass.

What this deliberately does not test

Sweep cadence and debounce windows are mechanism, not contract — a backend may capture create/update rows on any schedule, or (like fsstore) not at all. The suite therefore drives capture through the SYNCHRONOUS writer, which every versioning backend must implement, and asserts on what history READS back.

func RunVisibleSearchTests

func RunVisibleSearchTests(t *testing.T, vsf VisibleSearchFactory)

RunVisibleSearchTests is the conformance suite for search.VisibleSearcher. Any implementation — the generic scope-filter wrapper or a backend-native one — must pass it (TKT-BA8BSX).

Every case asserts two invariants on top of its specific expectation:

  • no-leak: every yielded hit is in the case's allowed set;
  • ordered-subsequence: the visible stream equals the ungated stream of the same backend with disallowed hits deleted, order preserved.

Fixture corpora stay far below every backend's candidate bound, so truncation never confounds the subsequence check; the post-visibility limit contract is pinned separately (LimitPostVisibility).

func RunWatcherTests

func RunWatcherTests(t *testing.T, f Factory)

RunWatcherTests runs event subscription conformance tests.

func RunWorldTests

func RunWorldTests(t *testing.T, f Factory)

RunWorldTests is the world-resolution conformance suite (TKT-WAV8XP): the three resolution rules, chain order, the at-most-one-prime invariant, exclude-vs-default fallback, and the zero-value default-world fast path. The other selection modes are RunFaceSelectionTests.

These cases define the contract BEFORE the second backend implements it. fs/mem resolve in shared Go (storeutil); pgstore resolves in SQL in PR-C — this suite is what stops the two from drifting, exactly as RunStateTests does for content states.

Gated by Capabilities.Worlds for one commit window; see that field.

Types

type BodyWatch

type BodyWatch struct {
	store.Store
	// contains filtered or unexported fields
}

BodyWatch decorates a store.Store and counts how many entity BODIES a read path was served, so a test can pin that a collection read never materializes the markdown it will not render (BUG-SDMD6O).

It is the third sibling of Counting and Breadth, and the three answer three different questions about one read path. Counting asks "how many round-trips?", which an N+1 inflates. Breadth asks "how wide was each round-trip?", which an over-fetch inflates and an N+1 does not move. BodyWatch asks "how much CONTENT crossed the seam?", which neither of the other two moves at all: swapping ListEntityHeaders for ListEntities keeps both the call count and the id breadth byte-identical while changing the bytes transferred by two orders of magnitude.

Why a counter and not a heap measurement

The motivating defect was measured at +101 MB vs +1 MB on pgstore to render 50 rows of a 5,000-row type, and the obvious instrument is therefore a heap assertion. That instrument is the wrong one here, for a reason the bug's own history records: the SAME probe on memstore reports ~1 MB whether or not the defect is present, because memstore's Clone shares the body STRING rather than copying it (Go strings are immutable, so a clone costs a header, not the bytes). A heap assertion is thus a test that cannot fail on the default backend, and one that can only run DB-gated — skipped on every developer machine without RELA_TEST_DATABASE_URL, and skipped in every CI job but one.

Counting bodies removes the backend from the question entirely. "The list pipeline was handed N bodies to render 50 rows" is true on memstore, fsstore and pgstore alike, is exact rather than statistical, needs no threshold, no GC, no tolerance for allocator noise, and fails identically everywhere. The backend-dependent quantity (what those bodies COST) is what varies; the defect itself — that they were read at all — does not.

A body is counted when a served row carries a non-empty Content, which is what the header/entity split exists to avoid. Rows are counted as they are YIELDED, so a lazily-drained iterator is charged only for what the caller actually pulled — the same accounting the retention it stands in for has.

Reads are counted; writes pass through untouched, matching its two siblings: a test seeds through the same handle and seeding is not the measurement.

func NewBodyWatch

func NewBodyWatch(s store.Store) *BodyWatch

NewBodyWatch wraps s.

func (*BodyWatch) Bodies

func (b *BodyWatch) Bodies() int

Bodies returns the total number of non-empty bodies served since the last Reset, across every read method.

Summed across methods rather than reported per method because the property under test is a property of the PIPELINE — "this request read no body" — and which call would have carried one is exactly the implementation detail a test should not encode. BodyWatch.String renders the per-method split for failure messages.

func (*BodyWatch) GetEntity

func (b *BodyWatch) GetEntity(ctx context.Context, ref entity.Ref) (*entity.Entity, error)

func (*BodyWatch) GraphQuery

func (b *BodyWatch) GraphQuery(ctx context.Context, q store.GraphQuery) iter.Seq2[*entity.Entity, error]

func (*BodyWatch) GraphQueryHeaders

func (b *BodyWatch) GraphQueryHeaders(
	ctx context.Context, q store.GraphQuery,
) iter.Seq2[store.EntityHeader, error]

GraphQueryHeaders mirrors ListEntityHeaders for the graph-query shape.

Forwarding through store.GraphQueryHeaders rather than asserting the capability here is deliberate: on a backend WITHOUT a native GraphHeaderQueryer the generic fallback drains GraphQuery, which this decorator also wraps — so the bodies that fallback materializes in-process are still counted, and a test cannot mistake "the backend has no header capability" for "the pipeline asked for no bodies". That is the honest reading: the fallback bounds retention, not transfer, and only the native path makes the bodies genuinely absent.

func (*BodyWatch) ListEntities

func (b *BodyWatch) ListEntities(ctx context.Context, q store.EntityQuery) iter.Seq2[*entity.Entity, error]

func (*BodyWatch) ListEntitiesPage

func (b *BodyWatch) ListEntitiesPage(ctx context.Context, q store.EntityQuery) (store.Page[*entity.Entity], error)

func (*BodyWatch) ListEntityHeaders

func (b *BodyWatch) ListEntityHeaders(
	ctx context.Context, q store.EntityQuery,
) iter.Seq2[store.EntityHeader, error]

ListEntityHeaders forwards to the wrapped store's header reader, or to the generic projection when it has none. Counted as a call, never as a body: a header carries no content by construction, which is the whole point of the capability. Declared so the decorator does not HIDE a capability the wrapped store has — dropping it would send every caller down the generic fallback and change the very routing under test.

func (*BodyWatch) MentionSources

func (b *BodyWatch) MentionSources(
	ctx context.Context, target string, after entity.Ref, limit int,
) ([]entity.Ref, error)

MentionSources forwards to the wrapped store's mention index, counted as a call and never as a body: it returns refs only. Declared so the recorder does not hide the capability.

func (*BodyWatch) Reset

func (b *BodyWatch) Reset()

Reset clears the recorded counts.

func (*BodyWatch) SoftDelete

func (b *BodyWatch) SoftDelete() store.SoftDeleter

SoftDelete forwards the wrapped store's soft-delete capability.

func (*BodyWatch) String

func (b *BodyWatch) String() string

String renders the counts as "Method=bodies/calls", sorted, for test failure messages. Both maps are read under ONE acquisition so the rendered line is a single consistent snapshot.

func (*BodyWatch) Tx

func (b *BodyWatch) Tx(ctx context.Context, fn func(store.Store) error) error

Tx forwards the transaction, wrapping the view so reads inside it count.

func (*BodyWatch) Unwrap

func (b *BodyWatch) Unwrap() store.Store

Unwrap returns the wrapped store, so a lookup keyed on the backend type (the database handle piles and state share) still finds it; see store.Decorator.

type Breadth

type Breadth struct {
	store.Store
	// contains filtered or unexported fields
}

Breadth decorates a store.Store and records how many IDS each batched read was handed, so a test can pin how WIDE an operation's store calls are.

It is the sibling of Counting, and the two answer different questions. Counting asks "how many round-trips?", which is what an N+1 inflates. Breadth asks "how big was each round-trip?", which an N+1 does not move at all — a caller that over-fetches does so in ONE call whose argument slice is enormous. Pinning only the count is therefore blind to a whole class of cost defect, and pinning only the breadth is blind to N+1; a read path that cares about both needs both.

The motivating defect (RR-HKHPYG, TKT-MJKZQ3) is exactly the blind spot: a `display: nested` section resolved its relation columns over every visible child rather than the rows its budget would emit, passing ~280,000 ids to render at most 2,000 rows. The call count was byte-identical before and after the fix, because the over-fetch was one very wide ListEntityHeaders.

Three batch carriers are recorded, and they are the ones a batching read path grows: EntityQuery.IDs, RelationQuery.EntityIDs, and the explicit ids argument to MatchingFaces (the ACL membership walk, one round-trip per distinct type rather than per row). An empty slice means "no id filter" rather than "no ids", so it records 0 and a test asserting a bound is unaffected by it.

Breadth is NOT a bound on how many entities a read RETURNS — a type-filtered query with no id list reads the whole type and records 0. It measures the size of the argument a caller chose to pass, which is the quantity a caller controls and an over-fetch inflates.

Reads are recorded; writes pass through untouched, matching Counting: a test seeds through the same handle and seeding is not the measurement.

func NewBreadth

func NewBreadth(s store.Store) *Breadth

NewBreadth wraps s.

func (*Breadth) IDs

func (b *Breadth) IDs(method string) int

IDs returns the total number of ids handed to method since the last Reset, summed across every call to it.

Summed rather than maxed because the bound a caller cares about is the total work it caused: two calls of 2,000 cost what one call of 4,000 costs. Breadth.String renders the ids/calls split for failure messages.

func (*Breadth) ListEntities

func (b *Breadth) ListEntities(ctx context.Context, q store.EntityQuery) iter.Seq2[*entity.Entity, error]

func (*Breadth) ListEntitiesPage

func (b *Breadth) ListEntitiesPage(ctx context.Context, q store.EntityQuery) (store.Page[*entity.Entity], error)

func (*Breadth) ListEntityHeaders

func (b *Breadth) ListEntityHeaders(ctx context.Context, q store.EntityQuery) iter.Seq2[store.EntityHeader, error]

func (*Breadth) ListRelations

func (b *Breadth) ListRelations(ctx context.Context, q store.RelationQuery) iter.Seq2[*entity.Relation, error]

func (*Breadth) ListRelationsPage

func (b *Breadth) ListRelationsPage(ctx context.Context, q store.RelationQuery) (store.Page[*entity.Relation], error)

func (*Breadth) MatchingFaces

func (b *Breadth) MatchingFaces(
	ctx context.Context, q store.GraphQuery, ids []string,
) (map[string][]entity.Face, error)

ListEntityHeaders forwards to the wrapped store's header reader, or to the generic projection when it has none — the same forwarding Counting does, so a consumer that type-asserts the header capability still finds it. MatchingFaces is the ACL membership-walk batch: one round-trip per distinct type rather than one per row, so its width grows with the row set exactly like the two query fields above.

func (*Breadth) MentionSources

func (b *Breadth) MentionSources(
	ctx context.Context, target string, after entity.Ref, limit int,
) ([]entity.Ref, error)

MentionSources forwards to the wrapped store's mention index, unrecorded: it takes no id batch. Declared so the recorder does not hide the capability.

func (*Breadth) Reset

func (b *Breadth) Reset()

Reset clears the recorded breadth.

func (*Breadth) SoftDelete

func (b *Breadth) SoftDelete() store.SoftDeleter

SoftDelete forwards the wrapped store's soft-delete capability.

func (*Breadth) String

func (b *Breadth) String() string

String renders the recorded breadth as "Method=ids/calls", sorted, for test failure messages.

Both maps are read under ONE acquisition so the rendered line is a single consistent snapshot; taking the lock per map would let a concurrent record land between them and print an id total that disagrees with its call count.

func (*Breadth) Tx

func (b *Breadth) Tx(ctx context.Context, fn func(store.Store) error) error

Tx forwards the transaction, wrapping the view so reads inside it record against this recorder.

The view delegates to the SAME Breadth, rather than being a second Breadth sharing its maps: a fresh struct would carry a fresh zero-value mutex, so it would lock its own lock while writing the parent's maps, which synchronizes nothing. The race detector runs in CI and would eventually say so.

func (*Breadth) Unwrap

func (b *Breadth) Unwrap() store.Store

Unwrap returns the wrapped store, so a lookup keyed on the backend type (the database handle piles and state share) still finds it; see store.Decorator.

type Capabilities

type Capabilities struct {
	// Attachments indicates whether the store implements
	// AttachmentManager. When false, attachment-related conformance
	// tests are skipped.
	Attachments bool

	// Observers builds a store with observers registered, for
	// [RunObserverTests]. Nil skips that suite — but every production backend
	// supports observers, so nil is only right for a store that genuinely
	// has none.
	Observers ObserverFactory

	// TxRollback declares that the backend provides the STRONG Tx contract
	// (DEC-8UIL0): an error from fn discards the transaction's writes, and no
	// events are delivered until commit. Setting it runs RunTxRollbackTests as
	// part of RunAll.
	//
	// This is a claim the backend makes about itself, and stating it here is
	// the point. Before TKT-8TJ2WN the rollback suite was reachable only by
	// calling RunTxRollbackTests separately, so forgetting that call produced
	// silence — identical to fsstore and memstore, which omit the strong
	// contract deliberately (they cannot promise crash atomicity either).
	// A backend that HAS rollback and forgets to say so gets no coverage of
	// its most safety-critical behavior and no warning that it is missing.
	TxRollback bool

	// Versioning declares that the backend implements the content-versioning
	// capability bundle ([store.VersionServiceProvider] yielding a
	// [store.VersionService]). Setting it runs [RunVersionTests] as part of
	// RunAll.
	//
	// Declared rather than sniffed, for the same reason as TxRollback: a type
	// assertion would silently pass for a backend that implements the
	// interface incorrectly or partially, and silently SKIP for one that meant
	// to. The flag makes the claim explicit and the suite then holds it to it.
	Versioning bool

	// SweepNow runs one version-sweep tick on s synchronously, treating every
	// row as settled. Required when Versioning is set: who a swept create or
	// update is attributed to is contract, not mechanism, and a backend whose
	// sweep cannot be driven cannot show it keeps that contract (BUG-07DNNY).
	// Its batch must be smaller than the backlog [RunSweepBacklogTests] seeds.
	SweepNow func(t *testing.T, s store.Store)

	// VersionTags declares that the backend's version service is a
	// [store.VersionTaggerProvider]. Setting it runs [RunVersionTagTests],
	// which needs Versioning and SweepNow too.
	VersionTags bool

	// SoftDelete declares that the backend implements
	// [store.SoftDeleteProvider]. Setting it runs [RunSoftDeleteTests], which
	// fails rather than skips when the capability is missing.
	SoftDelete bool

	// FamilyReplace declares that the backend implements
	// [store.FamilyReplacer] (entity type conversion). Setting it runs
	// [RunFamilyReplaceTests], which fails rather than skips when the
	// capability is missing.
	FamilyReplace bool
}

Capabilities declares which optional Store features an implementation supports. Conformance tests gate optional sections on these flags so implementations that omit a feature (e.g. attachments) can still run the rest of the suite.

type Counting

type Counting struct {
	store.Store
	// contains filtered or unexported fields
}

Counting decorates a store.Store and counts every read call by method name, so a test can pin how many store round-trips an operation costs — the backend-agnostic twin of the SQL accounting a postgres request gets through store.QueryStats (TKT-1U8XYN). An N+1 in a handler is N store calls whatever the backend, which is why this counts calls, not SQL.

Reads are counted; writes pass through uncounted (a test seeds through the same handle, and seeding is not the thing under measurement). The optional capabilities a consumer type-asserts are forwarded: the header readers, the matched counter and the mention index (counted), soft delete (a write, so uncounted), and the transaction view (its reads counted too).

Its exported methods are the store read surface it decorates plus the optional capabilities above, plus Unwrap for store.Decorator, so the count moves with store.Store rather than with this type.

func NewCounting

func NewCounting(s store.Store) *Counting

NewCounting wraps s.

func (*Counting) Calls

func (c *Counting) Calls() map[string]int

Calls returns a copy of the per-method counters.

func (*Counting) CountEntities

func (c *Counting) CountEntities(ctx context.Context, q store.EntityQuery) (int, error)

func (*Counting) CountMatched

func (c *Counting) CountMatched(ctx context.Context, q store.GraphQuery) (int, error)

CountMatched forwards to the wrapped store's matched counter, or to GraphCount's matched half — either way one read.

func (*Counting) CountRelations

func (c *Counting) CountRelations(ctx context.Context, q store.RelationQuery) (int, error)

func (*Counting) GetEntity

func (c *Counting) GetEntity(ctx context.Context, ref entity.Ref) (*entity.Entity, error)

func (*Counting) GetRelation

func (c *Counting) GetRelation(ctx context.Context, k entity.RelationKey) (*entity.Relation, error)

func (*Counting) GraphCount

func (c *Counting) GraphCount(ctx context.Context, q store.GraphQuery) (matched, total int, err error)

func (*Counting) GraphQuery

func (c *Counting) GraphQuery(ctx context.Context, q store.GraphQuery) iter.Seq2[*entity.Entity, error]

func (*Counting) GraphQueryHeaders

func (c *Counting) GraphQueryHeaders(ctx context.Context, q store.GraphQuery) iter.Seq2[store.EntityHeader, error]

GraphQueryHeaders forwards to the wrapped store's native header query, or to the generic projection of GraphQuery — either way one read.

func (*Counting) HighestID

func (c *Counting) HighestID(ctx context.Context, prefix string) (int, error)

func (*Counting) ListEntities

func (c *Counting) ListEntities(ctx context.Context, q store.EntityQuery) iter.Seq2[*entity.Entity, error]

func (*Counting) ListEntitiesPage

func (c *Counting) ListEntitiesPage(ctx context.Context, q store.EntityQuery) (store.Page[*entity.Entity], error)

func (*Counting) ListEntityHeaders

func (c *Counting) ListEntityHeaders(ctx context.Context, q store.EntityQuery) iter.Seq2[store.EntityHeader, error]

ListEntityHeaders forwards to the wrapped store's header reader, or to the generic projection when it has none — either way it is counted as one read.

func (*Counting) ListRelations

func (c *Counting) ListRelations(ctx context.Context, q store.RelationQuery) iter.Seq2[*entity.Relation, error]

func (*Counting) ListRelationsPage

func (c *Counting) ListRelationsPage(ctx context.Context, q store.RelationQuery) (store.Page[*entity.Relation], error)

func (*Counting) MatchingFaces

func (c *Counting) MatchingFaces(
	ctx context.Context, q store.GraphQuery, ids []string,
) (map[string][]entity.Face, error)

func (*Counting) MentionSources

func (c *Counting) MentionSources(
	ctx context.Context, target string, after entity.Ref, limit int,
) ([]entity.Ref, error)

MentionSources forwards to the wrapped store's mention index, counted as one read. A store without one fails the call: no index means no answer, not an empty one.

func (*Counting) Reads

func (c *Counting) Reads() int

Reads returns the total number of read calls since the last Reset.

func (*Counting) Reset

func (c *Counting) Reset()

Reset clears the counters.

func (*Counting) SoftDelete

func (c *Counting) SoftDelete() store.SoftDeleter

SoftDelete forwards the wrapped store's soft-delete capability. See [forwardedSoftDeleter] for a store without one.

func (*Counting) String

func (c *Counting) String() string

String renders the counters as "Method=n Method=n", sorted, for test failure messages.

func (*Counting) Tx

func (c *Counting) Tx(ctx context.Context, fn func(store.Store) error) error

Tx forwards the transaction, wrapping the view so reads inside it count. The view shares the parent's counter state, lock included.

func (*Counting) Unwrap

func (c *Counting) Unwrap() store.Store

Unwrap returns the wrapped store, so a lookup keyed on the backend type (the database handle piles and state share) still finds it; see store.Decorator.

type Factory

type Factory func(t *testing.T) store.Store

Factory returns a fresh, empty store for each test.

type FuzzFactory

type FuzzFactory func() store.Store

FuzzFactory returns a fresh store without needing *testing.T (for use inside f.Fuzz).

type ObserverFactory

type ObserverFactory func(t *testing.T, obs ...store.EntityObserver) store.Store

ObserverFactory returns a fresh store with the given observers registered at construction (observers cannot be added later on any backend).

type SearchFactory

type SearchFactory func(t *testing.T) (store.Store, search.Searcher)

SearchFactory returns a fresh store and its associated searcher for each test. The store is used for seeding data; the searcher is used for queries.

type VisibleSearchFactory

type VisibleSearchFactory func(t *testing.T) (store.Store, search.Searcher, search.VisibleSearcher)

VisibleSearchFactory returns a fresh store, the ungated searcher over it (the per-backend parity baseline), and the VisibleSearcher under test. The two searchers must share the backend: the suite's ordered-subsequence invariant compares the gated stream against the ungated stream of the SAME backend — cross-backend rankers legitimately differ, so order is never compared across implementations, only within one.

Jump to

Keyboard shortcuts

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