Skip to content

Add MySQL to goldeneye: generate relations.jsonl and check the analyze cases against a live server - #4616

Open
kyleconroy wants to merge 2 commits into
mainfrom
claude/goldeneye-mysql-support-n0dru2
Open

Add MySQL to goldeneye: generate relations.jsonl and check the analyze cases against a live server#4616
kyleconroy wants to merge 2 commits into
mainfrom
claude/goldeneye-mysql-support-n0dru2

Conversation

@kyleconroy

Copy link
Copy Markdown
Collaborator

Summary

Adds a mysql engine to internal/goldeneye, following the pattern of the other engines: Locate, Version, Generate and Analyze, tests that run the checks, and a job in the gen workflow. The server is named by MYSQL_SERVER_URI (a go-sql-driver DSN) and has to be MySQL 26, the major pinned in mysql.Major; the workflow runs the mysql:26.7 image.

What is generated

MySQL keeps no catalog of its types, functions or operators, so those files stay hand-written, the way ClickHouse's and SQLite's do. What it does describe is its data dictionary: internal/engine/dolphin/dialect/relations.jsonl is now generated from information_schema — every view of it, with columns, NOT NULL and the declared type spelled MySQL's way (bigint unsigned included). Names are written in lower case, since MySQL matches them in any case and the dolphin converter lowercases every identifier. The other schemas mysqld --initialize creates (mysql, performance_schema, sys) are base tables, and every table a dialect seeds is one the analysis core hands codegen as a model, so they are left out for now.

Two hand-written files change alongside:

  • types.jsonl gains <type> unsigned aliases on the numeric types, so the generated columns resolve to their base type instead of a user type.
  • functions.jsonl marks the aggregates that are NULL over no rows as nullable (AVG, MAX, MIN, SUM, STD*, VAR*, JSON_ARRAYAGG, JSON_OBJECTAGG), which is what MySQL reports; BIT_*, COUNT and ANY_VALUE are not. Their return type is any, so no generated Go changes; analyze_select/mysql's golden gains a nullable on max(created).

The analysis check

Analyze loads a case's schema and fixture into a database of its own and asks the server three things about each query:

  • What a driver sees: the query is run with every parameter a user variable set to NULL, and result columns come from the result set's metadata as go-sql-driver reports it.
  • What the resolver made of it: the optimizer trace's expanded_query prints each query block after name resolution and before optimisation, with every column qualified, aliases kept and SELECT * expanded, which gives provenance and each parameter's partner. Views and derived tables are kept unmerged so a column read through one is reported as its column, and an information_schema view is not resolved away into the dictionary tables behind it.
  • For statements the trace does not expand (INSERT ... VALUES, single-table UPDATE/DELETE), the note EXPLAIN FORMAT=TRADITIONAL leaves, which prints the statement the same way. A SELECT cannot be read from the note: it is printed after optimisation, and a unique-key lookup against an empty table has folded to NULL = (@x) there.

MySQL reports nothing about a parameter but its position, so a parameter is described by its partner: a column's type and nullability from information_schema, a derived table's column from what its block projects, an expression's from running it over the tables it reads. Two things the driver keeps to itself, and how the check works around them: the table a result column comes from (read from the trace), and the length that distinguishes the sizes of TEXT/BLOB on the wire (a column read from a table is spelled the way the table declares it).

All five MySQL analyze cases match the server byte for byte, including a new analyze_system_catalog/mysql case that exercises the generated relations end to end through sqlc analyze.

Also

  • endtoend.Rewrite now recognises sqlc.slice(...), and hands LIMIT to the second count of LIMIT ?, ?.
  • The engine table in the command gains a dir, since MySQL's dialect lives under dolphin.
  • README and CLAUDE.md describe the new engine.

Test plan

  • go test ./... in internal/goldeneye with MYSQL_SERVER_URI set: dialect and all five analyze cases match MySQL 26.7.0 (goldeneye check mysql says the same)
  • go test ./internal/... ./cmd/... in the main module, including TestReplay/base for every MySQL case
  • The analysis check exercised on a scratch case with BETWEEN, IN lists, NOT LIKE, function partners, LIMIT ?, ?, derived tables, correlated subqueries, multi-row INSERT without a column list, ON DUPLICATE KEY UPDATE, multi-table UPDATE, LEFT JOIN, UNION and a recursive CTE

🤖 Generated with Claude Code

https://claude.ai/code/session_012DTeySVan5NcA3RS6xxdUn


Generated by Claude Code

…lyze cases

Add a mysql package to goldeneye that reads a live server named by
MYSQL_SERVER_URI. MySQL keeps no catalog of its types, functions or
operators, so those files stay hand-written; what the server does describe
is its data dictionary, so relations.jsonl is generated from
information_schema, with names in lower case since MySQL matches them in
any case and sqlc's parser lowercases every identifier. The other system
schemas are tables rather than views, which the analysis core would hand
codegen as models, so they are left out for now. The dialect lives under
internal/engine/dolphin, so the command gains a dialect directory
distinct from the engine name.

The package also checks the analyze_*/mysql cases against the server.
Result columns come from the result set's metadata as go-sql-driver
reports it; provenance and parameters come from the optimizer trace's
expanded_query, which prints each block after resolution and before
optimisation, and from the note EXPLAIN leaves for the statements the
trace does not expand. Views and derived tables are kept unmerged so a
column read through one is still its column. A column read from a table
is spelled by its declaration, since the driver hides the length that
tells the sizes of TEXT apart.

Making sqlc agree: MySQL reports its aggregates as nullable, so the
aggregates that are NULL over no rows are marked so in functions.jsonl,
which changes the committed analyze_select output; "bigint unsigned" and
the other unsigned spellings become aliases of their types so the
generated relations resolve; and an analyze_system_catalog case covers
querying information_schema. The shared placeholder rewriter learns
sqlc.slice and the second count of LIMIT ?, ?.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DTeySVan5NcA3RS6xxdUn
The gen workflow runs the mysql:26.7 image, so the pinned major is 26 and
relations.jsonl is regenerated from 26.7.0, which adds the JSON duality
view and library views to information_schema. EXPLAIN defaults to the tree
format since MySQL 26, which leaves no rewritten statement behind, so the
analysis check asks for the traditional format.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DTeySVan5NcA3RS6xxdUn
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