diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index bdaca4180a..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,6 +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(parseCmd) 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..274525d334 --- /dev/null +++ b/internal/cmd/parse.go @@ -0,0 +1,102 @@ +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" +) + +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 + +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 + }, +} 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 }