From 4fe64c446666de856b39e37e14d0402fc3aae698 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 23:24:33 +0000 Subject: [PATCH 1/2] feat: add parse subcommand behind parsecmd experiment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cmd/cmd.go | 1 + internal/cmd/parse.go | 108 ++++++++++++++++++++++++++++++++++++ internal/opts/experiment.go | 9 ++- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/parse.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index bdaca4180a..d72ff39965 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -44,6 +44,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int rootCmd.AddCommand(diffCmd) rootCmd.AddCommand(genCmd) rootCmd.AddCommand(initCmd) + rootCmd.AddCommand(NewCmdParse()) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(verifyCmd) rootCmd.AddCommand(pushCmd) diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go new file mode 100644 index 0000000000..2b07f8f981 --- /dev/null +++ b/internal/cmd/parse.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/sqlc-dev/sqlc/internal/engine/dolphin" + "github.com/sqlc-dev/sqlc/internal/engine/postgresql" + "github.com/sqlc-dev/sqlc/internal/engine/sqlite" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func NewCmdParse() *cobra.Command { + cmd := &cobra.Command{ + Use: "parse [file]", + Short: "Parse SQL and output the AST as JSON (experimental)", + Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON. + +This command is experimental and requires the 'parsecmd' experiment to be enabled. +Enable it by setting: SQLCEXPERIMENT=parsecmd + +Examples: + # Parse a SQL file with PostgreSQL dialect + SQLCEXPERIMENT=parsecmd sqlc parse --dialect postgresql schema.sql + + # Parse from stdin with MySQL dialect + echo "SELECT * FROM users" | SQLCEXPERIMENT=parsecmd sqlc parse --dialect mysql + + # Parse SQLite SQL + SQLCEXPERIMENT=parsecmd sqlc parse --dialect sqlite queries.sql`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + env := ParseEnv(cmd) + if !env.Experiment.ParseCmd { + return fmt.Errorf("parse command requires the 'parsecmd' experiment to be enabled.\nSet SQLCEXPERIMENT=parsecmd to use this command") + } + + dialect, err := cmd.Flags().GetString("dialect") + if err != nil { + return err + } + if dialect == "" { + return fmt.Errorf("--dialect flag is required (postgresql, mysql, or sqlite)") + } + + // Determine input source + var input io.Reader + if len(args) == 1 { + file, err := os.Open(args[0]) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + input = file + } else { + // Check if stdin has data + stat, err := os.Stdin.Stat() + if err != nil { + return fmt.Errorf("failed to stat stdin: %w", err) + } + if (stat.Mode() & os.ModeCharDevice) != 0 { + return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin") + } + input = cmd.InOrStdin() + } + + // Parse SQL based on dialect + var stmts []ast.Statement + switch dialect { + case "postgresql", "postgres", "pg": + parser := postgresql.NewParser() + stmts, err = parser.Parse(input) + case "mysql": + parser := dolphin.NewParser() + stmts, err = parser.Parse(input) + case "sqlite": + parser := sqlite.NewParser() + stmts, err = parser.Parse(input) + default: + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", dialect) + } + if err != nil { + return fmt.Errorf("parse error: %w", err) + } + + // Output AST as JSON + stdout := cmd.OutOrStdout() + encoder := json.NewEncoder(stdout) + encoder.SetIndent("", " ") + + for _, stmt := range stmts { + if err := encoder.Encode(stmt.Raw); err != nil { + return fmt.Errorf("failed to encode AST: %w", err) + } + } + + return nil + }, + } + + cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)") + + return cmd +} diff --git a/internal/opts/experiment.go b/internal/opts/experiment.go index 00d4b1b6f1..345cba6cc1 100644 --- a/internal/opts/experiment.go +++ b/internal/opts/experiment.go @@ -28,6 +28,8 @@ type Experiment struct { // AnalyzerV2 enables the database-only analyzer mode (analyzer.database: only) // which uses the database for all type resolution instead of parsing schema files. AnalyzerV2 bool + // ParseCmd enables the parse subcommand which outputs AST as JSON. + ParseCmd bool } // ExperimentFromEnv returns an Experiment initialized from the SQLCEXPERIMENT @@ -75,7 +77,7 @@ func ExperimentFromString(val string) Experiment { // known experiment. func isKnownExperiment(name string) bool { switch strings.ToLower(name) { - case "analyzerv2": + case "analyzerv2", "parsecmd": return true default: return false @@ -87,6 +89,8 @@ func setExperiment(e *Experiment, name string, enabled bool) { switch strings.ToLower(name) { case "analyzerv2": e.AnalyzerV2 = enabled + case "parsecmd": + e.ParseCmd = enabled } } @@ -96,6 +100,9 @@ func (e Experiment) Enabled() []string { if e.AnalyzerV2 { enabled = append(enabled, "analyzerv2") } + if e.ParseCmd { + enabled = append(enabled, "parsecmd") + } return enabled } From 1e7caf229f7f2fb79bcf712db8384d0d34b69514 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 23:29:15 +0000 Subject: [PATCH 2/2] refactor: use parseCmd global instead of NewCmdParse function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cmd/cmd.go | 3 +- internal/cmd/parse.go | 138 ++++++++++++++++++++---------------------- 2 files changed, 68 insertions(+), 73 deletions(-) diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index d72ff39965..80a167353e 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -30,6 +30,7 @@ func init() { initCmd.Flags().BoolP("v1", "", false, "generate v1 config yaml file") initCmd.Flags().BoolP("v2", "", true, "generate v2 config yaml file") initCmd.MarkFlagsMutuallyExclusive("v1", "v2") + parseCmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)") } // Do runs the command logic. @@ -44,7 +45,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int rootCmd.AddCommand(diffCmd) rootCmd.AddCommand(genCmd) rootCmd.AddCommand(initCmd) - rootCmd.AddCommand(NewCmdParse()) + rootCmd.AddCommand(parseCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(verifyCmd) rootCmd.AddCommand(pushCmd) diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index 2b07f8f981..274525d334 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -14,11 +14,10 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -func NewCmdParse() *cobra.Command { - cmd := &cobra.Command{ - Use: "parse [file]", - Short: "Parse SQL and output the AST as JSON (experimental)", - Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON. +var parseCmd = &cobra.Command{ + Use: "parse [file]", + Short: "Parse SQL and output the AST as JSON (experimental)", + Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON. This command is experimental and requires the 'parsecmd' experiment to be enabled. Enable it by setting: SQLCEXPERIMENT=parsecmd @@ -32,77 +31,72 @@ Examples: # Parse SQLite SQL SQLCEXPERIMENT=parsecmd sqlc parse --dialect sqlite queries.sql`, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - env := ParseEnv(cmd) - if !env.Experiment.ParseCmd { - return fmt.Errorf("parse command requires the 'parsecmd' experiment to be enabled.\nSet SQLCEXPERIMENT=parsecmd to use this command") - } - - dialect, err := cmd.Flags().GetString("dialect") + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + env := ParseEnv(cmd) + if !env.Experiment.ParseCmd { + return fmt.Errorf("parse command requires the 'parsecmd' experiment to be enabled.\nSet SQLCEXPERIMENT=parsecmd to use this command") + } + + dialect, err := cmd.Flags().GetString("dialect") + if err != nil { + return err + } + if dialect == "" { + return fmt.Errorf("--dialect flag is required (postgresql, mysql, or sqlite)") + } + + // Determine input source + var input io.Reader + if len(args) == 1 { + file, err := os.Open(args[0]) if err != nil { - return err - } - if dialect == "" { - return fmt.Errorf("--dialect flag is required (postgresql, mysql, or sqlite)") - } - - // Determine input source - var input io.Reader - if len(args) == 1 { - file, err := os.Open(args[0]) - if err != nil { - return fmt.Errorf("failed to open file: %w", err) - } - defer file.Close() - input = file - } else { - // Check if stdin has data - stat, err := os.Stdin.Stat() - if err != nil { - return fmt.Errorf("failed to stat stdin: %w", err) - } - if (stat.Mode() & os.ModeCharDevice) != 0 { - return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin") - } - input = cmd.InOrStdin() - } - - // Parse SQL based on dialect - var stmts []ast.Statement - switch dialect { - case "postgresql", "postgres", "pg": - parser := postgresql.NewParser() - stmts, err = parser.Parse(input) - case "mysql": - parser := dolphin.NewParser() - stmts, err = parser.Parse(input) - case "sqlite": - parser := sqlite.NewParser() - stmts, err = parser.Parse(input) - default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", dialect) + return fmt.Errorf("failed to open file: %w", err) } + defer file.Close() + input = file + } else { + // Check if stdin has data + stat, err := os.Stdin.Stat() if err != nil { - return fmt.Errorf("parse error: %w", err) + return fmt.Errorf("failed to stat stdin: %w", err) } - - // Output AST as JSON - stdout := cmd.OutOrStdout() - encoder := json.NewEncoder(stdout) - encoder.SetIndent("", " ") - - for _, stmt := range stmts { - if err := encoder.Encode(stmt.Raw); err != nil { - return fmt.Errorf("failed to encode AST: %w", err) - } + if (stat.Mode() & os.ModeCharDevice) != 0 { + return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin") } + input = cmd.InOrStdin() + } + + // Parse SQL based on dialect + var stmts []ast.Statement + switch dialect { + case "postgresql", "postgres", "pg": + parser := postgresql.NewParser() + stmts, err = parser.Parse(input) + case "mysql": + parser := dolphin.NewParser() + stmts, err = parser.Parse(input) + case "sqlite": + parser := sqlite.NewParser() + stmts, err = parser.Parse(input) + default: + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", dialect) + } + if err != nil { + return fmt.Errorf("parse error: %w", err) + } + + // Output AST as JSON + stdout := cmd.OutOrStdout() + encoder := json.NewEncoder(stdout) + encoder.SetIndent("", " ") + + for _, stmt := range stmts { + if err := encoder.Encode(stmt.Raw); err != nil { + return fmt.Errorf("failed to encode AST: %w", err) + } + } - return nil - }, - } - - cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, or sqlite)") - - return cmd + return nil + }, }