Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
102 changes: 102 additions & 0 deletions internal/cmd/parse.go
Original file line number Diff line number Diff line change
@@ -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
},
}
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