sqlbtest

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

sqlbtest — the shared conformance suite

Twelve cases plus RunReturning, written once and run by every driver, because rendering SQL and executing it are two different claims.

Run holds a runtime dialect and therefore cannot ask for a capability, which is why the RETURNING case is RunReturning, called with a concrete dialect. contrib/driver-sqlite runs the suite in memory and file-backed; contrib/driver-pgx runs it and RunReturning against a real Postgres 17. A driver that passes it has proven the thing the builder's own tests cannot.

It also holds ZeroValues, which needs no database: it builds the zero value of every exported struct type in a package, found by reading the package's source, and fails on one that panics inside Build or builds without an error. ddl, pg, sqlite, mysql and ch run it, and a construct package from another repository can too.

Full documentation

The page for this package is docs/conformance.md, with runnable snippets — every one of them copied from a test whose expected output go test checks. The documentation index is ../docs/README.md and the repository README is ../README.md.

Documentation

Overview

Package sqlbtest is the conformance suite every sqlb driver integration runs. It exists because rendering SQL and executing it are different claims: the builder's own tests prove the text, and only a real database proves that the placeholders, the argument order and the scan targets line up. A driver integration that passes Run is known to carry the builder end to end, not merely to compile against it.

The suite has no third-party dependencies. It takes a sqlb.DBTX, so it runs against a *sql.DB, a *sql.Tx, or any wrapper a caller has of their own.

sqlb generates no DDL, so the caller supplies the schema in its engine's own spelling. The table and column declarations the suite binds to are exported here, and Schema names the shape they expect.

Index

Constants

View Source
const Schema = `sqlb_conformance_user(id, email, name, age, active); sqlb_conformance_post(id, user_id, title, views)`

Schema describes the tables Run needs, engine-neutral. A caller translates it into its own DDL; the types named here are the loosest that carry the suite, so any engine has a spelling for each.

Text must compare by its bytes, as Postgres and SQLite compare it by default; on MySQL that means a utf8mb4_bin collation. The case_sensitive_eq case checks it, and an integration running MySQL's default collation skips that case with the reason, as contrib/driver-mysql does.

sqlb_conformance_user(id integer primary key, email text not null,
                      name text null, age integer not null,
                      active boolean not null)
sqlb_conformance_post(id integer primary key, user_id integer not null,
                      title text not null, views integer not null)

Variables

View Source
var (
	Users = sqlb.Table{Name: "sqlb_conformance_user", Alias: "u"}
	Posts = sqlb.Table{Name: "sqlb_conformance_post", Alias: "p"}
)

The tables the suite works against. A caller creates them with its own DDL before calling Run. The names are prefixed so they cannot collide with an application's own tables in a shared test database.

View Source
var (
	UserID     = sqlb.Col[int64](Users, "id")
	UserEmail  = sqlb.Col[string](Users, "email")
	UserName   = sqlb.Col[string](Users, "name")
	UserAge    = sqlb.Col[int64](Users, "age")
	UserActive = sqlb.Col[bool](Users, "active")
)

Columns of Users. Name is the nullable one, on purpose: a NULL that reaches a non-pointer target is the failure a text-only test never sees.

View Source
var (
	PostID     = sqlb.Col[int64](Posts, "id")
	PostUserID = sqlb.Col[int64](Posts, "user_id")
	PostTitle  = sqlb.Col[string](Posts, "title")
	PostViews  = sqlb.Col[int64](Posts, "views")
)

Columns of Posts.

View Source
var CRUDTranscript = []string{
	"insert one: 1 affected",
	"insert many: 4 affected",
	"insert posts: 4 affected",
	"after create: user 1|Ada@Example.com|Ada|36|true",
	"after create: user 2|bo@example.com|Bo|41|true",
	"after create: user 3|CY@example.com|NULL|29|false",
	"after create: user 4|di@Example.COM|Di|52|true",
	"after create: user 5|50%off@shop|Deal|18|false",
	"after create: post 1|1|hello|10",
	"after create: post 2|1|again|30",
	"after create: post 3|2|first|5",
	"after create: post 4|4|only|70",
	"Eq exact: [2]",
	"EqFold: [1]",
	"In: [2 4]",
	"InFold: [3 4]",
	"NotIn: [3 4 5]",
	"Greater: [2 4]",
	"Between: [1 2 3]",
	"IsNull: [3]",
	"active: [1 2 4]",
	"ContainsFold: [1 2 3 4]",
	"Contains escapes %: [5]",
	"StartsWithFold: [4]",
	"EndsWithFold: [1 2 3 4]",
	"And Or Not: [1 4]",
	"empty In: []",
	"page 1: [4 2]",
	"page 2: [1 3]",
	"page 3: [5]",
	"aggregates: count=5 sum=176 min=18 max=52",
	"join: user 1 posts=2 views=40",
	"join: user 2 posts=1 views=5",
	"join: user 3 posts=0 views=0",
	"join: user 4 posts=1 views=70",
	"join: user 5 posts=0 views=0",
	"update age+1 where active: 3 affected",
	"update matching nothing: 0 affected",
	"update to the value it already has: 1 affected",
	"update name to NULL: 1 affected",
	"update by EqFold: 1 affected",
	"update posts views: 2 affected",
	"after update: user 1|Ada@Example.com|Ada|37|true",
	"after update: user 2|bo@example.com|Bo|42|true",
	"after update: user 3|CY@example.com|Cy|29|true",
	"after update: user 4|di@Example.COM|NULL|53|true",
	"after update: user 5|50%off@shop|Deal|18|false",
	"after update: post 1|1|hello|20",
	"after update: post 2|1|again|60",
	"after update: post 3|2|first|5",
	"after update: post 4|4|only|70",
	"delete posts of user 1: 2 affected",
	"delete by InFold: 2 affected",
	"delete matching nothing: 0 affected",
	"after delete: user 2|bo@example.com|Bo|42|true",
	"after delete: user 3|CY@example.com|Cy|29|true",
	"after delete: user 4|di@Example.COM|NULL|53|true",
	"after delete: post 3|2|first|5",
	"after delete: post 4|4|only|70",
}

