Repository navigation
Releases: kr9ly/sqlshape
Release list
v2.0.1
Changed
- A
-- @migrate backfilldeclaration left inschema.sqlafter its step is applied is now
an error (diffexits 1 and the UPDATE is not printed) when nothing else about its table
changes. Before, the UPDATE was silently printed again, and applying it overwrote data the
application had written since. A backfill in a step that does change the table — a new
column, a newCHECK/UNIQUE/ foreign key on existing rows — is emitted as before. mysql.Verifyno longer fails on asql_modename MySQL 8.4 does not define (a server of
another version or lineage runs with some): it compares the modes it knows and, on a
mismatch, says which names it left out.
Fixed
- On MySQL,
sqlshape applyruns the DDL under thesql_modethe schema declares (the 8.4
default when it declares none), the same setting the plan was verified under. Before, the
DDL ran under the server's global mode, so the pre-check and the real run could disagree. - On MySQL, a backfill of an existing column is printed before the keys,
CHECKconstraints
and foreign keys the step adds, soapplyno longer fails on rows the backfill was meant
to fix.
Documentation
- The README leads with what a reader needs to decide: what is checked, the supported
versions, that checking needs no database, and how sqlshape differs from generators and
ORMs. A Limitations section states what is out of scope, including that a server of an
undeclared version still runs but its divergences are not guaranteed to be caught. - The MySQL error catalog moved to docs/mysql-errors.md with an
index by error number; docs/mysql.md now leads with the type table, the
DSN requirements and the runtime. docs/migrations.md starts with the
prerequisites and gained a section on running migrations across several environments.
v2.0.0
Added
- MySQL 8.4. A
schema.sqlthat declares-- sqlshape: mysql 8.4is checked by a MySQL
analyzer: the parser and lexer are the server's own, the function catalog is read from the
server's source, and the verdicts (types, nullability, errors, theONLY_FULL_GROUP_BYcheck)
are measured against a runningmysqld.One, the obligations and the failure modes work
through the same contract PostgreSQL uses, with the constraints named as MySQL reports them
(PRIMARY,orders_ibfk_1,t_chk_1) andINSERT IGNORE/ON DUPLICATE KEY UPDATE/
REPLACEjudged as the server runs them. What has no MySQL counterpart is listed in
docs/mysql.md. - MySQL triggers, stored procedures and functions, and events: their bodies are analyzed
(NEW/OLD, variables, control flow, cursors,SIGNAL/RESIGNAL/HANDLER,CALL),
a statement inherits the failure modes of the triggers it fires and the routines it calls,
and the server's own refusals (a trigger writing its own table, 1442, among them) are
reported.-- sqlshape: error <code> = <Name>names a raised error, as on PostgreSQL;
-- sqlshape: not nullabove aCREATE FUNCTIONdeclares it never returns NULL. - On MySQL the checker also reads
LOAD DATA(an INSERT of the file's rows, with the server's
own 1263 for a NULL field in aNOT NULLcolumn),LOCK TABLES/UNLOCK TABLES,
SELECT ... INTO OUTFILE/DUMPFILE(no result set) and aSELECTwith a trailing
FOR UPDATE/FOR SHARE; the loader appliesALTER VIEW. Inside a routine or trigger body
the first three are the server's own 1314. - MySQL writes through views, analyzed as the server runs them (each rule measured): an
INSERT/REPLACE,UPDATEorDELETEthrough a merged view lands on its base table,
with the base table's own failure modes and the view's own (WITH CHECK OPTIONas the
violation 1369 -- onINSERTandREPLACEtoo -- and a base column with no default the
statement leaves unassigned as the view's 1423, both matched byViolatesunder the view's
name), and the server's refusals are statement errors under the server's own codes: the
view's updatable / insertable flags are computed over itsFROMleaves the way
sql_resolver.cccomputes them when it merges, a join view follows the server's rules for
each write kind,REPLACEandDELETEnever reach one, and a subquery reading the written
view itself is 1093. The rules are docs/mysql.md's. - The run-time failures MySQL decides by a constant's value are judged (each measured against
mysqld; the rules are docs/mysql.md's): a literal a column can never store
(1264 / 1265 / 1292 / 1366 / 1406 / 1416, in strict mode outsideIGNORE), a temporal
literal the server cannot read as its type (1525) or aTIMESTAMPcolumn cannot hold, the
spatial rules (1210 / 3037 / 3516 / 1416), constant arithmetic the server cannot compute
(1690), the constant conversions a strict write escalates (1292; a temporal column compared
with a constant string that is no datetime is 1525 whatever the statement), and the
functions that judge a constant argument's value:INET_ATON/INET6_ATON/UNHEX/
STR_TO_DATEfail a strict write outsideIGNORE(1411;STR_TO_DATEunder a port of the
server's ownextract_date_time),UUID_TO_BIN/BIN_TO_UUID(1411) andPERIOD_ADD/
PERIOD_DIFF(1210) fail every statement whatever the mode, andNAME_CONST,ESCAPE,
NTILE,NTH_VALUEandMATCH ... AGAINSTare refused at resolution, rows or none. Each
is the statement's own error where the server evaluates it before reading rows, and a
failure modeViolatesmatches by number ("1416","1690","1292","1411") where it
runs per row. A prefix key (UNIQUE (c(10))) or an expression key (UNIQUE ((n * 2))) is
a violable key (1062) for the writes that assign its columns. - A MySQL function call argument carrying an alias (
f(x AS a), the loadable function
syntax) is refused in the server's own order (1582 / 1583 / 1584, a data dictionary
function 3566), and an explicitly scoped system variable read must match the variable's
scope:@@session.xof a GLOBAL-only variable and@@global.xof a SESSION-only one are
the statement's 1238 wherever the read sits, over a scope table generated from the server
source (sql/sys_vars.cc) and pinned against mysqld entry by entry; a plugin's or
component's variable is not judged. - The statement facts the
Oneproof and the obligations are judged on are tested against a
running server the way the checker's types are:x/stmtprobegenerates schemas with rows
and statements over them (joins, views, derived tables, CTEs,EXISTS/INsubqueries,
GROUP BY,UNION ALL, writes), and refutes every claim of the facts -- a predicate holding
on every row, a fixed column, an at-most-one-row proof, the value a write stores -- with the
rows the server returns, on PostgreSQL 17 and MySQL 8.4. The same probe judges the predicted
failure modes: writes against a schema carrying every constraint kind (named and unnamed
keys, foreign keys with eachON DELETEaction, checks,NOT NULL;IGNORE/REPLACE/
ON DUPLICATE KEY UPDATEandON CONFLICT), with every constraint error the server raises
required to be a predicted violation under a keyViolatesmatches. - MySQL trigger, procedure and function bodies are tested against the server's own
CREATE-time verdict the same way (the body probe incheck/mysql/internal/analyze): every
construct the server refuses when the body is created, mixed into generated bodies, must
be refused by the checker too, and nothing the server accepts may be. - MySQL's own test corpus (mysql-test/t: 1,281 files, 137,000 statements) replays against a
running mysqld and the analyzer side by side (check/mysql/internal/analyze's corpus probe,
the counterpart of PostgreSQL's regress probe): each file on a server of its own, the
analyzer's schema rebuilt fromSHOW CREATEafter every DDL under the session'ssql_mode,
a SELECT's columns compared by name, type family and nullability, an error by number; the
remaining disagreements (2,525 statements) are pinned one by one as a baseline, and a new
one fails the build. mysqltest.StartOwnboots a server of its own even undermysqltest.Main, for a test that
changes accounts, global variables or other databases.postgres.WrapError(err)andmysql.WrapError(err)give an error from a statement run
outsideRun/Execthe wrappingViolatesjudges.sqlshape.Error(code)declares a schema's named error in Go (var OrderTooLarge = sqlshape.Error("30001"));go vetchecks the declaration against the schema both ways, an
expect line may use the Name, andViolatesaccepts the value.-- sqlshape: server <var> = <value>declares the server settings the checker's judgments
depend on (MySQL:sql_mode,lower_case_table_names,max_sp_recursion_depth); the
checker follows them, andmysqltest.Startstarts the server with them.- The MySQL runtime,
github.com/kr9ly/sqlshape/mysql/v2(Run,Collect,First,Exec,
Get/Find/ExecOne,ConstraintError,Violates) overdatabase/sql, and
github.com/kr9ly/sqlshape/mysqltest/v2, which boots themysqldonPATHwith the
application's schema; a package whoseTestMainruns throughmysqltest.Mainboots one
server per set of declared settings and hands it from test to test instead of booting one
perStart.examples/5-mysqlshows the whole path. sqlshape diff,applyandverify-schemaon MySQL: both sides are read as the server's own
SHOW CREATEoutput, the target canonicalized in a scratch database on the same server, and
the plan written in MySQL's own DDL, statement by statement. Tables with their options,
columns with their position, keys, foreign keys, checks, views, triggers, routines, events
and partitions are compared; seeded rows are not.- Partitions are migrated on both dialects: a partitioned table is created, a partition added,
dropped (under a-- @migrate dropdeclaration, since it takes its rows), attached, detached,
its bound moved with the rows that no longer fit moved along, and a populated table can be
partitioned after the fact, unpartitioned, or repartitioned, the rows redistributed by the
server. MySQL coversRANGE,LIST,COLUMNS,HASH,KEY,LINEARand subpartitioning
(a partition's explicitSUBPARTITIONname list included),
with-- @migrate drop partition <table>.<partition>for a partition that goes. - The migration planner is tested against a real server the way the checker is: generated
schema pairs, every kind of change the diff can report, applied with rows in the tables on
PostgreSQL 17 and 18 and on MySQL 8.4, with the plan required to land on the declared schema
and leave nothing to plan afterwards (docs/migrations.md).
The orderings and omissions this found are fixed and pinned as regression tests. - Row-level security, composite types, rules, extensions, schema moves, standalone sequences,
procedures, aggregates,EXCLUDE, partial and expression indexes,NULLS NOT DISTINCTand
DEFERRABLEare handled by the PostgreSQL migration plan;FULLTEXT/SPATIAL/ functional
/INVISIBLEkeys,ROW_FORMAT, table charset changes and column comments by MySQL's. x/dialect.Type.ElemNotNull: an array whose elements are provably not NULL
(array_agg((a, b)::t), an inner-joinedNOT NULLcolumn) binds to[]Twithout the "may
contain a NULL element" note;col:",notnull"on an array field asserts the elements too.-query=pkg.Func,pkg.Other:oneregisters a program...
v2.0.0-rc.2
Added
cmd/sqlshape/internal/vetgaineddocs_test.go: a harness that reads the go/sql fences out of
docs/checks.mdandchecks.ja.mdat test time (both the code and the diagnostic wording a
Rejected fence's trailing comment claims), completes each into a compiling program under
testdata/src/docsex_*, runs it through the checker, and asserts that every diagnostic docs
claims is a literal substring of one the checker actually produced, with nothing left over. It
covers "Result columns bind to fields by name", "A column that may be NULL needs a field that can
hold NULL", "Fix a unique key by equality", "Bulk loading with COPY", and "Name the errors a
trigger raises" (both dialects) so far, in both languages -- not the full set of fences yet (see
NOTES.local.md and the package doc for what is not wired up and why).x/dialect.TypegainedElemNotNull: a dialect that can prove an array's elements are
never NULL sets it (false, the default, means "unknown", not "may be NULL"). PostgreSQL
sets it forarray_agg(x)wherexis itself never NULL (a row constructor such as
(a, b)::T, an inner-joinedNOT NULLcolumn, a whole-row reference), for a literal
ARRAY[...]whose elements are all provably not NULL, and forARRAY(SELECT ...)over a
not-null single output column, propagating it through subqueries, CTEs, set operations and
views (a view's stored column snapshot carries it,schema.ViewColumn.ElemNotNull, refrozen
with the nullability). A plain array column never qualifies, since PostgreSQL declares nothing
about a column's own elements, socol:",notnull"on an array field asserts the elements too.
Changed
-
x/expand: anif/with/rangeover a path an earlier control in the same lineage
already decided (the same{{if .Status}}appearing twice, say, once in a column list and
once inVALUES) now follows that decision instead of branching it again, in both the full
and the sparse expansion; a template no longer yields impossible expansions the runtime's
evaluator, reading the value once per occurrence, was never actually going to produce.
dialect.Advicenow carries the table and column it is about, and-strictreports such
advice only in a package whose statements touch that relation or column (schema-wide advice
still applies everywhere), so an enum column no longer lectures every package under
-strict. A pointer parameter used inside the then branch of{{if .X}}/{{with .X}}on
that very path is recognized as not NULL there (an expansion'sGuardedTruecovers only the
exact path the taken guard proved, not paths below it, and the else branch proves nothing).
PostgreSQL:array_agg((i.sku, i.qty)::order_item)into[]Itemkeeps each nested row
field's own nullability through the cast to a named composite type, instead of discarding it
(a row constructor is never NULL;array_aggof a scalar does pass a NULL element through,
unaffected). Building on that: the checker's standing "may contain a NULL element" note
(and its-strictrejection) on an array result no longer fires whendialect.Type.ElemNotNull
says the elements are provably not NULL, soarray_agg((i.sku, i.qty)::order_item)into
[]Itemis now accepted with no note at all, whilearray_aggof a plain nullable column,
a literalARRAY[...]with aNULLelement, and an ordinary array column keep the note (and
the-strictrejection) unchanged. A parameter compared against a domain column carries the domain (PostgreSQL
resolves the placeholder to the base type, soParam.Typeis taken from the compared
column), so-strict's "carries domainyenas a plainint64" advice reaches parameters
too, and a mismatch names the domain (SQL expects email). -
MySQL:
-- sqlshape: not nullabove aCREATE FUNCTIONdeclares the function never returns
NULL, the same directive PostgreSQL already has; a call then types as NOT NULL. A PROCEDURE
or TRIGGER still refuses the directive (neither returns a value for it to describe). -
PostgreSQL after a third adversarial round against a running server (8 lanes, 23 findings, each a
regression test inadv_*_test.go), the failure modes first.TRUNCATEof a table another table's
foreign key still references (noCASCADE, the referrer not in the list) is reported as certain to
fail (0A000);ON CONFLICT (cols)absorbs a unique constraint only when it can be the arbiter, so a
DEFERRABLEkey (55000) or a partial index whose predicate theON CONFLICT ... WHEREdoes not
repeat (42P10) keeps its 23505 in the list, andON CONFLICT ON CONSTRAINT <exclusion> DO UPDATE
is reported as certain to fail (42809);ON CONFLICT DO NOTHINGwithout a target absorbs the
exclusion constraints and partial unique indexes too, and is certain to fail (55000) when any
key of the table isDEFERRABLE. ANOT NULLthat a domain, not the column, declares is
keyed by the domain's name (schema-qualified unless public): PostgreSQL reports no table or column
for it, andpostgres.ConstraintError.Key()now returns the domain's name for that error, so
Violates(err, "email")matches what the checker predicted.ORDER BY/GROUP BYresolve an
output name the way PostgreSQL does: two output columns of one name are ambiguous (42702) unless
they are the same expression; the ungrouped-column error names its table ("t1.v"); an error the
select list and theWHEREshare is reported at the select list's position. The result of a
set-returning function in the select list (unnest(arr)) is nullable whatever its argument is:
an element can be NULL when the array cannot. TheOneproof sees through a cast on the grouped
column, takes a one-element= ANY(ARRAY[v])and aNOT NULLcolumn'sIS NOT DISTINCT FROM v
for equalities, and matches a partial index's predicate with the literal casts a statement spells
out;col = NULLfixes nothing (it is never true), so it neither pins nor proves single, on MySQL
too.SET col = DEFAULTstores the column's literal default, so atransitionsdeclaration
judges it as a transition to that state (a non-literal default is refused as before, with a
message that says why).-strictrejects[]T/[N]Tfor an array whose Go element cannot be
NULL (integer[]into[]int32, a composite array into[]Item), and both modes note that
PostgreSQL never promises an array's elements are non-null even when the column is;interval
intotime.Durationcarries a Lossy note (a month flattened to 30 days disagrees with the
server's calendar arithmetic). vet no longer skips asqlshape.Query[R, P]called through a
package-level variable (var q = sqlshape.Query[R, P]; q(...)), and checks the fields an{{if}}
reads through a nested call ({{if gt (len .Xs) 0}}) againstP. Migrations: rewriting a
generated column's expression, or itsSTORED/VIRTUALkind (PostgreSQL 18), drops and restores
the indexes, constraints and referencing foreign keys theDROP COLUMNwould take with it, instead
of losing the index or emitting DDL the server refuses (2BP01); a kind-only change is no longer a
no-op. Anunfiltered/waivewritten above aCREATE TABLEis told where it belongs. Kept as
they were, and now written down:pairedrequires the write, not matching values;RETURNINGa
sensitivecolumn is reading it. -
pgtestandmysqltestare sqlshape's own test tooling, likecheck/*: they boot a real server
for the examples, the oracles and the conformance tests, and carry no compatibility promise. An
application tests its database with the server it runs on; the agreement between the checker
and the server is sqlshape's to keep. The documentation no longer presents them as part of the
API, and is reorganized for a reader who uses one database:docs/postgres.mdand
docs/mysql.mdgather what is each database's (the declaration, the Go type table, the
constraint names and error numbers, the runtime, what has no counterpart), the README has a
quickstart per database, andchecks.md/runtime.mdare written for both.
Added
-
sqlshape.Error(code)mirrors a schema's-- sqlshape: error <code> = <Name>annotation (a
trigger's or routine's own raised failure mode) in Go:var OrderTooLarge = sqlshape.Error("30001").go vetchecks it both ways -- the declaration's code must be one the
schema declares under that exact Name, no two declarations in a package may claim the same code,
and every named failure mode a package's statements can raise must have such a declaration
somewhere the checker can reach (a Fact carries the binding across packages, the way a declared
type's does). An expect line may spell the failure by its code or by the Name, whichever reads
better.postgres.Violates/mysql.Violatesnow take any~string, so a declared
sqlshape.Failurereads the same as the raw code always did; neither reads the schema, so they
judge only by the code the value holds, never by Name. The PostgreSQL runtime wraps a custom
SQLSTATE intoConstraintErrorfor any statement with an expect line at all now (it cannot
resolve a Name back to the code the checker matched statically).examples/3-database-api,
examples/4-everythingandexamples/5-mysqldeclare their triggers' and functions' errors this
way and testViolatesagainst a running server. -
sqlshape diff,applyandverify-schemawork on a MySQL schema. Both sides are read as the
server's ownSHOW CREATE TABLE/SHOW CREATE VIEW, parsed by the loader that reads
schema.sql; the target is canonicalized in a scratch database on the-dbserver (dropped when
done), or on amysqldfromPATHwith-from. Compared: tables, columns (their positi...
v2.0.0-rc.1
Changed
- The checker is the product; the runtime is a library per database. The root module
github.com/kr9ly/sqlshape/v2now holds only the declarations (Query,One,Stmt,Render,
the row-binding rules) and has no dependencies. Running a statement on pgx is the module
github.com/kr9ly/sqlshape/postgres/v2:postgres.Run(ctx, db, stmt, p),Collect,First,
Exec,Get/Find/ExecOneforOne, andMatView,Copy,Batch,LoadUserTypes,
ConstraintError,Violates,ErrNoRowsmoved there (the methodsstmt.Run(ctx, db, p)are
gone: a module cannot add methods to another's type).LabelledandUnknownLabelErrorstay in
the root.pgtestis its own module. The PostgreSQL analyzer and migration tools are the module
github.com/kr9ly/sqlshape/check/postgres/v2, the MySQL analyzercheck/mysql(wasmysql);
both are tool-facing, imported by the binary andpgtest. An application's go.mod carries the
root, its runtime and its own driver, nothing of the checker's. - The markers the checker recognizes are configurable:
-query=pkg.Func,pkg.Other:oneregisters
a program's own generic marker functions, read likeQuery/One. A program may run its
statements through a runtime of its own; what only sqlshape's runtime promises (the rendered SQL
is the checked SQL, violations come back under the expect line's names,Onerejects a second
row) is stated in docs/runtime.md.
Added
- MySQL, a first slice. A
schema.sqlthat declares-- sqlshape: mysql 8.4is loaded by the
MySQL schema loader and everyQueryis judged by the MySQL analyzer: the statement's own errors
(unknown table or column, ambiguity, syntax, with MySQL's message and error number), result
columns againstRby name, type and nullability, parameters againstPwhere their context
types them (compared with or assigned to a column,LIMIT, the argument positions a function
declares). Single-block SELECT, INSERT, UPDATE and DELETE over base tables. Expressions are typed
by the server's own rules, read out of its source: arithmetic promotion (Item_num_op),CASE/
IF/COALESCE/GREATESTthroughfield_type_merge, casts, aggregates, and every function of
the native registry through its Item class family andresolve_typefacts, so aSUMis a
nullable decimal, a comparison abigint(1)a Goboolcan carry,LENGTH(?)types its
placeholder as a string. The rules are checked against a real mysqld (a local test, not a CI
dependency): every registry function over representative argument types, 5,267 statements,
agrees with the server on type and nullability but for 10. Subqueries (scalar,IN,EXISTS,
ANY/ALL, correlated), derived tables (LATERALtoo), views, common table expressions
(recursive too),UNION/EXCEPT/INTERSECTandINSERT ... SELECTare analyzed with the
server's rules for what they produce: a scalar subquery is NULL unless its query is guaranteed a
row, a recursive CTE's columns are always nullable, a derived table the server materializes
retypes a small integer expression to an int, a merged view is updatable. What has no rule yet
(user variables, the temporal hybrids such asADDTIME) is accepted with a note;One,
MatView,Copyand the obligations are not supported yet. Parameters stay{{.X}}in the
template; the analyzer speaks?to MySQL.
Changed
- The binary is its own Go module,
github.com/kr9ly/sqlshape/cmd/sqlshape/v2, under the GNU
General Public License v2; install it withgo install github.com/kr9ly/sqlshape/cmd/sqlshape/v2@latest.
Everything a checked program imports stays Apache 2.0. Every module's path carries the/v2
major-version suffix Go requires of a 2.x module, so an import of 1.x is
github.com/kr9ly/sqlshapeand of 2.xgithub.com/kr9ly/sqlshape/v2. The modules of the
repository release together under one version, taggedvX.Y.Zand<module>/vX.Y.Zon the
same commit.
v1.2.0
Added
- PostgreSQL 18.
schema.sqldeclares its version with-- sqlshape: postgres 18, and the schema
and every statement are judged with 18's grammar and catalog:RETURNING old/new(with
WITH (OLD AS ..., NEW AS ...)),WITHOUT OVERLAPSkeys andPERIODforeign keys,NOT ENFORCED
constraints (which are no failure mode), namedNOT NULLconstraints,VIRTUALgenerated columns,
and 18's own checks (a JSON_VALUE DEFAULT whose collation is not the RETURNING type's, a JSON path
that is not a jsonpath, stricter datetime and aclitem input, an outer-level aggregate over a nested
CTE).pgtestand the migration commands run the declared version's PostgreSQL;diff,apply
andverify-schemawarn when the database runs another major version than the schema declares. Oneproves a single row through a temporal key: the scalar columns fixed by equality and the
range column equal to a known value or containing a known point (valid_at @> {{.Day}}::date).
Fixed
SELECT DISTINCT ONexpressions are matched to the leadingORDER BYexpressions by expression,
not by where each was written; the same expression in both no longer fails with 42P10.- An UPDATE that assigns a generated column reports PostgreSQL's message (
column "b" can only be updated to DEFAULT) rather than INSERT's.
Changed
schema.sqlmust declare its PostgreSQL version (-- sqlshape: postgres 17); a schema without
the line is not read. Add the line to move from 1.1.- The parser (libpg_query) is compiled to WebAssembly and run by wazero instead of being linked
through cgo.go installno longer needs a C compiler, and the binary is a plain Go build.
Parse trees, error messages and positions are unchanged. - Prebuilt binaries for every platform, cross-compiled by one job: Linux, macOS and Windows, amd64
and arm64 each (Windows and Intel macOS are new).
v1.1.0
Added
- Obligations:
schema.sqldeclares, above aCREATE TABLEorCREATE VIEW, what every statement
touching the relation must do, and vet judges each statement against it
(checks.md, Part 2). The general form is
-- sqlshape: require <what> [on <kinds>], where<what>is an SQL predicate,pinned(col),
immutable(col),via view,never,paired(table)orsingle. aggregate root (child, ...) [lock version]: the tables form one aggregate; children are reached
through the root's key, one statement touches one aggregate, and withlockevery write names
the root's version.transitions col: a -> b, b -> c | d: a status column is a state machine; an UPDATE setting it
must compare-and-set from a declared predecessor.sensitive label: col, colandcontext name: may read label: labelled columns are readable
only in a context that allows the label.context name: require ...; waive ...: obligations that differ per caller, selected by a
package's// sqlshape: context namecomment, vet's-context, orcheck -context.-- sqlshape: waive table [obligation]in a statement or a view definition: an opt-out,
reported with-strict.- Cross-table predicates:
require EXISTS (SELECT 1 FROM parent p WHERE p.id = parent_id AND ...)
is discharged by a join or subquery that witnesses it. sqlshape check: the same judgments for SQL outside Go (files or stdin), every judgment printed
as an audit line, exit code 1 on a failure.- The
internal/factspackage: what a statement provably does, in a form that does not depend on
the SQL dialect, so that the obligation checker can be shared with other dialects later.
Changed
visible where,-require-columns,-no-table-readsand-no-tablesare now obligations
(require <expr> on read,require pinned(col),require via view,require via view on all)
and are judged by the same checker. Judgment is per occurrence of a table: a self join or a
subquery reading the table again owes the obligation again;-require-columnsused to accept a
statement that pinned the column anywhere in it. A row-level security policy or a composite
foreign key can discharge a pin. MERGE's ON is judged like a WHERE.- Writes are counted by what the statement does to the table: each MERGE branch is a write of its
own kind, anINSERT ... ON CONFLICT DO UPDATEis an insert and an update,TRUNCATEis a
delete, and a write through an automatically updatable view is a write to the base table. pinnedon an UPDATE is satisfied by the WHERE only; assigning the column in SET is not a pin.- A
-- sqlshape:directive above a statement that takes none (ALTER TABLE,COMMENT ON) is a
schema problem instead of being ignored. - The docs are split: the README is an overview; docs/ has checks, templates, runtime,
migrations and flags, in English and Japanese.
Fixed
- A vet failure shared by every branch of a template is reported once without the branch tag.
- A diagnostic on a column referenced only in GROUP BY had no position.
v1.0.0
Full Changelog: v1.0.0-rc.1...v1.0.0