Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
b83465a
feat(sqlite): Add database analyzer using ncruces/go-sqlite3 (#4199)
kyleconroy Nov 29, 2025
7d08ef4
build(deps): bump actions/checkout from 5 to 6 (#4190)
dependabot[bot] Nov 29, 2025
cb887b5
build(deps): bump actions/upload-artifact from 4 to 5 (#4155)
dependabot[bot] Nov 29, 2025
71d4950
build(deps): bump golang from 1.25.3 to 1.25.4 (#4170)
dependabot[bot] Nov 29, 2025
2d98281
build(deps): bump certifi in /docs in the production-dependencies gro…
dependabot[bot] Nov 29, 2025
2e44b62
build(deps): bump google.golang.org/grpc (#4200)
dependabot[bot] Nov 29, 2025
3436b28
docs: add link to community python plugin (#4157)
rayakame Nov 29, 2025
405a905
feat(ast): implement comprehensive SQL AST formatting (#4205)
kyleconroy Nov 30, 2025
a9f7eae
feat(mysql): improve AST formatting and add DELETE JOIN support (#4206)
kyleconroy Nov 30, 2025
166398d
feat(sqlite): add SQLite support to format tests (#4207)
kyleconroy Dec 1, 2025
ebd32cf
refactor(ast): rename Formatter interface to Dialect (#4208)
kyleconroy Dec 1, 2025
21e6557
feat(expander): Add star expander for SELECT * and RETURNING * (Postg…
kyleconroy Dec 1, 2025
6222f15
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Dec 10, 2025
115624b
build(deps): bump urllib3 in /docs in the production-dependencies gro…
dependabot[bot] Dec 10, 2025
f73cede
build(deps): bump golang from 1.25.4 to 1.25.5 (#4214)
dependabot[bot] Dec 10, 2025
f6aee32
fix: replace manual loop with copy() builtin (#4166)
rubensantoniorosa2704 Dec 10, 2025
74ecda5
feat: add SQLCEXPERIMENT environment variable for experimental featur…
kyleconroy Dec 10, 2025
d48076c
build(deps): bump actions/upload-artifact from 5 to 6 (#4233)
dependabot[bot] Dec 18, 2025
31d3bec
build(deps): bump urllib3 in /docs in the production-dependencies gro…
dependabot[bot] Dec 18, 2025
a8b20cc
build(deps): bump google.golang.org/protobuf (#4230)
dependabot[bot] Dec 18, 2025
53b12f9
feat: add native database support for e2e tests without Docker (#4236)
kyleconroy Dec 18, 2025
68b2089
feat(postgresql): add analyzerv2 experiment for database-only analysi…
kyleconroy Dec 22, 2025
ba513e7
Add parse subcommand with AST JSON output (#4240)
kyleconroy Dec 22, 2025
67e865b
docs: Add Claude Code remote environment setup instructions (#4246)
kyleconroy Dec 24, 2025
9ef7ca0
feat: graduate parsecmd experiment (#4253)
kyleconroy Jan 5, 2026
d21b4cc
fix(native): make MySQL connection check immediate on first attempt (…
kyleconroy Jan 5, 2026
4382f5c
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Jan 5, 2026
e476db9
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Jan 6, 2026
2e0435c
Add GitHub Topic to the plugins page (#4258)
rayakame Jan 7, 2026
16c9afd
feat: add ClickHouse support to `sqlc parse` (#4267)
kyleconroy Jan 30, 2026
c048334
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Feb 1, 2026
744558d
build(deps): bump filippo.io/edwards25519 from 1.1.0 to 1.1.1 (#4303)
dependabot[bot] Feb 19, 2026
99be46a
Add sqlc-test-setup command for database test environment setup (#4304)
kyleconroy Feb 22, 2026
c998d72
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Feb 22, 2026
024b843
build(deps): bump golang from 1.25.5 to 1.26.0 (#4294)
dependabot[bot] Feb 22, 2026
0a3d50e
Install PostgreSQL from theseus-rs/postgresql-binaries instead of apt…
kyleconroy Feb 22, 2026
ce83d3f
Upgrade Go version to 1.26.0 (#4312)
kyleconroy Feb 24, 2026
d06009f
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Mar 20, 2026
dd83714
build(deps): bump golang from 1.26.0 to 1.26.1 (#4328)
dependabot[bot] Mar 20, 2026
4bf2159
build(deps): bump urllib3 from 2.6.2 to 2.6.3 in /docs (#4259)
dependabot[bot] Mar 20, 2026
7f6b186
Catch invalid ON CONFLICT DO UPDATE column references (#4366)
nikolayk812 Apr 17, 2026
98bdb52
build(deps): bump github.com/jackc/pgx/v5 from 5.8.0 to 5.9.0 (#4377)
dependabot[bot] Apr 17, 2026
21e4c47
docs: add sqlc-gen-sqlx to community language support (#4371)
jrandolf Apr 17, 2026
ee439b8
build(deps): bump actions/upload-artifact from 6 to 7 (#4320)
dependabot[bot] Apr 17, 2026
47cb4f9
Upgrade Go toolchain to 1.26.2 (#4378)
kyleconroy Apr 17, 2026
93e1a17
Remove github.com/jackc/pgx/v4 dependency (#4379)
kyleconroy Apr 17, 2026
a2f85e2
build(deps): bump requests from 2.32.5 to 2.33.0 in /docs (#4357)
dependabot[bot] Apr 17, 2026
924c12b
build(deps): bump pygments from 2.19.2 to 2.20.0 in /docs (#4363)
dependabot[bot] Apr 17, 2026
5757d94
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Apr 17, 2026
cff03f4
build(deps): bump the production-dependencies group across 1 director…
dependabot[bot] Apr 17, 2026
bba5fc9
Skip CI/RTD builds when the change is irrelevant (#4381)
kyleconroy Apr 17, 2026
3f41c61
Preserve MySQL optimizer hints in generated query text (#4382)
kyleconroy Apr 17, 2026
244a90b
Dedupe sqlc.arg parameters wrapped in a type cast for MySQL (#4384)
kyleconroy Apr 17, 2026
837d7c9
Coerce SQLite JSONB output regardless of type casing (#4385)
kyleconroy Apr 17, 2026
07c3808
Rename :one return variable when it conflicts with a parameter (#4383)
kyleconroy Apr 17, 2026
64d18fc
Map xid8 to pgtype.Uint64 for pgx/v5 (#4387)
kyleconroy Apr 18, 2026
9d9026b
Emit pointers for nullable enum columns when emit_pointers_for_null_t…
kyleconroy Apr 19, 2026
3e9e3d4
Upgrade github.com/pingcap/tidb/pkg/parser (#4389)
kyleconroy Apr 19, 2026
394bdc7
Strip psql meta-commands from schema files (fixes #4065) (#4390)
kyleconroy Apr 19, 2026
3e66a4a
Update CI workflow to remove paths-ignore (#4393)
kyleconroy Apr 20, 2026
420f5e7
Add changelog for 1.31.0 (#4392)
kyleconroy Apr 20, 2026
0f82157
Release 1.31.0 (#4394)
kyleconroy Apr 20, 2026
ca8dbe9
build(deps): bump github.com/jackc/pgx/v5 (#4398)
dependabot[bot] Apr 21, 2026
dadc7ed
Downgrade github.com/ncruces/go-sqlite3 to v0.32.0 (#4400)
kyleconroy Apr 21, 2026
428d4e6
Remove go.mod replace directive that breaks 'go install ...@latest' (…
kyleconroy Apr 21, 2026
b4fe67f
Add changelog for 1.31.1 (#4406)
kyleconroy Apr 22, 2026
a95e91d
Release 1.31.1 (#4407)
kyleconroy Apr 22, 2026
08df6fc
Merge upstream v1.31.1
ethanndickson May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(mysql): improve AST formatting and add DELETE JOIN support (sqlc…
…-dev#4206)

This PR continues the MySQL AST formatting work with several improvements:

**New AST Nodes:**
- `VariableExpr` - MySQL user variables (@var), distinct from sqlc @param
- `IntervalExpr` - MySQL INTERVAL expressions
- `OnDuplicateKeyUpdate` - MySQL ON DUPLICATE KEY UPDATE clause
- `ParenExpr` - Explicit parentheses for expression grouping

**DELETE with JOIN Support:**
- Extended DeleteStmt with Targets and FromClause fields
- Multi-table DELETE now properly formats: DELETE t1.*, t2.* FROM t1 JOIN t2...
- Updated compiler/output_columns.go to handle new structure

**Bug Fixes:**
- MySQL @variable now preserved as-is (not treated as sqlc named parameter)
- Column type lengths only output for types where meaningful (varchar, char)
- Fixed sqlc.arg() handling in ON DUPLICATE KEY UPDATE clause

**Documentation:**
- Added CLAUDE.md files documenting AST, astutils, named, rewrite packages
- Added CLAUDE.md for dolphin engine with conversion patterns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
  • Loading branch information
kyleconroy and claude authored Nov 30, 2025
commit a9f7eaec442ca46125888ed2c94eb5073b763c61
6 changes: 5 additions & 1 deletion internal/codegen/golang/mysql_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.C
}
return "sql.NullInt32"

case "bigint":
case "bigint", "bigint unsigned", "bigint signed":
// "bigint unsigned" and "bigint signed" are MySQL CAST types
// Note: We use int64 for CAST AS UNSIGNED to match original behavior,
// even though uint64 would be more semantically correct.
// The Unsigned flag on columns (from table schema) still uses uint64.
if notNull {
if unsigned {
return "uint64"
Expand Down
9 changes: 8 additions & 1 deletion internal/compiler/output_columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,14 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro
list := &ast.List{}
switch n := node.(type) {
case *ast.DeleteStmt:
list = n.Relations
if n.Relations != nil {
list = n.Relations
} else if n.FromClause != nil {
// Multi-table DELETE: walk FromClause to find tables
var tv tableVisitor
astutils.Walk(&tv, n.FromClause)
list = &tv.list
}
case *ast.InsertStmt:
list = &ast.List{
Items: []ast.Node{n.Relation},
Expand Down
54 changes: 47 additions & 7 deletions internal/endtoend/fmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ package main
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/format"
)

// sqlParser is an interface for SQL parsers
type sqlParser interface {
Parse(r io.Reader) ([]ast.Statement, error)
}

// sqlFormatter is an interface for formatters
type sqlFormatter interface {
format.Formatter
}

func TestFormat(t *testing.T) {
t.Parallel()
for _, tc := range FindTests(t, "testdata", "base") {
Expand All @@ -36,9 +49,38 @@ func TestFormat(t *testing.T) {
return
}

// For now, only test PostgreSQL since that's the only engine with Format support
engine := conf.SQL[0].Engine
if engine != config.EnginePostgreSQL {

// Select the appropriate parser and fingerprint function based on engine
var parse sqlParser
var formatter sqlFormatter
var fingerprint func(string) (string, error)

switch engine {
case config.EnginePostgreSQL:
pgParser := postgresql.NewParser()
parse = pgParser
formatter = pgParser
fingerprint = postgresql.Fingerprint
case config.EngineMySQL:
mysqlParser := dolphin.NewParser()
parse = mysqlParser
formatter = mysqlParser
// For MySQL, we use a "round-trip" fingerprint: parse the SQL, format it,
// and return the formatted string. This tests that our formatting produces
// valid SQL that parses to the same AST structure.
fingerprint = func(sql string) (string, error) {
stmts, err := mysqlParser.Parse(strings.NewReader(sql))
if err != nil {
return "", err
}
if len(stmts) == 0 {
return "", nil
}
return ast.Format(stmts[0].Raw, mysqlParser), nil
}
default:
// Skip unsupported engines
return
}

Expand Down Expand Up @@ -68,8 +110,6 @@ func TestFormat(t *testing.T) {
return
}

parse := postgresql.NewParser()

for _, queryFile := range queryFiles {
if _, err := os.Stat(queryFile); os.IsNotExist(err) {
continue
Expand Down Expand Up @@ -99,7 +139,7 @@ func TestFormat(t *testing.T) {
}
query := strings.TrimSpace(string(contents[start : start+length]))

expected, err := postgresql.Fingerprint(query)
expected, err := fingerprint(query)
if err != nil {
t.Fatal(err)
}
Expand All @@ -109,8 +149,8 @@ func TestFormat(t *testing.T) {
debug.Dump(r, err)
}

out := ast.Format(stmt.Raw, parse)
actual, err := postgresql.Fingerprint(out)
out := ast.Format(stmt.Raw, formatter)
actual, err := fingerprint(out)
if err != nil {
t.Error(err)
}
Expand Down
224 changes: 224 additions & 0 deletions internal/engine/dolphin/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Dolphin Engine (MySQL) - Claude Code Guide

The dolphin engine handles MySQL parsing and AST conversion using the TiDB parser.

## Architecture

### Parser Flow
```
SQL String → TiDB Parser → TiDB AST → sqlc AST → Analysis/Codegen
```

### Key Files
- `convert.go` - Converts TiDB AST nodes to sqlc AST nodes
- `format.go` - MySQL-specific formatting (identifiers, types, parameters)
- `parse.go` - Entry point for parsing MySQL SQL

## TiDB Parser

The TiDB parser (`github.com/pingcap/tidb/pkg/parser`) is used for MySQL parsing:

```go
import (
pcast "github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/parser/types"
)
```

### Common TiDB 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: sqlc.arg() not converted in ON DUPLICATE KEY UPDATE
**Cause**: `InsertStmt` case in `rewrite.go` didn't traverse `OnDuplicateKeyUpdate`
**Solution**: Add `a.apply(n, "OnDuplicateKeyUpdate", nil, n.OnDuplicateKeyUpdate)`

### Issue: MySQL @variable being treated as parameter
**Cause**: Converting `VariableExpr` to `A_Expr` with `@` operator
**Solution**: Use `ast.VariableExpr` instead, which is not detected by `named.IsParamSign()`

### 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
Loading