snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View File

@@ -0,0 +1,787 @@
package mysql
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"os"
"regexp"
"strconv"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// The analyze cases are checked against a live server, which is asked
// three things about each query. What a driver sees: the query is run,
// with every parameter a user variable set to NULL, and each result
// column's name, type and nullability are read from the result set's
// metadata, the way go-sql-driver reports them. What the resolver made of
// it: the optimizer trace prints each query block back after name
// resolution, with every column qualified, every alias kept and every
// SELECT * expanded, which says which table each result column is read
// from and what each parameter is compared with or assigned to. And for a
// statement the trace does not expand — an INSERT ... VALUES, a
// single-table UPDATE or DELETE — the note EXPLAIN leaves, which prints
// the statement the same way. Views and derived tables are kept as the
// query wrote them rather than merged, so that a column read through one
// is reported as its column. MySQL itself reports nothing about a
// parameter but its position, so a parameter is described by its partner:
// a column's type and nullability come from information_schema, a column
// of a derived table from what its block projects, and an expression's
// from running it, over the tables it reads, as a query of its own.
// placeholder is one parameter of a query as sqlc numbers them: each ? in
// turn, and each sqlc.arg name once, at its first appearance. Each becomes
// a user variable, which MySQL prints back by name, except in a LIMIT or
// OFFSET, where MySQL allows nothing but a number.
type placeholder struct {
Number int
Name string
Limit bool
Sentinel string // the variable's name, or the number
}
// limitBase is added to a LIMIT parameter's number, so that the count is
// unlike any a query would write.
const limitBase = 4000000000
func bind(sql string) (string, []placeholder) {
var phs []placeholder
numbers := map[string]int{}
out := endtoend.Rewrite(sql, func(name, lastWord string) string {
n, ok := numbers[name]
if name == "" || !ok {
n = len(phs) + 1
ph := placeholder{Number: n, Name: name}
switch strings.ToLower(lastWord) {
case "limit", "offset":
ph.Limit = true
ph.Sentinel = strconv.Itoa(limitBase + n)
default:
ph.Sentinel = "goldeneye_" + strconv.Itoa(n)
}
phs = append(phs, ph)
if name != "" {
numbers[name] = n
}
}
ph := phs[n-1]
if ph.Limit {
return ph.Sentinel
}
return "@" + ph.Sentinel
})
return out, phs
}
// column is what information_schema says about a column: its type as
// COLUMN_TYPE spells it, read into an expression.
type column struct {
name string
typ *analysis.TypeExpr
nullable bool
}
// typeOfColumn is a column's type as it was declared, from COLUMN_TYPE, which
// carries the arguments and the unsigned that DATA_TYPE does not: "decimal(10,2)
// unsigned" is the family "decimal unsigned" applied to 10 and 2, and
// "enum('a','b')" is enum applied to its members. A trailing word such as
// zerofill is part of the family too.
func typeOfColumn(columnType string) *analysis.TypeExpr {
s := strings.TrimSpace(columnType)
open := strings.IndexByte(s, '(')
if open < 0 {
return &analysis.TypeExpr{Name: strings.ToLower(s)}
}
close := strings.LastIndexByte(s, ')')
if close < open {
return &analysis.TypeExpr{Name: strings.ToLower(s)}
}
// The family is spelled in lower case; an enum's members keep theirs,
// since they are values.
name := strings.ToLower(strings.TrimSpace(s[:open]))
if rest := strings.ToLower(strings.TrimSpace(s[close+1:])); rest != "" {
name += " " + rest
}
t := &analysis.TypeExpr{Name: name}
for _, a := range splitArgs(s[open+1 : close]) {
a = strings.TrimSpace(a)
if strings.HasPrefix(a, "'") && strings.HasSuffix(a, "'") && len(a) >= 2 {
v := unquoteMember(a[1 : len(a)-1])
t.Args = append(t.Args, analysis.TypeArg{String: &v})
continue
}
if n, err := strconv.ParseInt(a, 10, 64); err == nil {
t.Args = append(t.Args, analysis.TypeArg{Int: &n})
}
}
return t
}
// splitArgs splits a type's argument list on the commas outside quotes. A
// backslash inside a quoted member escapes the character after it.
func splitArgs(s string) []string {
var out []string
start, quoted := 0, false
for i := 0; i < len(s); i++ {
switch {
case quoted && s[i] == '\\':
i++
case s[i] == '\'':
quoted = !quoted
case s[i] == ',' && !quoted:
out = append(out, s[start:i])
start = i + 1
}
}
return append(out, s[start:])
}
// unquoteMember reads the body of a quoted enum or set member as
// information_schema spells it: a quote is doubled and a backslash escapes
// the character after it.
func unquoteMember(s string) string {
var out strings.Builder
for i := 0; i < len(s); i++ {
switch {
case s[i] == '\\' && i+1 < len(s):
i++
out.WriteByte(s[i])
case s[i] == '\'' && i+1 < len(s) && s[i+1] == '\'':
i++
out.WriteByte('\'')
default:
out.WriteByte(s[i])
}
}
return out.String()
}
// withNullable copies a type with its nullability set.
func withNullable(t *analysis.TypeExpr, nullable bool) *analysis.TypeExpr {
out := *t
out.Nullable = nullable
return &out
}
// relation is a table the catalog knows, by schema and name.
type relation struct {
schema, name string
}
// analyzer holds the session a case runs in and the catalog: the case's
// own schema, and information_schema, whose names are matched and
// reported in lower case, since MySQL matches them in any case and the
// dialect spells them that way.
type analyzer struct {
conn *sql.Conn
db string // the case's schema
catalog map[relation][]column // columns in order
}
// table returns the catalog's name for a table of a schema, or "" when
// the catalog has no such table.
func (a *analyzer) table(schema, name string) (relation, bool) {
if schema == "" {
schema = a.db
}
if strings.EqualFold(schema, "information_schema") {
schema, name = "information_schema", strings.ToLower(name)
}
rel := relation{schema, name}
_, ok := a.catalog[rel]
return rel, ok
}
var dbNameRe = regexp.MustCompile(`[^A-Za-z0-9_]+`)
// Analyze loads a case's schema and fixture into a database of their own
// on the server, runs its queries there and returns what MySQL reports in
// the JSON shape sqlc analyze prints.
func Analyze(ctx context.Context, dsn string, c endtoend.Case) ([]byte, error) {
conn, err := open(ctx, dsn)
if err != nil {
return nil, err
}
defer conn.Close()
schema, err := os.ReadFile(c.Schema)
if err != nil {
return nil, err
}
var fixture []byte
if c.Fixture != "" {
if fixture, err = os.ReadFile(c.Fixture); err != nil {
return nil, err
}
}
queries, err := c.Queries()
if err != nil {
return nil, err
}
name := "goldeneye_" + dbNameRe.ReplaceAllString(c.Name, "_")
db := quote(name)
for _, stmt := range []string{
"DROP DATABASE IF EXISTS " + db,
"CREATE DATABASE " + db,
"USE " + db,
"SET SESSION optimizer_trace = 'enabled=on'",
"SET SESSION optimizer_trace_max_mem_size = 67108864",
// A view or derived table is kept as the query wrote it rather
// than merged into the block that reads it, so that a column read
// from one is reported as its column, and an information_schema
// view is not resolved away into the data dictionary tables
// behind it.
"SET SESSION optimizer_switch = 'derived_merge=off,derived_condition_pushdown=off'",
} {
if _, err := conn.ExecContext(ctx, stmt); err != nil {
return nil, err
}
}
defer conn.ExecContext(context.WithoutCancel(ctx), "DROP DATABASE IF EXISTS "+db)
if _, err := conn.ExecContext(ctx, string(schema)); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Schema, err)
}
if strings.TrimSpace(string(fixture)) != "" {
if _, err := conn.ExecContext(ctx, string(fixture)); err != nil {
return nil, fmt.Errorf("loading %s: %w", c.Fixture, err)
}
}
a := &analyzer{conn: conn, db: name}
if a.catalog, err = readCatalog(ctx, conn, name); err != nil {
return nil, err
}
out := make([]analysis.Query, 0, len(queries))
for _, q := range queries {
aq, err := a.analyzeQuery(ctx, q)
if err != nil {
return nil, fmt.Errorf("%s: %w", q.Name, err)
}
out = append(out, aq)
}
return analysis.Encode(out)
}
// Check compares what MySQL reports for a case with the output the case
// committed, returning a diff when they differ.
// Check compares a case's committed output with what MySQL reports. A
// column read from a table is compared in full, since information_schema
// spells its whole type; an expression's type comes from the wire, which
// carries the family and nothing of its arguments, so an expression, and
// a parameter typed by one, is compared by family alone.
func Check(ctx context.Context, dsn string, c endtoend.Case) (string, error) {
got, err := Analyze(ctx, dsn, c)
if err != nil {
return "", err
}
want, err := os.ReadFile(c.Output)
if err != nil {
return "", err
}
var queries []analysis.Query
if err := json.Unmarshal(want, &queries); err != nil {
return "", fmt.Errorf("%s: %w", c.Output, err)
}
for i := range queries {
for j := range queries[i].Columns {
familyOnly(&queries[i].Columns[j])
}
for j := range queries[i].Params {
familyOnly(&queries[i].Params[j].Column)
}
}
want, err = analysis.Encode(queries)
if err != nil {
return "", err
}
if bytes.Equal(want, got) {
return "", nil
}
return dialect.Diff(string(want), string(got)), nil
}
// familyOnly drops the arguments of a column's type when the column is not
// read from a table, which is as much as the wire says about it.
func familyOnly(col *analysis.Column) {
if col.Table == "" && col.Type != nil {
col.Type.Args = nil
}
}
const catalogQuery = `
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA IN (?, 'information_schema')
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION`
func readCatalog(ctx context.Context, conn *sql.Conn, db string) (map[relation][]column, error) {
rows, err := conn.QueryContext(ctx, catalogQuery, db)
if err != nil {
return nil, err
}
defer rows.Close()
catalog := map[relation][]column{}
for rows.Next() {
var schema, table, name, dataType, columnType, nullable string
if err := rows.Scan(&schema, &table, &name, &dataType, &columnType, &nullable); err != nil {
return nil, err
}
if schema == "information_schema" {
table, name = strings.ToLower(table), strings.ToLower(name)
}
rel := relation{schema, table}
catalog[rel] = append(catalog[rel], column{
name: name,
typ: typeOfColumn(columnType),
nullable: nullable == "YES",
})
}
return catalog, rows.Err()
}
// statement is everything the server said about one query.
type statement struct {
a *analyzer
trace trace
note *text // the statement as EXPLAIN printed it, nil without one
aliases map[string]tableRef
results []*sql.ColumnType // of the executed query, nil for one returning no rows
target *relation // the table an INSERT ... VALUES writes
targets []string // its columns, in row order
}
func (a *analyzer) analyzeQuery(ctx context.Context, q endtoend.Query) (analysis.Query, error) {
sql, phs := bind(q.SQL)
var sets []string
for _, ph := range phs {
if !ph.Limit {
sets = append(sets, "@"+ph.Sentinel+" = NULL")
}
}
if len(sets) > 0 {
if _, err := a.conn.ExecContext(ctx, "SET "+strings.Join(sets, ", ")); err != nil {
return analysis.Query{}, err
}
}
// EXPLAIN resolves and optimises the statement without running it,
// leaving its note and its trace behind. Only the traditional format
// leaves the note, and since MySQL 26 the default format is the tree.
if err := drain(a.conn.QueryContext(ctx, "EXPLAIN FORMAT=TRADITIONAL "+sql)); err != nil {
return analysis.Query{}, err
}
note, err := readNote(ctx, a.conn)
if err != nil {
return analysis.Query{}, err
}
tr, err := readTrace(ctx, a.conn)
if err != nil {
return analysis.Query{}, err
}
s := &statement{a: a, trace: tr, aliases: map[string]tableRef{}}
if note != "" {
t := tokenize(note)
s.note = &t
t.aliases(s.aliases)
s.target, s.targets = s.insertTargets(t)
}
for _, t := range tr.blocks {
t.aliases(s.aliases)
}
aq := analysis.Query{
Name: q.Name,
Cmd: q.Cmd,
Columns: []analysis.Column{},
Params: []analysis.Param{},
}
if returnsRows(sql) {
rows, err := a.conn.QueryContext(ctx, sql)
if err != nil {
return analysis.Query{}, err
}
s.results, err = rows.ColumnTypes()
rows.Close()
if err != nil {
return analysis.Query{}, err
}
// Names, types and nullability are the result set's. The top
// block's SELECT list adds which table each column is read from,
// when the two line up. MySQL sends every size of TEXT and BLOB
// as the one wire type and tells them apart by the length beside
// it, which the driver keeps to itself, so a column read from a
// table, directly or through a derived table, is spelled the way
// the table declares it.
var items []item
if top, ok := tr.blocks[1]; ok {
items = top.selectItems()
}
for i, ct := range s.results {
ac := analysis.Column{Name: ct.Name(), Type: typeOf(ct)}
if len(items) == len(s.results) {
top := tr.blocks[1]
if it := items[i]; it.end-it.start == 1 && top.at(it.start).kind == tkIdent {
tok := top.at(it.start)
if rel, ok := s.resolve(tok.parts); ok {
ac.Table = rel.name
}
if col, ok := s.origin(top, tok, 0); ok {
ac.Type = withNullable(col.typ, ac.Type.Nullable)
}
}
}
aq.Columns = append(aq.Columns, ac)
}
}
for _, ph := range phs {
ac := analysis.Column{}
if t, i, ok := s.locate(ph); ok {
ac = s.describe(ctx, t, t.partner(i), 0)
}
if ph.Name != "" {
ac.Name = ph.Name
}
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
}
return aq, nil
}
// drain reads a result set to the end and closes it.
func drain(rows *sql.Rows, err error) error {
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
}
return rows.Err()
}
// readNote returns the statement EXPLAIN printed back, from the note it
// leaves among the warnings, or "" when it left none.
func readNote(ctx context.Context, conn *sql.Conn) (string, error) {
rows, err := conn.QueryContext(ctx, "SHOW WARNINGS")
if err != nil {
return "", err
}
defer rows.Close()
note := ""
for rows.Next() {
var level, message string
var code int
if err := rows.Scan(&level, &code, &message); err != nil {
return "", err
}
if code == 1003 {
note = message
}
}
return note, rows.Err()
}
// readTrace returns the optimizer trace of the last statement.
func readTrace(ctx context.Context, conn *sql.Conn) (trace, error) {
rows, err := conn.QueryContext(ctx, "SELECT TRACE, MISSING_BYTES_BEYOND_MAX_MEM_SIZE FROM information_schema.OPTIMIZER_TRACE")
if err != nil {
return trace{}, err
}
defer rows.Close()
blob := ""
for rows.Next() {
var missing int64
if err := rows.Scan(&blob, &missing); err != nil {
return trace{}, err
}
if missing > 0 {
return trace{}, fmt.Errorf("the optimizer trace was truncated by %d bytes", missing)
}
}
if err := rows.Err(); err != nil {
return trace{}, err
}
return parseTrace(blob)
}
// returnsRows reports whether a statement produces a result set.
func returnsRows(sql string) bool {
head := strings.ToLower(strings.TrimSpace(sql))
if strings.HasPrefix(head, "(") {
return true
}
for _, kw := range []string{"select", "with", "show", "describe", "desc", "explain", "table", "values"} {
if strings.HasPrefix(head, kw) && (len(head) == len(kw) || !isWordByte(head[len(kw)])) {
return true
}
}
return false
}
// typeOf is a result column's type as the driver reports it, spelled the
// way a column declaration does: "bigint unsigned" rather than the
// driver's "UNSIGNED BIGINT".
func typeOf(ct *sql.ColumnType) *analysis.TypeExpr {
name := strings.ToLower(ct.DatabaseTypeName())
if rest, ok := strings.CutPrefix(name, "unsigned "); ok {
name = rest + " unsigned"
}
nullable, _ := ct.Nullable()
return &analysis.TypeExpr{Name: name, Nullable: nullable}
}
// resolve returns the catalog table a qualified column name is read from:
// the qualifier is an alias, or a table of the case's schema, or a table
// of the schema the name spells out.
func (s *statement) resolve(parts []string) (relation, bool) {
if len(parts) < 2 {
return relation{}, false
}
qualifier := parts[len(parts)-2]
if ref, ok := s.aliases[qualifier]; ok {
return s.a.table(ref.schema, ref.name)
}
schema := ""
if len(parts) >= 3 {
schema = parts[len(parts)-3]
}
return s.a.table(schema, qualifier)
}
// locate finds the text a placeholder was printed in and where: the
// innermost query block whose expanded_query has it, or else the note.
func (s *statement) locate(ph placeholder) (text, int, bool) {
find := func(t text) int {
if ph.Limit {
return t.findNumber(ph.Sentinel)
}
return t.findVar(ph.Sentinel)
}
for _, sel := range s.trace.order() {
t := s.trace.blocks[sel]
if i := find(t); i >= 0 {
return t, i, true
}
}
if s.note != nil {
if i := find(*s.note); i >= 0 {
return *s.note, i, true
}
}
return text{}, -1, false
}
// describe turns a parameter's partner into the column description sqlc
// prints for the parameter.
func (s *statement) describe(ctx context.Context, t text, p partner, depth int) analysis.Column {
switch p.kind {
case partnerOperand:
return s.operand(ctx, t, p.start, p.end, depth)
case partnerProjection:
if s.results != nil {
if top, ok := s.trace.blocks[1]; ok && top.src == t.src && p.index < len(s.results) {
ct := s.results[p.index]
return analysis.Column{Name: ct.Name(), Type: typeOf(ct)}
}
}
if items := t.selectItems(); p.index < len(items) {
return analysis.Column{Name: items[p.index].alias}
}
case partnerInsert:
if s.target != nil && len(s.targets) > 0 {
return s.column(*s.target, s.targets[p.index%len(s.targets)])
}
case partnerLimit:
// A LIMIT or OFFSET count is an unsigned 64-bit integer to MySQL.
return analysis.Column{Type: &analysis.TypeExpr{Name: "bigint unsigned"}}
}
return analysis.Column{}
}
// operand describes an operand: a column by what the schema says of it, a
// call by its name and what it returns, anything else by its type.
func (s *statement) operand(ctx context.Context, t text, start, end, depth int) analysis.Column {
tok := t.at(start)
if end-start == 1 && tok.kind == tkIdent {
return s.columnRef(ctx, t, tok, depth)
}
if tok.kind == tkVar {
return analysis.Column{}
}
ac := analysis.Column{}
if tok.kind == tkWord && t.at(start+1).kind == tkLParen && t.isCall(start) && !strings.HasPrefix(tok.text, "<") {
ac.Name = tok.text
}
ac.Type = s.eval(ctx, t, start, end)
return ac
}
// columnRef describes a column reference: a column of the schema, a column
// of a derived table by the expression its block projects, or a SELECT list
// alias by the expression it names.
func (s *statement) columnRef(ctx context.Context, t text, tok token, depth int) analysis.Column {
parts := tok.parts
if depth > 8 {
return analysis.Column{Name: parts[len(parts)-1]}
}
if len(parts) == 1 {
for _, it := range t.selectItems() {
if it.alias == parts[0] {
ac := s.operand(ctx, t, it.start, it.end, depth+1)
ac.Name = parts[0]
return ac
}
}
return analysis.Column{Name: parts[0]}
}
qualifier, name := parts[len(parts)-2], parts[len(parts)-1]
if rel, ok := s.resolve(parts); ok {
return s.column(rel, name)
}
if sel, ok := s.trace.derived[qualifier]; ok {
if block, ok := s.trace.blocks[sel]; ok {
for _, it := range block.selectItems() {
if it.alias == name {
ac := s.operand(ctx, block, it.start, it.end, depth+1)
ac.Name = name
return ac
}
}
}
}
return analysis.Column{Name: name}
}
// origin follows a column reference to the catalog column it reads, through
// any derived table or CTE whose block projects that column as it is.
func (s *statement) origin(t text, tok token, depth int) (column, bool) {
parts := tok.parts
if depth > 8 || len(parts) < 2 {
return column{}, false
}
qualifier, name := parts[len(parts)-2], parts[len(parts)-1]
if rel, ok := s.resolve(parts); ok {
if rel.schema == "information_schema" {
name = strings.ToLower(name)
}
for _, col := range s.a.catalog[rel] {
if col.name == name {
return col, true
}
}
return column{}, false
}
sel, ok := s.trace.derived[qualifier]
if !ok {
return column{}, false
}
block, ok := s.trace.blocks[sel]
if !ok {
return column{}, false
}
for _, it := range block.selectItems() {
if it.alias == name && it.end-it.start == 1 && block.at(it.start).kind == tkIdent {
return s.origin(block, block.at(it.start), depth+1)
}
}
return column{}, false
}
// column describes a column of a catalog table.
func (s *statement) column(rel relation, name string) analysis.Column {
if rel.schema == "information_schema" {
name = strings.ToLower(name)
}
for _, col := range s.a.catalog[rel] {
if col.name == name {
return analysis.Column{
Name: name,
Type: withNullable(col.typ, col.nullable),
Table: rel.name,
}
}
}
return analysis.Column{Name: name, Table: rel.name}
}
// eval finds the type of an expression by running it as a query of its
// own, over the tables its columns are read from under the aliases the
// statement gave them. An expression that reads a derived table cannot be
// run that way and gets no type.
func (s *statement) eval(ctx context.Context, t text, start, end int) *analysis.TypeExpr {
var from []string
seen := map[string]bool{}
for _, id := range t.idents(start, end) {
qualifier := id.parts[len(id.parts)-2]
if seen[qualifier] {
continue
}
seen[qualifier] = true
rel, ok := s.resolve(id.parts)
if !ok {
return nil
}
item := quote(rel.schema) + "." + quote(rel.name)
if qualifier != rel.name {
item += " " + quote(qualifier)
}
from = append(from, item)
}
query := "SELECT " + t.slice(start, end)
if len(from) > 0 {
query += " FROM " + strings.Join(from, ", ")
}
rows, err := s.a.conn.QueryContext(ctx, query+" LIMIT 0")
if err != nil {
return nil
}
defer rows.Close()
cts, err := rows.ColumnTypes()
if err != nil || len(cts) != 1 {
return nil
}
return typeOf(cts[0])
}
// insertTargets reads the table and columns an INSERT ... VALUES writes,
// as the note prints them: the columns listed, or every column of the
// table in order when none are.
func (s *statement) insertTargets(t text) (*relation, []string) {
for i, tok := range t.toks {
if tok.kind != tkWord || tok.text != "into" || t.at(i+1).kind != tkIdent {
continue
}
parts := t.at(i + 1).parts
schema := ""
if len(parts) >= 2 {
schema = parts[len(parts)-2]
}
rel, ok := s.a.table(schema, parts[len(parts)-1])
if !ok {
return nil, nil
}
var targets []string
if t.at(i+2).kind == tkLParen {
for _, m := range t.list(i + 2) {
if m[1]-m[0] == 1 && t.at(m[0]).kind == tkIdent {
parts := t.at(m[0]).parts
targets = append(targets, parts[len(parts)-1])
}
}
} else {
for _, col := range s.a.catalog[rel] {
targets = append(targets, col.name)
}
}
return &rel, targets
}
return nil, nil
}

