|
| 1 | +# Dolphin Engine (MySQL) - Claude Code Guide |
| 2 | + |
| 3 | +The dolphin engine handles MySQL parsing and AST conversion using the TiDB parser. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +### Parser Flow |
| 8 | +``` |
| 9 | +SQL String → TiDB Parser → TiDB AST → sqlc AST → Analysis/Codegen |
| 10 | +``` |
| 11 | + |
| 12 | +### Key Files |
| 13 | +- `convert.go` - Converts TiDB AST nodes to sqlc AST nodes |
| 14 | +- `format.go` - MySQL-specific formatting (identifiers, types, parameters) |
| 15 | +- `parse.go` - Entry point for parsing MySQL SQL |
| 16 | + |
| 17 | +## TiDB Parser |
| 18 | + |
| 19 | +The TiDB parser (`github.com/pingcap/tidb/pkg/parser`) is used for MySQL parsing: |
| 20 | + |
| 21 | +```go |
| 22 | +import ( |
| 23 | + pcast "github.com/pingcap/tidb/pkg/parser/ast" |
| 24 | + "github.com/pingcap/tidb/pkg/parser/mysql" |
| 25 | + "github.com/pingcap/tidb/pkg/parser/types" |
| 26 | +) |
| 27 | +``` |
| 28 | + |
| 29 | +### Common TiDB Types |
| 30 | +- `pcast.SelectStmt`, `pcast.InsertStmt`, etc. - Statement types |
| 31 | +- `pcast.ColumnNameExpr` - Column reference |
| 32 | +- `pcast.FuncCallExpr` - Function call |
| 33 | +- `pcast.BinaryOperationExpr` - Binary expression |
| 34 | +- `pcast.VariableExpr` - MySQL user variable (@var) |
| 35 | +- `pcast.Join` - JOIN clause with Left, Right, On, Using |
| 36 | + |
| 37 | +## Conversion Pattern |
| 38 | + |
| 39 | +Each TiDB node type has a corresponding converter method: |
| 40 | + |
| 41 | +```go |
| 42 | +func (c *cc) convertSelectStmt(n *pcast.SelectStmt) *ast.SelectStmt { |
| 43 | + return &ast.SelectStmt{ |
| 44 | + FromClause: c.convertTableRefsClause(n.From), |
| 45 | + WhereClause: c.convert(n.Where), |
| 46 | + // ... |
| 47 | + } |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +The main `convert()` method dispatches to specific converters: |
| 52 | +```go |
| 53 | +func (c *cc) convert(node pcast.Node) ast.Node { |
| 54 | + switch n := node.(type) { |
| 55 | + case *pcast.SelectStmt: |
| 56 | + return c.convertSelectStmt(n) |
| 57 | + case *pcast.InsertStmt: |
| 58 | + return c.convertInsertStmt(n) |
| 59 | + // ... |
| 60 | + } |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +## Key Conversions |
| 65 | + |
| 66 | +### Column References |
| 67 | +```go |
| 68 | +func (c *cc) convertColumnNameExpr(n *pcast.ColumnNameExpr) *ast.ColumnRef { |
| 69 | + var items []ast.Node |
| 70 | + if schema := n.Name.Schema.String(); schema != "" { |
| 71 | + items = append(items, NewIdentifier(schema)) |
| 72 | + } |
| 73 | + if table := n.Name.Table.String(); table != "" { |
| 74 | + items = append(items, NewIdentifier(table)) |
| 75 | + } |
| 76 | + items = append(items, NewIdentifier(n.Name.Name.String())) |
| 77 | + return &ast.ColumnRef{Fields: &ast.List{Items: items}} |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +### JOINs |
| 82 | +```go |
| 83 | +func (c *cc) convertJoin(n *pcast.Join) *ast.List { |
| 84 | + if n.Right != nil && n.Left != nil { |
| 85 | + return &ast.List{ |
| 86 | + Items: []ast.Node{&ast.JoinExpr{ |
| 87 | + Jointype: ast.JoinType(n.Tp), |
| 88 | + Larg: c.convert(n.Left), |
| 89 | + Rarg: c.convert(n.Right), |
| 90 | + Quals: c.convert(n.On), |
| 91 | + UsingClause: convertUsing(n.Using), |
| 92 | + }}, |
| 93 | + } |
| 94 | + } |
| 95 | + // No join - just return tables |
| 96 | + // ... |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### MySQL User Variables |
| 101 | +MySQL user variables (`@var`) are different from sqlc's `@param` syntax: |
| 102 | +```go |
| 103 | +func (c *cc) convertVariableExpr(n *pcast.VariableExpr) ast.Node { |
| 104 | + // Use VariableExpr to preserve as-is (NOT A_Expr which would be treated as sqlc param) |
| 105 | + return &ast.VariableExpr{ |
| 106 | + Name: n.Name, |
| 107 | + Location: n.OriginTextPosition(), |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### Type Casts (CAST AS) |
| 113 | +```go |
| 114 | +func (c *cc) convertFuncCastExpr(n *pcast.FuncCastExpr) ast.Node { |
| 115 | + typeName := types.TypeStr(n.Tp.GetType()) |
| 116 | + // Handle UNSIGNED/SIGNED specially |
| 117 | + if typeName == "bigint" { |
| 118 | + if mysql.HasUnsignedFlag(n.Tp.GetFlag()) { |
| 119 | + typeName = "bigint unsigned" |
| 120 | + } else { |
| 121 | + typeName = "bigint signed" |
| 122 | + } |
| 123 | + } |
| 124 | + return &ast.TypeCast{ |
| 125 | + Arg: c.convert(n.Expr), |
| 126 | + TypeName: &ast.TypeName{Name: typeName}, |
| 127 | + } |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +### Column Definitions |
| 132 | +```go |
| 133 | +func convertColumnDef(def *pcast.ColumnDef) *ast.ColumnDef { |
| 134 | + typeName := &ast.TypeName{Name: types.TypeToStr(def.Tp.GetType(), def.Tp.GetCharset())} |
| 135 | + |
| 136 | + // Only add Typmods for types where length is meaningful |
| 137 | + tp := def.Tp.GetType() |
| 138 | + flen := def.Tp.GetFlen() |
| 139 | + switch tp { |
| 140 | + case mysql.TypeVarchar, mysql.TypeString, mysql.TypeVarString: |
| 141 | + if flen >= 0 { |
| 142 | + typeName.Typmods = &ast.List{ |
| 143 | + Items: []ast.Node{&ast.Integer{Ival: int64(flen)}}, |
| 144 | + } |
| 145 | + } |
| 146 | + // Don't add for DATETIME, TIMESTAMP - internal flen is not user-specified |
| 147 | + } |
| 148 | + // ... |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +### Multi-Table DELETE |
| 153 | +MySQL supports `DELETE t1, t2 FROM t1 JOIN t2 ...`: |
| 154 | +```go |
| 155 | +func (c *cc) convertDeleteStmt(n *pcast.DeleteStmt) *ast.DeleteStmt { |
| 156 | + if n.IsMultiTable && n.Tables != nil { |
| 157 | + // Convert targets (t1.*, t2.*) |
| 158 | + targets := &ast.List{} |
| 159 | + for _, table := range n.Tables.Tables { |
| 160 | + // Build ColumnRef for each target |
| 161 | + } |
| 162 | + stmt.Targets = targets |
| 163 | + |
| 164 | + // Preserve JOINs in FromClause |
| 165 | + stmt.FromClause = c.convertTableRefsClause(n.TableRefs).Items[0] |
| 166 | + } else { |
| 167 | + // Single-table DELETE |
| 168 | + stmt.Relations = c.convertTableRefsClause(n.TableRefs) |
| 169 | + } |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## MySQL-Specific Formatting |
| 174 | + |
| 175 | +### format.go |
| 176 | +```go |
| 177 | +func (p *Parser) TypeName(ns, name string) string { |
| 178 | + switch name { |
| 179 | + case "bigint unsigned": |
| 180 | + return "UNSIGNED" |
| 181 | + case "bigint signed": |
| 182 | + return "SIGNED" |
| 183 | + } |
| 184 | + return name |
| 185 | +} |
| 186 | + |
| 187 | +func (p *Parser) Param(n int) string { |
| 188 | + return "?" // MySQL uses ? for all parameters |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +## Common Issues and Solutions |
| 193 | + |
| 194 | +### Issue: Panic in Walk/Apply |
| 195 | +**Cause**: New AST node type not handled in `astutils/walk.go` or `astutils/rewrite.go` |
| 196 | +**Solution**: Add case for the node type in both files |
| 197 | + |
| 198 | +### Issue: sqlc.arg() not converted in ON DUPLICATE KEY UPDATE |
| 199 | +**Cause**: `InsertStmt` case in `rewrite.go` didn't traverse `OnDuplicateKeyUpdate` |
| 200 | +**Solution**: Add `a.apply(n, "OnDuplicateKeyUpdate", nil, n.OnDuplicateKeyUpdate)` |
| 201 | + |
| 202 | +### Issue: MySQL @variable being treated as parameter |
| 203 | +**Cause**: Converting `VariableExpr` to `A_Expr` with `@` operator |
| 204 | +**Solution**: Use `ast.VariableExpr` instead, which is not detected by `named.IsParamSign()` |
| 205 | + |
| 206 | +### Issue: Type length appearing incorrectly (e.g., datetime(39)) |
| 207 | +**Cause**: Using internal `flen` for all types |
| 208 | +**Solution**: Only populate `Typmods` for types where length is user-specified (varchar, char, etc.) |
| 209 | + |
| 210 | +## Testing |
| 211 | + |
| 212 | +### TestFormat |
| 213 | +Tests that SQL can be: |
| 214 | +1. Parsed |
| 215 | +2. Formatted back to SQL |
| 216 | +3. Re-parsed |
| 217 | +4. Re-formatted to match |
| 218 | + |
| 219 | +### TestReplay |
| 220 | +Tests the full sqlc pipeline: |
| 221 | +1. Parse schema and queries |
| 222 | +2. Analyze |
| 223 | +3. Generate code |
| 224 | +4. Compare with expected output |
0 commit comments