Documentation
¶
Overview ¶
Package storetest contains provider-independent contract tests for vector-store implementations and their filter visitors.
Run verifies the exact capability set and validation boundary of a store, comparing the set the store declares against the set CapabilitiesOf detects. Exact means both directions: a capability the store implements but does not declare fails just as a declared one it does not implement does, which is what keeps a no-op vectorstore.Closer from passing as cleanup. VisitorConformance exercises the common filter AST shapes, while VisitorLifecycle verifies that a visitor can be safely reused.
Each vendor wires the suite up in a single test file:
func TestVisitor_Conformance(t *testing.T) {
storetest.VisitorConformance(t, func(src string) error {
expr, err := filter.Parse(src)
if err != nil {
return err
}
compiler := newVisitor(myFieldSchema)
return expr.Accept(compiler)
})
}
Output equivalence (the actual emitted SQL / filter struct) is NOT covered by the suite — backends emit heterogeneous output types and the vendor's own tests still own that responsibility. The suite only guarantees "every valid AST shape visits without error; every well-known invalid AST shape produces an error".
Field identifiers ¶
Every success case uses a disjoint field name per filter-value type so schema-required backends (redis, elasticsearch, opensearch, …) can declare each identifier with one fixed type:
author — string-comparable year — number-comparable published — bool-comparable n, a, b, c, d — number-comparable (used in ordering / AND / OR) tags — string-list (IN) years — number-list (IN) flags — bool-list (IN) title — string-pattern (LIKE) metadata['author'], metadata['a']['b'] — keyed access
Key paths ¶
An indexed key is a string literal, so the caller chooses its bytes. A compiler that writes a metadata key into the query language as text has those bytes read as syntax; one that binds the key as a value does not. Options.InterpolatesKeyPaths declares which kind a compiler is, and the suite asserts the matching direction: an interpolating compiler must refuse a key the language cannot name, and a binding compiler must keep accepting any key. Neither an injection nor a needless refusal can appear without failing here.
Numerals ¶
A compiler whose whole output is text has to write a number as a numeral, and the digits are the only thing between the caller's filter and a different one. Set Options.CompileText and the suite requires the literal's exact digits — the check that was missing when six compilers derived them from a Go scalar through an implementation-defined conversion, so 2^63 came out as 9223372036854775807 on arm64 and correctly on amd64 with every test passing. Options.NumericDomainIsFloat64 relaxes it to same-double equivalence for a provider whose numeric field is a double, because demanding one spelling there would demand precision the field does not keep.
Capability gaps ¶
A backend that genuinely doesn't support a shape (redis can't IN on numeric fields, for example) declares that case via Options.Unsupported. Each entry documents a real vendor capability gap; use sparingly.
Example ¶
package main
import (
"fmt"
"github.com/Tangerg/scope/core/vectorstore/storetest"
)
func main() {
capabilities := storetest.Capabilities{Indexer: true, Searcher: true}
fmt.Println(capabilities.Indexer, capabilities.Searcher)
}
Output: true true
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
func Run(t *testing.T, store any, expected Capabilities)
Run verifies the backend's exact capability set and the common operations that must complete before external I/O. Pass a non-nil zero-value *Store; the calls below must not reach provider dependencies.
func VisitorConformance ¶
VisitorConformance runs the standard expression-coverage suite against a vendor's visitor.
The case lists below are the union of what every backend's filter language must accept (success cases) and the known-rejected shapes every backend must error on (failure cases). Adding a new shape here exercises it across ALL vendors that opt into the suite — the single best lever for "no more silent visitor regressions on the 27th provider".
func VisitorLifecycle ¶
VisitorLifecycle verifies that a compiler resets before every visit and remains reusable after rejecting a malformed predicate.
Types ¶
type BuildFn ¶
BuildFn parses a filter expression source and feeds it through the vendor's visitor. It returns nil on success, an error on failure. Implementations are responsible for assembling the AST (typically via filter.Parse) and driving the vendor visitor.
type Capabilities ¶
type Capabilities struct {
Indexer bool
Searcher bool
HybridSearch bool
// MediaDocuments declares lossless storage of document media alongside text.
// A false value requires rejection of mixed content before external I/O.
MediaDocuments bool
IDDeleter bool
FilterDeleter bool
// Closer is true only for a store that created a resource of its own. A
// store handed its client, session or pool releases nothing, so a false
// flag forbids a Close method rather than permitting a no-op one: a no-op
// Close claims there was something to release and that calling it released
// it, which leaves a caller unable to tell the two kinds of store apart.
Closer bool
}
Capabilities is the exact interface, content, and search-semantics set a backend promises. A false interface flag forbids accidental implementation; a false HybridSearch flag requires rejection before external I/O.
func CapabilitiesOf ¶ added in v0.16.0
func CapabilitiesOf(store any) Capabilities
CapabilitiesOf reports the capability set a store actually implements.
Run compares it against the set the store declares. It is separate from that comparison so the interface detection can be exercised on its own: HybridSearch and MediaDocuments are absent because no interface expresses them; Run probes unsupported semantics through the operation boundary.
type Compiler ¶
Compiler exposes the lifecycle surface shared by provider filter compilers. Snapshot must return a value suitable for reflect.DeepEqual.
type Options ¶
type Options struct {
// Unsupported lists cases the vendor cannot represent exactly. The suite
// verifies that each one returns an error; capability gaps must never turn
// into silent approximations or unexecuted tests.
Unsupported []string
// InterpolatesKeyPaths declares that this compiler writes a metadata key
// into the query language as text rather than binding it as a value.
//
// It decides which way the suite reads a key the target language cannot
// name. An indexed key is a string literal, so the caller chooses its
// bytes; a compiler that pastes them into query text has the caller's key
// read as syntax — profile['a:1 OR b'] compiled to Lucene as
// profile.a:1 OR b, and the same shape reached Typesense's filter_by,
// Vespa's YQL, an OData filter and a RediSearch tag clause. None of those
// languages can quote a field name, so such a key has to be refused, and
// the suite requires it.
//
// A compiler that binds the key instead — a SQL map subscript, a BSON
// field name, a JSON object key — is not exposed, and the suite requires
// the opposite: it must keep accepting any key, because refusing one would
// take away a document it can otherwise filter perfectly well.
InterpolatesKeyPaths bool
// CompileText compiles a filter and returns the query text it produced.
//
// Set it when the compiler's whole output is text, because then a number
// has to be written as a numeral and the digits are the only thing standing
// between the caller's filter and a different one. Six compilers derived
// those digits from a Go scalar and decided integer-ness with
// float64(int64(value)) == value — an out-of-range float-to-int conversion
// Go leaves implementation-defined — so at 2^63 arm64 emitted
// 9223372036854775807 while amd64 emitted the right digits. Every existing
// test passed: nothing compared the digits to anything.
//
// Leave it nil when the compiler binds values as arguments or builds a
// provider structure. There is no numeral to get wrong then, and rendering
// one for the suite's benefit would assert something the store never sends.
CompileText func(source string) (string, error)
// NumericDomainIsFloat64 declares that the provider's numeric fields are
// doubles, so a numeral only has to denote the same double.
//
// RediSearch is the case: its NUMERIC range bounds are doubles, which is
// why that store refuses an integer past 2^53 outright. An integer that a
// double does hold exactly — 2^63, being a power of two — then comes out as
// the shortest decimal that reads back as the same double, which is
// 9223372036854776000 rather than 9223372036854775808. Demanding the
// literal's digits there would demand precision the field cannot keep, so
// the suite asks only that the numeral read back as the same double, which
// still catches a digit lost or invented along the way.
NumericDomainIsFloat64 bool
}
Options tunes the conformance suite for vendors with genuine capability gaps.