vet

package module
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: 22 Imported by: 0

README

vet

The analyzers behind sqlb vet: the checks the compiler cannot make, as standard golang.org/x/tools/go/analysis values, runnable from a CLI or from an application's own test, plus the one check that needs a database.

Contents

Setup

import (
	"github.com/ubgo/sqlb/contrib/vet"
)

What it checks

Six mistakes survive the type system, and Analyzers returns the analyzer for each. Every one compiles, renders, and is wrong.

Dialect — an engine expression used in a statement fixed to another engine. pg.ArrayAgg inside a sqlite.Select is universal by type, so it compiles and fails only at Build; the compiler cannot see it because an expression carries no dialect. The analyzer walks each method chain to its root call — an entry package's constructor, or NewSelect and its siblings given a dialect literal — to learn the statement's engine, then reports any selector into pg, sqlite, ch or an extension sub-package that names a different one. Packages are resolved through type information by import path (Module and below), so an import alias cannot hide one. A statement whose dialect is a runtime value has no engine here and is not checked; Build catches it.

Discarded — a builder method whose result is dropped. Every builder method returns a new statement and leaves its receiver alone, so q.Where(p) on its own line built the unfiltered query and the compiler cannot flag the ignored return. Any expression statement whose value is a pointer to a stmt statement type is reported. The engine packages' own statement kinds count too: pg.Merge, pg.Truncate and pg.LockTable return derived statements since 2026-09-21, so m.Into(t) on its own line is the same mistake, and the analyzer recognises any engine type with a Build method rather than a list of names, so a statement kind added later is covered without anyone remembering to add it.

Literal — Literal called with anything but a constant. A literal is SQL text the author wrote and is rendered verbatim, so Literal[string](s) with s from a variable is interpolation; the value belongs in Param, which binds it. The definition in expr and the facade's forwarder are skipped, since each passes its own parameter through.

StructLit — a statement or Query built as a struct literal outside stmt. A literal skips the constructor, so it has no exec guard and no typestate: &FilteredUpdate{} renders without a WHERE and Query{SQL: …} lets two goroutines interleave rows. The zero value Query{} returned beside an error is exempt; nothing executes it.

Keyword — a JoinType or SetOp keyword the statement's engine rejects, when the engine is statically known, and NATURAL in any join under any engine. The two 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. The tables are small and verified: STRAIGHT_JOIN, CROSS APPLY and OUTER APPLY are other engines' spellings on all three; INTERSECT ALL and EXCEPT ALL do not exist on SQLite; ClickHouse's set operations are not in the table, and an absent engine means no opinion. A Join literal is checked when written inline in the Join(...) call, which is where nearly every one is.

Rows — a RowIn, RowNotIn, RowEq, RowLess or RowGreater whose literal Row(...) values disagree in arity or element type. Row returns an untyped SQLer, so RowIn(Row(id, name), Row(Param(1), Param("a"), Param("b"))) compiles; the database rejects it. Rows are compared to the first, positionally, using the Expr[T] type argument of each element.

Run on this repository, the analyzers report exactly seven sites, all deliberate: three tests that pin the dialect residue by building an engine expression under the wrong dialect and asserting ErrUnsupportedDialect, one each for pg, sqlite and mysql; two that pin the same residue for an engine statement, a pg.Truncate or pg.LockTable embedded as a CTE body in a SQLite statement, which is the one path to another engine's dialect since their constructors became typed; and two that pin that a Query literal has no guard.

Against the database

Schema is the one check that needs a connection, because it checks the one claim no compiler can see: that a Col[T] declaration matches the real table.

findings, err := vet.Schema(ctx, db, "sqlite", ".", "./...")