View File

@@ -0,0 +1,150 @@
// Package mysql generates the MySQL dialect seed under
// internal/engine/dolphin/dialect from a live server, and verifies the
// MySQL analyze cases under internal/endtoend/testdata against the same
// server.
//
// MySQL keeps no catalog of its types, functions or operators — the server
// describes a built-in function no further than its name, in the help
// tables — so types.jsonl, functions.jsonl and operators.jsonl are written
// by hand and are not this package's business. What it does describe is
// its data dictionary: relations.jsonl is every view of information_schema,
// read from information_schema itself.
//
// The package also verifies the MySQL analyze cases under
// internal/endtoend/testdata against the same server: each case's schema
// and fixture are loaded into a database of their own and its queries run
// there. Result columns come from the result set's metadata as the driver
// reports it, provenance and parameters from the optimizer trace's
// expanded_query and the note EXPLAIN leaves, since MySQL reports nothing
// about a parameter but its position. The answer is printed in the JSON
// shape sqlc analyze prints and compared with the case's committed
// stdout.json byte for byte.
//
// The server is named by MYSQL_SERVER_URI, in the form the go-sql-driver
// DSN takes, and has to be the major release in Major, since each release
// adds to information_schema.
package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"strconv"
"strings"
driver "github.com/go-sql-driver/mysql"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// Engine is the name sqlc knows the dialect by, and the name of the analyze
// case directories under internal/endtoend/testdata.
const Engine = "mysql"
// Dir is the name of the engine directory the dialect lives under, which
// is named after the parser sqlc reads MySQL with rather than the engine.
const Dir = "dolphin"
// Major is the MySQL major release the dialect is generated from. Bumping
// it is a deliberate change: every release adds tables and columns to the
// system schemas, so regenerate and review the dialect after changing it.
const Major = 26
// Locate returns the server to generate from, named by MYSQL_SERVER_URI.
func Locate() (string, error) {
dsn := os.Getenv("MYSQL_SERVER_URI")
if dsn == "" {
return "", errors.New("MYSQL_SERVER_URI is not set")
}
if _, err := driver.ParseDSN(dsn); err != nil {
return "", fmt.Errorf("MYSQL_SERVER_URI: %w", err)
}
return dsn, nil
}
// open connects to the server. The connection runs several statements at
// once, so that a case's schema loads as one script, and holds one
// session, since the optimizer trace and the warnings a statement leaves
// are both per session.
func open(ctx context.Context, dsn string) (*sql.Conn, error) {
cfg, err := driver.ParseDSN(dsn)
if err != nil {
return nil, err
}
cfg.MultiStatements = true
db, err := sql.Open("mysql", cfg.FormatDSN())
if err != nil {
return nil, err
}
conn, err := db.Conn(ctx)
if err != nil {
db.Close()
return nil, err
}
return conn, nil
}
// Version reports the release a server is.
func Version(ctx context.Context, dsn string) (string, error) {
conn, err := open(ctx, dsn)
if err != nil {
return "", err
}
defer conn.Close()
return version(ctx, conn)
}
func version(ctx context.Context, conn *sql.Conn) (string, error) {
var v string
if err := conn.QueryRowContext(ctx, "SELECT version()").Scan(&v); err != nil {
return "", err
}
return "MySQL " + v, nil
}
// checkVersion refuses a server of another major release than the dialect
// is generated from, whose system schemas would differ from the committed
// ones without anything being wrong.
func checkVersion(ctx context.Context, conn *sql.Conn) error {
var v string
if err := conn.QueryRowContext(ctx, "SELECT version()").Scan(&v); err != nil {
return err
}
head, _, _ := strings.Cut(v, ".")
major, err := strconv.Atoi(head)
if err != nil {
return fmt.Errorf("version %q: %w", v, err)
}
if major != Major {
return fmt.Errorf("the dialect is generated from MySQL %d, but the server is MySQL %d", Major, major)
}
return nil
}
// Generate reads the dialect from the server: relations.jsonl, the views of
// information_schema.
func Generate(ctx context.Context, dsn string) (dialect.Files, error) {
conn, err := open(ctx, dsn)
if err != nil {
return nil, err
}
defer conn.Close()
if err := checkVersion(ctx, conn); err != nil {
return nil, err
}
var relations []dialect.Relation
for _, schema := range systemSchemas {
rels, err := readRelations(ctx, conn, schema)
if err != nil {
return nil, err
}
relations = append(relations, rels...)
}
blob, err := dialect.JSONL(relations)
if err != nil {
return nil, err
}
return dialect.Files{dialect.RelationsFile: blob}, nil
}

