Files
base/core/sqlite_math.go
zeekayandHanzo Dev 65fdb03b35
deploy / build (push) Waiting to run
Ask the SQLite engine for its math functions instead of asking the build
core/sqlite_math_required.go failed any cgo build not carrying the
sqlite_math_functions tag, by referencing a symbol that was never defined
anywhere. So `go build ./...` — no tags, default CGO_ENABLED — did not
work in this repo or in any repo importing core, and the error named a
symbol rather than the problem:

    undefined: cgoBuildNeedsSQLiteMathFunctions

The capability it was protecting is real. tools/search emits acos, cos,
sin and radians for geoDistance, and SQLite only has those when compiled
with SQLITE_ENABLE_MATH_FUNCTIONS. Measured, per config:

    CGO_ENABLED=0                              present   (what the Dockerfile ships)
    CGO_ENABLED=1                              ABSENT    <- the one deficient config
    CGO_ENABLED=1 -tags sqlite_math_functions  present
    CGO_ENABLED=1 -tags libsqlite3             present   <- blocked anyway

The tag was a proxy for the capability, and the proxy was wrong. Row four
is the config the driver's own doc comment calls production: that tag
links the system sqlcipher, so the amalgamation's -D flag is never
compiled and the tag decides nothing, yet the guard rejected the build.
It also could not have caught a system libsqlcipher genuinely missing the
functions, and no library can satisfy it from the inside, because build
tags are chosen by the top-level build command.

Replace it with a measurement. VerifySQLiteMathFunctions runs the
geoDistance expression once per process against a throwaway in-memory
database and DefaultDBConnect refuses to hand back a connection when the
answer is no, so a deficient binary dies at startup with the three ways
to fix it rather than at a customer's failed search. A probe that could
not run at all is not an error: this reports a measured absence, never a
failure to measure.

Detection is strictly better than before. tools/search already had
TestTokenFunctionsGeoDistanceExec executing the real emitted SQL, and it
fails on the deficient config today; core now carries its own, since
tools/search does not import core and its failure said nothing about
this package.

  build, all four configs above          exit 0 (was: 2 of 4 refused)
  go test ./... CGO_ENABLED=0            64 packages ok, 0 failures
  go test ./core -run SQLiteMath CGO=1   FAIL, as it should

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 09:27:45 -07:00

94 lines
4.1 KiB
Go

package core
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/hanzoai/dbx"
)
// Base's search layer emits SQL that calls acos, cos, sin and radians — see the
// geoDistance token function in tools/search. SQLite only has those when it was
// compiled with SQLITE_ENABLE_MATH_FUNCTIONS, so a build can link an engine
// whose SQL surface is smaller than the one the query builder writes against,
// and nothing complains until someone filters by geoDistance and gets
// "no such function: acos" from an endpoint that works elsewhere.
//
// Which builds are deficient is a property of the linked engine, not of any
// build tag. Measured on this tree:
//
// CGO_ENABLED=0 present (pure-Go modernc; what the Dockerfile ships)
// CGO_ENABLED=1 ABSENT <- the one deficient config
// CGO_ENABLED=1 -tags sqlite_math_functions present (csqlite compiles the amalgamation with -DSQLITE_ENABLE_MATH_FUNCTIONS)
// CGO_ENABLED=1 -tags libsqlite3 present (links the system sqlcipher, which already has them)
//
// This was previously a compile-time guard that failed any cgo build not
// carrying the sqlite_math_functions tag. It tested a proxy rather than the
// property: it rejected -tags libsqlite3 builds whose SQL surface is complete
// (row four — that tag selects the system library, so the amalgamation's -D
// flag is never compiled), it could never catch a system libsqlcipher that
// genuinely lacked the functions, and no library can satisfy it from the
// inside, because build tags are chosen by the top-level build command. Its
// only mechanism was an undefined symbol, so every consumer of this package
// lost `go build ./...` to an error that named a symbol rather than the
// problem.
//
// So ask the engine instead of the build.
// errNoSQLiteMathFunctions is returned by VerifySQLiteMathFunctions when the
// linked SQLite is measurably missing the math functions.
var errNoSQLiteMathFunctions = errors.New("base: the SQLite engine linked into this binary has no math functions")
// mathProbeSQL is the shape tools/search emits for geoDistance: every math
// function the filter builder can produce, in one round trip.
const mathProbeSQL = `SELECT 6371 * acos(cos(radians(1)) * cos(radians(2)) * cos(radians(3) - radians(4)) + sin(radians(1)) * sin(radians(2)))`
var (
mathOnce sync.Once
mathErr error
)
// VerifySQLiteMathFunctions reports whether the SQLite engine linked into this
// binary implements the math functions the search layer's geoDistance filter
// emits. It answers from a throwaway in-memory database, so a caller's own
// connection is neither opened nor kept warm on its behalf, and it measures
// once per process — the answer is a property of the linked engine, not of any
// database file.
//
// A definitive "no such function" is an error: a capability the query builder
// assumes is not something to discover from a customer's failed search. A probe
// that could not run at all — no "sqlite" driver registered under the
// no_default_driver tag, or an environment that refuses an in-memory database —
// is not: this reports a measured absence, never a failure to measure.
func VerifySQLiteMathFunctions() error {
mathOnce.Do(func() { mathErr = probeSQLiteMathFunctions() })
return mathErr
}
func probeSQLiteMathFunctions() error {
db, err := dbx.Open("sqlite", "file:base_sqlite_math_probe?mode=memory")
if err != nil {
return nil // inconclusive
}
defer db.Close()
var distance float64
if err := db.NewQuery(mathProbeSQL).Row(&distance); err != nil {
// Both backends phrase a missing scalar function this way; anything
// else is a probe that failed for reasons of its own.
if strings.Contains(err.Error(), "no such function") {
return fmt.Errorf(
"%w (%v) — search filters using geoDistance would fail at query time. "+
"Rebuild with CGO_ENABLED=0 (how Base ships), or with -tags sqlite_math_functions, "+
"or with -tags libsqlite3 to link a system SQLite that already has them",
errNoSQLiteMathFunctions, err,
)
}
return nil // inconclusive
}
return nil
}