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 ¶
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 ¶
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.
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.
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.
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 ¶
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 ¶
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.
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 ¶
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.