View File

@@ -0,0 +1,66 @@
package mysql
import (
"context"
"testing"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// TestDialect verifies the committed MySQL dialect against what the server
// reports. It skips unless MYSQL_SERVER_URI names a server.
func TestDialect(t *testing.T) {
dsn, err := Locate()
if err != nil {
t.Skip(err)
}
ctx := context.Background()
version, err := Version(ctx, dsn)
if err != nil {
t.Fatal(err)
}
files, err := Generate(ctx, dsn)
if err != nil {
t.Fatal(err)
}
dir, err := dialect.Dir(Dir)
if err != nil {
t.Fatal(err)
}
report, err := dialect.Check(dir, files)
if err != nil {
t.Fatal(err)
}
if report != "" {
t.Errorf("%s does not match what %s reports:\n%s", dir, version, report)
}
}
// TestAnalyzeCases verifies every MySQL analyze case under
// internal/endtoend/testdata against what the server reports. It skips
// unless MYSQL_SERVER_URI names a server.
func TestAnalyzeCases(t *testing.T) {
dsn, err := Locate()
if err != nil {
t.Skip(err)
}
cases, err := endtoend.Cases(Engine)
if err != nil {
t.Fatal(err)
}
if len(cases) == 0 {
t.Fatal("no mysql analyze cases found")
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
diff, err := Check(context.Background(), dsn, c)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Errorf("%s does not match what MySQL reports (-committed +mysql):\n%s", c.Output, diff)
}
})
}
}

