# Dolphin Engine (MySQL) - Claude Code Guide The dolphin engine handles MySQL parsing and AST conversion using the Marino parser (a sqlc-maintained fork of the TiDB / pingcap parser). ## Architecture ### Parser Flow ``` SQL String → Marino Parser → Marino AST → sqlc AST → Analysis/Codegen ``` ### Key Files - `convert.go` - Converts Marino AST nodes to sqlc AST nodes - `format.go` - MySQL-specific formatting (identifiers, types, parameters) - `parse.go` - Entry point for parsing MySQL SQL ## Marino Parser The Marino parser (`github.com/sqlc-dev/marino`) is used for MySQL parsing. It is a hard fork of `github.com/pingcap/tidb/pkg/parser` with a flatter package layout (no `pkg/parser/...` prefix) and the former `test_driver` types merged into the `ast` package as `ValueExprBase` / `ParamMarkerExprBase`. ```go import ( pcast "github.com/sqlc-dev/marino/ast" "github.com/sqlc-dev/marino/mysql" "github.com/sqlc-dev/marino/types" ) ``` ### Common Marino Types - `pcast.SelectStmt`, `pcast.InsertStmt`, etc. - Statement types - `pcast.ColumnNameExpr` - Column reference - `pcast.FuncCallExpr` - Function call - `pcast.BinaryOperationExpr` - Binary expression - `pcast.VariableExpr` - MySQL user variable (@var) - `pcast.Join` - JOIN clause with Left, Right, On, Using ## Conversion Pattern Each TiDB node type has a corresponding converter method: ```go func (c *cc) convertSelectStmt(n *pcast.SelectStmt) *ast.SelectStmt { return &ast.SelectStmt{ FromClause: c.convertTableRefsClause(n.From), WhereClause: c.convert(n.Where), // ... } } ``` The main `convert()` method dispatches to specific converters: ```go func (c *cc) convert(node pcast.Node) ast.Node { switch n := node.(type) { case *pcast.SelectStmt: return c.convertSelectStmt(n) case *pcast.InsertStmt: return c.convertInsertStmt(n) // ... } } ``` ## Key Conversions ### Column References ```go func (c *cc) convertColumnNameExpr(n *pcast.ColumnNameExpr) *ast.ColumnRef { var items []ast.Node if schema := n.Name.Schema.String(); schema != "" { items = append(items, NewIdentifier(schema)) } if table := n.Name.Table.String(); table != "" { items = append(items, NewIdentifier(table)) } items = append(items, NewIdentifier(n.Name.Name.String())) return &ast.ColumnRef{Fields: &ast.List{Items: items}} } ``` ### JOINs ```go func (c *cc) convertJoin(n *pcast.Join) *ast.List { if n.Right != nil && n.Left != nil { return &ast.List{ Items: []ast.Node{&ast.JoinExpr{ Jointype: ast.JoinType(n.Tp), Larg: c.convert(n.Left), Rarg: c.convert(n.Right), Quals: c.convert(n.On), UsingClause: convertUsing(n.Using), }}, } } // No join - just return tables // ... } ``` ### MySQL User Variables MySQL user variables (`@var`) are different from sqlc's `@param` syntax: ```go func (c *cc) convertVariableExpr(n *pcast.VariableExpr) ast.Node { // Use VariableExpr to preserve as-is (NOT A_Expr which would be treated as sqlc param) return &ast.VariableExpr{ Name: n.Name, Location: n.OriginTextPosition(), } } ``` ### Type Casts (CAST AS) ```go func (c *cc) convertFuncCastExpr(n *pcast.FuncCastExpr) ast.Node { typeName := types.TypeStr(n.Tp.GetType()) // Handle UNSIGNED/SIGNED specially if typeName == "bigint" { if mysql.HasUnsignedFlag(n.Tp.GetFlag()) { typeName = "bigint unsigned" } else { typeName = "bigint signed" } } return &ast.TypeCast{ Arg: c.convert(n.Expr), TypeName: &ast.TypeName{Name: typeName}, } } ``` ### Column Definitions ```go func convertColumnDef(def *pcast.ColumnDef) *ast.ColumnDef { typeName := &ast.TypeName{Name: types.TypeToStr(def.Tp.GetType(), def.Tp.GetCharset())} // Only add Typmods for types where length is meaningful tp := def.Tp.GetType() flen := def.Tp.GetFlen() switch tp { case mysql.TypeVarchar, mysql.TypeString, mysql.TypeVarString: if flen >= 0 { typeName.Typmods = &ast.List{ Items: []ast.Node{&ast.Integer{Ival: int64(flen)}}, } } // Don't add for DATETIME, TIMESTAMP - internal flen is not user-specified } // ... } ``` ### Multi-Table DELETE MySQL supports `DELETE t1, t2 FROM t1 JOIN t2 ...`: ```go func (c *cc) convertDeleteStmt(n *pcast.DeleteStmt) *ast.DeleteStmt { if n.IsMultiTable && n.Tables != nil { // Convert targets (t1.*, t2.*) targets := &ast.List{} for _, table := range n.Tables.Tables { // Build ColumnRef for each target } stmt.Targets = targets // Preserve JOINs in FromClause stmt.FromClause = c.convertTableRefsClause(n.TableRefs).Items[0] } else { // Single-table DELETE stmt.Relations = c.convertTableRefsClause(n.TableRefs) } } ``` ## MySQL-Specific Formatting ### format.go ```go func (p *Parser) TypeName(ns, name string) string { switch name { case "bigint unsigned": return "UNSIGNED" case "bigint signed": return "SIGNED" } return name } func (p *Parser) Param(n int) string { return "?" // MySQL uses ? for all parameters } ``` ## Common Issues and Solutions ### Issue: Panic in Walk/Apply **Cause**: New AST node type not handled in `astutils/walk.go` or `astutils/rewrite.go` **Solution**: Add case for the node type in both files ### Issue: MySQL @variable being treated as parameter **Cause**: The preprocessor's dialect said `@name` is sqlc syntax **Solution**: MySQL sets `AtSign: false` in `internal/sql/preprocess/dialect.go`, so `@user_id` is left alone before the parser ever sees it ### Issue: Type length appearing incorrectly (e.g., datetime(39)) **Cause**: Using internal `flen` for all types **Solution**: Only populate `Typmods` for types where length is user-specified (varchar, char, etc.) ## Testing ### TestFormat Tests that SQL can be: 1. Parsed 2. Formatted back to SQL 3. Re-parsed 4. Re-formatted to match ### TestReplay Tests the full sqlc pipeline: 1. Parse schema and queries 2. Analyze 3. Generate code 4. Compare with expected output