Skip to content

Fix MySQL lexer correctness issues - #584

Merged
JanJakes merged 7 commits into
trunkfrom
lexer-review-fixes
Nov 2, 2026
Merged

JanJakes merged 7 commits into
trunkfrom
lexer-review-fixes

Conversation

@JanJakes

@JanJakes JanJakes commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Both of our MySQL lexers, the new mysql-parser lexer and the mysql-on-sqlite lexer that the driver still uses, read some queries differently from MySQL. Some valid queries didn't parse, and some invalid ones did.

This change fixes these cases, so both lexers now read them like MySQL's own lexer:

  • t.1, t.1st, and t.1e5 are qualified names. Before, .1 was read as a decimal number.
  • A keyword right before a dot and a name is an identifier, so order.id works, while @@session.sql_mode keeps its meaning.
  • A name right after a dot is always an identifier, so t.x'41' is the column x followed by a string, not a hex literal.
  • A dot that ends a number doesn't make the next word a name, so SELECT 1.FROM dual now parses.
  • The name after @@ can't be empty, so SELECT @@ version and SELECT @@'version' are now syntax errors. It's also never a number or a literal, so @@123 is a variable name.

A dot after a quoted name or a space still starts a number, as in MySQL. All cases were checked against MySQL 8.4, and one more query from the MySQL server test corpus now parses.

The driver now stores CHECK and DEFAULT expressions without spaces around dots and after @@. It parses these expressions again to create the SQLite table. With spaces, t . order contains the ORDER keyword, and @@ max_connections has an empty variable name. Before, a constraint like CHECK (t.order > 0) failed with an internal error.

The lexers now track a state that each token sets for the next one, like MySQL's next_state. The states use MySQL's names, written out in full (for example, STATE_IDENTIFIER_SEPARATOR for MY_LEX_IDENT_SEP), so the code is easy to compare with sql_lex.cc. Lexing speed stays within noise of trunk, with and without opcache and JIT.

The PR also applies smaller improvements from the review of the new lexer:

  • Removes comments that only repeated the code or described its history.
  • Writes the backslash pattern in token unescaping as a plain literal.
  • Keeps the unquoted identifier characters in one shared constant.

Follows up on #429.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of qualified SQL names, including names that begin with digits or match reserved words.
    • Corrected parsing and display of system-variable references in stored expressions.
    • Rejected invalid or missing names after @@ while preserving support for backtick-quoted names.
    • Improved parsing of SQL statements that previously failed.

State what the inline block is and why, instead of how it came to be.
It mirrors scan_lexeme(), so the pointer keeps the two copies in sync.
Each of these comments repeated the byte that the condition right above
it already checks. Comments that label less obvious skips stay.
The pattern matches a single literal backslash, so it can be written
directly instead of being built with preg_quote() on every call.
Both lexers repeated the ASCII characters allowed in an unquoted
identifier in read_identifier() and in the "@" variable name scan.
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: de424b28-6562-4af4-b687-df674dd69776
Commits

Reviewing files that changed from the base of the PR and between 70bdaf3 and e6c72e6.