View File

@@ -0,0 +1,99 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// systemSchemas are the schemas whose relations are generated: the data
// dictionary's views, which are what a query of the server's catalog
// reads. The other schemas mysqld --initialize creates — mysql,
// performance_schema and sys — are tables rather than views, and every
// table a dialect seeds is one the analysis core hands codegen as a model,
// so they wait until codegen knows a system schema when it sees one.
var systemSchemas = []string{
"information_schema",
}
// relationQuery lists a schema's relations and their columns, in the order
// the columns were declared, so that the output is stable.
const relationQuery = `
SELECT t.TABLE_NAME, t.TABLE_TYPE,
c.COLUMN_NAME, c.DATA_TYPE, c.COLUMN_TYPE, c.IS_NULLABLE
FROM information_schema.TABLES t
JOIN information_schema.COLUMNS c
ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
WHERE t.TABLE_SCHEMA = ?
ORDER BY t.TABLE_NAME, c.ORDINAL_POSITION`
// readRelations reads a schema's tables and views. Their names are written
// in lower case: information_schema names its views in upper case and
// matches them in any case, MySQL matches every column name in any case,
// and sqlc's MySQL parser lowercases every identifier, so lower case is
// how a query reaches them. A column's type is spelled the way a
// declaration does, as COLUMN_TYPE reports it, arguments and UNSIGNED
// included: varchar(64), bigint unsigned.
func readRelations(ctx context.Context, conn *sql.Conn, schema string) ([]dialect.Relation, error) {
rows, err := conn.QueryContext(ctx, relationQuery, schema)
if err != nil {
return nil, fmt.Errorf("%s: %w", schema, err)
}
defer rows.Close()
var relations []dialect.Relation
var cur *dialect.Relation
for rows.Next() {
var name, kind, colName, dataType, columnType, nullable string
if err := rows.Scan(&name, &kind, &colName, &dataType, &columnType, &nullable); err != nil {
return nil, err
}
name = strings.ToLower(name)
if cur == nil || cur.Name != name {
rel := dialect.Relation{Catalog: "def", Schema: schema, Name: name}
if kind != "BASE TABLE" {
rel.Kind = "v"
}
relations = append(relations, rel)
cur = &relations[len(relations)-1]
}
cur.Columns = append(cur.Columns, dialect.Column{
Name: strings.ToLower(colName),
Type: typeName(dataType, columnType),
NotNull: nullable == "NO",
})
}
return relations, rows.Err()
}
// typeName spells a column's type the way a declaration does, which is
// COLUMN_TYPE as information_schema reports it: "varchar(64)", "bigint
// unsigned", "enum('a','b')". The family is spelled in lower case; the
// members of an enum or set are values and keep theirs.
func typeName(dataType, columnType string) string {
if columnType == "" {
return strings.ToLower(dataType)
}
var out strings.Builder
quoted := false
for i := 0; i < len(columnType); i++ {
c := columnType[i]
switch {
case quoted && c == '\\' && i+1 < len(columnType):
out.WriteByte(c)
i++
out.WriteByte(columnType[i])
continue
case c == '\'':
quoted = !quoted
}
if quoted {
out.WriteByte(c)
} else {
out.WriteString(strings.ToLower(string(c)))
}
}
return out.String()
}

