Repository navigation
fix(mysql): read joins, writes and literals as mysql does - #54
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
The new join-nesting speculation makes parse time exponential in a chain of bare JOINs. A statement MySQL accepts can hang the analysis, and with it the type check.
Reviewed changes
I read the whole diff of the vendored sqlparser changes, the engine's SELECT … INTO handling, the new and reblessed oracle cases, and the rebuilt MySQL artifact.
- Join nesting: a bare
JOINis now the left operand of the joins after it. The joins after it nest inside it only when anON/USINGis left over. The newmysql_join_nestingcase covers this. - Comma joins: in parentheses and in a multiple-table
UPDATE, a comma becomes aCROSS JOINof each later reference (comma_joined). - Operators and keywords:
->/->>bind tightly,!isNOT, andMODis supported.<@is no longer one token, and MySQL's reserved words can no longer be implicit aliases. - New syntax:
MATCH a AGAINST,INTERVAL(N, …),DELETE t.*targets, a charset inCAST/CONVERT,CHAR(… USING cs),LOCK IN SHARE MODE,FOR SHARE OF t1, t2, and anINTOat the end of the statement. - Literal printing: string literals are escaped according to the session's
NO_BACKSLASH_ESCAPES, set through a thread-local at the start of each analysis. Odd-length hex literals get a leading zero. - Engine: a
SELECTwhose rows go into variables now has cardinalitynone.
I checked that every analysis path sets the escape flag (analyze_text goes through analyze). site/content/docs/mysql.mdx already says that multi-table writes and select … into @v return nothing.
claude-opus-5-5 | 𝕏
The vendored parser nested a join with no ON to the right, so `a JOIN b RIGHT JOIN c ON x` typed a's columns non-null, and refused or misread statements MySQL accepts: comma joins in UPDATE and in parentheses, MATCH without parentheses, INTERVAL(), DELETE t.*, a trailing INTO, CAST charsets, MOD, and `->` precedence. A SELECT with INTO now returns no rows.
The join nesting read the joins after a bare one and went back to read them again when no ON was left over, so each join doubled the work.
ba2499f to
196ea07
Compare
There was a problem hiding this comment.
✅ No new issues found. Both fixes keep every reading as it was.
Reviewed changes
I reviewed the two commits pushed since the last review. Each one fixes a concern raised there.
- Fixed exponential join parsing:
parse_joinsno longer backtracks when noON/USINGis left over for the joins it read ahead. It keeps them: it pushes the current join with no constraint, thenextends with the joins it read. This is the same list the outer loop would have built, since the inner call stops at the same non-join token. Errors now propagate with?. The testa_chain_of_joins_is_read_in_one_passpins both nesting readings and a time bound, and covers 61-table chains. - Fixed
!before an arrow:!now parses its operand with a bound ofprec_value(DoubleColon) - 1. So!j->'$.a'is!(j->'$.a'), and every other binary operator still takes!xas its left operand. The other tokens at that precedence (::,[, postfix!) are not MySQL syntax. The test and the new oracle statementbang_before_arrow.sqlcover it, and the vendored README was updated to match.
claude-opus-5-5 | 𝕏

The MySQL engine's copy of
sqlparsernow reads statements the way MySQL 8.4 does. Measured against the server itself: every statement of the kinds the engine reads in MySQL's own test suite (mysql-test/t, 54,848 statements), prepared on MySQL 8.4 and parsed by the copy, and each statement both accept printed back and prepared again.A soundness fix
a JOIN b RIGHT JOIN c ON xis(a JOIN b) RIGHT JOIN c ON xin MySQL: a join with noONof its own is the left operand of the joins after it. The copy nested it the other way, so the engine typeda's columns non-null where MySQL null-extends them (checked on the server:count(ta.a)is 1 of 2 rows). It now nests only where anONis left over for the outer join (a JOIN b JOIN c ON x ON y), as MySQL does. The new oracle casemysql_join_nestingpins each reading; onmainitsright_after_bare_join.sqltypesaasint4.Statements now read as MySQL reads them
JOIN, read as aCROSS JOINof each later reference (parenthesized where it has joins of its own): multiple-tableUPDATE t1, t2 SET …, andFROM (t1, t2). A lone table may be parenthesized; a parenthesized join takes no alias.col->'path'andcol->>'path'bind tighter than any operator (datafusion-sqlparser-rs#2462);!xisNOT, as tightly;a MOD b;<@xis<and@x.AS, soINSERT … SELECT 5 ON DUPLICATE KEY UPDATE …reads itsON.MATCH a AGAINST (…)without parentheses,INTERVAL(N, N1, …),DELETE t1.*, t2.* FROM …,DELETE FROM t1.* USING …,CAST(x AS CHAR CHARACTER SET cs),CHAR(n USING cs),LOCK IN SHARE MODE,FOR SHARE OF t1, t2, and a statement'sINTOafter the rest of it.In the engine
SELECTwhose rows go into variables (INTO @v, after its columns or after the whole statement) returns no rows. Before,select id into @v from users where id = ?was typed as one row or none.sql_modeat each analysis.v like 'a%'and the predicates ofmysql_server_flagsarebool, and two JSON objects get their shapes. The oracle runs every one of them over its seeded values.The vendored crate's README lists every change; each changed file says so under its license header.