Skip to content

Commit ba513e7

Browse files
kyleconroyclaude
andauthored
Add parse subcommand with AST JSON output (#4240)
* 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]> * refactor: use parseCmd global instead of NewCmdParse function Match the style of other commands in cmd.go by using a global variable and registering flags in init(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 68b2089 commit ba513e7

File tree

3 files changed

+112
-1
lines changed

3 files changed

+112
-1
lines changed

internal/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func init() {
3030
initCmd.Flags().BoolP("v1", "", false, "generate v1 config yaml file")
3131
initCmd.Flags().BoolP("v2", "", true, "generate v2 config yaml file")
3232
initCmd.MarkFlagsMutuallyExclusive("v1", "v2")
33+
parseCmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)")
3334
}
3435

3536
// Do runs the command logic.
@@ -44,6 +45,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
4445
rootCmd.AddCommand(diffCmd)
4546
rootCmd.AddCommand(genCmd)
4647
rootCmd.AddCommand(initCmd)
48+
rootCmd.AddCommand(parseCmd)
4749
rootCmd.AddCommand(versionCmd)
4850
rootCmd.AddCommand(verifyCmd)
4951
rootCmd.AddCommand(pushCmd)

internal/cmd/parse.go

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

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)