Documentation
¶
Overview ¶
Package vet holds the analyzers behind `sqlb vet`: the checks the compiler cannot make, as standard golang.org/x/tools/go/analysis values.
Two mistakes survive the type system and are caught here instead. An engine expression used in another engine's statement — pg.ArrayAgg inside a sqlite.Select — is universal by type and fails only at Build; the compiler cannot see it because an expression carries no dialect. And a builder method whose result is discarded — q.Where(x) with no assignment — does nothing, because every builder method returns a new statement, and the compiler cannot flag an ignored return value.
The analyzers are ordinary analysis.Analyzer values so they run three ways with one implementation: from cmd/sqlb as `sqlb vet ./...`, from `go vet -vettool`, and from an application's own test through Check, which needs no binary installed. This is a contrib module because it depends on x/tools, and the core depends on nothing.
Index ¶
- Constants
- Variables
- func Analyzers() []*analysis.Analyzer
- func CatalogueKey(schemaName, name string) string
- func Check(t TB, patterns ...string)
- func Failing(fs []Finding) bool
- type Catalogue
- type CatalogueColumn
- type Declaration
- type Declared
- type Finding
- func CheckTables(ctx context.Context, db *sql.DB, dialect string, tables []schema.Table) ([]Finding, error)
- func Compare(dialect string, decls Declared, cat Catalogue) []Finding
- func CompareKeys(tables []schema.Table, keys KeyCatalogue) []Finding
- func CompareTables(dialect string, tables []schema.Table, cat Catalogue) []Finding
- func Run(dir string, patterns ...string) ([]Finding, error)
- func RunWith(dir string, analyzers []*analysis.Analyzer, patterns ...string) ([]Finding, error)
- func Schema(ctx context.Context, db *sql.DB, dialect, dir string, patterns ...string) ([]Finding, error)
- type KeyCatalogue
- type TB
- type TableDeclaration
- type TableKeys
Constants ¶
const Module = "github.com/ubgo/sqlb"
Module is the import path of the core; the engine and entry packages hang off it. A path is compared rather than a package name, so an import alias cannot hide a package from the check.
Variables ¶
var Dialect = &analysis.Analyzer{ Name: "sqlbdialect", Doc: "reports an engine expression (pg., sqlite., ch.) used in a statement fixed to another engine", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runDialect, }
Dialect reports an engine expression used inside a statement whose engine is statically known to be a different one.
The statement's engine comes from its root call: an entry package's constructor (pg.Select), or NewSelect and its siblings given a dialect literal. A statement whose dialect is a runtime value has no engine here and is not checked; Build catches it, and that is documented. The engine of an expression comes from the import path of the package it is selected from, resolved through type information so an alias does not hide it.
var Discarded = &analysis.Analyzer{ Name: "sqlbdiscarded", Doc: "reports a statement builder call whose result is discarded", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runDiscarded, }
Discarded reports a builder method call whose result is not used.
Every builder method returns a derived statement and leaves its receiver alone, so `q.Where(p)` on its own line builds the unfiltered query and the compiler cannot say so.
var ErrUnknownAction = errors.New("sqlb vet: catalogue reports an unknown referential action")
ErrUnknownAction is returned when a catalogue reports a referential action this reader has no name for, rather than comparing it as something else.
var Escapes = &analysis.Analyzer{ Name: "sqlbescape", Doc: "lists every escape hatch use for review: Raw, Literal, New, PredicateOf, JoinType, SetOp, non-constant Col", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runEscapes, }
Escapes lists every use of an escape hatch: Raw, Literal, New, PredicateOf, a JoinType or SetOp keyword, and a Col whose name is not a constant. It reports nothing wrong; it is the review list for the places where the compiler has been asked to step aside, which is why it is not in Analyzers and runs only when asked for.
var Keyword = &analysis.Analyzer{ Name: "sqlbkeyword", Doc: "reports a JoinType or SetOp keyword the statement's engine rejects, and NATURAL joins everywhere", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runKeyword, }
Keyword reports a JoinType or SetOp keyword the statement's engine does not accept, when the engine is statically known, and NATURAL in any join under any engine. JoinType and SetOp take a string on purpose, as the escape hatch for a keyword the constants omit; this is the check that the hatch was used for a keyword the engine has.
var Literal = &analysis.Analyzer{ Name: "sqlbliteral", Doc: "reports expr.Literal called with a non-constant argument, which interpolates a value into SQL text", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runLiteral, }
Literal reports expr.Literal receiving anything but a constant. A literal is SQL text the author wrote and is rendered verbatim, so a variable there is interpolation; a value belongs in Param, which binds it.
var Rows = &analysis.Analyzer{ Name: "sqlbrow", Doc: "reports RowIn, RowEq and RowLess rows whose arity or element types differ", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runRows, }
Rows reports a row comparison whose literal rows disagree: RowIn, RowEq and the rest compare Row(...) values positionally, and a row with the wrong arity or an element of another type is an SQL error the compiler cannot see because Row returns an untyped SQLer.
var StructLit = &analysis.Analyzer{ Name: "sqlbstructlit", Doc: "reports a stmt statement or Query built as a struct literal, which has no exec guard and no typestate", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runStructLit, }
StructLit reports a statement or Query built as a struct literal outside the stmt package. A literal skips the constructor, so it has no exec guard and no typestate: a literal Update renders without a WHERE and a literal Query lets two goroutines interleave rows. The core pins that the literal is possible; this is where a caller is told not to.
Functions ¶
func CatalogueKey ¶
CatalogueKey is the key a Catalogue stores a table under: the bare name when schemaName is empty, otherwise "schemaName.name".
Types ¶
type Catalogue ¶
type Catalogue map[string]map[string]CatalogueColumn
Catalogue is what a database says a column is, keyed by CatalogueKey and then by column name. Type is the engine's own spelling, lower-cased; Nullable is whether NULL is allowed.
A table in the connection's default schema, Postgres's current_schema(), MySQL's DATABASE() or SQLite's main, is stored under its bare name and under "schema.name", so a declaration that names no schema and one that names the default both find it. A table in any other schema is stored only qualified. Keying by the bare name alone, as this did until 2026-09-22, read only the default schema and reported every table of a library that owns a schema of its own, auth.users say, as missing, and would have merged two same-named tables in different schemas into one.
func ReadCatalogue ¶
ReadCatalogue reads every user table's columns from the database, in every user schema. dialect is render.DialectPostgres, render.DialectSQLite or render.DialectMySQL; the query is the engine's own catalogue.
type CatalogueColumn ¶
type CatalogueColumn struct {
Type string
Nullable bool
// Default is the column's default as SQL text, nil for none. Postgres and
// SQLite report it as SQL already; MySQL reports a literal's value
// unquoted, which ReadCatalogue quotes so every engine's is SQL. Compare
// by meaning with CompareTables, never by string: see defaults.go.
Default *string
// ColumnType is MySQL's column_type, the type with its length and
// precision, varchar(255) where Type is varchar; empty on the other
// engines, whose Type already carries what CompareTables needs.
ColumnType string
}
CatalogueColumn is one column as the database describes it.
type Declaration ¶
type Declaration struct {
Pos string
Table string
Schema string
Column string
GoType string
Nullable bool // the Go type is a pointer
}
Declaration is one Col[T] declaration found in source: the table and column it names, the Go type it claims, and where it was written.
type Declared ¶
type Declared struct {
Columns []Declaration
Tables []TableDeclaration
}
Declared is everything Declarations found: the columns and the tables they were resolved against.
func Declarations ¶
Declarations finds every expr.Col[T] and sqlb.Col[T] call in the packages the patterns name, resolving each table argument to the package-level Table literal it refers to.
It reads source rather than a runtime registry, which is the point: a declaration in a code path nothing executes is still checked. A table that is not a package-level literal with a literal Name cannot be resolved and is reported as a Declaration with an empty Table, so it is visible rather than silently skipped.
type Finding ¶
Finding is one diagnostic, positioned. A Warning is reported but does not fail a check: it names something worth a look that the code may be right to do, such as a column the database has that nothing declares.
func CheckTables ¶
func CheckTables(ctx context.Context, db *sql.DB, dialect string, tables []schema.Table) ([]Finding, error)
CheckTables is the boot check in one call: it reads the column and key catalogues and returns CompareTables's findings followed by CompareKeys's.
func Compare ¶
Compare diffs declarations against a catalogue and returns one Finding per mismatch: a table or column the database does not have, a Go type the column's type cannot hold, and a nullable column declared without a pointer. A declaration whose table could not be resolved is reported as unverifiable rather than skipped. A column the database has that no declaration names is a warning on the table's declaration, so a migration that adds a column is not invisible; it is a warning because a library may deliberately declare only the columns it reads.
func CompareKeys ¶
func CompareKeys(tables []schema.Table, keys KeyCatalogue) []Finding
CompareKeys diffs the keys a library's DDL describes against the database:
- the primary key must be the same columns in the same order;
- every index the DDL declares must exist over the same columns in the same order, and a unique one must be unique there;
- every foreign key the DDL declares must exist over the same columns, referencing the same table and columns, with the same ON DELETE and ON UPDATE, an unstated action being NO ACTION as every engine reports it.
Keys are matched by their columns, never by name: SQLite reports no foreign key names, and an unnamed key's generated name is the engine's. What the database has and the DDL does not is a warning for a unique index or a foreign key, which a later ALTER may have added, and nothing for a plain index, since MySQL creates one for every foreign key and CREATE INDEX is not part of a CREATE TABLE's description. A table missing from keys is CompareTables's finding and is skipped here.
func CompareTables ¶
CompareTables diffs tables a library's DDL describes, from ddl.CreateTableReady.Describe, against a catalogue: a table or column the database does not have, a column whose nullability differs in either direction, and a portable type the catalogue's type cannot be. It is the check a library wants at boot, "the database is not what this library's DDL creates", and unlike Compare it reads the DDL rather than the Col[T] declarations, so it sees nullability and the declared SQL type.
A column declared with a Raw spelling, an enum or a domain or a citext, is checked for existence and nullability only: the catalogue spells such types its own way, and matching the spelling would be a guess. A column the database has and the DDL does not is a warning, as in Compare. Each finding is positioned at "table.column", or "schema.table.column" for a table with a schema, since a description has no source line. A column wrong in both nullability and type gets a finding for each. A default is compared by meaning, as defaults.go describes: a default on one side only, or two literals that differ, is an error, and two expressions that still differ once normalised are a warning, since the engine reprints an expression in a form nobody documents. Keys, indexes and foreign keys are CompareKeys's.
func Run ¶
Run loads the packages the patterns name and runs every analyzer over them, returning the findings. It is what Check and cmd/sqlb share.
type KeyCatalogue ¶
KeyCatalogue is every table's keys, keyed by CatalogueKey as Catalogue is, with the connection's default schema so a reference the DDL writes without one can be matched to the qualified name the database reports.
type TB ¶
type TB interface {
Helper()
Logf(format string, args ...any)
Errorf(format string, args ...any)
Fatalf(format string, args ...any)
}
TB is the part of testing.TB Check needs, so it is callable from a test without importing testing here.
type TableDeclaration ¶
TableDeclaration is one package-level Table literal: where it was written and the name it states, so a warning about the table has a position.
type TableKeys ¶
type TableKeys struct {
PrimaryKey []string
Indexes []schema.Index
ForeignKeys []schema.ForeignKey
}
TableKeys is a table's keys as the database has them: its primary key, its indexes on plain columns, and its foreign keys. An expression or partial index is left out, because schema.Index has no place for one and comparing its column list alone would call it something it is not. RefTable on a foreign key is always "schema.table".
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
sqlb
command
Command sqlb is the command-line front for the sqlb toolkit.
|
Command sqlb is the command-line front for the sqlb toolkit. |
|
sqlbvet
command
Command sqlbvet is the analyzers as a go vet tool, so they run inside the standard toolchain with no subcommand of their own:
|
Command sqlbvet is the analyzers as a go vet tool, so they run inside the standard toolchain with no subcommand of their own: |
|
internal
|
|
|
probe/clean
Package clean has nothing for the analyzers to report.
|
Package clean has nothing for the analyzers to report. |
|
probe/constnames
Package constnames declares tables and columns whose names are constants rather than literals, which is how many libraries write them, for the test that Declarations resolves them.
|
Package constnames declares tables and columns whose names are constants rather than literals, which is how many libraries write them, for the test that Declarations resolves them. |
|
probe/constnames/lookalike
Package lookalike has a function named Col with Col's signature, so the test can check that Declarations reads only sqlb's Col and not every Col.
|
Package lookalike has a function named Col with Col's signature, so the test can check that Declarations reads only sqlb's Col and not every Col. |
|
probe/discard
Package discard drops the result of a builder call.
|
Package discard drops the result of a builder call. |
|
probe/mixed
Package mixed uses one engine's expression in another engine's statement.
|
Package mixed uses one engine's expression in another engine's statement. |
|
probe/schema
Package schema declares columns for the --dsn mode's test, some right and some wrong against the table the test creates.
|
Package schema declares columns for the --dsn mode's test, some right and some wrong against the table the test creates. |
|
probe/static
Package static holds one instance of each mistake the static analyzers report, and one correct use of each construct beside it.
|
Package static holds one instance of each mistake the static analyzers report, and one correct use of each construct beside it. |