Declarations reads every expr.Col[T] and sqlb.Col[T] call in the packages named and returns a Declared value: one Declaration per column, carrying its Pos, Table, Schema, Column, the GoType it claims and whether that type is a pointer (Nullable), and one TableDeclaration per package-level Table literal the columns were resolved against. It reads source rather than a runtime registry, so a declaration in a code path nothing executes is still checked. A table's Name and Schema and a column's name are read as the constant the compiler folds them to, so Name: usersTable with const usersTable = prefix + "users" is auth_users, and a typed constant works as a literal does; only sqlb and expr Col calls count, so a function of another package that happens to be called Col is not read as one. ReadCatalogue reads every user table's columns, in every user schema, from information_schema.columns on Postgres and MySQL or pragma_table_info on SQLite into a Catalogue, a map of table to column to CatalogueColumn — the engine's own type spelling, lower-cased, and whether NULL is allowed. Tables are keyed by CatalogueKey(schema, name): a table in the connection's default schema (current_schema(), DATABASE(), SQLite's main) is under its bare name and under schema.name, any other only qualified, so auth.users is compared with auth.users and never with a users in the default schema. Postgres shows only the columns the connecting role can access, so a table the role has no privilege on reads as missing. On SQLite a primary-key column is nullable unless it is declared NOT NULL, is the rowid itself, or its table is STRICT or WITHOUT ROWID, which is SQLite's own rule; INTEGER PRIMARY KEY DESC is not the rowid and is nullable, and a test inserts a NULL key into each shape to prove the catalogue agrees with the engine. Compare diffs the two and returns a Finding per mismatch, by file and line:

Finding Kind
the table is not in the database error
the column is not in the database error
the Go type cannot hold the column's type — Col[string] on an integer error
the column is nullable and the declaration has no pointer — Col[string] where Col[*string] is needed error
the table argument is not a package-level literal, so the declaration cannot be verified error
the column's name is a variable, not a constant, so the declaration cannot be verified error
the database has columns no declaration names warning

A warning is reported and does not fail the check; a library may deliberately declare only the columns it reads, and the warning is there so a migration that adds a column is not invisible.

ReadCatalogue reads MySQL's information_schema.columns too, and the --dsn flag takes mysql://user:pw@tcp(host:3306)/db for it.

Against the DDL

CompareTables(dialect, tables, catalogue) compares the database with what the library's DDL creates, rather than with its Col[T] declarations. The tables are schema.Table values from ddl.CreateTableReady.Describe, so the check sees what a Col[T] cannot: nullability as the DDL declares it, in both directions, and the declared SQL type. The type check is strict where Compare's is lenient: Compare asks whether a Go type can hold a column, so timestamp and timestamptz both hold a time.Time; CompareTables asks whether the column is the one the DDL created, so it compares the catalogue's spelling with the one schema.EngineType writes, reading MySQL's CatalogueColumn.ColumnType (varchar(255), where Type is varchar) for the length.

desc, err := pg.CreateTable(Users).Column(/* … */).Describe()
cat, err := vet.ReadCatalogue(ctx, db, "postgres")
findings := vet.CompareTables("postgres", []schema.Table{desc}, cat)
Finding Kind
the table is not in the database error
the column is not in the database error
the DDL says NOT NULL and the database allows NULL, or the other way round error
the column is not the type the DDL writes: a timestamp without time zone where the DDL says timestamptz, a real for double precision, a MySQL varchar(100) for VARCHAR(255) error
the database has columns the DDL does not create warning

A primary-key column the DDL leaves nullable is not a finding, because Postgres and MySQL make every key column NOT NULL themselves; a key column the DDL says is NOT NULL and the database allows NULL in is, and SQLite can have one. A column wrong in both nullability and type gets both findings.

A default is compared by what it means, because no two engines print one alike: Postgres reads DEFAULT -5 back as '-5'::integer, MySQL returns TRUE as 1 and 2.5 on a DECIMAL(10,2) as 2.50, and SQLite returns the text as written. CatalogueColumn.Default holds each as SQL text, MySQL's unquoted value quoted by the reader, and two literals are equal when their values are: numbers numerically, booleans as 1 and 0, times as instants, strings exactly. A default on one side only, or two literals that differ, is an error; two expressions that still differ once lower-cased and stripped of whitespace, parentheses, casts and charset introducers are a warning, since the engine reprints an expression in a form nobody documents.

Keys, indexes and foreign keys

ReadKeys(ctx, db, dialect) reads every table's primary key, its indexes on plain columns and its foreign keys into a KeyCatalogue: a TableKeys per table keyed like a Catalogue, and DefaultSchema, the connection's default schema, which lets a reference the DDL writes unqualified match the qualified name the database reports. CompareKeys(tables, keys) compares them with the DDL's descriptions:

