8000 fix(mysql): read joins, writes and literals as mysql does by colinhacks · Pull Request #54 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(mysql): read joins, writes and literals as mysql does - #54

Merged
colinhacks merged 3 commits into
mainfrom
mysql-fidelity
Oct 23, 2026
Merged

colinhacks merged 3 commits into
mainfrom
mysql-fidelity

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

The MySQL engine's copy of sqlparser now 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.

Before After
Valid statements the parser refuses 964 of 54,648 226
Reprints the server reads differently 94 3
Invalid statements the parser accepts 71 of 188 71

A soundness fix

a JOIN b RIGHT JOIN c ON x is (a JOIN b) RIGHT JOIN c ON x in MySQL: a join with no ON of its own is the left operand of the joins after it. The copy nested it the other way, so the engine typed a'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 an ON is left over for the outer join (a JOIN b JOIN c ON x ON y), as MySQL does. The new oracle case mysql_join_nesting pins each reading; on main its right_after_bare_join.sql types a as int4.

Statements now read as MySQL reads them

  • A comma between table references binds looser than any JOIN, read as a CROSS JOIN of each later reference (parenthesized where it has joins of its own): multiple-table UPDATE t1, t2 SET …, and FROM (t1, t2). A lone table may be parenthesized; a parenthesized join takes no alias.
  • col->'path' and col->>'path' bind tighter than any operator (datafusion-sqlparser-rs#2462); !x is NOT, as tightly; a MOD b; <@x is < and @x.
  • MySQL's reserved words are never an alias without AS, so INSERT … SELECT 5 ON DUPLICATE KEY UPDATE … reads its ON.
  • 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's INTO after the rest of it.
  • String literals print as MySQL reads them back (backslashes escaped where the session reads escapes), and an odd-length hex literal with a leading zero, so the probes the engine prepares are the statement written.

In the engine

  • A SELECT whose 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.
  • The printer's escape mode is set from the session's sql_mode at each analysis.
  • Three oracle cases are reblessed where a statement that used to fall back to the server's describe now parses: v like 'a%' and the predicates of mysql_server_flags are bool, 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.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 00:11
@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 3:00am UTC

Request Review

Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 JOIN is now the left operand of the joins after it. The joins after it nest inside it only when an ON/USING is left over. The new mysql_join_nesting case covers this.
  • Comma joins: in parentheses and in a multiple-table UPDATE, a comma becomes a CROSS JOIN of each later reference (comma_joined).
  • Operators and keywords: ->/->> bind tightly, ! is NOT, and MOD is 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 in CAST/CONVERT, CHAR(… USING cs), LOCK IN SHARE MODE, FOR SHARE OF t1, t2, and an INTO at 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 SELECT whose rows go into variables now has cardinality none.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using 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.
@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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_joins no longer backtracks when no ON/USING is left over for the joins it read ahead. It keeps them: it pushes the current join with no constraint, then extends 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 test a_chain_of_joins_is_read_in_one_pass pins both nesting readings and a time bound, and covers 61-table chains.
  • Fixed ! before an arrow: ! now parses its operand with a bound of prec_value(DoubleColon) - 1. So !j->'$.a' is !(j->'$.a'), and every other binary operator still takes !x as its left operand. The other tokens at that precedence (::, [, postfix !) are not MySQL syntax. The test and the new oracle statement bang_before_arrow.sql cover it, and the vendored README was updated to match.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit 8639b01 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the mysql-fidelity branch October 23, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0