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
226 lines
6.2 KiB
Markdown
226 lines
6.2 KiB
Markdown
# 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
|