Finding Kind
the primary key is other columns, or the same columns in another order error
an index the DDL declares is not over the same columns error
a unique index the DDL declares is not unique in the database error
a foreign key the DDL declares is not in the database, references another table or columns, or has another ON DELETE or ON UPDATE error
the database has a unique index or a foreign key the DDL does not create warning

Keys are matched by their columns, never by name, because SQLite reports no foreign-key names and an unnamed key's name is the engine's; an unstated action is NO ACTION, which every engine reports for it. An extra plain index is not reported, since MySQL makes one for every foreign key and CREATE INDEX is not part of a CREATE TABLE's description, and an expression or partial index is skipped because schema.Index has no place for one. A catalogue action this reader has no name for is ErrUnknownAction rather than a guess. CheckTables(ctx, db, dialect, tables) is the boot check in one call: both catalogues read, both comparisons made.

findings, err := vet.CheckTables(ctx, db, "postgres", []schema.Table{desc})

The same parent and child tables, with every default kind the DDL writes and a unique key and a foreign key with an action, are created by each engine's builders, checked clean, and drifted five ways with that engine's own SQL; TestCheckTables_KeysAndDefaults_* require exactly the five findings on SQLite, Postgres and MySQL. The readers' parsing and every error return also run on every go test against canned catalogue rows from a fault-injecting driver. A column declared with a raw spelling (an enum, a domain, citext) is checked for existence and nullability only, since matching the catalogue's own spelling would be a guess. Each finding is positioned at table.column, or schema.table.column for a table with a schema, since a description has no source line. The same drift, made with each engine's own SQL, is reported identically on SQLite, on Postgres with SQLB_PG_DSN set, and on MySQL with SQLB_MYSQL_DSN set.

The diff is tested against an in-memory SQLite database on every go test, and against a real Postgres when SQLB_PG_DSN is set — the same variable contrib/driver-pgx uses — with a skip that says SKIPPED, NOT PASSED rather than passing quietly without it. and a real MySQL when SQLB_MYSQL_DSN is set. Every engine reports the same findings for the same probe.

The type table is schema's, the same one contrib/migrate renders CREATE TABLE from, so the vet and the DDL renderer cannot disagree about what a bigint is: a catalogue type is mapped back to the portable schema.Type values it spells, and the Go type must be one schema.GoTypes allows for any of them. On Postgres the catalogue's spelling is normalised (timestamp with time zone, character varying); on SQLite a declared type is free text resolved by the engine's own affinity rule, so a TEXT column may be a text, a timestamp, a uuid or a json column and the declaration is right if it fits any of them, and a column of NUMERIC affinity — which is what DATETIME and DECIMAL resolve to — holds every storage class and gets no opinion. Only a basic type, []byte and time.Time are checked; a caller's own type such as pg.JSON[T] or anything implementing sql.Scanner carries its own contract with the driver and is left alone. A catalogue type the schema layer has no portable name for, such as an array or an enum, is accepted rather than guessed at.

The escape-hatch listing

Escapes is not in Analyzers; it reports nothing wrong. It lists every place the compiler has been asked to step aside — Raw, Literal, New, PredicateOf, a JoinType or SetOp keyword, and a Col whose name is not a constant — as warnings, for review. It runs when asked for, with --escapes from the CLI or RunWith from code.

Running it

Four ways, one implementation.

From a test in the application, on every go test, with no binary installed — Check takes a TB, fails the test once per error Finding and logs each warning:

func TestSQL(t *testing.T) { vet.Check(t, "./...") }

From the command line, via cmd/sqlb, which prints one finding per line as file:line:col: analyzer: message and exits 1 when any is an error, 0 when there are only warnings, 2 on a load error:

go run github.com/ubgo/sqlb/contrib/vet/cmd/sqlb vet ./...
go run github.com/ubgo/sqlb/contrib/vet/cmd/sqlb vet --escapes ./...
go run github.com/ubgo/sqlb/contrib/vet/cmd/sqlb vet --dsn postgres://localhost/app ./...
go run github.com/ubgo/sqlb/contrib/vet/cmd/sqlb vet --dsn ./app.db ./...

