Repository navigation
v2.0.0-rc.2
Pre-releaseAdded
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 position
included, which the plan settles withMODIFY COLUMN ... AFTER), keys, foreign keys, checks and
views; not seeded rows. The plan emits MySQL's own definitions;-- @migrate
is the same grammar, withenumnaming the ENUM column.applyruns statement by statement
(MySQL's DDL commits implicitly) and reports the statement that failed. The MySQL analyzer now
rejects an expression the server rejects in an INSERT's or UPDATE's value (SET total = nope,
1054) instead of typing it as unknown. -
MySQL:
CREATE TRIGGER/CREATE PROCEDURE/CREATE FUNCTION(DEFINER,IF NOT EXISTS, the
characteristics), andDROP/ALTERof each, are read by the schema loader with noDELIMITER
needed (a compound body's own;is read as one statement; a mysql-clientDELIMITER xline is
read too), the server's own CREATE-time refusals reported as problems (no such table 1146,
already exists 1359 / 1304, no such trigger or routine 1360 / 1305, no suchFOLLOWS/
PRECEDESanchor 3011);DROP TABLEtakes a table's triggers,RENAME TABLEmoves them.
-- sqlshape: error <key> = <Name>above a trigger or routine names its failure mode, the way
it already does for PostgreSQL. Migrations read triggers and routines back withSHOW CREATE TRIGGER/PROCEDURE/FUNCTION(DEFINERdropped; a trigger'sFOLLOWSsynthesized from
information_schemasinceSHOW CREATE TRIGGERnever spells one), diff compares them by
definition text, and the plan replaces a changed one withDROP+CREATE(MySQL has no
CREATE OR REPLACE TRIGGER) in dependency order: a trigger'sDROPbefore its table's, a
routine'sCREATEbefore the views and tables, a trigger'sCREATEafter the backfills so it
does not fire on the migration's own writes. -
MySQL: the checker reads trigger and stored routine bodies once per schema (cached, so a
routine called from several statements or firing several triggers analyzes once).NEW.col/
OLD.coltype as the trigger's own table's columns (unknown column 1054, the wrong row for the
event or timing 1362 / 1363 -- aNOT NULLcolumn'sNEW.colcan still be NULL in a BEFORE
trigger, measured); aDECLAREd variable or a routine's parameter shadows a column of the same
name.IF/CASE/LOOP/WHILE/REPEAT, labelled blocks withLEAVE/ITERATE,
RETURN,SET,SELECT ... INTO, cursors,CALL,SIGNAL/RESIGNALand
DECLARE ... HANDLER FORare walked, with the server's own CREATE-time refusals (1308, 1313,
1320, 1324, 1327, 1328, 1222, 1415, 1422) reported on the definition; a trigger writing its own
table is 1442 on every one of the 18 timing x event x write combinations (measured). A statement
on a table takes the SIGNALs of its triggers for its event (REPLACEfires the INSERT and
DELETE triggers,ON DUPLICATE KEY UPDATEthe INSERT and UPDATE ones, measured), the constraints
the trigger's own writes may violate, and what those writes fire in turn (a cycle is cut). A
SIGNAL's key is itsMYSQL_ERRNOin decimal when it sets one, else its SQLSTATE (SQLSTATE class
01a warning, an unhandled021643, anything else unhandled 1644); a namedCONDITION
resolves to its value. ADECLARE ... HANDLER FORabsorbs the matching failure modes of its
block (SQLEXCEPTIONeverything but the01and02classes,SQLWARNING/NOT FOUNDtheir
class, a SQLSTATE or number itself);INSERT/UPDATE IGNOREabsorbs none of a trigger's
SIGNALs (measured).SELECT ... INTOcarries 1172 unless the query is provably at most one row.
A call to a stored FUNCTION the catalog does not know resolves to the schema's (an unqualified
name that is also a native function is the native one, as on the server;db.fnames the
routine), typed fromRETURNS(always nullable), taking 1318 for a wrong argument count and
1305 for no such routine, and bringing the body's own failure modes to the calling statement; a
function that writes a table the statement already references is 1442 on every run (reading the
table is enough, measured).CALL p(...)analyzesIN/INOUTarguments by the parameters, an
OUTargument must be a variable (1414; a?is one), factsKind Call, the result columns
from the body's own INTO-lessSELECTs when they agree (none: no columns; several of different
shapes: the checker's own error), and the body's own failure modes. Cross-checked against a
runningmysqld. -
The MySQL runtime: a SIGNAL raised by a trigger or routine comes back as a
ConstraintError
whoseKeyis the SQLSTATE when the server numbers it itself (1644, 1643), or the decimal
MYSQL_ERRNOwhen the SIGNAL set its own error number;Violates(err, "45000")/
Violates(err, "30001")read like the checker's own keys. A01xxxSIGNAL is a warning and the
statement succeeds. -
The schema declares the server settings its judgments depend on, one per line next to the
version:-- sqlshape: server sql_mode = 'ANSI,STRICT_ALL_TABLES',
-- sqlshape: server lower_case_table_names = 1. MySQL reads these two (any other variable, a
mode 8.4 does not have, or a value out of range is a problem of the schema; PostgreSQL reads no
variable yet and reports each one). The checker follows them: the parser's bits
(ANSI_QUOTES,PIPES_AS_CONCAT,IGNORE_SPACE,NO_BACKSLASH_ESCAPES,
HIGH_NOT_PRECEDENCE,REAL_AS_FLOAT),ONLY_FULL_GROUP_BYfor the group check, strict mode
for the nullability of string functions and the 1048 failure mode (without it only a single-row
INSERT/REPLACErejects aNULL),NO_UNSIGNED_SUBTRACTION, andlower_case_table_names
for how table and view names compare (1 lower-cases them, 2 ignores case). Without a
declaration the checker assumes the server's defaults, as it did.mysqltest.Startpasses the
declared variables tomysqld, so the application's tests and the analyzer's conformance
tests run under the declared mode (22 statements and 6 writes verified against 8.4 under
'',ANSIandNO_UNSIGNED_SUBTRACTION);mysql.Verify(ctx, db, schemaSQL)compares a
connection's session@@sql_modeand the server'slower_case_table_nameswith the
declaration.x/sqlmodeholds MySQL's sql_mode names and bits. -
mysqltest.Startfails as soon asmysqldexits (an option it rejects) instead of waiting a
minute, and quotes the[ERROR]lines of its log. -
MySQL: an aggregate without
GROUP BYproves one row wherever the aggregate sits in the
select list orHAVING(COALESCE(SUM(total), 0),COUNT(*) + 1,MAX(id)alike), not only
when every select item is a bare aggregate call, so aSELECT COALESCE(SUM(x), 0) INTO vin a
routine no longer carries 1172 and the same query passesOne. Which aggregates are the
block's own is the group check's answer (a subquery's aggregate over its own columns leaves the
outer query one row per input row, as before). -
MySQL:
CREATE EVENTis read (both schedule forms,STARTS/ENDS,ON COMPLETION, the
status,COMMENT),ALTER EVENT(each clause replacing its part,RENAME TOincluded) and
DROP EVENTare applied. An event's body is analyzed like a routine's: a table it names that the
schema does not have is a schema problem (the server accepts it at CREATE time and fails at every
run, measured), its statements are judged for the obligations, aRETURNis 1313.diff/
apply/verify-schemamanage events: read back throughSHOW CREATE EVENT, compared by
schedule, bounds, options and body, a changed one aDROP EVENTand aCREATE EVENT. A time
schema.sqlleaves to the server (an omitted or computedSTARTS, a computedAT) is not
compared, since the server fills it in at creation and reads it back as a literal (measured);
a literal time is compared as written. FormerlyCREATE EVENTwas refused by the loader. -
MySQL: a qualified function call,
db.f(...), names the schema's own FUNCTION even when a
native function has the same name (an unqualifiedabs(-1)is still the native one,db.abs(-1)
the schema's,db.nope(1)1305; measured), where it used to type as the native function. The
1442 overlap check now also counts a table the statement names inFROMwithout reading a
column of it, follows writes through nested calls (a function thatCALLs a procedure writing
t, orRETURNs a function that does), runs per statement inside a trigger's or routine's body
(UPDATE t SET v = f(v)with f writing t is 1442 when the routine isCALLed; two separate
statements are not), and counts a trigger's own table against every statement of its body (a
trigger calling a function that writes the trigger's table is 1442, as a direct write already
was); aCALLinside a body is resolved like a top-level one (1305 / 1318 / 1414) and the
callee's failure modes and writes become the body's. All measured against mysqld 8.4
(TestNestedCallsServer). An AFTER trigger'sNEW.colfollowing the column's declared
nullability is now measured rather than inferred.
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. - The
Oneproof is written once, over the statement's facts, for every dialect: the
PostgreSQL analyzer no longer proves cardinality on its own tree but records what the proof
needs (unique keys with a partial index's predicate, temporal keys, a view's or subquery's
body with its output columns, GROUP BY terms, LIMIT / VALUES / set-operation shape). Two
verdicts change: aGROUP BY 1(an ordinal, or an alias) names an output column and is no
longer read as a pinned constant, so such a statement is not proved single; aLIMIT 1
over aUNIONand an aggregate over aFULL JOINare one row and are proved.
Added
- The MySQL runtime,
github.com/kr9ly/sqlshape/mysql/v2:Run/Collect/First/
Exec,Get/Find/ExecOne, overdatabase/sqlwith go-sql-driver/mysql;{{.X}}
becomes?on the wire with the arguments in placeholder order, rows map by the shared
binding rules, a constraint violation is aConstraintErrorunder the schema's name for it
(Violates(err, "users_email_key")).github.com/kr9ly/sqlshape/mysqltest/v2boots the
mysqldon PATH with the application's schema for its tests (the analyzer's oracle runs on
it too).examples/5-mysqlshows the whole path. Oneon MySQL, and the proof it rests on written once for every dialect: the analyzer
records a statement's facts (x/facts: leaves with their unique keys, the equalities of
WHERE and of the joins, what they fix) andx/cardinalityproves from the facts alone that
at most one row is touched — a unique key fixed by equality, carried across joins, a
derived table proved inside, an aggregate without GROUP BY,LIMIT 1, a singleVALUES
row. The MySQL analyzer also resolvesORDER BY,GROUP BYandHAVINGthe way the
server does: positions, select-list aliases, a table column shadowing an alias in
GROUP BY,HAVINGreaching the select list; unknown names are reported in MySQL's
words (in 'order clause',in 'group statement',in 'having clause').- The obligations and the failure modes on MySQL, through the same contract PostgreSQL uses.
The-- sqlshape:directives above aCREATE TABLE/CREATE VIEW(require ...,
aggregate,context,sensitive,transitions,visible where; a view'sunfiltered/
waive) are read by the MySQL schema loader, a declared predicate is typed against the
table by the MySQL analyzer, and every statement (and every view body) is judged: the
analyzer now records the columns a statement uses, the nested blocks (derived tables, CTE
bodies, the subqueries of its conditions, the arms of a set operation) and what a write
stores.-- sqlshape: expecton MySQL: a write may violate aPRIMARY/UNIQUEkey
(MySQL error 1062; a key the server numbers itself, or one left NULL, cannot), aFOREIGN KEY(1452 from the child, 1451 from the parent it still refers to, followingON DELETE / UPDATE CASCADE), aCHECK(3819) and NOT NULL (1048,table.column, dropped when the Go
type cannot be nil);INSERT / UPDATE / DELETE IGNOREviolates nothing andON DUPLICATE KEY UPDATEabsorbs the insert's unique violations. The constraints are named as the server
reports them (PRIMARY,orders_ibfk_1,t_chk_1), verified against a runningmysqld;
mysql.Violates(err, "users.name")matches a NOT NULL violation by that spelling. - The MySQL server's checks on a grouped, aggregated or
DISTINCTblock, as sql_mode
ONLY_FULL_GROUP_BYruns them (Group_check): a select-list,HAVING,ORDER BYor window
PARTITION BY/ORDER BYexpression must be aGROUP BYexpression, an aggregate, or made
of columns functionally dependent on the group columns -- through a table'sPRIMARY/
UNIQUEkey (a nullable key column only where a conjunct rejects its NULL), theWHEREand
inner-join equalitiescol = colandcol = constant(a literal or an outer reference; not a
parameter), an outer join'sONinto its nullable side (aWHEREthat rejects the nullable
side's NULL makes the join inner, a non-deterministicONgives nothing), and a derived
table's, view's or CTE's body seen through its outputs;ROLLUPallows only the group
expressions themselves. MySQL error 1055 withGROUP BY, 1140 for an aggregated query without
it (whoseORDER BYthe server drops unchecked). With them the rules the check rests on: a
column named outside an aggregate inHAVINGmust be a select-list column or alias or aGROUP BYcolumn (1054, also for a nested query's reference to it), withDISTINCTanORDER BY
expression not in the select list may read only select-list columns (3065), and an aggregate in
theORDER BYof a query that aggregates nowhere else (3029) or of a set operation (3028) is
rejected. 376 statements agree with a runningmysqld. The derived leaves of the facts now
carryOutputson MySQL too, soOnelooks into a derived table's or view's body. - MySQL: a parenthesized join as a side of a join, a set operation's
ORDER BYexpression
(typed against the result columns),x NOT IN (subquery),QUALIFYrejected as 8.4 does
without the hypergraph optimizer (6037),<=> ALL / ANYas the syntax error it is (1064).
The facts record anEXISTSor single-columnINsubquery conjunct as an Exists predicate
carrying the body (a correlated reference to the enclosing block is an Outer term), so the
obligations judge the body under its predicate; the conjuncts the server folds away (WHERE 1 = 1) are dropped; a view body's positions are cleared. A MySQL placeholder carries the table
column it stands for (compared with, or stored into), so vet binds Go named types to key
identities and diffsENUMlabels against typed constants on MySQL too.REGEXP_LIKEis a
bigint(1),REGEXP_REPLACEnullable,QUOTEof a binary a character string: the probe
golden has no disagreement left. - MySQL, after an adversarial round against a running mysqld (7 lanes, 21 findings, every one
a regression test now): aUSINGorNATURALjoin coalesces its common columns (an
unqualified name is not ambiguous,SELECT *lists it once);GROUP BYon the alias of an
aggregate is 1056; multi-tableDELETE t1, t2 FROM .../DELETE FROM t1 USING ...is
analyzed, its targets judged for the failure modes; the select list is resolved before the
WHERE, as the server does, so its error comes first; aBIToperand is unsigned in
arithmetic;WITH ROLLUPmakes every non-aggregated result column nullable;STR_TO_DATE
follows a literal format intoDATE/TIME/DATETIME;col -> 'path'and->>parse.
The facts no longer take an expression bound to the execution (RAND()) for a known value,
soGROUP BY RAND()is not one row andtenant_id = FLOOR(RAND() * 3)pins nothing. The
failure modes:REPLACEviolates no key but may violate a referencing foreign key (1451), a
multi-tableUPDATEjudges each assignment against its own table, anUPDATEthrough an
updatable view judges the base table (its parameters carry the base column too). The
schema loader names unnamedCHECKandFOREIGN KEYconstraints the server's way, one above
the highest generated number, soALTER TABLE ... DROP CHECK t_chk_1finds its constraint,
and reads table and view names case-sensitively (lower_case_table_names=0). The runtime's
ExecOneaccepts the 0 / 1 / 2 rowsINSERT ... ON DUPLICATE KEY UPDATEandREPLACE
report for the one row their key names. - 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.