diff --git a/internal/cmd/analyze.go b/internal/cmd/analyze.go new file mode 100644 index 0000000000..51de2605b6 --- /dev/null +++ b/internal/cmd/analyze.go @@ -0,0 +1,235 @@ +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/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 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, or sqlite)") + } + + 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 + default: + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, or sqlite)", 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) + 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, or sqlite)") + 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"` + DataType string `json:"data_type"` + NotNull bool `json:"not_null"` + IsArray bool `json:"is_array"` + 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, + DataType: col.DataType, + NotNull: col.NotNull, + IsArray: col.IsArray, + } + if col.Table != nil { + ac.Table = col.Table.Name + } + return ac +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 4079b3c1d3..d1e83b2a12 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -32,7 +32,6 @@ 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. @@ -45,7 +44,8 @@ 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(newParseCmd()) + rootCmd.AddCommand(newAnalyzeCmd()) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(verifyCmd) rootCmd.AddCommand(pushCmd) diff --git a/internal/cmd/parse.go b/internal/cmd/parse.go index aca01511f1..a68ad1bee8 100644 --- a/internal/cmd/parse.go +++ b/internal/cmd/parse.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "github.com/spf13/cobra" @@ -12,13 +13,36 @@ import ( "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/metadata" + "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -var parseCmd = &cobra.Command{ - Use: "parse [file]", - Short: "Parse SQL and output the AST as JSON", - Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON. +// dialectParser is the subset of the engine parsers that the parse command +// needs: parsing SQL into statements and reporting the dialect's comment syntax +// (used to extract the sqlc query name and command). +type dialectParser interface { + Parse(io.Reader) ([]ast.Statement, error) + CommentSyntax() source.CommentSyntax +} + +// parsedStatement is the JSON representation of a single parsed statement. The +// name and cmd are extracted from the sqlc query annotation (e.g. +// "-- name: GetAuthor :one") and are omitted when the statement has none. +type parsedStatement struct { + Name string `json:"name,omitempty"` + Cmd string `json:"cmd,omitempty"` + AST *ast.RawStmt `json:"ast"` +} + +func newParseCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "parse [file]", + Short: "Parse SQL and output the AST as JSON", + Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON. + +Each statement is reported with its sqlc query name and command (when the +statement carries a "-- name:" annotation) alongside the AST. Examples: # Parse a SQL file with PostgreSQL dialect @@ -32,70 +56,93 @@ Examples: # Parse ClickHouse SQL sqlc parse --dialect clickhouse queries.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, or clickhouse)") - } - - // Determine input source - var input io.Reader - if len(args) == 1 { - file, err := os.Open(args[0]) + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + dialect, err := cmd.Flags().GetString("dialect") if err != nil { - return fmt.Errorf("failed to open file: %w", err) + return err } - defer file.Close() - input = file - } else { - // Check if stdin has data - stat, err := os.Stdin.Stat() + if dialect == "" { + return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, or clickhouse)") + } + + // 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() + } + + // Select the parser for the requested dialect + var parser dialectParser + switch dialect { + case "postgresql", "postgres", "pg": + parser = postgresql.NewParser() + case "mysql": + parser = dolphin.NewParser() + case "sqlite": + parser = sqlite.NewParser() + case "clickhouse": + parser = clickhouse.NewParser() + default: + return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", dialect) + } + + // Read the full source so each statement's name and command can be + // extracted from its annotation comment. + src, err := io.ReadAll(input) + if err != nil { + return fmt.Errorf("failed to read input: %w", err) + } + + stmts, err := parser.Parse(strings.NewReader(string(src))) if err != nil { - return fmt.Errorf("failed to stat stdin: %w", err) + return fmt.Errorf("parse error: %w", err) } - if (stat.Mode() & os.ModeCharDevice) != 0 { - return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin") + + commentSyntax := metadata.CommentSyntax(parser.CommentSyntax()) + + // Output the AST as a single JSON document + out := make([]parsedStatement, 0, len(stmts)) + for _, stmt := range stmts { + ps := parsedStatement{AST: stmt.Raw} + rawSQL, err := source.Pluck(string(src), stmt.Raw.StmtLocation, stmt.Raw.StmtLen) + if err != nil { + return fmt.Errorf("failed to read statement source: %w", err) + } + name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, commentSyntax) + if err != nil { + return fmt.Errorf("failed to parse query annotation: %w", err) + } + ps.Name = name + ps.Cmd = cmd + out = append(out, ps) } - 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) - case "clickhouse": - parser := clickhouse.NewParser() - stmts, err = parser.Parse(input) - default: - return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, or clickhouse)", 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 { + + stdout := cmd.OutOrStdout() + encoder := json.NewEncoder(stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(out); err != nil { return fmt.Errorf("failed to encode AST: %w", err) } - } - return nil - }, + return nil + }, + } + cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, or clickhouse)") + return cmd } diff --git a/internal/endtoend/case_test.go b/internal/endtoend/case_test.go index 4389a4da28..183b965a2a 100644 --- a/internal/endtoend/case_test.go +++ b/internal/endtoend/case_test.go @@ -15,6 +15,7 @@ type Testcase struct { Path string ConfigName string Stderr []byte + Stdout []byte Exec *Exec } @@ -24,6 +25,7 @@ type ExecMeta struct { type Exec struct { Command string `json:"command"` + Args []string `json:"args"` Contexts []string `json:"contexts"` Process string `json:"process"` OS []string `json:"os"` @@ -50,6 +52,29 @@ func parseStderr(t *testing.T, dir, testctx string) []byte { return nil } +func parseStdout(t *testing.T, dir string) []byte { + t.Helper() + path := filepath.Join(dir, "stdout.txt") + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil + } + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return blob +} + +// hasSQLCConfig reports whether dir contains an sqlc configuration file. +func hasSQLCConfig(dir string) bool { + for _, name := range []string{"sqlc.json", "sqlc.yaml", "sqlc.yml"} { + if _, err := os.Stat(filepath.Join(dir, name)); err == nil { + return true + } + } + return false +} + func parseExec(t *testing.T, dir string) *Exec { t.Helper() path := filepath.Join(dir, "exec.json") @@ -76,17 +101,34 @@ func FindTests(t *testing.T, root, testctx string) []*Testcase { if err != nil { return err } - if info.Name() == "sqlc.json" || info.Name() == "sqlc.yaml" || info.Name() == "sqlc.yml" { + name := info.Name() + if name == "sqlc.json" || name == "sqlc.yaml" || name == "sqlc.yml" { dir := filepath.Dir(path) tcs = append(tcs, &Testcase{ Path: dir, Name: strings.TrimPrefix(dir, root+string(filepath.Separator)), - ConfigName: info.Name(), + ConfigName: name, Stderr: parseStderr(t, dir, testctx), + Stdout: parseStdout(t, dir), Exec: parseExec(t, dir), }) return filepath.SkipDir } + // Config-less command tests (e.g. parse, analyze) are discovered by + // their exec.json when no sqlc config is present in the directory. + if name == "exec.json" { + dir := filepath.Dir(path) + if !hasSQLCConfig(dir) { + tcs = append(tcs, &Testcase{ + Path: dir, + Name: strings.TrimPrefix(dir, root+string(filepath.Separator)), + Stderr: parseStderr(t, dir, testctx), + Stdout: parseStdout(t, dir), + Exec: parseExec(t, dir), + }) + return filepath.SkipDir + } + } return nil }) if err != nil { diff --git a/internal/endtoend/ddl_test.go b/internal/endtoend/ddl_test.go index bed9333743..689b48df77 100644 --- a/internal/endtoend/ddl_test.go +++ b/internal/endtoend/ddl_test.go @@ -20,6 +20,11 @@ func TestValidSchema(t *testing.T) { } } + // Config-less command tests (parse, analyze) have no schema to validate. + if replay.ConfigName == "" { + continue + } + file := filepath.Join(replay.Path, replay.ConfigName) rd, err := os.Open(file) if err != nil { diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index f8bb5a6e0f..9eeb70d8bc 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "fmt" "os" osexec "os/exec" "path/filepath" @@ -298,6 +299,28 @@ func TestReplay(t *testing.T) { } case "vet": err = cmd.Vet(ctx, path, "", &opts) + case "parse", "analyze": + // These commands are config-less and flag-driven. Run them + // through the real CLI entry point from inside the test + // directory so file arguments resolve and the output stays + // independent of the absolute path. + var stdout bytes.Buffer + wd, werr := os.Getwd() + if werr != nil { + t.Fatal(werr) + } + if cerr := os.Chdir(path); cerr != nil { + t.Fatal(cerr) + } + code := cmd.Do(append([]string{args.Command}, args.Args...), nil, &stdout, &stderr) + if cerr := os.Chdir(wd); cerr != nil { + t.Fatal(cerr) + } + if code != 0 { + err = fmt.Errorf("%s exited with code %d", args.Command, code) + } else if diff := cmp.Diff(strings.TrimSpace(string(tc.Stdout)), strings.TrimSpace(stdout.String()), lineEndings()); diff != "" { + t.Errorf("stdout differed (-want +got):\n%s", diff) + } default: t.Fatalf("unknown command") } diff --git a/internal/endtoend/fmt_test.go b/internal/endtoend/fmt_test.go index eac3fa0390..f1be75bf4d 100644 --- a/internal/endtoend/fmt_test.go +++ b/internal/endtoend/fmt_test.go @@ -32,6 +32,10 @@ func TestFormat(t *testing.T) { t.Parallel() for _, tc := range FindTests(t, "testdata", "base") { tc := tc + // Config-less command tests (parse, analyze) have no config to format. + if tc.ConfigName == "" { + continue + } t.Run(tc.Name, func(t *testing.T) { // Parse the config file to determine the engine configPath := filepath.Join(tc.Path, tc.ConfigName) diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/exec.json b/internal/endtoend/testdata/analyze_ast/postgresql/exec.json new file mode 100644 index 0000000000..7d04ef8cab --- /dev/null +++ b/internal/endtoend/testdata/analyze_ast/postgresql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "postgresql", "--schema", "schema.sql", "--ast", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/query.sql b/internal/endtoend/testdata/analyze_ast/postgresql/query.sql new file mode 100644 index 0000000000..17af794d2a --- /dev/null +++ b/internal/endtoend/testdata/analyze_ast/postgresql/query.sql @@ -0,0 +1,2 @@ +-- name: GetAuthorName :one +SELECT name FROM authors WHERE id = $1; diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/schema.sql b/internal/endtoend/testdata/analyze_ast/postgresql/schema.sql new file mode 100644 index 0000000000..69b607d902 --- /dev/null +++ b/internal/endtoend/testdata/analyze_ast/postgresql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt new file mode 100644 index 0000000000..b74264f687 --- /dev/null +++ b/internal/endtoend/testdata/analyze_ast/postgresql/stdout.txt @@ -0,0 +1,122 @@ +[ + { + "name": "GetAuthorName", + "cmd": ":one", + "columns": [ + { + "name": "name", + "data_type": "text", + "not_null": true, + "is_array": false, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "bigserial", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ], + "ast": { + "Stmt": { + "DistinctClause": { + "Items": null + }, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": { + "Items": null + }, + "Val": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "name" + } + ] + }, + "Location": 35 + }, + "Location": 35 + } + ] + }, + "FromClause": { + "Items": [ + { + "Catalogname": null, + "Schemaname": null, + "Relname": "authors", + "Inh": true, + "Relpersistence": 112, + "Alias": null, + "Location": 45 + } + ] + }, + "WhereClause": { + "Kind": 1, + "Name": { + "Items": [ + { + "Str": "=" + } + ] + }, + "Lexpr": { + "Name": "", + "Fields": { + "Items": [ + { + "Str": "id" + } + ] + }, + "Location": 59 + }, + "Rexpr": { + "Number": 1, + "Location": 64, + "Dollar": true + }, + "Location": 62 + }, + "GroupClause": { + "Items": null + }, + "HavingClause": {}, + "WindowClause": { + "Items": null + }, + "ValuesLists": { + "Items": null + }, + "SortClause": { + "Items": null + }, + "LimitOffset": {}, + "LimitCount": {}, + "LockingClause": { + "Items": null + }, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 0, + "StmtLen": 66 + } + } +] diff --git a/internal/endtoend/testdata/analyze_basic/mysql/exec.json b/internal/endtoend/testdata/analyze_basic/mysql/exec.json new file mode 100644 index 0000000000..a5b24d3361 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mysql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "mysql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_basic/mysql/query.sql b/internal/endtoend/testdata/analyze_basic/mysql/query.sql new file mode 100644 index 0000000000..137c1d1a42 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mysql/query.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id, name FROM users WHERE id = ?; diff --git a/internal/endtoend/testdata/analyze_basic/mysql/schema.sql b/internal/endtoend/testdata/analyze_basic/mysql/schema.sql new file mode 100644 index 0000000000..52f994807a --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mysql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id BIGINT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + bio TEXT +); diff --git a/internal/endtoend/testdata/analyze_basic/mysql/stdout.txt b/internal/endtoend/testdata/analyze_basic/mysql/stdout.txt new file mode 100644 index 0000000000..e599e249aa --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/mysql/stdout.txt @@ -0,0 +1,34 @@ +[ + { + "name": "GetUser", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "varchar", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "bigint", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_basic/postgresql/exec.json b/internal/endtoend/testdata/analyze_basic/postgresql/exec.json new file mode 100644 index 0000000000..b102755fb6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/postgresql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "postgresql", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_basic/postgresql/query.sql b/internal/endtoend/testdata/analyze_basic/postgresql/query.sql new file mode 100644 index 0000000000..55ef1faf82 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/postgresql/query.sql @@ -0,0 +1,2 @@ +-- name: GetAuthor :one +SELECT * FROM authors WHERE id = $1; diff --git a/internal/endtoend/testdata/analyze_basic/postgresql/schema.sql b/internal/endtoend/testdata/analyze_basic/postgresql/schema.sql new file mode 100644 index 0000000000..69b607d902 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/postgresql/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/analyze_basic/postgresql/stdout.txt b/internal/endtoend/testdata/analyze_basic/postgresql/stdout.txt new file mode 100644 index 0000000000..b93421c32a --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/postgresql/stdout.txt @@ -0,0 +1,41 @@ +[ + { + "name": "GetAuthor", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "bigserial", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "name", + "data_type": "text", + "not_null": true, + "is_array": false, + "table": "authors" + }, + { + "name": "bio", + "data_type": "text", + "not_null": false, + "is_array": false, + "table": "authors" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "bigserial", + "not_null": true, + "is_array": false, + "table": "authors" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/analyze_basic/sqlite/exec.json b/internal/endtoend/testdata/analyze_basic/sqlite/exec.json new file mode 100644 index 0000000000..aa77909cb2 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/sqlite/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "sqlite", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_basic/sqlite/query.sql b/internal/endtoend/testdata/analyze_basic/sqlite/query.sql new file mode 100644 index 0000000000..137c1d1a42 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/sqlite/query.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id, name FROM users WHERE id = ?; diff --git a/internal/endtoend/testdata/analyze_basic/sqlite/schema.sql b/internal/endtoend/testdata/analyze_basic/sqlite/schema.sql new file mode 100644 index 0000000000..884e5c9a77 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/sqlite/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + age INTEGER +); diff --git a/internal/endtoend/testdata/analyze_basic/sqlite/stdout.txt b/internal/endtoend/testdata/analyze_basic/sqlite/stdout.txt new file mode 100644 index 0000000000..9a80444890 --- /dev/null +++ b/internal/endtoend/testdata/analyze_basic/sqlite/stdout.txt @@ -0,0 +1,34 @@ +[ + { + "name": "GetUser", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "INTEGER", + "not_null": true, + "is_array": false, + "table": "users" + }, + { + "name": "name", + "data_type": "TEXT", + "not_null": true, + "is_array": false, + "table": "users" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "INTEGER", + "not_null": true, + "is_array": false, + "table": "users" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/exec.json b/internal/endtoend/testdata/parse_basic/clickhouse/exec.json new file mode 100644 index 0000000000..9481db4c86 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/clickhouse/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "clickhouse", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/query.sql b/internal/endtoend/testdata/parse_basic/clickhouse/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/clickhouse/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt new file mode 100644 index 0000000000..e2c49df3fa --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt @@ -0,0 +1,42 @@ +[ + { + "ast": { + "Stmt": { + "DistinctClause": null, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": null, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 31 + }, + "Location": 31 + } + ] + }, + "FromClause": null, + "WhereClause": null, + "GroupClause": null, + "HavingClause": null, + "WindowClause": null, + "ValuesLists": null, + "SortClause": null, + "LimitOffset": null, + "LimitCount": null, + "LockingClause": null, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 24, + "StmtLen": 0 + } + } +] diff --git a/internal/endtoend/testdata/parse_basic/mysql/exec.json b/internal/endtoend/testdata/parse_basic/mysql/exec.json new file mode 100644 index 0000000000..b3326c09a0 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mysql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "mysql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/mysql/query.sql b/internal/endtoend/testdata/parse_basic/mysql/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mysql/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/mysql/stdout.txt b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt new file mode 100644 index 0000000000..e9ed28784f --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/mysql/stdout.txt @@ -0,0 +1,50 @@ +[ + { + "name": "GetValue", + "cmd": ":one", + "ast": { + "Stmt": { + "DistinctClause": null, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": null, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 30 + }, + "Location": 30 + } + ] + }, + "FromClause": { + "Items": null + }, + "WhereClause": null, + "GroupClause": { + "Items": null + }, + "HavingClause": null, + "WindowClause": { + "Items": [] + }, + "ValuesLists": null, + "SortClause": null, + "LimitOffset": null, + "LimitCount": null, + "LockingClause": null, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 0, + "StmtLen": 31 + } + } +] diff --git a/internal/endtoend/testdata/parse_basic/postgresql/exec.json b/internal/endtoend/testdata/parse_basic/postgresql/exec.json new file mode 100644 index 0000000000..0a75ff458d --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/postgresql/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "postgresql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/postgresql/query.sql b/internal/endtoend/testdata/parse_basic/postgresql/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/postgresql/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt new file mode 100644 index 0000000000..fe35a664c7 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/postgresql/stdout.txt @@ -0,0 +1,60 @@ +[ + { + "name": "GetValue", + "cmd": ":one", + "ast": { + "Stmt": { + "DistinctClause": { + "Items": null + }, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": { + "Items": null + }, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 30 + }, + "Location": 30 + } + ] + }, + "FromClause": { + "Items": null + }, + "WhereClause": {}, + "GroupClause": { + "Items": null + }, + "HavingClause": {}, + "WindowClause": { + "Items": null + }, + "ValuesLists": { + "Items": null + }, + "SortClause": { + "Items": null + }, + "LimitOffset": {}, + "LimitCount": {}, + "LockingClause": { + "Items": null + }, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 0, + "StmtLen": 31 + } + } +] diff --git a/internal/endtoend/testdata/parse_basic/sqlite/exec.json b/internal/endtoend/testdata/parse_basic/sqlite/exec.json new file mode 100644 index 0000000000..13abc589ed --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/sqlite/exec.json @@ -0,0 +1,5 @@ +{ + "command": "parse", + "args": ["--dialect", "sqlite", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/parse_basic/sqlite/query.sql b/internal/endtoend/testdata/parse_basic/sqlite/query.sql new file mode 100644 index 0000000000..11dff59f08 --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/sqlite/query.sql @@ -0,0 +1,2 @@ +-- name: GetValue :one +SELECT 1; diff --git a/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt new file mode 100644 index 0000000000..c1303a9a1e --- /dev/null +++ b/internal/endtoend/testdata/parse_basic/sqlite/stdout.txt @@ -0,0 +1,52 @@ +[ + { + "name": "GetValue", + "cmd": ":one", + "ast": { + "Stmt": { + "DistinctClause": null, + "IntoClause": null, + "TargetList": { + "Items": [ + { + "Name": null, + "Indirection": null, + "Val": { + "Val": { + "Ival": 1 + }, + "Location": 30 + }, + "Location": 30 + } + ] + }, + "FromClause": { + "Items": null + }, + "WhereClause": null, + "GroupClause": { + "Items": null + }, + "HavingClause": null, + "WindowClause": { + "Items": null + }, + "ValuesLists": { + "Items": null + }, + "SortClause": null, + "LimitOffset": null, + "LimitCount": null, + "LockingClause": null, + "WithClause": null, + "Op": 0, + "All": false, + "Larg": null, + "Rarg": null + }, + "StmtLocation": 0, + "StmtLen": 31 + } + } +]