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
276 lines
7.9 KiB
Go
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
|
|
}
|