--dsn picks the driver from the connection string: a postgres:// or postgresql:// URL is Postgres through pgx, anything else is a SQLite path through modernc.org/sqlite. The source checks always run; --dsn adds the schema diff.

Under go vet, through cmd/sqlbvet, a unitchecker main over the same analyzers, for the places that already invoke go vet such as a golangci-lint configuration or an editor. The schema diff and the escape-hatch listing stay in cmd/sqlb, because the vet protocol carries neither a flag nor a connection. TestVettool_RunsUnderGoVet builds it and drives it through go vet -vettool over packages it generates, since a unitchecker main does nothing outside that protocol. One caveat, found by that test and reproduced by hand on Go 1.27.1: go vet caches a package's vet result in a way that does not carry a custom tool's findings, so a second go vet -vettool over an unchanged package — or one a plain go vet has already seen — reports nothing. Changed sources or go clean -cache make it run again. For a gate, use vet.Check or cmd/sqlb vet, which load and analyse every time; the vettool suits an editor, where the package at hand is the one that just changed:

go build -o sqlbvet github.com/ubgo/sqlb/contrib/vet/cmd/sqlbvet
go vet -vettool=./sqlbvet ./...

Programmatically, Run(dir, patterns...) returns the Finding values — each carrying Pos, Analyzer, Message and Warning — and RunWith takes the analyzer set; Failing says whether any finding is an error, which is what decides an exit code.

Why a contrib module

The analyzers depend on golang.org/x/tools, the schema check on two database drivers, and the core depends on nothing but the standard library. The CLI lives here for the same reason, as contrib/vet/cmd/sqlb. The drivers are imported directly rather than through contrib/driver-sqlite and contrib/driver-pgx, because those add pragmas and pool settings a checker reading a catalogue does not need, and no contrib module depends on a sibling.

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

View Source
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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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 Analyzers

func Analyzers() []*analysis.Analyzer

Analyzers returns every analyzer this package defines, for a runner.

func CatalogueKey

func CatalogueKey(schemaName, name string) string

CatalogueKey is the key a Catalogue stores a table under: the bare name when schemaName is empty, otherwise "schemaName.name".

func Check

func Check(t TB, patterns ...string)

Check runs the analyzers over the patterns from inside a test and fails it on any finding. An application includes the check at build time with one test function and no binary installed:

func TestSQL(t *testing.T) { vet.Check(t, "./...") }

func Failing

func Failing(fs []Finding) bool

Failing reports whether any finding is an error rather than a warning, which is what decides a process exit code and a test's outcome.

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

func ReadCatalogue(ctx context.Context, db *sql.DB, dialect string) (Catalogue, error)

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

func Declarations(dir string, patterns ...string) (Declared, error)

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

type Finding struct {
	Pos      string
	Analyzer string
	Message  string
	Warning  bool
}

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

func Compare(dialect string, decls Declared, cat Catalogue) []Finding

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

func CompareTables(dialect string, tables []schema.Table, cat Catalogue) []Finding

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

func Run(dir string, patterns ...string) ([]Finding, error)

Run loads the packages the patterns name and runs every analyzer over them, returning the findings. It is what Check and cmd/sqlb share.

func RunWith

func RunWith(dir string, analyzers []*analysis.Analyzer, patterns ...string) ([]Finding, error)

RunWith is Run over a chosen set of analyzers, which is how cmd/sqlb adds the escape-hatch listing on request without making it a default.

func Schema

func Schema(ctx context.Context, db *sql.DB, dialect, dir string, patterns ...string) ([]Finding, error)

Schema is the whole --dsn mode: declarations from source, the catalogue from db, and the diff. dialect is render.DialectPostgres or render.DialectSQLite.

type KeyCatalogue

type KeyCatalogue struct {
	DefaultSchema string
	Tables        map[string]TableKeys
}

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.

func ReadKeys

func ReadKeys(ctx context.Context, db *sql.DB, dialect string) (KeyCatalogue, error)

ReadKeys reads every user table's primary key, indexes and foreign keys, in every user schema. dialect is render.DialectPostgres, render.DialectSQLite or render.DialectMySQL.

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

type TableDeclaration struct {
	Pos    string
	Name   string
	Schema string
}

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.

Jump to

Keyboard shortcuts

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