Skip to content

Commit 4fe64c4

Browse files
committed
feat: add parse subcommand behind parsecmd experiment
Add a new `parse` subcommand that parses SQL and outputs the AST as JSON. This is useful for debugging and understanding how sqlc parses SQL statements. The command requires the `parsecmd` experiment to be enabled via SQLCEXPERIMENT=parsecmd. Usage: sqlc parse --dialect postgresql|mysql|sqlite [file] If no file is provided, reads from stdin. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 68b2089 commit 4fe64c4

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

internal/cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
4444
rootCmd.AddCommand(diffCmd)
4545
rootCmd.AddCommand(genCmd)
4646
rootCmd.AddCommand(initCmd)
47+
rootCmd.AddCommand(NewCmdParse())
4748
rootCmd.AddCommand(versionCmd)
4849
rootCmd.AddCommand(verifyCmd)
4950
rootCmd.AddCommand(pushCmd)

internal/cmd/parse.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
12+
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
13+
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
14+
"github.com/sqlc-dev/sqlc/internal/sql/ast"
15+
)
16+
17+
func NewCmdParse() *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "parse [file]",
20+
Short: "Parse SQL and output the AST as JSON (experimental)",
21+
Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON.
22+
23+
This command is experimental and requires the 'parsecmd' experiment to be enabled.
24+
Enable it by setting: SQLCEXPERIMENT=parsecmd
25+
26+
Examples:
27+
# Parse a SQL file with PostgreSQL dialect
28+
SQLCEXPERIMENT=parsecmd sqlc parse --dialect postgresql schema.sql
29+
30+
# Parse from stdin with MySQL dialect
31+
echo "SELECT * FROM users" | SQLCEXPERIMENT=parsecmd sqlc parse --dialect mysql
32+
33+
# Parse SQLite SQL
34+
SQLCEXPERIMENT=parsecmd sqlc parse --dialect sqlite queries.sql`,
35+
Args: cobra.MaximumNArgs(1),
36+
RunE: func(cmd *cobra.Command, args []string) error {
37+
env := ParseEnv(cmd)
38+
if !env.Experiment.ParseCmd {
39+
return fmt.Errorf("parse command requires the 'parsecmd' experiment to be enabled.\nSet SQLCEXPERIMENT=parsecmd to use this command")
40+
}
41+
42+
dialect, err := cmd.Flags().GetString("dialect")
43+
if err != nil {
44+
return err
45+
}
46+
if dialect == "" {
47+
return fmt.Errorf("--dialect flag is required (postgresql, mysql, or sqlite)")
48+
}
49+
50+
// Determine input source
51+
var input io.Reader
52+
if len(args) == 1 {
53+
file, err := os.Open(args[0])
54+
if err != nil {
55+
return fmt.Errorf("failed to open file: %w", err)
56+
}
57+
defer file.Close()
58+
input = file
59+
} else {
60+
// Check if stdin has data
61+
stat, err := os.Stdin.Stat()
62+
if err != nil {
63+
return fmt.Errorf("failed to stat stdin: %w", err)
64+
}
65+
if (stat.Mode() & os.ModeCharDevice) != 0 {
66+
return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin")
67+
}
68+
input = cmd.InOrStdin()
69+
}
70+
71+
// Parse SQL based on dialect
72+
var stmts []ast.Statement
73+
switch dialect {
74+
case "postgresql", "postgres", "pg":
75+
parser := postgresql.NewParser()
76+
stmts, err = parser.Parse(input)
77+
case "mysql":
78+
parser := dolphin.NewParser()
79+
stmts, err = parser.Parse(input)
80+
case "sqlite":
81+
parser := sqlite.NewParser()
82+
stmts, err = parser.Parse(input)
83+
default:
84+
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", dialect)
85+
}
86+
if err != nil {
87+
return fmt.Errorf("parse error: %w", err)
88+
}
89+
90+
// Output AST as JSON
91+
stdout := cmd.OutOrStdout()
92+
encoder := json.NewEncoder(stdout)
93+
encoder.SetIndent("", " ")
94+
95+
for _, stmt := range stmts {
96+
if err := encoder.Encode(stmt.Raw); err != nil {
97+
return fmt.Errorf("failed to encode AST: %w", err)
98+
}
99+
}
100+
101+
return nil
102+
},
103+
}
104+
105+
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)")
106+
107+
return cmd
108+
}

internal/opts/experiment.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type Experiment struct {
2828
// AnalyzerV2 enables the database-only analyzer mode (analyzer.database: only)
2929
// which uses the database for all type resolution instead of parsing schema files.
3030
AnalyzerV2 bool
31+
// ParseCmd enables the parse subcommand which outputs AST as JSON.
32+
ParseCmd bool
3133
}
3234

3335
// ExperimentFromEnv returns an Experiment initialized from the SQLCEXPERIMENT
@@ -75,7 +77,7 @@ func ExperimentFromString(val string) Experiment {
7577
// known experiment.
7678
func isKnownExperiment(name string) bool {
7779
switch strings.ToLower(name) {
78-
case "analyzerv2":
80+
case "analyzerv2", "parsecmd":
7981
return true
8082
default:
8183
return false
@@ -87,6 +89,8 @@ func setExperiment(e *Experiment, name string, enabled bool) {
8789
switch strings.ToLower(name) {
8890
case "analyzerv2":
8991
e.AnalyzerV2 = enabled
92+
case "parsecmd":
93+
e.ParseCmd = enabled
9094
}
9195
}
9296

@@ -96,6 +100,9 @@ func (e Experiment) Enabled() []string {
96100
if e.AnalyzerV2 {
97101
enabled = append(enabled, "analyzerv2")
98102
}
103+
if e.ParseCmd {
104+
enabled = append(enabled, "parsecmd")
105+
}
99106
return enabled
100107
}
101108

0 commit comments

Comments
 (0)