View File

@@ -0,0 +1,678 @@
package mysql
import (
"strings"
)
// MySQL prints a statement back with every name resolved, every table
// alias kept, every SELECT * expanded and every binary operation
// parenthesised — in the optimizer trace's expanded_query, once per query
// block, and in the note EXPLAIN leaves. Such a text is tokenised here and
// read for three things: the tables it names and their aliases, the
// columns each query block projects, and the operand a parameter is
// compared with or assigned to.
type tokKind int
const (
tkIdent tokKind = iota // `a`.`b`.`c`
tkVar // @`name`
tkNumber // 42, 1.5
tkString // 'text'
tkWord // a keyword, a function name or a <marker>
tkOp // =, <=>, +, like is a word
tkLParen
tkRParen
tkComma
)
type token struct {
kind tokKind
text string // the token as printed; a word is lower-cased
parts []string // an identifier's parts, unquoted; a variable's name
start int // offsets into the source
end int
}
// text is a tokenised statement.
type text struct {
src string
toks []token
match []int // the index of each parenthesis's partner, or -1
}
func tokenize(src string) text {
var toks []token
i := 0
emit := func(kind tokKind, start, end int, parts []string) {
toks = append(toks, token{kind: kind, text: src[start:end], parts: parts, start: start, end: end})
}
for i < len(src) {
c := src[i]
switch {
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
i++
case strings.HasPrefix(src[i:], "/*"):
end := strings.Index(src[i:], "*/")
if end < 0 {
i = len(src)
} else {
i += end + 2
}
case c == '`':
start := i
var parts []string
for {
part, end := readQuoted(src, i)
parts = append(parts, part)
i = end
if i+1 < len(src) && src[i] == '.' && src[i+1] == '`' {
i++
continue
}
break
}
emit(tkIdent, start, i, parts)
case c == '@':
start := i
i++
for i < len(src) && src[i] == '@' {
i++
}
if i < len(src) && src[i] == '`' {
name, end := readQuoted(src, i)
i = end
emit(tkVar, start, i, []string{name})
} else {
for i < len(src) && isWordByte(src[i]) {
i++
}
emit(tkVar, start, i, []string{src[start+1 : i]})
}
case c == '\'' || c == '"':
start := i
i = skipString(src, i)
emit(tkString, start, i, nil)
case c >= '0' && c <= '9' || c == '.' && i+1 < len(src) && src[i+1] >= '0' && src[i+1] <= '9':
start := i
for i < len(src) && (src[i] >= '0' && src[i] <= '9' || src[i] == '.') {
i++
}
if i < len(src) && (src[i] == 'e' || src[i] == 'E') {
i++
if i < len(src) && (src[i] == '+' || src[i] == '-') {
i++
}
for i < len(src) && src[i] >= '0' && src[i] <= '9' {
i++
}
}
emit(tkNumber, start, i, nil)
case c == '<' && i+1 < len(src) && isWordByte(src[i+1]) && strings.IndexByte(src[i:], '>') > 0 && isMarker(src[i:i+strings.IndexByte(src[i:], '>')+1]):
// <cache>, <in_optimizer>, <exists>: what the optimizer wrapped
// an expression in. <cache> says nothing about the expression
// and is dropped; the others are read as calls.
end := i + strings.IndexByte(src[i:], '>') + 1
if src[i:end] != "<cache>" {
emit(tkWord, i, end, nil)
toks[len(toks)-1].text = src[i+1 : end-1]
}
i = end
case c == '(':
emit(tkLParen, i, i+1, nil)
i++
case c == ')':
emit(tkRParen, i, i+1, nil)
i++
case c == ',':
emit(tkComma, i, i+1, nil)
i++
case isWordByte(c):
start := i
for i < len(src) && isWordByte(src[i]) {
i++
}
emit(tkWord, start, i, nil)
toks[len(toks)-1].text = strings.ToLower(src[start:i])
default:
n := 1
for _, op := range operators {
if strings.HasPrefix(src[i:], op) {
n = len(op)
break
}
}
emit(tkOp, i, i+n, nil)
i += n
}
}
t := text{src: src, toks: toks, match: make([]int, len(toks))}
var open []int
for i, tok := range toks {
t.match[i] = -1
switch tok.kind {
case tkLParen:
open = append(open, i)
case tkRParen:
if len(open) > 0 {
j := open[len(open)-1]
open = open[:len(open)-1]
t.match[i] = j
t.match[j] = i
}
}
}
return t
}
// operators is every symbolic operator MySQL prints, longest first.
var operators = []string{
"<=>", "->>", "<>", "!=", ">=", "<=", "->", "<<", ">>", "||", "&&", ":=",
"=", "<", ">", "+", "-", "*", "/", "%", "!", "^", "&", "|", "~", ".",
}
func isMarker(s string) bool {
for _, c := range []byte(s[1 : len(s)-1]) {
if !isWordByte(c) {
return false
}
}
return true
}
func isWordByte(c byte) bool {
return c == '_' || c == '$' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
// readQuoted reads the backticked identifier at i, unescaping a doubled
// backtick, and returns it with the index just past its closing quote.
func readQuoted(s string, i int) (string, int) {
var b strings.Builder
j := i + 1
for j < len(s) {
if s[j] == '`' {
if j+1 < len(s) && s[j+1] == '`' {
b.WriteByte('`')
j += 2
continue
}
return b.String(), j + 1
}
b.WriteByte(s[j])
j++
}
return b.String(), j
}
// skipString returns the index just past the string literal starting at i,
// honouring backslash escapes and doubled quotes.
func skipString(s string, i int) int {
q := s[i]
j := i + 1
for j < len(s) {
switch {
case s[j] == '\\' && j+1 < len(s):
j += 2
case s[j] == q && j+1 < len(s) && s[j+1] == q:
j += 2
case s[j] == q:
return j + 1
default:
j++
}
}
return len(s)
}
// wordOperators are the operators MySQL spells as words. and/or/not/is
// are left out: MySQL parenthesises each predicate, so a parameter that
// is a whole predicate has nothing to be compared with.
var wordOperators = map[string]bool{
"like": true, "in": true, "regexp": true, "rlike": true, "div": true, "mod": true,
"xor": true, "sounds": true, "escape": true, "between": true, "collate": true,
"member": true,
}
// keywords are the words that can precede a parenthesis without naming a
// function, so that "(a, b)" after one is a list rather than a call.
var keywords = map[string]bool{
"in": true, "values": true, "value": true, "and": true, "or": true, "not": true,
"where": true, "on": true, "from": true, "select": true, "having": true,
"set": true, "when": true, "then": true, "else": true, "case": true, "end": true,
"like": true, "between": true, "by": true, "group": true, "order": true,
"limit": true, "offset": true, "using": true, "join": true, "as": true, "is": true,
"xor": true, "div": true, "mod": true, "interval": true, "distinct": true,
"all": true, "any": true, "some": true, "escape": true, "regexp": true,
"rlike": true, "sounds": true, "collate": true, "union": true, "except": true,
"intersect": true, "with": true, "insert": true, "into": true, "update": true,
"delete": true, "replace": true, "key": true, "duplicate": true, "do": true,
"partition": true, "over": true, "window": true, "rows": true, "range": true,
"straight_join": true, "cross": true, "inner": true, "left": true, "right": true,
"natural": true, "lateral": true, "of": true, "for": true, "lock": true,
"share": true, "null": true, "true": true, "false": true, "unknown": true,
"binary": true, "row": true, "asc": true, "desc": true, "sql_calc_found_rows": true,
"high_priority": true, "low_priority": true, "ignore": true, "table": true,
"member": true, "exists": true,
}
// literalWords are the words that are operands on their own.
var literalWords = map[string]bool{
"null": true, "true": true, "false": true, "unknown": true,
"current_timestamp": true, "current_date": true, "current_time": true,
"current_user": true, "localtime": true, "localtimestamp": true, "utc_timestamp": true,
}
func (t text) at(i int) token {
if i < 0 || i >= len(t.toks) {
return token{kind: -1}
}
return t.toks[i]
}
func (t text) isWord(i int, word string) bool {
tok := t.at(i)
return tok.kind == tkWord && tok.text == word
}
// isCall reports whether the word at i names a function, given that a
// parenthesis follows it.
func (t text) isCall(i int) bool {
tok := t.at(i)
return tok.kind == tkWord && !keywords[tok.text] || tok.kind == tkWord && tok.text == "exists"
}
// isOperator reports whether the token at i joins two operands.
func (t text) isOperator(i int) bool {
tok := t.at(i)
switch tok.kind {
case tkOp:
return tok.text != "." && tok.text != "~" && tok.text != "!"
case tkWord:
return wordOperators[tok.text]
}
return false
}
// operandEnd returns the index just past the operand starting at i, or -1
// when no operand starts there: a name, a literal, a call with its
// arguments, or a parenthesised group.
func (t text) operandEnd(i int) int {
tok := t.at(i)
switch tok.kind {
case tkIdent, tkVar, tkNumber, tkString:
return i + 1
case tkLParen:
if t.match[i] < 0 {
return -1
}
return t.match[i] + 1
case tkWord:
if literalWords[tok.text] {
return i + 1
}
if t.at(i+1).kind == tkLParen && t.isCall(i) {
return t.operandEnd(i + 1)
}
if tok.text == "not" || tok.text == "binary" || tok.text == "interval" {
return t.operandEnd(i + 1)
}
case tkOp:
if tok.text == "-" || tok.text == "+" || tok.text == "~" || tok.text == "!" {
return t.operandEnd(i + 1)
}
}
return -1
}
// operandStart returns the index of the operand ending just before j, or
// -1 when none ends there.
func (t text) operandStart(j int) int {
k := j - 1
tok := t.at(k)
switch tok.kind {
case tkIdent, tkVar, tkNumber, tkString:
return k
case tkRParen:
open := t.match[k]
if open < 0 {
return -1
}
if t.at(open-1).kind == tkWord && t.isCall(open-1) {
return open - 1
}
return open
case tkWord:
if literalWords[tok.text] {
return k
}
}
return -1
}
// group returns the index of the parenthesis enclosing token i, or -1.
func (t text) group(i int) int {
depth := 0
for k := i - 1; k >= 0; k-- {
switch t.toks[k].kind {
case tkRParen:
depth++
case tkLParen:
if depth == 0 {
return k
}
depth--
}
}
return -1
}
// list splits the tokens strictly inside the group opened at open into its
// comma-separated members, each a token range.
func (t text) list(open int) [][2]int {
close := t.match[open]
if close < 0 {
return nil
}
var members [][2]int
start := open + 1
for i := open + 1; i < close; i++ {
switch t.toks[i].kind {
case tkLParen:
i = t.match[i]
case tkComma:
members = append(members, [2]int{start, i})
start = i + 1
}
}
if start < close {
members = append(members, [2]int{start, close})
}
return members
}
// item is one entry of a SELECT list: the expression and the name it is
// projected as.
type item struct {
start, end int
alias string
}
// selectItems reads the list of the first SELECT at the top level of the
// text, which is the statement's own for a query block's expanded_query
// and for a statement whose CTEs come first.
func (t text) selectItems() []item {
depth := 0
start := -1
for i, tok := range t.toks {
switch tok.kind {
case tkLParen:
depth++
case tkRParen:
depth--
case tkWord:
if depth == 0 && tok.text == "select" {
start = i + 1
}
}
if start >= 0 {
break
}
}
if start < 0 {
return nil
}
for t.at(start).kind == tkWord && (t.at(start).text == "distinct" || t.at(start).text == "all" || t.at(start).text == "sql_calc_found_rows" || t.at(start).text == "straight_join" || t.at(start).text == "high_priority") {
start++
}
end := len(t.toks)
for i := start; i < len(t.toks); i++ {
switch tok := t.toks[i]; tok.kind {
case tkLParen:
i = t.match[i]
if i < 0 {
return nil
}
case tkWord:
switch tok.text {
case "from", "where", "group", "having", "order", "limit", "union", "into", "window", "for", "lock", "except", "intersect":
end = i
}
}
if end != len(t.toks) {
break
}
}
var items []item
itemStart := start
flush := func(itemEnd int) {
it := item{start: itemStart, end: itemEnd}
if itemEnd-itemStart >= 3 && t.isWord(itemEnd-2, "as") && t.at(itemEnd-1).kind == tkIdent {
it.alias = t.toks[itemEnd-1].parts[0]
it.end = itemEnd - 2
}
items = append(items, it)
}
for i := start; i < end; i++ {
switch t.toks[i].kind {
case tkLParen:
i = t.match[i]
case tkComma:
flush(i)
itemStart = i + 1
}
}
if itemStart < end {
flush(end)
}
return items
}
// tableRef is a table as a FROM clause names it, with the schema when it
// spells one out.
type tableRef struct {
schema, name string
}
// aliases reads the table aliases the text declares: a table name followed
// by a bare identifier, as `users` `u` in a FROM clause. A SELECT list
// alias is always introduced by AS, so the shape means nothing else.
func (t text) aliases(into map[string]tableRef) {
for i := 0; i+1 < len(t.toks); i++ {
tok, next := t.toks[i], t.toks[i+1]
if tok.kind != tkIdent || next.kind != tkIdent || len(next.parts) != 1 || len(tok.parts) > 2 {
continue
}
if t.at(i-1).kind == tkOp && t.at(i-1).text == "." {
continue
}
ref := tableRef{name: tok.parts[len(tok.parts)-1]}
if len(tok.parts) == 2 {
ref.schema = tok.parts[0]
}
into[next.parts[0]] = ref
}
}
// find returns the index of the variable named name, or -1.
func (t text) findVar(name string) int {
for i, tok := range t.toks {
if tok.kind == tkVar && len(tok.parts) == 1 && tok.parts[0] == name {
return i
}
}
return -1
}
// findNumber returns the index of the number printed as lit, or -1.
func (t text) findNumber(lit string) int {
for i, tok := range t.toks {
if tok.kind == tkNumber && tok.text == lit {
return i
}
}
return -1
}
// A partner is what a parameter stands against in a statement.
type partnerKind int
const (
partnerNone partnerKind = iota
partnerOperand // the operand it is compared with, or the argument beside it
partnerProjection // the SELECT list item it is projected as
partnerInsert // the column of an INSERT ... VALUES row it is written to
partnerLimit // the count of a LIMIT or OFFSET
)
type partner struct {
kind partnerKind
start, end int // partnerOperand: the operand's tokens
index int // partnerProjection: the item; partnerInsert: the position in the row
}
// partner finds what the parameter at token index s stands against.
func (t text) partner(s int) partner {
lo, hi := s, s+1
if t.at(lo-1).kind == tkLParen && t.at(hi).kind == tkRParen {
lo, hi = lo-1, hi+1
}
if t.at(hi).kind == tkWord && t.at(hi).text == "as" {
for i, it := range t.selectItems() {
if it.start <= lo && hi <= it.end {
return partner{kind: partnerProjection, index: i}
}
}
}
// The count of a LIMIT or OFFSET, which is a number rather than a
// variable since MySQL allows nothing else there.
if t.at(lo-1).kind == tkWord && (t.at(lo-1).text == "limit" || t.at(lo-1).text == "offset") ||
t.at(lo-1).kind == tkComma && t.at(lo-2).kind == tkNumber && t.isWord(lo-3, "limit") {
return partner{kind: partnerLimit}
}
// Compared: the operand on the other side of the operator.
if t.isOperator(lo - 1) {
q := lo - 2
if t.isWord(q, "not") {
q--
}
if st := t.operandStart(q + 1); st >= 0 {
// The second bound of BETWEEN ... AND: the operand before BETWEEN.
if t.isWord(lo-1, "and") {
if t.isWord(st-1, "between") || t.isWord(st-1, "not") && t.isWord(st-2, "between") {
q = st - 2
if t.isWord(q, "not") {
q--
}
if st2 := t.operandStart(q + 1); st2 >= 0 {
return partner{kind: partnerOperand, start: st2, end: q + 1}
}
}
}
return partner{kind: partnerOperand, start: st, end: q + 1}
}
}
if t.isWord(lo-1, "and") {
if st := t.operandStart(lo - 1); st >= 0 && (t.isWord(st-1, "between") || t.isWord(st-1, "not") && t.isWord(st-2, "between")) {
q := st - 2
if t.isWord(q, "not") {
q--
}
if st2 := t.operandStart(q + 1); st2 >= 0 {
return partner{kind: partnerOperand, start: st2, end: q + 1}
}
}
}
if t.isOperator(hi) {
n := hi + 1
if t.isWord(n, "not") {
n++
}
if end := t.operandEnd(n); end >= 0 {
return partner{kind: partnerOperand, start: n, end: end}
}
}
// A member of a list: the row of an INSERT ... VALUES, the values of an
// IN, or the arguments of a call, where the argument beside it says
// what it is, a column above an expression above a constant.
if t.at(lo-1).kind == tkComma || t.at(lo-1).kind == tkLParen {
open := t.group(lo)
if open < 0 {
return partner{}
}
members := t.list(open)
index := -1
for i, m := range members {
if m[0] <= lo && hi <= m[1] {
index = i
}
}
head := open - 1
for t.at(head).kind == tkComma && t.at(head-1).kind == tkRParen {
head = t.match[head-1] - 1
}
switch {
case t.isWord(head, "values") || t.isWord(head, "value"):
return partner{kind: partnerInsert, index: index}
case t.isWord(head, "in"):
q := head - 1
if t.isWord(q, "not") {
q--
}
if st := t.operandStart(q + 1); st >= 0 {
return partner{kind: partnerOperand, start: st, end: q + 1}
}
case t.at(head).kind == tkWord && t.isCall(head):
best, bestRank := -1, 3
for i, m := range members {
if i == index {
continue
}
if r := t.rank(m[0], m[1]); r < bestRank {
best, bestRank = i, r
}
}
if best >= 0 {
return partner{kind: partnerOperand, start: members[best][0], end: members[best][1]}
}
}
}
return partner{}
}
// rank orders operands by how much they say about a parameter beside them.
func (t text) rank(start, end int) int {
switch tok := t.at(start); {
case end-start == 1 && tok.kind == tkIdent:
return 0
case tok.kind == tkVar:
return 3
case end-start == 1:
return 2
}
return 1
}
// idents lists the qualified identifiers in a token range: the columns an
// expression reads.
func (t text) idents(start, end int) []token {
var out []token
for i := start; i < end; i++ {
if tok := t.toks[i]; tok.kind == tkIdent && len(tok.parts) >= 2 {
out = append(out, tok)
}
}
return out
}
// slice is the source text of a token range.
func (t text) slice(start, end int) string {
if start >= end {
return ""
}
return t.src[t.toks[start].start:t.toks[end-1].end]
}
// quote backticks an identifier.
func quote(name string) string {
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
}

View File

@@ -0,0 +1,87 @@
package mysql
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// trace is what the optimizer trace says about a statement: the text of
// each query block after name resolution and before optimisation, by the
// number MySQL gives the block, and the block each derived table or CTE is
// materialised from.
//
// The trace is the one place MySQL prints a query block as it was resolved
// rather than as it was optimised. The note EXPLAIN leaves is printed
// after optimisation, and a lookup on a unique key against an empty table
// folds to `NULL = (@x)` there, losing the column the parameter was
// compared with; the expanded_query of the block still has it.
type trace struct {
blocks map[int]text
derived map[string]int
}
// parseTrace reads the JSON of the OPTIMIZER_TRACE table's TRACE column.
func parseTrace(blob string) (trace, error) {
tr := trace{blocks: map[int]text{}, derived: map[string]int{}}
if strings.TrimSpace(blob) == "" {
return tr, nil
}
var v any
if err := json.Unmarshal([]byte(blob), &v); err != nil {
return tr, fmt.Errorf("reading the optimizer trace: %w", err)
}
tr.walk(v, 0)
return tr, nil
}
// walk collects the first expanded_query printed for each block — the one
// join_preparation prints, before join_optimization prints its rewrite —
// and the derived tables. Lists are walked in order and objects in key
// order, so the result does not depend on map iteration.
func (tr *trace) walk(v any, sel int) {
switch v := v.(type) {
case map[string]any:
if n, ok := v["select#"].(float64); ok {
sel = int(n)
}
if d, ok := v["derived"].(map[string]any); ok {
if table, ok := d["table"].(string); ok {
if n, ok := d["select#"].(float64); ok {
tr.derived[strings.Trim(table, "` ")] = int(n)
}
}
}
if q, ok := v["expanded_query"].(string); ok {
if _, seen := tr.blocks[sel]; !seen {
tr.blocks[sel] = tokenize(q)
}
}
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
tr.walk(v[k], sel)
}
case []any:
for _, c := range v {
tr.walk(c, sel)
}
}
}
// order lists the block numbers innermost first: a block's text includes
// its subqueries' text, and a subquery is numbered after the block it is
// in, so the block a parameter belongs to is the highest-numbered block
// whose text has it.
func (tr *trace) order() []int {
sels := make([]int, 0, len(tr.blocks))
for sel := range tr.blocks {
sels = append(sels, sel)
}
sort.Sort(sort.Reverse(sort.IntSlice(sels)))
return sels
}