Files
upstream-sqlc/internal/cmd/analyze.go
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

276 lines
7.9 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/sqlc-dev/sqlc/internal/compiler"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/core"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
)
func newAnalyzeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "analyze [query-file]",
Short: "Analyze a query against a schema and output the result columns and parameters",
Long: `Analyze a query file against a schema file and output the inferred result
columns and parameters as JSON.
Unlike "sqlc generate", this command does not require a configuration file and
does not connect to a database. It uses sqlc's native static analysis to infer
types from the provided schema.
Examples:
# Analyze a PostgreSQL query
sqlc analyze --dialect postgresql --schema schema.sql query.sql
# Analyze a MySQL query
sqlc analyze --dialect mysql --schema schema.sql query.sql
# Analyze a SQLite query
sqlc analyze --dialect sqlite --schema schema.sql query.sql
# Analyze a ClickHouse query
sqlc analyze --dialect clickhouse --schema schema.sql query.sql
# Analyze a GoogleSQL (BigQuery, Spanner) query
sqlc analyze --dialect googlesql --schema schema.sql query.sql
# Analyze a SQL Server (T-SQL) query
sqlc analyze --dialect mssql --schema schema.sql query.sql
# Analyze a DuckDB query
sqlc analyze --dialect duckdb --schema schema.sql query.sql
# Analyze a query piped via stdin
echo "-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;" | sqlc analyze --dialect postgresql --schema schema.sql
# Include the statement AST in the output
sqlc analyze --dialect postgresql --schema schema.sql --ast query.sql`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dialect, err := cmd.Flags().GetString("dialect")
if err != nil {
return err
}
if dialect == "" {
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
}
schemaPath, err := cmd.Flags().GetString("schema")
if err != nil {
return err
}
if schemaPath == "" {
return fmt.Errorf("--schema flag is required")
}
includeAST, err := cmd.Flags().GetBool("ast")
if err != nil {
return err
}
// The query comes from a file argument or, when none is given, from
// stdin. The compiler reads queries from files, so stdin is written to
// a temporary file.
var queryPath string
if len(args) == 1 {
queryPath = args[0]
} else {
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 query provided. Specify a query file or pipe SQL via stdin")
}
data, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return fmt.Errorf("failed to read stdin: %w", err)
}
tmp, err := os.CreateTemp("", "sqlc-analyze-*.sql")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmp.Name())
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("failed to write temp file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
queryPath = tmp.Name()
}
var engine config.Engine
switch dialect {
case "postgresql", "postgres", "pg":
engine = config.EnginePostgreSQL
case "mysql":
engine = config.EngineMySQL
case "sqlite":
engine = config.EngineSQLite
case "clickhouse":
engine = config.EngineClickHouse
case "googlesql":
engine = config.EngineGoogleSQL
case "mssql", "sqlserver":
engine = config.EngineMSSQL
case "duckdb":
engine = config.EngineDuckDB
default:
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)", dialect)
}
sql := config.SQL{
Engine: engine,
Schema: config.Paths{schemaPath},
Queries: config.Paths{queryPath},
}
combo := config.Combine(config.Config{}, sql)
parserOpts := opts.Parser{}
ctx := cmd.Context()
c, err := compiler.NewCompiler(sql, combo, parserOpts, compiler.WithCoreAnalysis())
if err != nil {
return fmt.Errorf("error creating compiler: %w", err)
}
defer c.Close(ctx)
if err := c.ParseCatalog(sql.Schema); err != nil {
return fmt.Errorf("error parsing schema: %w", formatParseError(err))
}
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
return fmt.Errorf("error parsing queries: %w", formatParseError(err))
}
result := c.Result()
out := make([]analyzedQuery, 0, len(result.Queries))
for _, q := range result.Queries {
out = append(out, newAnalyzedQuery(q, includeAST))
}
stdout := cmd.OutOrStdout()
encoder := json.NewEncoder(stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(out); err != nil {
return fmt.Errorf("failed to encode analysis: %w", err)
}
return nil
},
}
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
cmd.Flags().StringP("schema", "s", "", "path to the schema file")
cmd.Flags().BoolP("ast", "", false, "include the statement AST in the output")
return cmd
}
// formatParseError unwraps a multierr.Error into a single error containing all
// of the underlying file errors, so the analyze command can report each one with
// its file location.
func formatParseError(err error) error {
parserErr, ok := err.(*multierr.Error)
if !ok {
return err
}
var msgs []string
for _, fileErr := range parserErr.Errs() {
msgs = append(msgs, fmt.Sprintf("%s:%d:%d: %s",
fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err))
}
if len(msgs) == 0 {
return err
}
return fmt.Errorf("%s", strings.Join(msgs, "; "))
}
type analyzedQuery struct {
Name string `json:"name"`
Cmd string `json:"cmd"`
Columns []analyzedColumn `json:"columns"`
Params []analyzedParam `json:"params"`
AST *ast.RawStmt `json:"ast,omitempty"`
}
type analyzedColumn struct {
Name string `json:"name"`
Type *core.TypeExpr `json:"type,omitempty"`
Table string `json:"table,omitempty"`
}
type analyzedParam struct {
Number int `json:"number"`
Column analyzedColumn `json:"column"`
}
func newAnalyzedQuery(q *compiler.Query, includeAST bool) analyzedQuery {
aq := analyzedQuery{
Name: q.Metadata.Name,
Cmd: q.Metadata.Cmd,
Columns: make([]analyzedColumn, 0, len(q.Columns)),
Params: make([]analyzedParam, 0, len(q.Params)),
}
for _, col := range q.Columns {
aq.Columns = append(aq.Columns, newAnalyzedColumn(col))
}
for _, p := range q.Params {
aq.Params = append(aq.Params, analyzedParam{
Number: p.Number,
Column: newAnalyzedColumn(p.Column),
})
}
if includeAST {
aq.AST = q.RawStmt
}
return aq
}
func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
if col == nil {
return analyzedColumn{}
}
ac := analyzedColumn{
Name: col.Name,
Type: newAnalyzedType(col),
}
if col.Table != nil {
ac.Table = col.Table.Name
}
return ac
}
// newAnalyzedType is the column's type as an expression: the one the
// analysis core wrote when it did, otherwise the flat description the
// compiler holds, which is the data type wrapped in one array node per
// dimension with the column's nullability on the outermost node.
func newAnalyzedType(col *compiler.Column) *core.TypeExpr {
if col.TypeExpr != nil {
return col.TypeExpr
}
if col.DataType == "" {
return nil
}
t := core.ParseTypeExpr(col.DataType)
dims := col.ArrayDims
if col.IsArray && dims == 0 {
dims = 1
}
for i := 0; i < dims; i++ {
t = &core.TypeExpr{Name: "array", Args: []core.TypeArg{{Type: t}}}
}
t.Nullable = !col.NotNull
return t
}