storetest

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

func VisitorConformance(t *testing.T, build BuildFn, options ...Options)

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

func VisitorLifecycle(t *testing.T, factory func() Compiler)

VisitorLifecycle verifies that a compiler resets before every visit and remains reusable after rejecting a malformed predicate.

Types

type BuildFn

type BuildFn func(source string) error

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

type Compiler struct {
	Visit    func(filter.Predicate) error
	Snapshot func() any
}

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.

Jump to

Keyboard shortcuts

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