CRUDTranscript is what the workload must produce, line for line, on every engine. It is the golden: an engine that differs anywhere fails naming the line, and changing a line here is changing what every engine is held to.

Functions

func Run

func Run(t *testing.T, c Config)

Run executes the whole suite as subtests. A driver integration's test is one call to this.

func RunReturning

func RunReturning[D sqlb.ReturningDialect](t *testing.T, c Config, d D)

RunReturning executes the RETURNING case, which the main suite cannot hold.

Run takes the dialect as a runtime value, so its statements are Select[render.Dialect] and their kin: universal SQL only, by construction. RETURNING is a capability, and InsertReturning is constrained to a dialect type that has it, so a statement whose dialect is only known at runtime cannot ask for it and the compiler says so. A driver whose dialect has RETURNING calls this with its concrete dialect; a driver whose dialect does not cannot write the call, which is the skip made into a type.

func ZeroValues

func ZeroValues(t testing.TB, cfg ZeroConfig)

ZeroValues checks that no zero value of an exported type in a package can make Build panic or succeed.

Every exported builder, statement and clause here is a struct, so a caller can write T{} as well as use its constructor, and a struct literal skips whatever the constructor guaranteed. pg.ForeignKeyClause{} indexed its first column pair and panicked inside Build until 2026-09-22, and so did pg.DropColumnClause{}; nothing had constructed either one without its constructor. So each Value is built, a clause by calling WriteSQL on inline Args, a statement by calling Build, each under recover, and each must fail with an error rather than panic or render. A type that has neither method must be in Probes or Exempt, and the source is scanned for every exported struct type, so a new one fails the check until someone decides which of the three it is. The scan fails closed: finding no types is an error.

Types

type Case

type Case struct {
	Name string
	Why  string
	// contains filtered or unexported fields
}

Case is one conformance case. Cases is the full list, in run order, so a caller can see what Run covers without reading the source.

func Cases

func Cases() []Case

Cases returns the suite, in run order.

type Config

type Config struct {
	// DB is the handle the suite executes against. It must already point at
	// a database where Setup has been applied.
	DB sqlb.DBTX

	// Dialect is the dialect the integration builds with. The suite asserts
	// nothing about its spelling; it only uses it to build.
	Dialect sqlb.Dialect

	// Setup runs once before the suite, and Teardown once after. Both are
	// raw DDL because sqlb generates none. Setup must create the two tables
	// Schema describes; Teardown usually drops them.
	Setup    []string
	Teardown []string

	// Skip excuses a case by name with a reason that is printed. Use it for
	// a genuine engine limitation, never to hide a failure: the reason ends
	// up in the test log, where a reviewer reads it.
	Skip map[string]string
}

Config is what a driver integration passes to Run.

type FoldQuery

type FoldQuery struct {
	Name string
	Pred sqlb.Predicate
	Want []int64
}

FoldQuery is one query of the case_fold case: a predicate and the ids it must select from the fold data on every engine.

func FoldQueries

func FoldQueries() []FoldQuery

FoldQueries is the query set case_fold runs. Exported so an integration can run the same set under a configuration Run does not cover, a second collation say, and hold it to the same answers.

type ZeroConfig

type ZeroConfig struct {
	// Dir is the package's source directory, read to find every exported
	// struct type, so a type added later is checked or fails the gate.
	Dir string
	// Dialect is what a clause's WriteSQL renders under.
	Dialect sqlb.Dialect
	// Values holds the zero value of every type with a Build or WriteSQL
	// method, as T{} or &T{}; a generic type is listed instantiated, since
	// Go cannot instantiate one from its name. The element is any because
	// the list holds values of unrelated types, which is what the check
	// inspects by reflection; no generic type can name that set.
	Values []any
	// Probes holds, by type name, a check for a type whose zero value is not
	// rendered by itself: an object value passed to its constructor, a column
	// definition placed in a table. A probe returns the error Build returned.
	Probes map[string]func() error
	// Controls holds, for every probe, the same construct built with a real
	// value, which must succeed. Without it a probe that fails for a reason of
	// its own, a wrong argument in the test, passes and proves nothing.
	Controls map[string]func() error
	// Exempt names a type that needs no probe and says why: a value that
	// renders nothing and reaches no Build with its zero value unguarded.
	Exempt map[string]string
}

ZeroConfig describes one package's zero-value check: the package whose exported struct types are checked, the dialect its clauses render under, and what to do with each type.

Jump to

Keyboard shortcuts

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