Files ignored due to path filters: 1
  • packages/mysql-parser/tests/data/corpus-failures.csv is excluded by !**/*.csv
Files selected for processing: 10
  • packages/mysql-on-sqlite/src/mysql/class-wp-mysql-lexer.php
  • packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php
  • packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
  • packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Lexer_Tests.php
  • packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php
  • packages/mysql-parser/src/class-wp-mysql-lexer.php
  • packages/mysql-parser/src/class-wp-mysql-token.php
  • packages/mysql-parser/tests/WP_MySQL_Lexer_Tests.php
  • packages/mysql-parser/tests/WP_MySQL_Parser_Tests.php
  • packages/mysql-parser/tests/WP_MySQL_Server_Suite_Parser_Tests.php
Files with no reviewable changes: 1
  • packages/mysql-on-sqlite/tests/mysql/WP_MySQL_Server_Suite_Parser_Tests.php

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Walkthrough

The MySQL lexers now use scanner states to tokenize qualified names and system-variable names. The SQLite expression serializer changes spacing around selected tokens. New tests cover tokenization, parsing, and SQLite schema expressions.

Changes: MySQL tokenization and expression round-tripping

Layer / File(s) Summary
Qualified-name and system-variable tokenization
packages/mysql-parser/src/class-wp-mysql-lexer.php, packages/mysql-on-sqlite/src/mysql/class-wp-mysql-lexer.php, packages/mysql-parser/src/class-wp-mysql-token.php, packages/mysql-parser/tests/*, packages/mysql-on-sqlite/tests/mysql/*
Both lexers use scanner state to distinguish qualified-name parts, keywords, numbers, and names following @@. Tests cover tokenization, parse acceptance and rejection, and an additional MySQL parser-suite query. The token backslash-removal pattern is now written directly.
Expression serialization and SQLite coverage
packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
Expression serialization omits spaces around parentheses and dots and after @@. Tests cover qualified references, CHECK constraints, and DEFAULT or CHECK expressions containing system variables.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Possibly related issues

Suggested reviewers: adamziel, wojtekn

Merge Risk: Minimal · up to e6c72

No concrete issue is established by the supplied evidence. Merge after the normal test checks.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: correcting lexer behavior. The phrase “small improvements” is broad, but it does not obscure the primary change.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 14 functions across 8 files. (1 skipped: 1 too large.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

PHPStan (2.3.1)

PHPStan reported 5 non-file analysis error(s); analysis is incomplete

PHPMD (3.0.0)
packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php

PHPMD could not process this file (exit code 255): 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 2097152 bytes) in phar:///usr/bin/phpmd/vendor/pdepend/pdepend/src/Util/Cache/Driver/FileCacheDriver.php on line 183

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Lexer benchmark

Changes to lexer-related files were detected and triggered a benchmark:

Config Base (QPS) This PR (QPS) Speedup
no JIT 85,785 84,732 0.99×
tracing JIT 189,443 187,727 0.99×

Note: Hosted runners are noisy, and absolute numbers vary. Treat the results with caution and verify them locally.

To reproduce locally:

cd packages/mysql-on-sqlite && composer run bench-lexer

@JanJakes JanJakes changed the title Fix qualified name lexing and apply lexer review notes Fix lexing of qualified names like t.1 and order.id Oct 29, 2026
@JanJakes
JanJakes force-pushed the lexer-review-fixes branch 2 times, most recently from 88fedea to d2af147 Compare October 30, 2026 11:51
@JanJakes JanJakes changed the title Fix lexing of qualified names like t.1 and order.id Fix lexer correctness issues and apply small improvements Oct 30, 2026
MySQL reads "t.1", "t.1st", and "order.id" as qualified names, but both
lexers failed on them: they scanned ".1" as a decimal number, and they
emitted "order" as the ORDER keyword.

Follow MySQL's lexer, where a token sets a state for the next one
("next_state" in sql/sql_lex.cc):

- A name followed by "." and a name character sets
  STATE_IDENTIFIER_SEPARATOR. The "." then separates qualified name
  parts, even before digits ("t.1"), and a keyword before it is an
  identifier ("order.id").
- A "." token followed by a name character sets STATE_IDENTIFIER_START.
  The name after it is then always an identifier ("t.order", "t.1"),
  even when it looks like a literal prefix ("x" in "t.x'41'").
- "@@" followed by a name character sets STATE_IDENTIFIER_OR_KEYWORD.
  The name after it is then an identifier or a keyword, never a number
  or a literal ("@@123"), so system variable scopes like
  "@@session.sql_mode" stay keywords.

In the last two states, the lexer reads the name before any other
scanning, as MySQL's lex_one_token() does.

A "." that ends a number sets no state, so "1.FROM dual" is a number and
the FROM keyword, as in MySQL. A "." after a quoted name or a space still
starts a number. The lexing speed stays within noise of trunk.

The fix applies to the mysql-parser lexer and to the mysql-on-sqlite lexer
that the driver still uses. The MySQL server corpus query that covers this
case now parses in both, and driver tests run such queries end to end.

See: https://github.com/mysql/mysql-server/blob/mysql-8.4.10/sql/sql_lex.cc
The driver stores CHECK and DEFAULT expressions by joining their tokens
with spaces, and it parses them again to create the SQLite table. With
spaces, "t . order" and "order . id" contain the ORDER keyword, and
"t . 1" contains a number, so the stored text no longer parses. For
example, "CHECK (t.order > 0)" failed with an internal error, while
MySQL accepts it.

Join the tokens around "." without whitespace, as the serializer already
does for parentheses, and list each of these token types once.
MySQL reads the name after "@@" in its MY_LEX_IDENT_OR_KEYWORD state,
which fails on an empty name. Only a backtick skips that state. So
"SELECT @@ version" and "SELECT @@'version'" are syntax errors in MySQL,
while "SELECT @@`version`" works. Both lexers accepted all of them.

Treat "@@" followed by neither a name character nor a backtick as
invalid input, as MySQL does.

The driver stores CHECK and DEFAULT expressions by joining their tokens
with spaces, and it parses them again on ALTER TABLE. Join "@@" and the
variable name without a space, so that "CHECK (a < @@max_connections)"
isn't stored as "a < @@ max_connections", which no longer parses. MySQL
rejects variables in these expressions, but the driver accepts them.

See: https://github.com/mysql/mysql-server/blob/mysql-8.4.10/sql/sql_lex.cc
@JanJakes
JanJakes marked this pull request as ready for review November 2, 2026 11:45
@JanJakes
JanJakes merged commit ca62f60 into trunk Nov 2, 2026
22 checks passed
@JanJakes
JanJakes deleted the lexer-review-fixes branch November 2, 2026 11:54
@JanJakes JanJakes mentioned this pull request Nov 3, 2026
@JanJakes JanJakes changed the title Fix lexer correctness issues and apply small improvements Fix MySQL lexer correctness issues Nov 3, 2026
JanJakes added a commit that referenced this pull request Nov 3, 2026
## Release `3.1.3`

Version bump and changelog update for release `3.1.3`.

**Changelog draft:**
* Fix `BINARY` and `VARBINARY` column defaults
([#596](#596))
* Quote SQLite identifiers in `LOCK TABLES`, `SHOW TABLES`, and compound
primary key indexes
([#599](#599))
* Name lexer tokens after the MySQL grammar and default to MySQL 8.4.10
([#607](#607))
* Fix MySQL lexer correctness issues
([#584](#584))
* Require MySQL proxy authentication and default to loopback
([#540](#540))
* Speed up PHPUnit and avoid E2E setup timeouts
([#579](#579))
* Fix reserved prefix checks for case variants and foreign keys
([#580](#580))

**Full changelog:**
v3.1.2...release/v3.1.3

## Next steps

1. **Review** the changes in this pull request.
2. **Push** any additional edits to this branch (`release/v3.1.3`).
3. **Merge** this pull request to complete the release.

Merging will automatically build the plugin ZIP, create a [GitHub
release](https://github.com/WordPress/sqlite-database-integration/releases),
and deploy to
[WordPress.org](https://wordpress.org/plugins/sqlite-database-integration/).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of binary column defaults and SQLite identifier
quoting.
  * Corrected MySQL lexer tokens, defaults, and lexer behavior.
* Updated MySQL proxy authentication and loopback defaults, test timing,
and reserved-prefix checks.
* **Release**
  * Updated the plugin and driver version to 3.1.3.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

1 participant