Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
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 <noreply@anthropic.com>
  • Loading branch information
claude committed Dec 22, 2025
commit 4fe64c446666de856b39e37e14d0402fc3aae698
1 change: 1 addition & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
108 changes: 108 additions & 0 deletions internal/cmd/parse.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 8 additions & 1 deletion internal/opts/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}

Expand All @@ -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
}

Expand Down
Loading