diff --git a/internal/compiler/analyze.go b/internal/compiler/analyze.go index 264dbef6f5..020f7865aa 100644 --- a/internal/compiler/analyze.go +++ b/internal/compiler/analyze.go @@ -8,7 +8,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/named" - "github.com/sqlc-dev/sqlc/internal/sql/rewrite" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/validate" ) @@ -114,15 +114,15 @@ func combineAnalysis(prev *analysis, a *analyzer.Analysis) *analysis { return prev } -func (c *Compiler) analyzeQuery(raw *ast.RawStmt, query string) (*analysis, error) { - return c._analyzeQuery(raw, query, true) +func (c *Compiler) analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) { + return c._analyzeQuery(raw, query, pre, true) } -func (c *Compiler) inferQuery(raw *ast.RawStmt, query string) (*analysis, error) { - return c._analyzeQuery(raw, query, false) +func (c *Compiler) inferQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) { + return c._analyzeQuery(raw, query, pre, false) } -func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool) (*analysis, error) { +func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement, failfast bool) (*analysis, error) { errors := make([]error, 0) check := func(err error) error { if failfast { @@ -134,12 +134,12 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool) return nil } - numbers, dollar, err := validate.ParamRef(raw) - if err := check(err); err != nil { + dollar := pre.Dollar + if err := check(pre.ParamErr); err != nil { return nil, err } - raw, namedParams, edits := rewrite.NamedParameters(c.conf.Engine, raw, numbers, dollar) + namedParams := pre.Params var table *ast.TableName switch n := raw.Stmt.(type) { @@ -158,7 +158,7 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool) return nil, err } - if err := check(validate.In(c.catalog, raw)); err != nil { + if err := check(validate.Slice(raw, pre.Slices)); err != nil { return nil, err } rvs := rangeVars(raw.Stmt) @@ -175,7 +175,7 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool) } else { sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number }) } - raw, embeds := rewrite.Embeds(raw) + embeds := pre.Embeds qc, err := c.buildQueryCatalog(c.catalog, raw.Stmt, embeds) if err := check(err); err != nil { return nil, err @@ -190,11 +190,10 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool) return nil, err } - expandEdits, err := c.expand(qc, raw) + edits, err := c.expand(qc, raw) if check(err); err != nil { return nil, err } - edits = append(edits, expandEdits...) expanded, err := source.Mutate(query, edits) if err != nil { return nil, err diff --git a/internal/compiler/compile.go b/internal/compiler/compile.go index 61b3301f0b..11e67f91fb 100644 --- a/internal/compiler/compile.go +++ b/internal/compiler/compile.go @@ -16,6 +16,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/rpc" "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" ) @@ -103,19 +104,30 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) { continue } src := string(blob) - stmts, err := c.parser.Parse(strings.NewReader(src)) + + // sqlc syntax is not SQL. Rewrite it to native placeholders before the + // engine parser ever sees the query, so parsers only handle SQL. + pp := preprocess.File(c.conf.Engine, src) + + stmts, err := c.parser.Parse(strings.NewReader(pp.Text)) if err != nil { merr.Add(filename, src, 0, err) continue } for _, stmt := range stmts { - query, err := c.parseQuery(stmt.Raw, src, o) + query, err := c.parseQuery(stmt.Raw, pp, o) if err != nil { var e *sqlerr.Error loc := stmt.Raw.Pos() if errors.As(err, &e) && e.Location != 0 { loc = e.Location } + // Locations are reported against the rewritten query; map them + // back so errors point at what the user wrote. + loc = pp.Origin(loc) + if e != nil && e.Location != 0 { + e.Location = loc + } merr.Add(filename, src, loc, err) // If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail if errors.Is(err, rpc.ErrUnauthenticated) { @@ -130,7 +142,7 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) { queryName := query.Metadata.Name if queryName != "" { if _, exists := set[queryName]; exists { - merr.Add(filename, src, stmt.Raw.Pos(), fmt.Errorf("duplicate query name: %s", queryName)) + merr.Add(filename, src, pp.Origin(stmt.Raw.Pos()), fmt.Errorf("duplicate query name: %s", queryName)) continue } set[queryName] = struct{}{} diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index c60b7618b2..98dd82cbdc 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -166,34 +166,27 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) } } - var oldString string - var oldFunc func(string) int - - // use the sqlc.embed string instead - if embed, ok := qc.embeds.Find(ref); ok { - oldString = embed.Orig() - } else { - oldFunc = func(s string) int { - length := 0 - for i, o := range old { - if hasSeparator := i > 0; hasSeparator { - length++ - } - if strings.HasPrefix(s[length:], o) { - length += len(o) - } else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) { - length += len(quoted) - } else { - length += len(o) - } + // An embed was rewritten to "table.*" in the query text, so it is + // measured the same way as a star reference the user wrote. + oldFunc := func(s string) int { + length := 0 + for i, o := range old { + if hasSeparator := i > 0; hasSeparator { + length++ + } + if strings.HasPrefix(s[length:], o) { + length += len(o) + } else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) { + length += len(quoted) + } else { + length += len(o) } - return length } + return length } edits = append(edits, source.Edit{ Location: res.Location - raw.StmtLocation, - Old: oldString, OldFunc: oldFunc, New: strings.Join(cols, ", "), }) diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index 6cc2567ebe..ef3d662057 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -258,7 +258,7 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er if hasStarRef(n) { // add a column with a reference to an embedded table - if embed, ok := qc.embeds.Find(n); ok { + if embed, ok := qc.embeds.Find(n.Location, res.Location); ok { cols = append(cols, &Column{ Name: embed.Table.Name, EmbedTable: embed.Table, diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index be510d71d4..796f1a6fb1 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -12,15 +12,32 @@ import ( "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/astutils" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/validate" "github.com/sqlc-dev/sqlc/internal/sqlcdebug" ) var debugDumpAST = sqlcdebug.New("dumpast") -func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) { +func (c *Compiler) parseQuery(stmt ast.Node, pp *preprocess.Result, o opts.Parser) (*Query, error) { + raw, ok := stmt.(*ast.RawStmt) + if !ok { + return nil, errors.New("node is not a statement") + } + + // The preprocessor already replaced every sqlc.* construct with native SQL + // and recorded what it replaced. + pre := pp.Statement(raw.StmtLocation) + if pre.Err != nil { + return nil, pre.Err + } + + // Engines number bind parameters in whatever order they convert the AST. + // Restore the numbering the preprocessor assigned in source order. + renumberParams(raw, pre.Numbers) + if c.coreCatalog != nil { - return c.parseQueryCore(stmt, src) + return c.parseQueryCore(raw, pp.Text, pre) } ctx := context.Background() @@ -29,18 +46,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, debug.Dump(stmt) } - // validate sqlc-specific syntax - if err := validate.SqlcFunctions(stmt); err != nil { - return nil, err - } - - // rewrite queries to remove sqlc.* functions - - raw, ok := stmt.(*ast.RawStmt) - if !ok { - return nil, errors.New("node is not a statement") - } - rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen) + rawSQL, err := source.Pluck(pp.Text, raw.StmtLocation, raw.StmtLen) if err != nil { return nil, err } @@ -128,7 +134,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, Query: expandedQuery, } } else if c.analyzer != nil { - inference, _ := c.inferQuery(raw, rawSQL) + inference, _ := c.inferQuery(raw, rawSQL, pre) if inference == nil { inference = &analysis{} } @@ -156,7 +162,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, // FOOTGUN: combineAnalysis mutates inference anlys = combineAnalysis(inference, result) } else { - anlys, err = c.analyzeQuery(raw, rawSQL) + anlys, err = c.analyzeQuery(raw, rawSQL, pre) if err != nil { return nil, err } @@ -188,6 +194,23 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, }, nil } +// renumberParams applies the parameter numbers the preprocessor assigned, +// matching each node by the location of its placeholder in the query text. +func renumberParams(raw *ast.RawStmt, numbers map[int]int) { + if len(numbers) == 0 { + return + } + astutils.Walk(astutils.VisitorFunc(func(node ast.Node) { + ref, ok := node.(*ast.ParamRef) + if !ok { + return + } + if n, ok := numbers[ref.Location]; ok { + ref.Number = n + } + }), raw) +} + func rangeVars(root ast.Node) []*ast.RangeVar { var vars []*ast.RangeVar find := astutils.VisitorFunc(func(node ast.Node) { diff --git a/internal/compiler/parse_core.go b/internal/compiler/parse_core.go index 5cede4356b..2fc1c1272e 100644 --- a/internal/compiler/parse_core.go +++ b/internal/compiler/parse_core.go @@ -10,15 +10,11 @@ import ( "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/named" - "github.com/sqlc-dev/sqlc/internal/sql/rewrite" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/validate" ) -func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { - raw, ok := stmt.(*ast.RawStmt) - if !ok { - return nil, errors.New("node is not a statement") - } +func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess.Statement) (*Query, error) { rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen) if err != nil { return nil, err @@ -48,21 +44,17 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { return nil, err } - numbers, dollar, err := validate.ParamRef(raw) - if err != nil { - return nil, err - } - rewritten, namedParams, edits := rewrite.NamedParameters(c.conf.Engine, raw, numbers, dollar) - expanded, err := source.Mutate(rawSQL, edits) - if err != nil { - return nil, err + if pre.ParamErr != nil { + return nil, pre.ParamErr } + namedParams := pre.Params + expanded := rawSQL var cols []*Column var params []Parameter - switch rewritten.Stmt.(type) { + switch raw.Stmt.(type) { case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt: - res, err := coreanalyzer.Prepare(c.coreCatalog, rewritten) + res, err := coreanalyzer.Prepare(c.coreCatalog, raw) if err != nil { return nil, err } @@ -123,12 +115,17 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column { col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table} col.OriginalName = p.Source.Column } - if col.Name == "" { - if name, ok := params.NameFor(p.Number); ok && name != "" { - col.Name = name - } else if p.Source != nil { - col.Name = p.Source.Column - } + if col.Name == "" && p.Source != nil { + col.Name = p.Source.Column + } + // Merge in what the user asked for: sqlc.narg() makes the parameter + // nullable and sqlc.slice() marks it as a slice, whichever way the + // analyzer typed it. + if param, isNamed := params.FetchMerge(p.Number, named.NewInferredParam(col.Name, p.NotNull)); isNamed { + col.Name = param.Name() + col.NotNull = param.NotNull() + col.IsSqlcSlice = param.IsSqlcSlice() + col.IsNamedParam = true } return col } diff --git a/internal/compiler/query_catalog.go b/internal/compiler/query_catalog.go index 80b59d876c..c4972415ad 100644 --- a/internal/compiler/query_catalog.go +++ b/internal/compiler/query_catalog.go @@ -5,16 +5,16 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" - "github.com/sqlc-dev/sqlc/internal/sql/rewrite" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" ) type QueryCatalog struct { catalog *catalog.Catalog ctes map[string]*Table - embeds rewrite.EmbedSet + embeds preprocess.EmbedSet } -func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds rewrite.EmbedSet) (*QueryCatalog, error) { +func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds preprocess.EmbedSet) (*QueryCatalog, error) { var with *ast.WithClause switch n := node.(type) { case *ast.DeleteStmt: diff --git a/internal/compiler/resolve.go b/internal/compiler/resolve.go index d926f2b1fc..f47a01d7a8 100644 --- a/internal/compiler/resolve.go +++ b/internal/compiler/resolve.go @@ -9,7 +9,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/astutils" "github.com/sqlc-dev/sqlc/internal/sql/catalog" "github.com/sqlc-dev/sqlc/internal/sql/named" - "github.com/sqlc-dev/sqlc/internal/sql/rewrite" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" ) @@ -21,7 +21,7 @@ func dataType(n *ast.TypeName) string { } } -func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, args []paramRef, params *named.ParamSet, embeds rewrite.EmbedSet) ([]Parameter, error) { +func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, args []paramRef, params *named.ParamSet, embeds preprocess.EmbedSet) ([]Parameter, error) { c := comp.catalog aliasMap := map[string]*ast.TableName{} diff --git a/internal/core/analyzer/bench_test.go b/internal/core/analyzer/bench_test.go index fa4a67dad4..8d6bc6a478 100644 --- a/internal/core/analyzer/bench_test.go +++ b/internal/core/analyzer/bench_test.go @@ -4,14 +4,11 @@ import ( "strings" "testing" - "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/core/analyzer" coreschema "github.com/sqlc-dev/sqlc/internal/core/schema" "github.com/sqlc-dev/sqlc/internal/engine/googlesql" "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/rewrite" - "github.com/sqlc-dev/sqlc/internal/sql/validate" ) const benchSchema = ` @@ -50,23 +47,11 @@ func parseAll(t testing.TB, src string) []ast.Node { return out } -// parseQueries mirrors the compiler pipeline: parse, then rewrite named -// parameters into ParamRef nodes before handing the statement to the analyzer. +// parseQueries mirrors the compiler pipeline. GoogleSQL is not preprocessed — +// "@name" is native syntax the converter turns into a ParamRef — so parsing is +// all it takes. func parseQueries(t testing.TB, src string) []ast.Node { - out := make([]ast.Node, 0) - for _, n := range parseAll(t, src) { - raw, ok := n.(*ast.RawStmt) - if !ok { - t.Fatalf("not a raw statement: %T", n) - } - numbers, dollar, err := validate.ParamRef(raw) - if err != nil { - t.Fatal(err) - } - rewritten, _, _ := rewrite.NamedParameters(config.EngineGoogleSQL, raw, numbers, dollar) - out = append(out, rewritten) - } - return out + return parseAll(t, src) } func newBenchCatalog(t testing.TB) (*core.Catalog, []ast.Node) { diff --git a/internal/endtoend/testdata/analyze_params/clickhouse/exec.json b/internal/endtoend/testdata/analyze_params/clickhouse/exec.json new file mode 100644 index 0000000000..c92bfa3dab --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/clickhouse/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "clickhouse", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_params/clickhouse/query.sql b/internal/endtoend/testdata/analyze_params/clickhouse/query.sql new file mode 100644 index 0000000000..5405a5baa8 --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/clickhouse/query.sql @@ -0,0 +1,5 @@ +-- name: GetEvent :one +SELECT id, name FROM events WHERE id = ?; + +-- name: FilterEvents :many +SELECT id, name FROM events WHERE name = ? AND amount > ?; diff --git a/internal/endtoend/testdata/analyze_params/clickhouse/schema.sql b/internal/endtoend/testdata/analyze_params/clickhouse/schema.sql new file mode 100644 index 0000000000..29960ee63d --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/clickhouse/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE events ( + id UInt64, + name String, + tag Nullable(String), + amount Float64, + created DateTime +) ENGINE = MergeTree ORDER BY id; diff --git a/internal/endtoend/testdata/analyze_params/clickhouse/stdout.txt b/internal/endtoend/testdata/analyze_params/clickhouse/stdout.txt new file mode 100644 index 0000000000..6fb0cc654b --- /dev/null +++ b/internal/endtoend/testdata/analyze_params/clickhouse/stdout.txt @@ -0,0 +1,76 @@ +[ + { + "name": "GetEvent", + "cmd": ":one", + "columns": [ + { + "name": "id", + "data_type": "uint64", + "not_null": true, + "is_array": false, + "table": "events" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "events" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "id", + "data_type": "uint64", + "not_null": true, + "is_array": false, + "table": "events" + } + } + ] + }, + { + "name": "FilterEvents", + "cmd": ":many", + "columns": [ + { + "name": "id", + "data_type": "uint64", + "not_null": true, + "is_array": false, + "table": "events" + }, + { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "events" + } + ], + "params": [ + { + "number": 1, + "column": { + "name": "name", + "data_type": "string", + "not_null": true, + "is_array": false, + "table": "events" + } + }, + { + "number": 2, + "column": { + "name": "amount", + "data_type": "float64", + "not_null": true, + "is_array": false, + "table": "events" + } + } + ] + } +] diff --git a/internal/endtoend/testdata/sqlc_arg_invalid/mysql/stderr.txt b/internal/endtoend/testdata/sqlc_arg_invalid/mysql/stderr.txt index 73966182fb..6e2cf1379b 100644 --- a/internal/endtoend/testdata/sqlc_arg_invalid/mysql/stderr.txt +++ b/internal/endtoend/testdata/sqlc_arg_invalid/mysql/stderr.txt @@ -1,6 +1,6 @@ # package querytest -query.sql:1:1: function "sqlc.argh" does not exist +query.sql:2:45: function "sqlc.argh" does not exist query.sql:5:45: expected 1 parameter to sqlc.arg; got 2 query.sql:8:45: expected 1 parameter to sqlc.arg; got 0 -query.sql:11:45: expected parameter to sqlc.arg to be string or reference; got *ast.FuncCall -query.sql:14:45: expected parameter to sqlc.arg to be string or reference; got *ast.ParamRef +query.sql:11:45: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(target_id)" +query.sql:14:45: expected parameter to sqlc.arg to be string or reference; got "?" diff --git a/internal/endtoend/testdata/sqlc_arg_invalid/postgresql/stderr.txt b/internal/endtoend/testdata/sqlc_arg_invalid/postgresql/stderr.txt index 8dbea1f949..2bc3407422 100644 --- a/internal/endtoend/testdata/sqlc_arg_invalid/postgresql/stderr.txt +++ b/internal/endtoend/testdata/sqlc_arg_invalid/postgresql/stderr.txt @@ -1,7 +1,6 @@ # package querytest -query.sql:1:1: function "sqlc.argh" does not exist +query.sql:2:45: function "sqlc.argh" does not exist query.sql:5:45: expected 1 parameter to sqlc.arg; got 2 query.sql:8:45: expected 1 parameter to sqlc.arg; got 0 -query.sql:11:45: expected parameter to sqlc.arg to be string or reference; got *ast.FuncCall -query.sql:14:45: expected parameter to sqlc.arg to be string or reference; got *ast.ParamRef - +query.sql:11:45: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(target_id)" +query.sql:14:45: expected parameter to sqlc.arg to be string or reference; got "$1" diff --git a/internal/endtoend/testdata/sqlc_arg_invalid/sqlite/stderr.txt b/internal/endtoend/testdata/sqlc_arg_invalid/sqlite/stderr.txt index be38c8b505..4cf898bcfb 100644 --- a/internal/endtoend/testdata/sqlc_arg_invalid/sqlite/stderr.txt +++ b/internal/endtoend/testdata/sqlc_arg_invalid/sqlite/stderr.txt @@ -1,6 +1,6 @@ # package querytest -query.sql:7:1: function "sqlc.argh" does not exist +query.sql:7:45: function "sqlc.argh" does not exist query.sql:10:45: expected 1 parameter to sqlc.arg; got 2 query.sql:13:45: expected 1 parameter to sqlc.arg; got 0 -query.sql:16:45: expected parameter to sqlc.arg to be string or reference; got *ast.FuncCall -query.sql:19:45: expected parameter to sqlc.arg to be string or reference; got *ast.ParamRef +query.sql:16:45: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(target_id)" +query.sql:19:45: expected parameter to sqlc.arg to be string or reference; got "?" diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go index 79bf442cf5..ea4f342e2c 100644 --- a/internal/engine/clickhouse/convert.go +++ b/internal/engine/clickhouse/convert.go @@ -472,6 +472,7 @@ func (c *cc) convertBinaryExpr(n *chast.BinaryExpr) ast.Node { // Handle other operators return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, Name: &ast.List{ Items: []ast.Node{&ast.String{Str: n.Op}}, }, @@ -692,6 +693,7 @@ func (c *cc) convertUnaryExpr(n *chast.UnaryExpr) ast.Node { } return &ast.A_Expr{ + Kind: ast.A_Expr_Kind_OP, Name: &ast.List{ Items: []ast.Node{&ast.String{Str: n.Op}}, }, diff --git a/internal/engine/clickhouse/seed.go b/internal/engine/clickhouse/seed.go index 986838285c..a9e033d756 100644 --- a/internal/engine/clickhouse/seed.go +++ b/internal/engine/clickhouse/seed.go @@ -32,20 +32,60 @@ var clickhouseTypes = []chType{ {"Map", "U"}, {"Tuple", "U"}, {"Nested", "U"}, {"Nothing", "U"}, } +var comparisonOperators = []string{"=", "<>", "!=", "<", "<=", ">", ">="} + +var arithmeticOperators = []string{"+", "-", "*", "/"} + func Seed(cat *core.Catalog) error { dialectOID, err := cat.CreateDialect("clickhouse") if err != nil { return err } + oids := make(map[string]int64, len(clickhouseTypes)) for _, t := range clickhouseTypes { - if _, err := cat.CreateTypeSpec(core.TypeSpec{ + oid, err := cat.CreateTypeSpec(core.TypeSpec{ Name: t.name, Typtype: "b", Category: t.category, DialectOID: dialectOID, - }); err != nil { + }) + if err != nil { return fmt.Errorf("seed clickhouse type %q: %w", t.name, err) } + oids[t.name] = oid + } + + boolOID := oids["Bool"] + for _, t := range clickhouseTypes { + if t.category == "A" || t.category == "U" { + continue + } + for _, op := range comparisonOperators { + if _, err := cat.CreateOperator(core.OperatorSpec{ + Name: op, + DialectOID: dialectOID, + LeftTypeOID: oids[t.name], + RightTypeOID: oids[t.name], + ResultTypeOID: boolOID, + }); err != nil { + return fmt.Errorf("seed clickhouse operator %q(%s): %w", op, t.name, err) + } + } + if t.category != "N" { + continue + } + for _, op := range arithmeticOperators { + if _, err := cat.CreateOperator(core.OperatorSpec{ + Name: op, + DialectOID: dialectOID, + LeftTypeOID: oids[t.name], + RightTypeOID: oids[t.name], + ResultTypeOID: oids[t.name], + }); err != nil { + return fmt.Errorf("seed clickhouse operator %q(%s): %w", op, t.name, err) + } + } } + return nil } diff --git a/internal/engine/dolphin/CLAUDE.md b/internal/engine/dolphin/CLAUDE.md index 6b5ffad384..0a11012dab 100644 --- a/internal/engine/dolphin/CLAUDE.md +++ b/internal/engine/dolphin/CLAUDE.md @@ -199,13 +199,10 @@ func (p *Parser) Param(n int) string { **Cause**: New AST node type not handled in `astutils/walk.go` or `astutils/rewrite.go` **Solution**: Add case for the node type in both files -### Issue: sqlc.arg() not converted in ON DUPLICATE KEY UPDATE -**Cause**: `InsertStmt` case in `rewrite.go` didn't traverse `OnDuplicateKeyUpdate` -**Solution**: Add `a.apply(n, "OnDuplicateKeyUpdate", nil, n.OnDuplicateKeyUpdate)` - ### Issue: MySQL @variable being treated as parameter -**Cause**: Converting `VariableExpr` to `A_Expr` with `@` operator -**Solution**: Use `ast.VariableExpr` instead, which is not detected by `named.IsParamSign()` +**Cause**: The preprocessor's dialect said `@name` is sqlc syntax +**Solution**: MySQL sets `AtSign: false` in `internal/sql/preprocess/dialect.go`, so +`@user_id` is left alone before the parser ever sees it ### Issue: Type length appearing incorrectly (e.g., datetime(39)) **Cause**: Using internal `flen` for all types diff --git a/internal/engine/googlesql/convert.go b/internal/engine/googlesql/convert.go index acf0af4657..e94304096f 100644 --- a/internal/engine/googlesql/convert.go +++ b/internal/engine/googlesql/convert.go @@ -11,6 +11,9 @@ import ( type cc struct { paramCount int + // namedParams tracks the number assigned to each "@name" so repeated uses + // share a single parameter. + namedParams map[string]int } func (c *cc) convert(node zjast.Node) ast.Node { @@ -511,17 +514,24 @@ func (c *cc) convertIntLiteral(n *zjast.IntLiteral) ast.Node { // convertParameterExpr converts "@name" and "?" query parameters. // -// Named parameters are mapped to the shape the PostgreSQL and SQLite engines -// produce for "@name": an A_Expr with the operator "@" and the parameter name -// as the right operand. That is the representation sqlc's named-parameter -// rewriter (internal/sql/rewrite, named.IsParamSign) already understands. -// Positional "?" parameters become ParamRef nodes numbered by their 1-based -// position in the statement. +// Both become ParamRef nodes. "@name" is native GoogleSQL syntax rather than +// sqlc syntax, so nothing rewrites it before the parser runs and the numbering +// here is what the rest of the pipeline sees: repeated uses of a name share a +// number, and positional parameters are numbered by their 1-based position in +// the statement. func (c *cc) convertParameterExpr(n *zjast.ParameterExpr) ast.Node { if n.Name != nil { - return &ast.A_Expr{ - Name: &ast.List{Items: []ast.Node{&ast.String{Str: "@"}}}, - Rexpr: &ast.String{Str: n.Name.Name}, + number, ok := c.namedParams[n.Name.Name] + if !ok { + c.paramCount++ + number = c.paramCount + if c.namedParams == nil { + c.namedParams = map[string]int{} + } + c.namedParams[n.Name.Name] = number + } + return &ast.ParamRef{ + Number: number, Location: n.Pos(), } } diff --git a/internal/engine/postgresql/utils.go b/internal/engine/postgresql/utils.go index b8d49b1a97..0f4be00634 100644 --- a/internal/engine/postgresql/utils.go +++ b/internal/engine/postgresql/utils.go @@ -29,16 +29,6 @@ func isNotNull(n *nodes.ColumnDef) bool { return false } -func IsNamedParamFunc(node *nodes.Node) bool { - fun, ok := node.Node.(*nodes.Node_FuncCall) - return ok && joinNodes(fun.FuncCall.Funcname, ".") == "sqlc.arg" -} - -func IsNamedParamSign(node *nodes.Node) bool { - expr, ok := node.Node.(*nodes.Node_AExpr) - return ok && joinNodes(expr.AExpr.Name, ".") == "@" -} - func makeByte(s string) byte { var b byte if s == "" { diff --git a/internal/sql/ast/CLAUDE.md b/internal/sql/ast/CLAUDE.md index e769fbfca6..ee62274d6a 100644 --- a/internal/sql/ast/CLAUDE.md +++ b/internal/sql/ast/CLAUDE.md @@ -105,9 +105,11 @@ case *ast.VariableExpr: ## Important Distinctions ### MySQL @variable vs sqlc @param -- MySQL user variables (`@user_id`) use `VariableExpr` - preserved as-is in output -- sqlc named parameters (`@param`) use `A_Expr` with `@` operator - replaced with `?` -- The `named.IsParamSign()` function checks for `A_Expr` with `@` operator +- sqlc named parameters (`@param`) never reach the AST: `internal/sql/preprocess` + replaces them with the dialect's native placeholder before parsing, so engines + only ever see a `ParamRef` +- `@name` is only sqlc syntax where the preprocessor's dialect says so. MySQL is + excluded, so `@user_id` there stays a user variable (`VariableExpr`) ### Type Modifiers - `TypeName.Typmods` holds type modifiers like `varchar(255)` diff --git a/internal/sql/astutils/CLAUDE.md b/internal/sql/astutils/CLAUDE.md index b7903542c5..7575ea9a12 100644 --- a/internal/sql/astutils/CLAUDE.md +++ b/internal/sql/astutils/CLAUDE.md @@ -88,17 +88,22 @@ astutils.Walk(&tv, stmt.FromClause) // tv.list now contains all RangeVar nodes ``` -### Replacing Named Parameters -The `rewrite/parameters.go` uses Apply to replace `sqlc.arg()` calls with `ParamRef`: +### Rewriting Nodes In Place +Apply replaces a node with a new one as the tree is walked: ```go astutils.Apply(root, func(cr *astutils.Cursor) bool { - if named.IsParamFunc(cr.Node()) { - cr.Replace(&ast.ParamRef{Number: nextParam()}) + if _, ok := cr.Node().(*ast.SomeType); ok { + cr.Replace(&ast.OtherType{}) } return true }, nil) ``` +Note that sqlc syntax (`sqlc.arg()`, `@name`) is *not* handled here. It is +rewritten to native SQL by `internal/sql/preprocess` before the engine parses a +query, so a node type missing from walk.go/rewrite.go can no longer silently +drop a parameter. + ## Node Types That Must Be Handled All node types in `internal/sql/ast/` must have cases in both walk.go and rewrite.go. Key MySQL-specific nodes: diff --git a/internal/sql/named/CLAUDE.md b/internal/sql/named/CLAUDE.md index 05ba358ee9..b1f1cae22e 100644 --- a/internal/sql/named/CLAUDE.md +++ b/internal/sql/named/CLAUDE.md @@ -1,94 +1,58 @@ # Named Parameters Package - Claude Code Guide -This package provides utilities for identifying sqlc's named parameter syntax. +This package models a query's parameters: their names, their nullability and +whether they came from `sqlc.slice()`. -## Named Parameter Styles +It no longer knows anything about sqlc's *syntax*. `sqlc.arg()`, `sqlc.narg()`, +`sqlc.slice()` and `@name` are rewritten to native placeholders by +`internal/sql/preprocess` before any engine parser runs, and that package builds +the `ParamSet` while it rewrites. -sqlc supports two styles of named parameters: +## Param -### 1. Function-style: `sqlc.arg(name)`, `sqlc.narg(name)`, `sqlc.slice(name)` -Identified by `IsParamFunc()`: -```go -func IsParamFunc(node ast.Node) bool { - call, ok := node.(*ast.FuncCall) - if !ok { - return false - } - return call.Func.Schema == "sqlc" && - (call.Func.Name == "arg" || call.Func.Name == "narg" || call.Func.Name == "slice") -} -``` +A `Param` carries the user-facing name plus a nullability that combines what was +inferred from the schema with what the user asked for: + +- `NewParam(name)` — unspecified nullability, from `sqlc.arg()` or `@name` +- `NewUserNullableParam(name)` — from `sqlc.narg()`, always nullable +- `NewSqlcSlice(name)` — from `sqlc.slice()` +- `NewInferredParam(name, notNull)` — what the compiler inferred + +A user-specified nullability outranks an inferred one. `mergeParam` ORs the +nullability bits together, so the order the two sources arrive in does not +matter. + +## ParamSet + +`ParamSet` maps placeholder numbers to `Param` values for a single statement. -### 2. At-sign style: `@param_name` (PostgreSQL only) -Identified by `IsParamSign()`: ```go -func IsParamSign(node ast.Node) bool { - expr, ok := node.(*ast.A_Expr) - return ok && astutils.Join(expr.Name, ".") == "@" -} +ps := named.NewParamSet(numbersAlreadyUsed, hasNamedSupport) +n := ps.Add(named.NewParam("author_id")) // returns the placeholder number ``` -## Important Distinction: sqlc @param vs MySQL @variable +`hasNamedSupport` is false for dialects that send one argument per `?` +(MySQL, ClickHouse). There, every occurrence of a name gets its own number. For +`$1`/`?1`/`@name` dialects repeated uses of a name share a number. + +The compiler reads the set back with: -**sqlc named parameters** (`@param` in PostgreSQL queries): -- Represented as `A_Expr` with `Kind=A_Expr_Kind_OP` and `Name=["@"]` -- Detected by `IsParamSign()` -- Replaced with positional parameters (`$1`, `$2` for PostgreSQL, `?` for MySQL) +- `NameFor(number)` — the user-facing name for a placeholder +- `FetchMerge(number, inferred)` — the merged param and whether it was named -**MySQL user variables** (`@user_id` in MySQL queries): -- Represented as `VariableExpr` -- NOT detected by `IsParamSign()` (it checks for `A_Expr`, not `VariableExpr`) -- Preserved as-is in the output SQL +## MySQL @variable vs sqlc @param + +`@name` is only sqlc syntax where the preprocessor's dialect says it is. MySQL +sets `AtSign: false`, so `@user_id` there is a user variable and reaches the +parser untouched (converted to `ast.VariableExpr`). PostgreSQL, SQLite and +GoogleSQL treat `@name` as a named parameter. -This distinction is critical: ```sql -- PostgreSQL with sqlc @param syntax: SELECT * FROM users WHERE id = @user_id --- Becomes: SELECT * FROM users WHERE id = $1 +-- Preprocessed to: SELECT * FROM users WHERE id = $1 -- MySQL with user variable: SELECT * FROM users WHERE id != @user_id -- Stays: SELECT * FROM users WHERE id != @user_id ``` - -## Usage in Parameter Rewriting - -The `rewrite/parameters.go` package uses these functions to find and replace named parameters: - -```go -// Find all named parameters -params := astutils.Search(root, func(node ast.Node) bool { - return named.IsParamFunc(node) || named.IsParamSign(node) -}) - -// Replace with positional parameters -astutils.Apply(root, func(cr *astutils.Cursor) bool { - if named.IsParamFunc(cr.Node()) || named.IsParamSign(cr.Node()) { - cr.Replace(&ast.ParamRef{Number: nextParam()}) - } - return true -}, nil) -``` - -## Converting MySQL @variable Correctly - -When converting TiDB's `VariableExpr` in `dolphin/convert.go`: - -```go -// CORRECT - preserves MySQL user variable as-is -func (c *cc) convertVariableExpr(n *pcast.VariableExpr) ast.Node { - return &ast.VariableExpr{ - Name: n.Name, - Location: n.OriginTextPosition(), - } -} - -// WRONG - would be treated as sqlc named parameter -func (c *cc) convertVariableExpr(n *pcast.VariableExpr) ast.Node { - return &ast.A_Expr{ - Kind: ast.A_Expr_Kind_OP, - Name: &ast.List{Items: []ast.Node{&ast.String{Str: "@"}}}, - Rexpr: &ast.String{Str: n.Name}, - } -} -``` diff --git a/internal/sql/named/is.go b/internal/sql/named/is.go deleted file mode 100644 index d53c1d9905..0000000000 --- a/internal/sql/named/is.go +++ /dev/null @@ -1,26 +0,0 @@ -package named - -import ( - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" -) - -// IsParamFunc fulfills the astutils.Search -func IsParamFunc(node ast.Node) bool { - call, ok := node.(*ast.FuncCall) - if !ok { - return false - } - - if call.Func == nil { - return false - } - - isValid := call.Func.Schema == "sqlc" && (call.Func.Name == "arg" || call.Func.Name == "narg" || call.Func.Name == "slice") - return isValid -} - -func IsParamSign(node ast.Node) bool { - expr, ok := node.(*ast.A_Expr) - return ok && astutils.Join(expr.Name, ".") == "@" -} diff --git a/internal/sql/preprocess/CLAUDE.md b/internal/sql/preprocess/CLAUDE.md new file mode 100644 index 0000000000..a7f4d9aafe --- /dev/null +++ b/internal/sql/preprocess/CLAUDE.md @@ -0,0 +1,114 @@ +# SQL Preprocess Package - Claude Code Guide + +This package rewrites sqlc's query syntax into native SQL **before** any engine +parser sees a query. After it runs, no sqlc syntax remains in the text, so +parsers, converters and `astutils` traversals only ever deal with SQL. + +## What it rewrites + +| syntax | becomes | +| --- | --- | +| `sqlc.arg(name)` / `sqlc.narg(name)` | the dialect's native placeholder | +| `sqlc.slice(name)` | the placeholder wrapped in `/*SLICE:name*/` | +| `sqlc.embed(table)` | `table.*` | +| `@name` | the native placeholder, on dialects where `@` is sqlc syntax | + +Native placeholders by dialect: + +| engine | placeholder | `@name` | +| --- | --- | --- | +| postgresql | `$1` | sqlc syntax | +| mysql | `?` | user variable, left alone | +| sqlite | `?1` | sqlc syntax | + +GoogleSQL and ClickHouse are deliberately absent. They handle their own +parameter syntax, so `File` returns their source unchanged and sqlc syntax is +not available to them — `sqlc.arg()` reaches the parser as the function call it +looks like. + +## How it works + +`lexer` is a dialect-parameterized scanner. It does not parse SQL — it knows +only enough to skip the regions where sqlc syntax must not be rewritten: +comments (`--`, `/* */`, `#`), string literals, quoted identifiers, backticks +and PostgreSQL dollar-quoted strings. `Dialect` (dialect.go) is the single +place those lexical rules live. + +`File(engine, src)` looks up the dialect; an engine with no entry gets its +source back untouched. Otherwise it walks the whole file, splits it at +top-level semicolons and, for each statement: + +1. `scan` collects every sqlc construct **and** every native placeholder, in + source order. +2. `number` assigns placeholder numbers. Numbered dialects keep the numbers the + user wrote and fill in the gaps; `?` dialects renumber everything in source + order, because each `?` is its own argument. +3. The statement is rewritten into the output buffer and a `Statement` records + what changed. + +## The side table + +`Result.Statement(offset)` returns the `Statement` covering an offset in the +**rewritten** text: + +- `Params` — the `named.ParamSet` for the statement +- `Embeds` — each rewritten `table.*`, keyed by its location so the compiler can + tell it apart from a star reference the user wrote +- `Slices` — the location of each `sqlc.slice()` placeholder +- `Numbers` — location → parameter number, used by the compiler to override the + numbers an engine assigned in AST-conversion order +- `Dollar` / `ParamErr` — the placeholder-style validation that used to live in + `validate.ParamRef` +- `Err` — a sqlc syntax error (unknown `sqlc.*` function, wrong arity, an + argument that is not an identifier or string) + +`Result.Origin(offset)` maps an offset in the rewritten text back to the +original source, so errors point at what the user wrote. + +## Invariants + +- **A rewrite never adds a line.** Replacements are single-line, so a line + number taken from the rewritten text is never past the end of the original. It + can *lose* lines, when the sqlc call itself spanned several — map offsets + through `Origin` rather than trusting line numbers. +- **An invalid statement is copied through untouched.** The engine still parses + what the user wrote and the error is reported per statement, not per file. +- **Nothing inside a comment or literal is rewritten.** Query annotations like + `-- name: GetAuthor :one` are comments, so they are always safe. + +## Tests + +`testdata///` holds the input and the expected results: + +| file | contents | +| --- | --- | +| `input.sql` | the query file | +| `output.sql` | the rewritten SQL | +| `side_table.json` | everything the preprocessor recorded | +| `stderr.txt` | reported errors, only when the input is invalid | + +`TestRewrite` runs one subtest per directory. `side_table.json` is rendered by +reading the result back through the same API the compiler uses, so it covers +parameter names and nullability, embed and slice spans, placeholder numbering +and the offset map — a parameter's `location` is its offset in the rewritten +text and its `origin` is where it came from. + +Regenerate every golden with: + +```bash +go test ./internal/sql/preprocess -update +``` + +The cases cover every shape of sqlc syntax that appears in +`internal/endtoend`, per preprocessed engine — each function against a bare +reference, a string constant and a quoted identifier, the placeholder styles, +and the constructs that must be left alone (comments, literals, MySQL user +variables and `$1`, PostgreSQL's `@>` and `?` operators). When you add a shape +to the corpus, add it here too. + +## Adding a dialect + +Add an entry to `dialects` in dialect.go. Nothing else in the codebase needs to +change for `sqlc.arg`/`narg`/`slice`/`embed` to work for a new engine — but only +add one if the engine should support sqlc syntax at all. Leaving an engine out +is a deliberate choice, not an oversight. diff --git a/internal/sql/preprocess/dialect.go b/internal/sql/preprocess/dialect.go new file mode 100644 index 0000000000..29340bb1f4 --- /dev/null +++ b/internal/sql/preprocess/dialect.go @@ -0,0 +1,106 @@ +package preprocess + +import ( + "github.com/sqlc-dev/sqlc/internal/config" +) + +// Style is the native placeholder syntax a dialect uses for bind parameters. +type Style int + +const ( + // StyleDollar numbers parameters as $1, $2, ... (PostgreSQL) + StyleDollar Style = iota + // StyleQuestion uses an unnumbered ? for every parameter (MySQL) + StyleQuestion + // StyleOrdinal numbers parameters as ?1, ?2, ... (SQLite) + StyleOrdinal +) + +// Dialect describes the lexical rules the preprocessor needs to walk a query +// without parsing it: how the dialect quotes strings and identifiers, how it +// writes comments and which placeholder syntax it accepts. +// +// The preprocessor never interprets SQL. It only needs to know enough to skip +// the regions of a query where sqlc syntax must not be rewritten. +type Dialect struct { + // Style is the native placeholder syntax used for rewritten parameters. + Style Style + + // AtSign reports whether a bare "@name" is sqlc named-parameter syntax. + // It is false for MySQL, where "@name" is a user variable. + AtSign bool + + // DollarQuote reports whether $tag$ ... $tag$ string literals are valid. + DollarQuote bool + + // DollarNumber reports whether $1 is a bind parameter. + DollarNumber bool + + // Question reports whether ? is a bind parameter. + Question bool + + // Backtick reports whether `ident` is a quoted identifier. + Backtick bool + + // HashComment reports whether # starts a line comment. + HashComment bool + + // DoubleQuoteString reports whether "..." is a string literal rather than + // a quoted identifier. MySQL treats it as a string unless ANSI_QUOTES is + // enabled. + DoubleQuoteString bool + + // NestedBlockComment reports whether /* ... /* ... */ ... */ nests. + NestedBlockComment bool + + // Backslash reports whether a backslash escapes the next character inside + // a string literal. + Backslash bool + + // FoldIdentifier reports whether the dialect lowercases unquoted + // identifiers. It decides the case of a parameter named by a bare + // reference, e.g. sqlc.arg(FooBar). + FoldIdentifier bool +} + +var dialects = map[config.Engine]Dialect{ + config.EnginePostgreSQL: { + Style: StyleDollar, + AtSign: true, + DollarQuote: true, + DollarNumber: true, + NestedBlockComment: true, + FoldIdentifier: true, + }, + config.EngineMySQL: { + Style: StyleQuestion, + Question: true, + Backtick: true, + HashComment: true, + DoubleQuoteString: true, + Backslash: true, + FoldIdentifier: true, + }, + config.EngineSQLite: { + Style: StyleOrdinal, + AtSign: true, + Question: true, + Backtick: true, + Backslash: true, + }, +} + +// DialectFor returns the lexical rules for an engine, and whether the engine is +// preprocessed at all. GoogleSQL and ClickHouse are not: they handle their own +// parameter syntax, so their queries reach the parser unchanged. +func DialectFor(engine config.Engine) (Dialect, bool) { + d, ok := dialects[engine] + return d, ok +} + +// hasNamedSupport reports whether repeated uses of the same parameter name can +// share a single placeholder. MySQL sends an argument per "?", so every +// occurrence needs its own number. +func (d Dialect) hasNamedSupport() bool { + return d.Style != StyleQuestion +} diff --git a/internal/sql/preprocess/lexer.go b/internal/sql/preprocess/lexer.go new file mode 100644 index 0000000000..d17d80b077 --- /dev/null +++ b/internal/sql/preprocess/lexer.go @@ -0,0 +1,284 @@ +package preprocess + +import ( + "strings" +) + +// lexer is a dialect-parameterized scanner over a SQL file. It does not parse +// SQL; it knows just enough about comments, string literals, quoted identifiers +// and dollar-quoted strings to walk a query without ever looking inside a +// region where sqlc syntax must be left alone. +type lexer struct { + d Dialect + src string +} + +func isIdentStart(c byte) bool { + return c == '_' || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + c >= 0x80 +} + +func isIdentPart(c byte) bool { + return isIdentStart(c) || (c >= '0' && c <= '9') || c == '$' +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func isSpace(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' +} + +func (l *lexer) at(i int) byte { + if i < 0 || i >= len(l.src) { + return 0 + } + return l.src[i] +} + +// skip returns the offset just past the comment, string literal, quoted +// identifier or dollar-quoted string that starts at i. It returns -1 when no +// such token starts at i. +func (l *lexer) skip(i int) int { + switch c := l.at(i); c { + case '\'': + return l.skipQuoted(i, '\'', l.d.Backslash || l.isEscapeString(i)) + case '"': + return l.skipQuoted(i, '"', l.d.DoubleQuoteString && l.d.Backslash) + case '`': + if !l.d.Backtick { + return -1 + } + return l.skipQuoted(i, '`', false) + case '-': + if l.at(i+1) == '-' { + return l.skipLine(i) + } + case '#': + if l.d.HashComment { + return l.skipLine(i) + } + case '/': + if l.at(i+1) == '*' { + return l.skipBlockComment(i) + } + case '$': + if l.d.DollarQuote { + if end := l.skipDollarQuoted(i); end >= 0 { + return end + } + } + } + return -1 +} + +// isEscapeString reports whether the string literal starting at i is a +// PostgreSQL escape string constant, written E'...', where a backslash escapes +// the following character. +func (l *lexer) isEscapeString(i int) bool { + if !l.d.DollarQuote { // PostgreSQL-only syntax + return false + } + c := l.at(i - 1) + if c != 'e' && c != 'E' { + return false + } + // The E must not be the tail of a longer identifier. + return !isIdentPart(l.at(i - 2)) +} + +func (l *lexer) skipQuoted(i int, quote byte, backslash bool) int { + for j := i + 1; j < len(l.src); j++ { + switch l.src[j] { + case '\\': + if backslash { + j++ + } + case quote: + // A doubled quote is an escaped quote, not the end. + if l.at(j+1) == quote { + j++ + continue + } + return j + 1 + } + } + return len(l.src) +} + +func (l *lexer) skipLine(i int) int { + if idx := strings.IndexByte(l.src[i:], '\n'); idx >= 0 { + return i + idx + 1 + } + return len(l.src) +} + +func (l *lexer) skipBlockComment(i int) int { + depth := 0 + for j := i; j < len(l.src)-1; j++ { + if l.src[j] == '/' && l.src[j+1] == '*' { + depth++ + j++ + continue + } + if l.src[j] == '*' && l.src[j+1] == '/' { + depth-- + j++ + if depth == 0 || !l.d.NestedBlockComment { + return j + 1 + } + } + } + return len(l.src) +} + +// skipDollarQuoted handles PostgreSQL's $tag$ ... $tag$ string literals. It +// returns -1 when i does not start a dollar quote. +func (l *lexer) skipDollarQuoted(i int) int { + tag := l.dollarTag(i) + if tag == "" { + return -1 + } + if idx := strings.Index(l.src[i+len(tag):], tag); idx >= 0 { + return i + len(tag) + idx + len(tag) + } + return len(l.src) +} + +// dollarTag returns the full opening delimiter ("$$" or "$tag$") when one +// starts at i, and "" otherwise. +func (l *lexer) dollarTag(i int) string { + if l.at(i) != '$' { + return "" + } + j := i + 1 + for j < len(l.src) && l.src[j] != '$' { + c := l.src[j] + if isDigit(c) && j == i+1 { + return "" // $1 is a bind parameter, not a tag + } + if !isIdentPart(c) || c == '$' { + return "" + } + j++ + } + if j >= len(l.src) { + return "" + } + return l.src[i : j+1] +} + +// identEnd returns the offset just past the identifier that starts at i. +func (l *lexer) identEnd(i int) int { + j := i + for j < len(l.src) && isIdentPart(l.src[j]) { + j++ + } + return j +} + +// skipSpace advances past whitespace and comments. +func (l *lexer) skipSpace(i int) int { + for i < len(l.src) { + if isSpace(l.src[i]) { + i++ + continue + } + if l.src[i] == '-' && l.at(i+1) == '-' { + i = l.skipLine(i) + continue + } + if l.src[i] == '/' && l.at(i+1) == '*' { + i = l.skipBlockComment(i) + continue + } + if l.src[i] == '#' && l.d.HashComment { + i = l.skipLine(i) + continue + } + break + } + return i +} + +// statements splits the source into statement spans at top-level semicolons. +// The spans cover the whole source, so every byte belongs to exactly one +// statement. +func (l *lexer) statements() [][2]int { + var out [][2]int + start := 0 + for i := 0; i < len(l.src); { + if end := l.skip(i); end >= 0 { + i = end + continue + } + if l.src[i] == ';' { + out = append(out, [2]int{start, i + 1}) + start = i + 1 + } + i++ + } + if start < len(l.src) { + out = append(out, [2]int{start, len(l.src)}) + } + return out +} + +// matchParen returns the offset of the ")" that closes the "(" at i, or -1. +func (l *lexer) matchParen(i int) int { + depth := 0 + for j := i; j < len(l.src); { + if end := l.skip(j); end >= 0 { + j = end + continue + } + switch l.src[j] { + case '(': + depth++ + case ')': + depth-- + if depth == 0 { + return j + } + } + j++ + } + return -1 +} + +// splitArgs splits the text between parentheses on top-level commas. Empty or +// whitespace-only input yields no arguments. +func (l *lexer) splitArgs(start, end int) []string { + var args []string + depth := 0 + last := start + for j := start; j < end; { + if stop := l.skip(j); stop >= 0 { + if stop > end { + stop = end + } + j = stop + continue + } + switch l.src[j] { + case '(': + depth++ + case ')': + depth-- + case ',': + if depth == 0 { + args = append(args, l.src[last:j]) + last = j + 1 + } + } + j++ + } + tail := l.src[last:end] + if len(args) == 0 && strings.TrimSpace(tail) == "" { + return nil + } + return append(args, tail) +} diff --git a/internal/sql/preprocess/preprocess.go b/internal/sql/preprocess/preprocess.go new file mode 100644 index 0000000000..76501cb60a --- /dev/null +++ b/internal/sql/preprocess/preprocess.go @@ -0,0 +1,398 @@ +// Package preprocess rewrites sqlc's query syntax into native SQL before any +// engine parser sees a query. +// +// sqlc.arg(), sqlc.narg(), sqlc.slice(), sqlc.embed() and @name are sqlc +// syntax, not SQL. Historically they survived all the way through each engine's +// parser and were replaced by walking the resulting AST, which meant every +// engine had to round-trip a schema-qualified function call it did not +// understand. This package removes that requirement: it scans the query text +// with a dialect-parameterized lexer, replaces each construct with the engine's +// native placeholder, and records what it replaced. Engines then only ever see +// valid SQL. +package preprocess + +import ( + "errors" + "fmt" + "strings" + + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/named" + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +// Embed is a rewritten sqlc.embed(table) call. +type Embed struct { + Table *ast.TableName + + // Location is the offset of the rewritten "table.*" in the preprocessed + // text. The compiler uses it to tell a rewritten star reference apart from + // one the user wrote. + Location int + + param string +} + +// Orig returns the sqlc syntax this embed was rewritten from. +func (e Embed) Orig() string { + return fmt.Sprintf("sqlc.embed(%s)", e.param) +} + +// EmbedSet is the set of embeds in a single statement. +type EmbedSet []*Embed + +// Find returns the embed recorded at any of the given locations. Engines do not +// all populate the same location fields, so callers pass every location that +// could identify the node. +func (es EmbedSet) Find(locations ...int) (*Embed, bool) { + for _, e := range es { + for _, loc := range locations { + if loc == e.Location { + return e, true + } + } + } + return nil, false +} + +// Statement holds everything the preprocessor learned about a single statement. +type Statement struct { + // Start and End delimit the statement in the rewritten text. + Start, End int + + // Params maps each placeholder number to its sqlc metadata. + Params *named.ParamSet + + // Embeds records the sqlc.embed() calls that were rewritten to "table.*". + Embeds EmbedSet + + // Slices holds the offset, in the rewritten text, of every placeholder that + // came from sqlc.slice(), mapped to the parameter name. + Slices map[int]string + + // Numbers maps the offset of every placeholder in the rewritten text to the + // number assigned to it. Engines number bind parameters in whatever order + // they happen to convert the AST, so the compiler uses this to restore + // source order. + Numbers map[int]int + + // Dollar reports whether the statement's own placeholders are numbered. + Dollar bool + + // ParamErr is set when the placeholders the user wrote are inconsistent: + // numbered and unnumbered styles mixed, or a gap in the numbering. + ParamErr error + + // Err is a validation error found while rewriting. The statement text is + // left untouched when this is set, so the engine still sees the original + // query and the error can be reported against it. + Err error +} + +// Result is the outcome of preprocessing a SQL file. +type Result struct { + // Text is the rewritten SQL. It has the same number of statements and the + // same line structure as the input. + Text string + + stmts []*Statement + edits []edit +} + +// edit records one rewritten span, in both coordinate systems. +type edit struct { + oldStart, oldEnd int + newStart, newEnd int +} + +// Statements returns every statement in the rewritten text, in source order. +func (r *Result) Statements() []*Statement { + return r.stmts +} + +// Statement returns the preprocessing results for the statement covering the +// given offset in the rewritten text. It never returns nil. +func (r *Result) Statement(location int) *Statement { + for _, s := range r.stmts { + if location < s.End { + return s + } + } + return &Statement{Params: named.NewParamSet(nil, true)} +} + +// Origin maps an offset in the rewritten text back to the offset in the +// original source, so errors can be reported against what the user wrote. +func (r *Result) Origin(location int) int { + delta := 0 + for _, e := range r.edits { + if location < e.newStart { + break + } + if location < e.newEnd { + return e.oldStart + } + delta += (e.oldEnd - e.oldStart) - (e.newEnd - e.newStart) + } + return location + delta +} + +type kind int + +const ( + kindArg kind = iota + kindNarg + kindSlice + kindEmbed + kindNative +) + +// occurrence is one construct found in the query text. +type occurrence struct { + kind kind + start int // offset in the original source + end int + name string + // ident is the argument exactly as written, used to rebuild "table.*" for + // embeds so quoting is preserved. + ident string + // number is the placeholder number assigned to this occurrence. + number int + // explicit reports whether a native placeholder carried its own number. + explicit bool + err *sqlerr.Error +} + +// File rewrites every sqlc construct in src to native SQL for the given engine. +// +// Engines that are not preprocessed — GoogleSQL and ClickHouse, which handle +// their own parameter syntax — get the source back unchanged, with an empty +// side table. sqlc.arg() and friends are not rewritten for them, so they reach +// the parser as the function calls they look like. +func File(engine config.Engine, src string) *Result { + d, ok := DialectFor(engine) + if !ok { + return &Result{Text: src} + } + return Dialected(d, src) +} + +// Dialected rewrites src using an explicit dialect. +func Dialected(d Dialect, src string) *Result { + l := &lexer{d: d, src: src} + res := &Result{} + + var b strings.Builder + b.Grow(len(src)) + + spans := l.statements() + for i, span := range spans { + start, end := span[0], span[1] + + // Whatever trails the last semicolon is only whitespace in a + // well-formed file. It still belongs in the output, but it is not a + // statement. + if i == len(spans)-1 && strings.TrimSpace(src[start:end]) == "" { + b.WriteString(src[start:end]) + continue + } + + occs := l.scan(start, end) + stmt := &Statement{Start: b.Len()} + + // A statement that fails validation is copied through unchanged so the + // engine parses what the user actually wrote and the error keeps its + // original shape. + if err := firstError(occs); err != nil { + b.WriteString(src[start:end]) + stmt.End = b.Len() + err.Location = stmt.Start + (err.Location - start) + stmt.Err = err + stmt.Params = named.NewParamSet(nil, d.hasNamedSupport()) + stmt.Dollar = true + res.stmts = append(res.stmts, stmt) + continue + } + + stmt.Dollar, stmt.ParamErr = validatePlaceholders(occs) + stmt.Params = number(d, occs) + stmt.Numbers = map[int]int{} + + prev := start + for i := range occs { + occ := &occs[i] + b.WriteString(src[prev:occ.start]) + newStart := b.Len() + replacement := d.replacement(occ) + b.WriteString(replacement) + newEnd := b.Len() + prev = occ.end + + if replacement != src[occ.start:occ.end] { + res.edits = append(res.edits, edit{ + oldStart: occ.start, oldEnd: occ.end, + newStart: newStart, newEnd: newEnd, + }) + } + + if occ.kind != kindEmbed { + stmt.Numbers[newStart+sliceMarkerLen(d, occ)] = occ.number + } + + switch occ.kind { + case kindEmbed: + stmt.Embeds = append(stmt.Embeds, &Embed{ + Table: &ast.TableName{Name: occ.name}, + Location: newStart, + param: occ.ident, + }) + case kindSlice: + if stmt.Slices == nil { + stmt.Slices = map[int]string{} + } + // The placeholder follows the /*SLICE:name*/ marker, so record + // the offset the engine will report for the parameter node. + stmt.Slices[newStart+sliceMarkerLen(d, occ)] = occ.name + } + } + b.WriteString(src[prev:end]) + stmt.End = b.Len() + res.stmts = append(res.stmts, stmt) + } + + res.Text = b.String() + return res +} + +func firstError(occs []occurrence) *sqlerr.Error { + for _, occ := range occs { + if occ.err != nil { + return occ.err + } + } + return nil +} + +// sliceMarkerLen returns the length of the /*SLICE:name*/ prefix a slice +// placeholder is written with, or 0 when there is none. +func sliceMarkerLen(d Dialect, occ *occurrence) int { + if occ.kind != kindSlice || d.Style == StyleDollar { + return 0 + } + return len("/*SLICE:") + len(occ.name) + len("*/") +} + +// validatePlaceholders checks the placeholders the user wrote, reproducing the +// rules the AST-based validator applied before sqlc syntax was rewritten: a +// query may not mix numbered and unnumbered placeholders, and the numbers it +// does use may not have gaps. +func validatePlaceholders(occs []occurrence) (dollar bool, err error) { + var numbered, unnumbered bool + seen := map[int]bool{} + for _, occ := range occs { + if occ.kind != kindNative { + continue + } + if occ.explicit { + numbered = true + seen[occ.number] = true + } else { + unnumbered = true + } + } + if numbered && unnumbered { + return false, errors.New("can not mix $1 format with ? format") + } + for i := 1; i <= len(seen); i++ { + if !seen[i] { + return !unnumbered, &sqlerr.Error{ + Code: "42P18", + Message: fmt.Sprintf("could not determine data type of parameter $%d", i), + } + } + } + return !unnumbered, nil +} + +// number assigns a placeholder number to every occurrence and builds the +// parameter set for the statement. +func number(d Dialect, occs []occurrence) *named.ParamSet { + if d.Style == StyleQuestion { + // Every "?" is sent as its own argument, so existing and rewritten + // placeholders are numbered together in source order. + ps := named.NewParamSet(nil, false) + for i := range occs { + occ := &occs[i] + if occ.kind == kindEmbed { + continue + } + occ.number = ps.Add(param(occ)) + } + return ps + } + + // Numbered dialects keep the numbers the user wrote and fill in the gaps. + numbs := map[int]bool{} + count := 0 + for i := range occs { + occ := &occs[i] + if occ.kind != kindNative { + continue + } + count++ + if !occ.explicit { + occ.number = count + } + numbs[occ.number] = true + } + ps := named.NewParamSet(numbs, d.hasNamedSupport()) + for i := range occs { + occ := &occs[i] + if occ.kind == kindEmbed || occ.kind == kindNative { + continue + } + occ.number = ps.Add(param(occ)) + } + return ps +} + +func param(occ *occurrence) named.Param { + switch occ.kind { + case kindNarg: + return named.NewUserNullableParam(occ.name) + case kindSlice: + return named.NewSqlcSlice(occ.name) + default: + return named.NewParam(occ.name) + } +} + +// replacement returns the native SQL that stands in for an occurrence. +func (d Dialect) replacement(occ *occurrence) string { + switch occ.kind { + case kindEmbed: + return occ.ident + ".*" + case kindNative: + return occ.ident + } + switch d.Style { + case StyleDollar: + // PostgreSQL passes a slice as a single array parameter, so it needs no + // expansion marker. + return fmt.Sprintf("$%d", occ.number) + case StyleOrdinal: + if occ.kind == kindSlice { + // This sequence is also replicated in + // internal/codegen/golang.Field since it's needed during template + // generation for replacement. + return fmt.Sprintf("/*SLICE:%s*/?", occ.name) + } + return fmt.Sprintf("?%d", occ.number) + default: + if occ.kind == kindSlice { + return fmt.Sprintf("/*SLICE:%s*/?", occ.name) + } + return "?" + } +} diff --git a/internal/sql/preprocess/preprocess_test.go b/internal/sql/preprocess/preprocess_test.go new file mode 100644 index 0000000000..5b0eb3e0aa --- /dev/null +++ b/internal/sql/preprocess/preprocess_test.go @@ -0,0 +1,232 @@ +package preprocess_test + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/named" + "github.com/sqlc-dev/sqlc/internal/sql/preprocess" + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +var update = flag.Bool("update", false, "update the testdata golden files") + +// TestRewrite runs every case under testdata. Each case is a directory holding +// the input and the expected results, grouped by engine: +// +// testdata///input.sql the query file +// testdata///output.sql the rewritten SQL +// testdata///side_table.json what the preprocessor recorded +// testdata///stderr.txt reported errors (only when invalid) +// +// Regenerate the golden files with `go test ./internal/sql/preprocess -update`. +func TestRewrite(t *testing.T) { + for _, dir := range cases(t) { + t.Run(filepath.ToSlash(dir), func(t *testing.T) { + path := filepath.Join("testdata", dir) + engine := config.Engine(filepath.Dir(dir)) + + input := readFile(t, filepath.Join(path, "input.sql")) + res := preprocess.File(engine, input) + + compare(t, filepath.Join(path, "output.sql"), res.Text) + compare(t, filepath.Join(path, "side_table.json"), sideTable(res)) + compare(t, filepath.Join(path, "stderr.txt"), errors(input, res)) + + // A rewrite only ever replaces text with something shorter or + // equal in lines, so a line number taken from the rewritten text is + // never past the end of the original. + if want, got := strings.Count(input, "\n"), strings.Count(res.Text, "\n"); got > want { + t.Errorf("rewrite added lines: got %d, want at most %d", got, want) + } + }) + } +} + +// The JSON shape of the side table the preprocessor records alongside the +// rewritten SQL. Locations are offsets into the rewritten text; origins are the +// offsets they map back to in the original source. +type statement struct { + Start int `json:"start"` + End int `json:"end"` + Dollar bool `json:"dollar"` + Params []parameter `json:"params,omitempty"` + Embeds []embed `json:"embeds,omitempty"` + Error string `json:"error,omitempty"` + ErrorAt int `json:"error_at,omitempty"` + ParamError string `json:"param_error,omitempty"` +} + +type parameter struct { + Number int `json:"number"` + Location int `json:"location"` + Origin int `json:"origin"` + Name string `json:"name,omitempty"` + // Named is false for a placeholder the user wrote themselves, which the + // preprocessor numbers but does not name. + Named bool `json:"named"` + // Nullable is true when sqlc.narg() forced the parameter nullable, even + // though the schema says it is NOT NULL. + Nullable bool `json:"nullable,omitempty"` + Slice bool `json:"slice,omitempty"` +} + +type embed struct { + Table string `json:"table"` + Location int `json:"location"` + Origin int `json:"origin"` + Orig string `json:"orig"` +} + +// sideTable renders everything the preprocessor recorded, reading it back +// through the same API the compiler uses. +func sideTable(res *preprocess.Result) string { + out := make([]statement, 0, len(res.Statements())) + for _, stmt := range res.Statements() { + s := statement{Start: stmt.Start, End: stmt.End, Dollar: stmt.Dollar} + if stmt.Err != nil { + s.Error = stmt.Err.Error() + if e, ok := stmt.Err.(*sqlerr.Error); ok { + s.ErrorAt = e.Location + } + } + if stmt.ParamErr != nil { + s.ParamError = stmt.ParamErr.Error() + } + + locations := make([]int, 0, len(stmt.Numbers)) + for loc := range stmt.Numbers { + locations = append(locations, loc) + } + sort.Ints(locations) + for _, loc := range locations { + number := stmt.Numbers[loc] + name, _ := stmt.Params.NameFor(number) + // Merging against a NOT NULL parameter is how the compiler asks + // whether the user overrode nullability with sqlc.narg(). + p, isNamed := stmt.Params.FetchMerge(number, named.NewInferredParam(name, true)) + _, isSlice := stmt.Slices[loc] + s.Params = append(s.Params, parameter{ + Number: number, + Location: loc, + Origin: res.Origin(loc), + Name: name, + Named: isNamed, + Nullable: isNamed && !p.NotNull(), + Slice: isSlice, + }) + } + + for _, e := range stmt.Embeds { + s.Embeds = append(s.Embeds, embed{ + Table: e.Table.Name, + Location: e.Location, + Origin: res.Origin(e.Location), + Orig: e.Orig(), + }) + } + out = append(out, s) + } + + blob, err := json.MarshalIndent(out, "", " ") + if err != nil { + panic(err) + } + return string(blob) + "\n" +} + +// errors renders every validation error the preprocessor recorded, one per +// line, as "line:column: message" against the original source. +func errors(input string, res *preprocess.Result) string { + var out []string + for _, stmt := range res.Statements() { + for _, err := range []error{stmt.Err, stmt.ParamErr} { + if err == nil { + continue + } + loc := stmt.Start + if e, ok := err.(*sqlerr.Error); ok && e.Location != 0 { + loc = e.Location + } + line, column := source.LineNumber(input, res.Origin(loc)) + out = append(out, fmt.Sprintf("%d:%d: %s", line, column, err)) + } + } + if len(out) == 0 { + return "" + } + return strings.Join(out, "\n") + "\n" +} + +// cases returns every testdata directory that holds an input.sql, relative to +// testdata and in a stable order. +func cases(t *testing.T) []string { + t.Helper() + var dirs []string + err := filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || info.Name() != "input.sql" { + return nil + } + rel, err := filepath.Rel("testdata", filepath.Dir(path)) + if err != nil { + return err + } + dirs = append(dirs, rel) + return nil + }) + if err != nil { + t.Fatal(err) + } + if len(dirs) == 0 { + t.Fatal("no test cases found in testdata") + } + sort.Strings(dirs) + return dirs +} + +func readFile(t *testing.T, path string) string { + t.Helper() + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(blob) +} + +// compare checks actual against a golden file. An empty expectation means the +// file should not exist. +func compare(t *testing.T, path, actual string) { + t.Helper() + if *update { + if actual == "" { + os.Remove(path) + return + } + if err := os.WriteFile(path, []byte(actual), 0644); err != nil { + t.Fatal(err) + } + return + } + var expected string + if blob, err := os.ReadFile(path); err == nil { + expected = string(blob) + } else if !os.IsNotExist(err) { + t.Fatal(err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("%s differed (-want +got):\n%s", filepath.Base(path), diff) + } +} diff --git a/internal/sql/preprocess/scan.go b/internal/sql/preprocess/scan.go new file mode 100644 index 0000000000..b2b4b1f534 --- /dev/null +++ b/internal/sql/preprocess/scan.go @@ -0,0 +1,209 @@ +package preprocess + +import ( + "fmt" + "strconv" + "strings" + + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +// scan walks a single statement and returns every sqlc construct and native +// placeholder it contains, in source order. +func (l *lexer) scan(start, end int) []occurrence { + var out []occurrence + for i := start; i < end; { + if stop := l.skip(i); stop >= 0 { + if stop <= i { + stop = i + 1 + } + i = stop + continue + } + c := l.src[i] + switch { + case isIdentStart(c): + j := l.identEnd(i) + if strings.EqualFold(l.src[i:j], "sqlc") { + if occ, next, ok := l.sqlcCall(i, j); ok { + out = append(out, occ) + i = next + continue + } + } + i = j + + case c == '@' && l.d.AtSign && isIdentStart(l.at(i+1)): + j := l.identEnd(i + 1) + out = append(out, occurrence{ + kind: kindArg, + start: i, + end: j, + name: l.src[i+1 : j], + ident: l.src[i:j], + }) + i = j + + case c == '$' && l.d.DollarNumber && isDigit(l.at(i+1)): + j := i + 1 + for j < len(l.src) && isDigit(l.src[j]) { + j++ + } + n, _ := strconv.Atoi(l.src[i+1 : j]) + out = append(out, occurrence{ + kind: kindNative, + start: i, + end: j, + number: n, + explicit: true, + ident: l.src[i:j], + }) + i = j + + case c == '?' && l.d.Question: + j := i + 1 + for j < len(l.src) && isDigit(l.src[j]) { + j++ + } + occ := occurrence{kind: kindNative, start: i, end: j, ident: l.src[i:j]} + if j > i+1 { + occ.number, _ = strconv.Atoi(l.src[i+1 : j]) + occ.explicit = true + } + out = append(out, occ) + i = j + + default: + i++ + } + } + return out +} + +// sqlcCall parses a "sqlc.()" call that starts at start, where +// afterSqlc is the offset just past the "sqlc" identifier. It reports false +// when the text is not a sqlc call at all, in which case scanning continues +// normally. +func (l *lexer) sqlcCall(start, afterSqlc int) (occurrence, int, bool) { + i := l.skipSpace(afterSqlc) + if l.at(i) != '.' { + return occurrence{}, 0, false + } + i = l.skipSpace(i + 1) + if !isIdentStart(l.at(i)) { + return occurrence{}, 0, false + } + nameEnd := l.identEnd(i) + fn := strings.ToLower(l.src[i:nameEnd]) + + open := l.skipSpace(nameEnd) + if l.at(open) != '(' { + return occurrence{}, 0, false + } + closing := l.matchParen(open) + if closing < 0 { + return occurrence{}, 0, false + } + + occ := occurrence{start: start, end: closing + 1} + switch fn { + case "arg": + occ.kind = kindArg + case "narg": + occ.kind = kindNarg + case "slice": + occ.kind = kindSlice + case "embed": + occ.kind = kindEmbed + default: + occ.err = sqlerr.FunctionNotFound("sqlc." + fn) + occ.err.Location = start + return occ, occ.end, true + } + + args := l.splitArgs(open+1, closing) + if len(args) != 1 { + occ.err = &sqlerr.Error{ + Message: fmt.Sprintf("expected 1 parameter to sqlc.%s; got %d", fn, len(args)), + Location: start, + } + return occ, occ.end, true + } + + name, ident, ok := argument(l.d, args[0]) + if !ok { + occ.err = &sqlerr.Error{ + Message: fmt.Sprintf("expected parameter to sqlc.%s to be string or reference; got %q", fn, strings.TrimSpace(args[0])), + Location: start, + } + return occ, occ.end, true + } + occ.name = name + occ.ident = ident + return occ, occ.end, true +} + +// argument interprets the single argument to a sqlc function. It returns the +// parameter name and the text to reuse when rebuilding a reference, which keeps +// any identifier quoting the user wrote. +func argument(d Dialect, arg string) (name, ident string, ok bool) { + arg = strings.TrimSpace(arg) + if arg == "" { + return "", "", false + } + switch arg[0] { + case '\'': + // A string constant names the parameter but is not a reference, so the + // rebuilt identifier drops the quotes. + s, ok := unquote(arg, '\'') + return s, s, ok + case '"': + s, ok := unquote(arg, '"') + if !ok { + return "", "", false + } + if d.DoubleQuoteString { + return s, s, true + } + return s, arg, true + case '`': + if !d.Backtick { + return "", "", false + } + s, ok := unquote(arg, '`') + if !ok { + return "", "", false + } + return s, arg, true + } + if !isIdentStart(arg[0]) { + return "", "", false + } + for i := 0; i < len(arg); i++ { + if !isIdentPart(arg[i]) && arg[i] != '.' { + return "", "", false + } + } + // A qualified reference flattens to a single name, matching how the AST + // rewrite joined the parts of a ColumnRef. + name = strings.ReplaceAll(arg, ".", "") + if d.FoldIdentifier { + name = strings.ToLower(name) + } + return name, arg, true +} + +func unquote(s string, quote byte) (string, bool) { + if len(s) < 2 || s[0] != quote || s[len(s)-1] != quote { + return "", false + } + inner := s[1 : len(s)-1] + if strings.IndexByte(inner, quote) >= 0 { + doubled := string([]byte{quote, quote}) + if strings.Contains(inner, doubled) { + return strings.ReplaceAll(inner, doubled, string(quote)), true + } + return "", false + } + return inner, true +} diff --git a/internal/sql/preprocess/testdata/mysql/arg/input.sql b/internal/sql/preprocess/testdata/mysql/arg/input.sql new file mode 100644 index 0000000000..180ea26b3b --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/arg/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.arg(x); diff --git a/internal/sql/preprocess/testdata/mysql/arg/output.sql b/internal/sql/preprocess/testdata/mysql/arg/output.sql new file mode 100644 index 0000000000..0091b6db39 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/arg/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ? AND b = ?; diff --git a/internal/sql/preprocess/testdata/mysql/arg/side_table.json b/internal/sql/preprocess/testdata/mysql/arg/side_table.json new file mode 100644 index 0000000000..78b83b34a6 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/arg/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 43, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 41, + "origin": 51, + "name": "x", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/input.sql b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/input.sql new file mode 100644 index 0000000000..d933db8127 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/input.sql @@ -0,0 +1 @@ +SELECT * FROM second_view WHERE val2 = $1; diff --git a/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/output.sql b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/output.sql new file mode 100644 index 0000000000..d933db8127 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/output.sql @@ -0,0 +1 @@ +SELECT * FROM second_view WHERE val2 = $1; diff --git a/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/side_table.json b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/side_table.json new file mode 100644 index 0000000000..05701e1815 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/dollar_number_is_not_a_placeholder/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 42, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/mysql/double_quoted_argument/input.sql b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/input.sql new file mode 100644 index 0000000000..37f43141bf --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/input.sql @@ -0,0 +1,4 @@ +SELECT + (CASE WHEN sqlc.arg("Timezone") = "calendar" THEN cal.Timezone ELSE sqlc.arg("Timezone") END), + COALESCE(sqlc.narg("calName"), "") +FROM Calendar cal; diff --git a/internal/sql/preprocess/testdata/mysql/double_quoted_argument/output.sql b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/output.sql new file mode 100644 index 0000000000..201532c117 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/output.sql @@ -0,0 +1,4 @@ +SELECT + (CASE WHEN ? = "calendar" THEN cal.Timezone ELSE ? END), + COALESCE(?, "") +FROM Calendar cal; diff --git a/internal/sql/preprocess/testdata/mysql/double_quoted_argument/side_table.json b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/side_table.json new file mode 100644 index 0000000000..711a8833c2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/double_quoted_argument/side_table.json @@ -0,0 +1,31 @@ +[ + { + "start": 0, + "end": 106, + "dollar": true, + "params": [ + { + "number": 1, + "location": 22, + "origin": 22, + "name": "Timezone", + "named": true + }, + { + "number": 2, + "location": 60, + "origin": 79, + "name": "Timezone", + "named": true + }, + { + "number": 3, + "location": 81, + "origin": 119, + "name": "calName", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/embed/input.sql b/internal/sql/preprocess/testdata/mysql/embed/input.sql new file mode 100644 index 0000000000..bb5188a008 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/embed/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(users) FROM users; diff --git a/internal/sql/preprocess/testdata/mysql/embed/output.sql b/internal/sql/preprocess/testdata/mysql/embed/output.sql new file mode 100644 index 0000000000..e9d0968e18 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/embed/output.sql @@ -0,0 +1 @@ +SELECT users.* FROM users; diff --git a/internal/sql/preprocess/testdata/mysql/embed/side_table.json b/internal/sql/preprocess/testdata/mysql/embed/side_table.json new file mode 100644 index 0000000000..840d08c39d --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/embed/side_table.json @@ -0,0 +1,15 @@ +[ + { + "start": 0, + "end": 26, + "dollar": true, + "embeds": [ + { + "table": "users", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(users)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/error_invalid_calls/input.sql b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/input.sql new file mode 100644 index 0000000000..51f7ddb8ee --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/input.sql @@ -0,0 +1,11 @@ +-- name: WrongFunc :one +SELECT id FROM users WHERE id = sqlc.argh(target_id); + +-- name: TooManyArgs :one +SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar'); + +-- name: TooFewArgs :one +SELECT id FROM users WHERE id = sqlc.arg(); + +-- name: InvalidArgPlaceholder :one +SELECT id FROM users WHERE id = sqlc.arg(?); diff --git a/internal/sql/preprocess/testdata/mysql/error_invalid_calls/output.sql b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/output.sql new file mode 100644 index 0000000000..51f7ddb8ee --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/output.sql @@ -0,0 +1,11 @@ +-- name: WrongFunc :one +SELECT id FROM users WHERE id = sqlc.argh(target_id); + +-- name: TooManyArgs :one +SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar'); + +-- name: TooFewArgs :one +SELECT id FROM users WHERE id = sqlc.arg(); + +-- name: InvalidArgPlaceholder :one +SELECT id FROM users WHERE id = sqlc.arg(?); diff --git a/internal/sql/preprocess/testdata/mysql/error_invalid_calls/side_table.json b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/side_table.json new file mode 100644 index 0000000000..a36ba15e41 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 77, + "dollar": true, + "error": "function \"sqlc.argh\" does not exist", + "error_at": 56 + }, + { + "start": 77, + "end": 160, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 2", + "error_at": 137 + }, + { + "start": 160, + "end": 230, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 0", + "error_at": 219 + }, + { + "start": 230, + "end": 312, + "dollar": true, + "error": "expected parameter to sqlc.arg to be string or reference; got \"?\"", + "error_at": 300 + } +] diff --git a/internal/sql/preprocess/testdata/mysql/error_invalid_calls/stderr.txt b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/stderr.txt new file mode 100644 index 0000000000..aae154ee46 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/error_invalid_calls/stderr.txt @@ -0,0 +1,4 @@ +2:33: function "sqlc.argh" does not exist +5:33: expected 1 parameter to sqlc.arg; got 2 +8:33: expected 1 parameter to sqlc.arg; got 0 +11:33: expected parameter to sqlc.arg to be string or reference; got "?" diff --git a/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/input.sql b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/input.sql new file mode 100644 index 0000000000..9b9c75668e --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ? AND b = sqlc.arg(x) AND c = ?; diff --git a/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/output.sql b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/output.sql new file mode 100644 index 0000000000..8f52c1ae1b --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ? AND b = ? AND c = ?; diff --git a/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/side_table.json b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/side_table.json new file mode 100644 index 0000000000..2f8a512eef --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/existing_placeholders_are_numbered_together/side_table.json @@ -0,0 +1,28 @@ +[ + { + "start": 0, + "end": 53, + "dollar": false, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "named": false + }, + { + "number": 2, + "location": 41, + "origin": 41, + "name": "x", + "named": true + }, + { + "number": 3, + "location": 51, + "origin": 61, + "named": false + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/narg_identifier/input.sql b/internal/sql/preprocess/testdata/mysql/narg_identifier/input.sql new file mode 100644 index 0000000000..c36f3d6aaa --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/narg_identifier/input.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = sqlc.narg(name); diff --git a/internal/sql/preprocess/testdata/mysql/narg_identifier/output.sql b/internal/sql/preprocess/testdata/mysql/narg_identifier/output.sql new file mode 100644 index 0000000000..e48f33043d --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/narg_identifier/output.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = ?; diff --git a/internal/sql/preprocess/testdata/mysql/narg_identifier/side_table.json b/internal/sql/preprocess/testdata/mysql/narg_identifier/side_table.json new file mode 100644 index 0000000000..c3b25fbcda --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/narg_identifier/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 35, + "dollar": true, + "params": [ + { + "number": 1, + "location": 33, + "origin": 33, + "name": "name", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/input.sql b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/input.sql new file mode 100644 index 0000000000..4da5824466 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/input.sql @@ -0,0 +1,2 @@ +INSERT INTO t (a) VALUES (sqlc.arg(val)) +ON DUPLICATE KEY UPDATE a = sqlc.arg(new_val); diff --git a/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/output.sql b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/output.sql new file mode 100644 index 0000000000..9681976c94 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/output.sql @@ -0,0 +1,2 @@ +INSERT INTO t (a) VALUES (?) +ON DUPLICATE KEY UPDATE a = ?; diff --git a/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/side_table.json b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/side_table.json new file mode 100644 index 0000000000..6b4e506b56 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/on_duplicate_key_update/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 59, + "dollar": true, + "params": [ + { + "number": 1, + "location": 26, + "origin": 26, + "name": "val", + "named": true + }, + { + "number": 2, + "location": 57, + "origin": 69, + "name": "new_val", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/input.sql b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/input.sql new file mode 100644 index 0000000000..ae9b08ce68 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/input.sql @@ -0,0 +1 @@ +SELECT 'a\' sqlc.arg(nope)'; diff --git a/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/output.sql b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/output.sql new file mode 100644 index 0000000000..ae9b08ce68 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/output.sql @@ -0,0 +1 @@ +SELECT 'a\' sqlc.arg(nope)'; diff --git a/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/side_table.json b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/side_table.json new file mode 100644 index 0000000000..7ab9eaedd2 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backslash_escape/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 28, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/input.sql b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/input.sql new file mode 100644 index 0000000000..124b3d3a96 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/input.sql @@ -0,0 +1 @@ +SELECT `sqlc.arg(nope)` FROM t; diff --git a/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/output.sql b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/output.sql new file mode 100644 index 0000000000..124b3d3a96 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/output.sql @@ -0,0 +1 @@ +SELECT `sqlc.arg(nope)` FROM t; diff --git a/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/side_table.json b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/side_table.json new file mode 100644 index 0000000000..cdde9a6d54 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_backtick_identifier/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 31, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/input.sql b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/input.sql new file mode 100644 index 0000000000..3fb25577ba --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/input.sql @@ -0,0 +1 @@ +SELECT "sqlc.arg(nope)" FROM t; diff --git a/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/output.sql b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/output.sql new file mode 100644 index 0000000000..3fb25577ba --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/output.sql @@ -0,0 +1 @@ +SELECT "sqlc.arg(nope)" FROM t; diff --git a/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/side_table.json b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/side_table.json new file mode 100644 index 0000000000..cdde9a6d54 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_double_quoted_string/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 31, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/mysql/skip_hash_comment/input.sql b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/input.sql new file mode 100644 index 0000000000..755efa7a13 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/input.sql @@ -0,0 +1,2 @@ +# sqlc.arg(nope) +SELECT 1; diff --git a/internal/sql/preprocess/testdata/mysql/skip_hash_comment/output.sql b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/output.sql new file mode 100644 index 0000000000..755efa7a13 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/output.sql @@ -0,0 +1,2 @@ +# sqlc.arg(nope) +SELECT 1; diff --git a/internal/sql/preprocess/testdata/mysql/skip_hash_comment/side_table.json b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/side_table.json new file mode 100644 index 0000000000..667e960a18 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/skip_hash_comment/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 26, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/mysql/slice/input.sql b/internal/sql/preprocess/testdata/mysql/slice/input.sql new file mode 100644 index 0000000000..f5d89640d7 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (sqlc.slice(ids)); diff --git a/internal/sql/preprocess/testdata/mysql/slice/output.sql b/internal/sql/preprocess/testdata/mysql/slice/output.sql new file mode 100644 index 0000000000..542215158c --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (/*SLICE:ids*/?); diff --git a/internal/sql/preprocess/testdata/mysql/slice/side_table.json b/internal/sql/preprocess/testdata/mysql/slice/side_table.json new file mode 100644 index 0000000000..4b16794ab9 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 50, + "dollar": true, + "params": [ + { + "number": 1, + "location": 47, + "origin": 34, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/input.sql b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/input.sql new file mode 100644 index 0000000000..3fdbab315d --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/input.sql @@ -0,0 +1 @@ +SELECT * FROM foo WHERE retyped IN (sqlc.slice(paramName)); diff --git a/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/output.sql b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/output.sql new file mode 100644 index 0000000000..451d50ce5b --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/output.sql @@ -0,0 +1 @@ +SELECT * FROM foo WHERE retyped IN (/*SLICE:paramname*/?); diff --git a/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/side_table.json b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/side_table.json new file mode 100644 index 0000000000..43154fe710 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/slice_folds_identifier/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 58, + "dollar": true, + "params": [ + { + "number": 1, + "location": 55, + "origin": 36, + "name": "paramname", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/string_constant/input.sql b/internal/sql/preprocess/testdata/mysql/string_constant/input.sql new file mode 100644 index 0000000000..21f5701a00 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg('x') AND b = sqlc.narg('y') AND c IN (sqlc.slice('ids')); diff --git a/internal/sql/preprocess/testdata/mysql/string_constant/output.sql b/internal/sql/preprocess/testdata/mysql/string_constant/output.sql new file mode 100644 index 0000000000..7dd7adcca6 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ? AND b = ? AND c IN (/*SLICE:ids*/?); diff --git a/internal/sql/preprocess/testdata/mysql/string_constant/side_table.json b/internal/sql/preprocess/testdata/mysql/string_constant/side_table.json new file mode 100644 index 0000000000..45ffcd24b8 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant/side_table.json @@ -0,0 +1,32 @@ +[ + { + "start": 0, + "end": 69, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 41, + "origin": 53, + "name": "y", + "named": true, + "nullable": true + }, + { + "number": 3, + "location": 66, + "origin": 78, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/input.sql b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/input.sql new file mode 100644 index 0000000000..beec84e61c --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('MixedCase'), sqlc.arg(MixedCase); diff --git a/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/output.sql b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/output.sql new file mode 100644 index 0000000000..99dcdde756 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/output.sql @@ -0,0 +1 @@ +SELECT ?, ?; diff --git a/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/side_table.json b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/side_table.json new file mode 100644 index 0000000000..16a138bfc4 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/string_constant_keeps_case/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 12, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "MixedCase", + "named": true + }, + { + "number": 2, + "location": 10, + "origin": 30, + "name": "mixedcase", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/input.sql b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/input.sql new file mode 100644 index 0000000000..ce518fcf22 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/input.sql @@ -0,0 +1 @@ +SELECT @row := @row + 1 FROM users WHERE id = sqlc.arg(id); diff --git a/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/output.sql b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/output.sql new file mode 100644 index 0000000000..4db8a1b06c --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/output.sql @@ -0,0 +1 @@ +SELECT @row := @row + 1 FROM users WHERE id = ?; diff --git a/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/side_table.json b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/side_table.json new file mode 100644 index 0000000000..ccf1ada0f7 --- /dev/null +++ b/internal/sql/preprocess/testdata/mysql/user_variables_are_not_parameters/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 48, + "dollar": true, + "params": [ + { + "number": 1, + "location": 46, + "origin": 46, + "name": "id", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/arg/input.sql b/internal/sql/preprocess/testdata/postgresql/arg/input.sql new file mode 100644 index 0000000000..4f78775148 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/arg/input.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id FROM users WHERE name = sqlc.arg(name); diff --git a/internal/sql/preprocess/testdata/postgresql/arg/output.sql b/internal/sql/preprocess/testdata/postgresql/arg/output.sql new file mode 100644 index 0000000000..e6d2e2ea8a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/arg/output.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id FROM users WHERE name = $1; diff --git a/internal/sql/preprocess/testdata/postgresql/arg/side_table.json b/internal/sql/preprocess/testdata/postgresql/arg/side_table.json new file mode 100644 index 0000000000..d01162f091 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/arg/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 59, + "dollar": true, + "params": [ + { + "number": 1, + "location": 56, + "origin": 56, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign/input.sql b/internal/sql/preprocess/testdata/postgresql/at_sign/input.sql new file mode 100644 index 0000000000..57c7883786 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE name = @name AND age = @age; diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign/output.sql b/internal/sql/preprocess/testdata/postgresql/at_sign/output.sql new file mode 100644 index 0000000000..90875ab313 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE name = $1 AND age = $2; diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign/side_table.json b/internal/sql/preprocess/testdata/postgresql/at_sign/side_table.json new file mode 100644 index 0000000000..bebc9a14d7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 50, + "dollar": true, + "params": [ + { + "number": 1, + "location": 34, + "origin": 34, + "name": "name", + "named": true + }, + { + "number": 2, + "location": 47, + "origin": 50, + "name": "age", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_cast/input.sql b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/input.sql new file mode 100644 index 0000000000..a167ed5961 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE ok = @flag::bool; diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_cast/output.sql b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/output.sql new file mode 100644 index 0000000000..28cdd95fca --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE ok = $1::bool; diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_cast/side_table.json b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/side_table.json new file mode 100644 index 0000000000..02ec768518 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_cast/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 41, + "dollar": true, + "params": [ + { + "number": 1, + "location": 32, + "origin": 32, + "name": "flag", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/input.sql b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/input.sql new file mode 100644 index 0000000000..d06af4e9eb --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/input.sql @@ -0,0 +1,3 @@ +-- name: SkipVetAll :exec +-- @sqlc-vet-disable always-fail no-exec +SELECT true FROM users WHERE id = sqlc.arg(id); diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/output.sql b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/output.sql new file mode 100644 index 0000000000..5bbca3dd62 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/output.sql @@ -0,0 +1,3 @@ +-- name: SkipVetAll :exec +-- @sqlc-vet-disable always-fail no-exec +SELECT true FROM users WHERE id = $1; diff --git a/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/side_table.json b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/side_table.json new file mode 100644 index 0000000000..3430150bb0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/at_sign_in_vet_directive/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 104, + "dollar": true, + "params": [ + { + "number": 1, + "location": 101, + "origin": 101, + "name": "id", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/comment_inside_call/input.sql b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/input.sql new file mode 100644 index 0000000000..58bb27fc93 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg/* here */(name); diff --git a/internal/sql/preprocess/testdata/postgresql/comment_inside_call/output.sql b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/output.sql new file mode 100644 index 0000000000..37699f7c74 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/output.sql @@ -0,0 +1 @@ +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/comment_inside_call/side_table.json b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/side_table.json new file mode 100644 index 0000000000..80979ab73b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/comment_inside_call/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 10, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/embed/input.sql b/internal/sql/preprocess/testdata/postgresql/embed/input.sql new file mode 100644 index 0000000000..551607433a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(users), sqlc.embed(pets) FROM users JOIN pets ON true; diff --git a/internal/sql/preprocess/testdata/postgresql/embed/output.sql b/internal/sql/preprocess/testdata/postgresql/embed/output.sql new file mode 100644 index 0000000000..59d0cab37b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed/output.sql @@ -0,0 +1 @@ +SELECT users.*, pets.* FROM users JOIN pets ON true; diff --git a/internal/sql/preprocess/testdata/postgresql/embed/side_table.json b/internal/sql/preprocess/testdata/postgresql/embed/side_table.json new file mode 100644 index 0000000000..ed71a0d974 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed/side_table.json @@ -0,0 +1,21 @@ +[ + { + "start": 0, + "end": 52, + "dollar": true, + "embeds": [ + { + "table": "users", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(users)" + }, + { + "table": "pets", + "location": 16, + "origin": 26, + "orig": "sqlc.embed(pets)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/input.sql b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/input.sql new file mode 100644 index 0000000000..bec18076c7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(a), b.* FROM a, b; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/output.sql b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/output.sql new file mode 100644 index 0000000000..e3ea7edc61 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/output.sql @@ -0,0 +1 @@ +SELECT a.*, b.* FROM a, b; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/side_table.json b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/side_table.json new file mode 100644 index 0000000000..d30b005ee8 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_and_user_star/side_table.json @@ -0,0 +1,15 @@ +[ + { + "start": 0, + "end": 26, + "dollar": true, + "embeds": [ + { + "table": "a", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(a)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/input.sql b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/input.sql new file mode 100644 index 0000000000..86af411c74 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed("Users") FROM "Users"; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/output.sql b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/output.sql new file mode 100644 index 0000000000..390936296b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/output.sql @@ -0,0 +1 @@ +SELECT "Users".* FROM "Users"; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/side_table.json b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/side_table.json new file mode 100644 index 0000000000..02f9e3ddb4 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_quoted_identifier/side_table.json @@ -0,0 +1,15 @@ +[ + { + "start": 0, + "end": 30, + "dollar": true, + "embeds": [ + { + "table": "Users", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(\"Users\")" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/embed_string_constant/input.sql b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/input.sql new file mode 100644 index 0000000000..77203a9818 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed('t') FROM t; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_string_constant/output.sql b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/output.sql new file mode 100644 index 0000000000..269c8bb8c7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/output.sql @@ -0,0 +1 @@ +SELECT t.* FROM t; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_string_constant/side_table.json b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/side_table.json new file mode 100644 index 0000000000..b7b97cfaf3 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_string_constant/side_table.json @@ -0,0 +1,15 @@ +[ + { + "start": 0, + "end": 18, + "dollar": true, + "embeds": [ + { + "table": "t", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(t)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/embed_with_args/input.sql b/internal/sql/preprocess/testdata/postgresql/embed_with_args/input.sql new file mode 100644 index 0000000000..68b9efb581 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_with_args/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(a), b.name FROM a JOIN b ON b.id = a.b_id WHERE a.id = sqlc.arg(id); diff --git a/internal/sql/preprocess/testdata/postgresql/embed_with_args/output.sql b/internal/sql/preprocess/testdata/postgresql/embed_with_args/output.sql new file mode 100644 index 0000000000..aaa0860aa2 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_with_args/output.sql @@ -0,0 +1 @@ +SELECT a.*, b.name FROM a JOIN b ON b.id = a.b_id WHERE a.id = $1; diff --git a/internal/sql/preprocess/testdata/postgresql/embed_with_args/side_table.json b/internal/sql/preprocess/testdata/postgresql/embed_with_args/side_table.json new file mode 100644 index 0000000000..93a6a0f157 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/embed_with_args/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 66, + "dollar": true, + "params": [ + { + "number": 1, + "location": 63, + "origin": 73, + "name": "id", + "named": true + } + ], + "embeds": [ + { + "table": "a", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(a)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/input.sql b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/input.sql new file mode 100644 index 0000000000..f1f94d0d0a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/input.sql @@ -0,0 +1,2 @@ +SELECT sqlc.argh(a); +SELECT sqlc.arg(b); diff --git a/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/output.sql b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/output.sql new file mode 100644 index 0000000000..43f926284c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/output.sql @@ -0,0 +1,2 @@ +SELECT sqlc.argh(a); +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/side_table.json new file mode 100644 index 0000000000..e4c5d10b30 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 20, + "dollar": true, + "error": "function \"sqlc.argh\" does not exist", + "error_at": 7 + }, + { + "start": 20, + "end": 31, + "dollar": true, + "params": [ + { + "number": 1, + "location": 28, + "origin": 28, + "name": "b", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/stderr.txt new file mode 100644 index 0000000000..c55d98608c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_is_per_statement/stderr.txt @@ -0,0 +1 @@ +1:8: function "sqlc.argh" does not exist diff --git a/internal/sql/preprocess/testdata/postgresql/error_nested_call/input.sql b/internal/sql/preprocess/testdata/postgresql/error_nested_call/input.sql new file mode 100644 index 0000000000..e8d7485252 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_nested_call/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg(sqlc.arg(a)); diff --git a/internal/sql/preprocess/testdata/postgresql/error_nested_call/output.sql b/internal/sql/preprocess/testdata/postgresql/error_nested_call/output.sql new file mode 100644 index 0000000000..e8d7485252 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_nested_call/output.sql @@ -0,0 +1 @@ +SELECT sqlc.arg(sqlc.arg(a)); diff --git a/internal/sql/preprocess/testdata/postgresql/error_nested_call/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_nested_call/side_table.json new file mode 100644 index 0000000000..988c57ab6e --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_nested_call/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 29, + "dollar": true, + "error": "expected parameter to sqlc.arg to be string or reference; got \"sqlc.arg(a)\"", + "error_at": 7 + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_nested_call/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_nested_call/stderr.txt new file mode 100644 index 0000000000..7faabf74a6 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_nested_call/stderr.txt @@ -0,0 +1 @@ +1:8: expected parameter to sqlc.arg to be string or reference; got "sqlc.arg(a)" diff --git a/internal/sql/preprocess/testdata/postgresql/error_no_arguments/input.sql b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/input.sql new file mode 100644 index 0000000000..540bfb17b1 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg(); diff --git a/internal/sql/preprocess/testdata/postgresql/error_no_arguments/output.sql b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/output.sql new file mode 100644 index 0000000000..540bfb17b1 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/output.sql @@ -0,0 +1 @@ +SELECT sqlc.arg(); diff --git a/internal/sql/preprocess/testdata/postgresql/error_no_arguments/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/side_table.json new file mode 100644 index 0000000000..3f37493ae4 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 18, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 0", + "error_at": 7 + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_no_arguments/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/stderr.txt new file mode 100644 index 0000000000..46c7fcd096 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_no_arguments/stderr.txt @@ -0,0 +1 @@ +1:8: expected 1 parameter to sqlc.arg; got 0 diff --git a/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/input.sql b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/input.sql new file mode 100644 index 0000000000..5a0ead05c7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = $2 AND b = $3; diff --git a/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/output.sql b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/output.sql new file mode 100644 index 0000000000..5a0ead05c7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = $2 AND b = $3; diff --git a/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/side_table.json new file mode 100644 index 0000000000..18933caa30 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/side_table.json @@ -0,0 +1,22 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 2, + "location": 31, + "origin": 31, + "named": false + }, + { + "number": 3, + "location": 42, + "origin": 42, + "named": false + } + ], + "param_error": "could not determine data type of parameter $1" + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/stderr.txt new file mode 100644 index 0000000000..a9cbefb9c2 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_numbering_gap/stderr.txt @@ -0,0 +1 @@ +1:1: could not determine data type of parameter $1 diff --git a/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/input.sql b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/input.sql new file mode 100644 index 0000000000..3589b18fb6 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg($1); diff --git a/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/output.sql b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/output.sql new file mode 100644 index 0000000000..3589b18fb6 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/output.sql @@ -0,0 +1 @@ +SELECT sqlc.arg($1); diff --git a/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/side_table.json new file mode 100644 index 0000000000..918be18b6d --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 20, + "dollar": true, + "error": "expected parameter to sqlc.arg to be string or reference; got \"$1\"", + "error_at": 7 + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/stderr.txt new file mode 100644 index 0000000000..16d54e925a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_placeholder_argument/stderr.txt @@ -0,0 +1 @@ +1:8: expected parameter to sqlc.arg to be string or reference; got "$1" diff --git a/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/input.sql b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/input.sql new file mode 100644 index 0000000000..d5e1c628b6 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('a', 'b'); diff --git a/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/output.sql b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/output.sql new file mode 100644 index 0000000000..d5e1c628b6 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/output.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('a', 'b'); diff --git a/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/side_table.json new file mode 100644 index 0000000000..501e713b55 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 26, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 2", + "error_at": 7 + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/stderr.txt new file mode 100644 index 0000000000..92e71bd342 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_too_many_arguments/stderr.txt @@ -0,0 +1 @@ +1:8: expected 1 parameter to sqlc.arg; got 2 diff --git a/internal/sql/preprocess/testdata/postgresql/error_unknown_function/input.sql b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/input.sql new file mode 100644 index 0000000000..5a344b175a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/input.sql @@ -0,0 +1 @@ +SELECT sqlc.argh(a); diff --git a/internal/sql/preprocess/testdata/postgresql/error_unknown_function/output.sql b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/output.sql new file mode 100644 index 0000000000..5a344b175a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/output.sql @@ -0,0 +1 @@ +SELECT sqlc.argh(a); diff --git a/internal/sql/preprocess/testdata/postgresql/error_unknown_function/side_table.json b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/side_table.json new file mode 100644 index 0000000000..99e103eaa7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/side_table.json @@ -0,0 +1,9 @@ +[ + { + "start": 0, + "end": 20, + "dollar": true, + "error": "function \"sqlc.argh\" does not exist", + "error_at": 7 + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/error_unknown_function/stderr.txt b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/stderr.txt new file mode 100644 index 0000000000..c55d98608c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/error_unknown_function/stderr.txt @@ -0,0 +1 @@ +1:8: function "sqlc.argh" does not exist diff --git a/internal/sql/preprocess/testdata/postgresql/existing_placeholder/input.sql b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/input.sql new file mode 100644 index 0000000000..1d7497ac8b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = $1 AND b = sqlc.arg(x); diff --git a/internal/sql/preprocess/testdata/postgresql/existing_placeholder/output.sql b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/output.sql new file mode 100644 index 0000000000..1742cc3d85 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = $1 AND b = $2; diff --git a/internal/sql/preprocess/testdata/postgresql/existing_placeholder/side_table.json b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/side_table.json new file mode 100644 index 0000000000..8f0974672b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/existing_placeholder/side_table.json @@ -0,0 +1,22 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "named": false + }, + { + "number": 2, + "location": 42, + "origin": 42, + "name": "x", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/extra_whitespace/input.sql b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/input.sql new file mode 100644 index 0000000000..1ddb921525 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg ( name ), sqlc . arg(other); diff --git a/internal/sql/preprocess/testdata/postgresql/extra_whitespace/output.sql b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/output.sql new file mode 100644 index 0000000000..ca9e5428bf --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/output.sql @@ -0,0 +1 @@ +SELECT $1, $2; diff --git a/internal/sql/preprocess/testdata/postgresql/extra_whitespace/side_table.json b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/side_table.json new file mode 100644 index 0000000000..58dc58d402 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/extra_whitespace/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 14, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "name", + "named": true + }, + { + "number": 2, + "location": 11, + "origin": 28, + "name": "other", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/input.sql b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/input.sql new file mode 100644 index 0000000000..523e2d7ef0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/input.sql @@ -0,0 +1,2 @@ +SELECT * FROM transactions +WHERE jsonb_extract_path(transactions.data, '$.transaction.signatures[0]') @> to_jsonb(sqlc.arg('data')::text); diff --git a/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/output.sql b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/output.sql new file mode 100644 index 0000000000..acc0a99dc5 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/output.sql @@ -0,0 +1,2 @@ +SELECT * FROM transactions +WHERE jsonb_extract_path(transactions.data, '$.transaction.signatures[0]') @> to_jsonb($1::text); diff --git a/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/side_table.json b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/side_table.json new file mode 100644 index 0000000000..36f37fe0bb --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/jsonb_operator_and_path_string/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 124, + "dollar": true, + "params": [ + { + "number": 1, + "location": 114, + "origin": 114, + "name": "data", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/multiline_call/input.sql b/internal/sql/preprocess/testdata/postgresql/multiline_call/input.sql new file mode 100644 index 0000000000..d377f42ba0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/multiline_call/input.sql @@ -0,0 +1,3 @@ +SELECT sqlc.arg( + name +); diff --git a/internal/sql/preprocess/testdata/postgresql/multiline_call/output.sql b/internal/sql/preprocess/testdata/postgresql/multiline_call/output.sql new file mode 100644 index 0000000000..37699f7c74 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/multiline_call/output.sql @@ -0,0 +1 @@ +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/multiline_call/side_table.json b/internal/sql/preprocess/testdata/postgresql/multiline_call/side_table.json new file mode 100644 index 0000000000..80979ab73b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/multiline_call/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 10, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/narg/input.sql b/internal/sql/preprocess/testdata/postgresql/narg/input.sql new file mode 100644 index 0000000000..7e5109ad6e --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg/input.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id FROM users WHERE name = sqlc.narg(name); diff --git a/internal/sql/preprocess/testdata/postgresql/narg/output.sql b/internal/sql/preprocess/testdata/postgresql/narg/output.sql new file mode 100644 index 0000000000..e6d2e2ea8a --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg/output.sql @@ -0,0 +1,2 @@ +-- name: GetUser :one +SELECT id FROM users WHERE name = $1; diff --git a/internal/sql/preprocess/testdata/postgresql/narg/side_table.json b/internal/sql/preprocess/testdata/postgresql/narg/side_table.json new file mode 100644 index 0000000000..7d3175896b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 59, + "dollar": true, + "params": [ + { + "number": 1, + "location": 56, + "origin": 56, + "name": "name", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/narg_string_constant/input.sql b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/input.sql new file mode 100644 index 0000000000..1563e71033 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/input.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = COALESCE(sqlc.narg('name'), name); diff --git a/internal/sql/preprocess/testdata/postgresql/narg_string_constant/output.sql b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/output.sql new file mode 100644 index 0000000000..50612d0580 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/output.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = COALESCE($1, name); diff --git a/internal/sql/preprocess/testdata/postgresql/narg_string_constant/side_table.json b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/side_table.json new file mode 100644 index 0000000000..a076148449 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/narg_string_constant/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 52, + "dollar": true, + "params": [ + { + "number": 1, + "location": 42, + "origin": 42, + "name": "name", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/input.sql b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/input.sql new file mode 100644 index 0000000000..80e729d3de --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/input.sql @@ -0,0 +1,2 @@ +SELECT sqlc.arg(a); +SELECT sqlc.arg(b); diff --git a/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/output.sql b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/output.sql new file mode 100644 index 0000000000..aa0274e34d --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/output.sql @@ -0,0 +1,2 @@ +SELECT $1; +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/side_table.json b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/side_table.json new file mode 100644 index 0000000000..2921bcb179 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/numbering_restarts_per_statement/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 10, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "a", + "named": true + } + ] + }, + { + "start": 10, + "end": 21, + "dollar": true, + "params": [ + { + "number": 1, + "location": 18, + "origin": 27, + "name": "b", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/quoted_names/input.sql b/internal/sql/preprocess/testdata/postgresql/quoted_names/input.sql new file mode 100644 index 0000000000..155e2f5963 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/quoted_names/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('a'), sqlc.arg("b"); diff --git a/internal/sql/preprocess/testdata/postgresql/quoted_names/output.sql b/internal/sql/preprocess/testdata/postgresql/quoted_names/output.sql new file mode 100644 index 0000000000..ca9e5428bf --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/quoted_names/output.sql @@ -0,0 +1 @@ +SELECT $1, $2; diff --git a/internal/sql/preprocess/testdata/postgresql/quoted_names/side_table.json b/internal/sql/preprocess/testdata/postgresql/quoted_names/side_table.json new file mode 100644 index 0000000000..c6fedbd2fa --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/quoted_names/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 14, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "a", + "named": true + }, + { + "number": 2, + "location": 11, + "origin": 22, + "name": "b", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/repeated_name/input.sql b/internal/sql/preprocess/testdata/postgresql/repeated_name/input.sql new file mode 100644 index 0000000000..1f6121aa81 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/repeated_name/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg(x) OR b = sqlc.arg(x) OR c = sqlc.arg(y); diff --git a/internal/sql/preprocess/testdata/postgresql/repeated_name/output.sql b/internal/sql/preprocess/testdata/postgresql/repeated_name/output.sql new file mode 100644 index 0000000000..b138d6c0cc --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/repeated_name/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = $1 OR b = $1 OR c = $2; diff --git a/internal/sql/preprocess/testdata/postgresql/repeated_name/side_table.json b/internal/sql/preprocess/testdata/postgresql/repeated_name/side_table.json new file mode 100644 index 0000000000..4203a72bca --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/repeated_name/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 54, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 1, + "location": 41, + "origin": 50, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 51, + "origin": 69, + "name": "y", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_block_comment/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/input.sql new file mode 100644 index 0000000000..eb0245e5c0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/input.sql @@ -0,0 +1,3 @@ +/* sqlc.arg(nope) + @nope */ +SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_block_comment/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/output.sql new file mode 100644 index 0000000000..eb0245e5c0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/output.sql @@ -0,0 +1,3 @@ +/* sqlc.arg(nope) + @nope */ +SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_block_comment/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/side_table.json new file mode 100644 index 0000000000..33fdd7c2f2 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_block_comment/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 39, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/input.sql new file mode 100644 index 0000000000..448b6aef67 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/input.sql @@ -0,0 +1 @@ +CREATE FUNCTION f() RETURNS int AS $$ SELECT sqlc.arg(nope); $$ LANGUAGE sql; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/output.sql new file mode 100644 index 0000000000..448b6aef67 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/output.sql @@ -0,0 +1 @@ +CREATE FUNCTION f() RETURNS int AS $$ SELECT sqlc.arg(nope); $$ LANGUAGE sql; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/side_table.json new file mode 100644 index 0000000000..d67323c3c8 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_dollar_quoted/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 77, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_escape_string/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/input.sql new file mode 100644 index 0000000000..800fd62008 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/input.sql @@ -0,0 +1 @@ +SELECT E'\' sqlc.arg(nope)'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_escape_string/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/output.sql new file mode 100644 index 0000000000..800fd62008 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/output.sql @@ -0,0 +1 @@ +SELECT E'\' sqlc.arg(nope)'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_escape_string/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/side_table.json new file mode 100644 index 0000000000..7ab9eaedd2 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_escape_string/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 28, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/input.sql new file mode 100644 index 0000000000..a41da4767c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/input.sql @@ -0,0 +1 @@ +SELECT 1 FROM t WHERE a @> b AND j ? 'a'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/output.sql new file mode 100644 index 0000000000..a41da4767c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/output.sql @@ -0,0 +1 @@ +SELECT 1 FROM t WHERE a @> b AND j ? 'a'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/side_table.json new file mode 100644 index 0000000000..85b41b892c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_jsonb_operators/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 41, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_line_comment/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/input.sql new file mode 100644 index 0000000000..b718e04c02 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/input.sql @@ -0,0 +1,2 @@ +-- name: Get :one -- sqlc.arg(nope) @nope +SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_line_comment/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/output.sql new file mode 100644 index 0000000000..b718e04c02 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/output.sql @@ -0,0 +1,2 @@ +-- name: Get :one -- sqlc.arg(nope) @nope +SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_line_comment/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/side_table.json new file mode 100644 index 0000000000..93bd6b47a1 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_line_comment/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 51, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/input.sql new file mode 100644 index 0000000000..46ac007ae0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/input.sql @@ -0,0 +1 @@ +/* /* sqlc.arg(nope) */ @nope */ SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/output.sql new file mode 100644 index 0000000000..46ac007ae0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/output.sql @@ -0,0 +1 @@ +/* /* sqlc.arg(nope) */ @nope */ SELECT 1; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/side_table.json new file mode 100644 index 0000000000..05701e1815 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_nested_block_comment/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 42, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/input.sql new file mode 100644 index 0000000000..07cf2dc8da --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/input.sql @@ -0,0 +1 @@ +SELECT sqlcx.arg(a), other.embed(b); diff --git a/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/output.sql new file mode 100644 index 0000000000..07cf2dc8da --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/output.sql @@ -0,0 +1 @@ +SELECT sqlcx.arg(a), other.embed(b); diff --git a/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/side_table.json new file mode 100644 index 0000000000..c279f2b3d7 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_other_schemas/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 36, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/input.sql new file mode 100644 index 0000000000..3fb25577ba --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/input.sql @@ -0,0 +1 @@ +SELECT "sqlc.arg(nope)" FROM t; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/output.sql new file mode 100644 index 0000000000..3fb25577ba --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/output.sql @@ -0,0 +1 @@ +SELECT "sqlc.arg(nope)" FROM t; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/side_table.json new file mode 100644 index 0000000000..cdde9a6d54 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_quoted_identifier/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 31, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_string_literal/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/input.sql new file mode 100644 index 0000000000..4e5ef9f87f --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/input.sql @@ -0,0 +1 @@ +SELECT 'sqlc.arg(nope) @nope'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_string_literal/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/output.sql new file mode 100644 index 0000000000..4e5ef9f87f --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/output.sql @@ -0,0 +1 @@ +SELECT 'sqlc.arg(nope) @nope'; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_string_literal/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/side_table.json new file mode 100644 index 0000000000..06317d80bf --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_string_literal/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 30, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/input.sql b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/input.sql new file mode 100644 index 0000000000..8b4be6fdd8 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/input.sql @@ -0,0 +1 @@ +SELECT $tag$ sqlc.arg(nope) @nope $tag$; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/output.sql b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/output.sql new file mode 100644 index 0000000000..8b4be6fdd8 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/output.sql @@ -0,0 +1 @@ +SELECT $tag$ sqlc.arg(nope) @nope $tag$; diff --git a/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/side_table.json b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/side_table.json new file mode 100644 index 0000000000..3bd689e974 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/skip_tagged_dollar_quoted/side_table.json @@ -0,0 +1,7 @@ +[ + { + "start": 0, + "end": 40, + "dollar": true + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/input.sql b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/input.sql new file mode 100644 index 0000000000..0c69e21e7f --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id = ANY(sqlc.slice(ids)); diff --git a/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/output.sql b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/output.sql new file mode 100644 index 0000000000..06bb99772d --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id = ANY($1); diff --git a/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/side_table.json b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/side_table.json new file mode 100644 index 0000000000..f9350ea654 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_is_an_array/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 40, + "dollar": true, + "params": [ + { + "number": 1, + "location": 36, + "origin": 36, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/slice_string_constant/input.sql b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/input.sql new file mode 100644 index 0000000000..3132a9fa60 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/input.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE id = ANY(sqlc.slice('ids')); diff --git a/internal/sql/preprocess/testdata/postgresql/slice_string_constant/output.sql b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/output.sql new file mode 100644 index 0000000000..bd7ba1f310 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/output.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE id = ANY($1); diff --git a/internal/sql/preprocess/testdata/postgresql/slice_string_constant/side_table.json b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/side_table.json new file mode 100644 index 0000000000..d7f67eea34 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/slice_string_constant/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 39, + "dollar": true, + "params": [ + { + "number": 1, + "location": 35, + "origin": 35, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/input.sql b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/input.sql new file mode 100644 index 0000000000..1d254ec1a0 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/input.sql @@ -0,0 +1,2 @@ +SELECT ';', sqlc.arg(a); +SELECT sqlc.arg(b); diff --git a/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/output.sql b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/output.sql new file mode 100644 index 0000000000..f5baebfc00 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/output.sql @@ -0,0 +1,2 @@ +SELECT ';', $1; +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/side_table.json b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/side_table.json new file mode 100644 index 0000000000..a3e7e0545b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/statement_terminator_in_string/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 15, + "dollar": true, + "params": [ + { + "number": 1, + "location": 12, + "origin": 12, + "name": "a", + "named": true + } + ] + }, + { + "start": 15, + "end": 26, + "dollar": true, + "params": [ + { + "number": 1, + "location": 23, + "origin": 32, + "name": "b", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/input.sql b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/input.sql new file mode 100644 index 0000000000..1643d3792c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('it''s'); diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/output.sql b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/output.sql new file mode 100644 index 0000000000..37699f7c74 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/output.sql @@ -0,0 +1 @@ +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/side_table.json b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/side_table.json new file mode 100644 index 0000000000..5c7870733f --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_escaped_quote/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 10, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "it's", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/input.sql b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/input.sql new file mode 100644 index 0000000000..beec84e61c --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/input.sql @@ -0,0 +1 @@ +SELECT sqlc.arg('MixedCase'), sqlc.arg(MixedCase); diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/output.sql b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/output.sql new file mode 100644 index 0000000000..ca9e5428bf --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/output.sql @@ -0,0 +1 @@ +SELECT $1, $2; diff --git a/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/side_table.json b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/side_table.json new file mode 100644 index 0000000000..48ccbea4b5 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/string_constant_keeps_case/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 14, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "MixedCase", + "named": true + }, + { + "number": 2, + "location": 11, + "origin": 30, + "name": "mixedcase", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/postgresql/uppercase/input.sql b/internal/sql/preprocess/testdata/postgresql/uppercase/input.sql new file mode 100644 index 0000000000..4931f5942b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/uppercase/input.sql @@ -0,0 +1 @@ +SELECT SQLC.ARG(name); diff --git a/internal/sql/preprocess/testdata/postgresql/uppercase/output.sql b/internal/sql/preprocess/testdata/postgresql/uppercase/output.sql new file mode 100644 index 0000000000..37699f7c74 --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/uppercase/output.sql @@ -0,0 +1 @@ +SELECT $1; diff --git a/internal/sql/preprocess/testdata/postgresql/uppercase/side_table.json b/internal/sql/preprocess/testdata/postgresql/uppercase/side_table.json new file mode 100644 index 0000000000..80979ab73b --- /dev/null +++ b/internal/sql/preprocess/testdata/postgresql/uppercase/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 10, + "dollar": true, + "params": [ + { + "number": 1, + "location": 7, + "origin": 7, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/arg/input.sql b/internal/sql/preprocess/testdata/sqlite/arg/input.sql new file mode 100644 index 0000000000..238d2d1c85 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/arg/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.arg(y); diff --git a/internal/sql/preprocess/testdata/sqlite/arg/output.sql b/internal/sql/preprocess/testdata/sqlite/arg/output.sql new file mode 100644 index 0000000000..8d719f8cc6 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/arg/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = ?2; diff --git a/internal/sql/preprocess/testdata/sqlite/arg/side_table.json b/internal/sql/preprocess/testdata/sqlite/arg/side_table.json new file mode 100644 index 0000000000..1e687873c6 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/arg/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 42, + "origin": 51, + "name": "y", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/at_sign/input.sql b/internal/sql/preprocess/testdata/sqlite/at_sign/input.sql new file mode 100644 index 0000000000..00688da206 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/at_sign/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE name = @name; diff --git a/internal/sql/preprocess/testdata/sqlite/at_sign/output.sql b/internal/sql/preprocess/testdata/sqlite/at_sign/output.sql new file mode 100644 index 0000000000..e232a5b5bc --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/at_sign/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE name = ?1; diff --git a/internal/sql/preprocess/testdata/sqlite/at_sign/side_table.json b/internal/sql/preprocess/testdata/sqlite/at_sign/side_table.json new file mode 100644 index 0000000000..5b95487501 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/at_sign/side_table.json @@ -0,0 +1,16 @@ +[ + { + "start": 0, + "end": 37, + "dollar": true, + "params": [ + { + "number": 1, + "location": 34, + "origin": 34, + "name": "name", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/embed/input.sql b/internal/sql/preprocess/testdata/sqlite/embed/input.sql new file mode 100644 index 0000000000..2c00b3bee4 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/embed/input.sql @@ -0,0 +1 @@ +SELECT sqlc.embed(users) FROM users WHERE id = sqlc.arg(id); diff --git a/internal/sql/preprocess/testdata/sqlite/embed/output.sql b/internal/sql/preprocess/testdata/sqlite/embed/output.sql new file mode 100644 index 0000000000..022b9929db --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/embed/output.sql @@ -0,0 +1 @@ +SELECT users.* FROM users WHERE id = ?1; diff --git a/internal/sql/preprocess/testdata/sqlite/embed/side_table.json b/internal/sql/preprocess/testdata/sqlite/embed/side_table.json new file mode 100644 index 0000000000..2a2c4fc4ad --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/embed/side_table.json @@ -0,0 +1,24 @@ +[ + { + "start": 0, + "end": 40, + "dollar": true, + "params": [ + { + "number": 1, + "location": 37, + "origin": 47, + "name": "id", + "named": true + } + ], + "embeds": [ + { + "table": "users", + "location": 7, + "origin": 7, + "orig": "sqlc.embed(users)" + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/input.sql b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/input.sql new file mode 100644 index 0000000000..51f7ddb8ee --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/input.sql @@ -0,0 +1,11 @@ +-- name: WrongFunc :one +SELECT id FROM users WHERE id = sqlc.argh(target_id); + +-- name: TooManyArgs :one +SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar'); + +-- name: TooFewArgs :one +SELECT id FROM users WHERE id = sqlc.arg(); + +-- name: InvalidArgPlaceholder :one +SELECT id FROM users WHERE id = sqlc.arg(?); diff --git a/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/output.sql b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/output.sql new file mode 100644 index 0000000000..51f7ddb8ee --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/output.sql @@ -0,0 +1,11 @@ +-- name: WrongFunc :one +SELECT id FROM users WHERE id = sqlc.argh(target_id); + +-- name: TooManyArgs :one +SELECT id FROM users WHERE id = sqlc.arg('foo', 'bar'); + +-- name: TooFewArgs :one +SELECT id FROM users WHERE id = sqlc.arg(); + +-- name: InvalidArgPlaceholder :one +SELECT id FROM users WHERE id = sqlc.arg(?); diff --git a/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/side_table.json b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/side_table.json new file mode 100644 index 0000000000..a36ba15e41 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/side_table.json @@ -0,0 +1,30 @@ +[ + { + "start": 0, + "end": 77, + "dollar": true, + "error": "function \"sqlc.argh\" does not exist", + "error_at": 56 + }, + { + "start": 77, + "end": 160, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 2", + "error_at": 137 + }, + { + "start": 160, + "end": 230, + "dollar": true, + "error": "expected 1 parameter to sqlc.arg; got 0", + "error_at": 219 + }, + { + "start": 230, + "end": 312, + "dollar": true, + "error": "expected parameter to sqlc.arg to be string or reference; got \"?\"", + "error_at": 300 + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/stderr.txt b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/stderr.txt new file mode 100644 index 0000000000..aae154ee46 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_invalid_calls/stderr.txt @@ -0,0 +1,4 @@ +2:33: function "sqlc.argh" does not exist +5:33: expected 1 parameter to sqlc.arg; got 2 +8:33: expected 1 parameter to sqlc.arg; got 0 +11:33: expected parameter to sqlc.arg to be string or reference; got "?" diff --git a/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/input.sql b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/input.sql new file mode 100644 index 0000000000..0c98f3e515 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = ?; diff --git a/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/output.sql b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/output.sql new file mode 100644 index 0000000000..0c98f3e515 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = ?; diff --git a/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/side_table.json b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/side_table.json new file mode 100644 index 0000000000..ae8f878474 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/side_table.json @@ -0,0 +1,22 @@ +[ + { + "start": 0, + "end": 44, + "dollar": false, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "named": false + }, + { + "number": 2, + "location": 42, + "origin": 42, + "named": false + } + ], + "param_error": "can not mix $1 format with ? format" + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/stderr.txt b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/stderr.txt new file mode 100644 index 0000000000..9d9039890f --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/error_mixed_placeholder_styles/stderr.txt @@ -0,0 +1 @@ +1:1: can not mix $1 format with ? format diff --git a/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/input.sql b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/input.sql new file mode 100644 index 0000000000..838caa52c8 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = sqlc.arg(x); diff --git a/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/output.sql b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/output.sql new file mode 100644 index 0000000000..8d719f8cc6 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = ?2; diff --git a/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/side_table.json b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/side_table.json new file mode 100644 index 0000000000..8f0974672b --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/existing_numbered_placeholder/side_table.json @@ -0,0 +1,22 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "named": false + }, + { + "number": 2, + "location": 42, + "origin": 42, + "name": "x", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/narg_identifier/input.sql b/internal/sql/preprocess/testdata/sqlite/narg_identifier/input.sql new file mode 100644 index 0000000000..c36f3d6aaa --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/narg_identifier/input.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = sqlc.narg(name); diff --git a/internal/sql/preprocess/testdata/sqlite/narg_identifier/output.sql b/internal/sql/preprocess/testdata/sqlite/narg_identifier/output.sql new file mode 100644 index 0000000000..58a07d1185 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/narg_identifier/output.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE name = ?1; diff --git a/internal/sql/preprocess/testdata/sqlite/narg_identifier/side_table.json b/internal/sql/preprocess/testdata/sqlite/narg_identifier/side_table.json new file mode 100644 index 0000000000..dfa9deb94a --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/narg_identifier/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 36, + "dollar": true, + "params": [ + { + "number": 1, + "location": 33, + "origin": 33, + "name": "name", + "named": true, + "nullable": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/repeated_name/input.sql b/internal/sql/preprocess/testdata/sqlite/repeated_name/input.sql new file mode 100644 index 0000000000..180ea26b3b --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/repeated_name/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = sqlc.arg(x) AND b = sqlc.arg(x); diff --git a/internal/sql/preprocess/testdata/sqlite/repeated_name/output.sql b/internal/sql/preprocess/testdata/sqlite/repeated_name/output.sql new file mode 100644 index 0000000000..143e91e71b --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/repeated_name/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE a = ?1 AND b = ?1; diff --git a/internal/sql/preprocess/testdata/sqlite/repeated_name/side_table.json b/internal/sql/preprocess/testdata/sqlite/repeated_name/side_table.json new file mode 100644 index 0000000000..490774d5fd --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/repeated_name/side_table.json @@ -0,0 +1,23 @@ +[ + { + "start": 0, + "end": 45, + "dollar": true, + "params": [ + { + "number": 1, + "location": 31, + "origin": 31, + "name": "x", + "named": true + }, + { + "number": 1, + "location": 42, + "origin": 51, + "name": "x", + "named": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/slice/input.sql b/internal/sql/preprocess/testdata/sqlite/slice/input.sql new file mode 100644 index 0000000000..f5d89640d7 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/slice/input.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (sqlc.slice(ids)); diff --git a/internal/sql/preprocess/testdata/sqlite/slice/output.sql b/internal/sql/preprocess/testdata/sqlite/slice/output.sql new file mode 100644 index 0000000000..542215158c --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/slice/output.sql @@ -0,0 +1 @@ +SELECT id FROM users WHERE id IN (/*SLICE:ids*/?); diff --git a/internal/sql/preprocess/testdata/sqlite/slice/side_table.json b/internal/sql/preprocess/testdata/sqlite/slice/side_table.json new file mode 100644 index 0000000000..4b16794ab9 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/slice/side_table.json @@ -0,0 +1,17 @@ +[ + { + "start": 0, + "end": 50, + "dollar": true, + "params": [ + { + "number": 1, + "location": 47, + "origin": 34, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/preprocess/testdata/sqlite/string_constant/input.sql b/internal/sql/preprocess/testdata/sqlite/string_constant/input.sql new file mode 100644 index 0000000000..3efd6a9c11 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/string_constant/input.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE a = sqlc.arg('x') AND b = sqlc.narg('y') AND c IN (sqlc.slice('ids')); diff --git a/internal/sql/preprocess/testdata/sqlite/string_constant/output.sql b/internal/sql/preprocess/testdata/sqlite/string_constant/output.sql new file mode 100644 index 0000000000..ea33ad30e7 --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/string_constant/output.sql @@ -0,0 +1 @@ +SELECT * FROM users WHERE a = ?1 AND b = ?2 AND c IN (/*SLICE:ids*/?); diff --git a/internal/sql/preprocess/testdata/sqlite/string_constant/side_table.json b/internal/sql/preprocess/testdata/sqlite/string_constant/side_table.json new file mode 100644 index 0000000000..f48044c3bc --- /dev/null +++ b/internal/sql/preprocess/testdata/sqlite/string_constant/side_table.json @@ -0,0 +1,32 @@ +[ + { + "start": 0, + "end": 70, + "dollar": true, + "params": [ + { + "number": 1, + "location": 30, + "origin": 30, + "name": "x", + "named": true + }, + { + "number": 2, + "location": 41, + "origin": 52, + "name": "y", + "named": true, + "nullable": true + }, + { + "number": 3, + "location": 67, + "origin": 77, + "name": "ids", + "named": true, + "slice": true + } + ] + } +] diff --git a/internal/sql/rewrite/CLAUDE.md b/internal/sql/rewrite/CLAUDE.md deleted file mode 100644 index 6ea885016e..0000000000 --- a/internal/sql/rewrite/CLAUDE.md +++ /dev/null @@ -1,104 +0,0 @@ -# SQL Rewrite Package - Claude Code Guide - -This package handles AST transformations, primarily for parameter handling. - -## Key Functions - -### NamedParameters -`NamedParameters(engine config.Engine, raw *ast.RawStmt, ...) (*ast.RawStmt, map[int]Parameter, error)` - -Finds and replaces named parameters (`sqlc.arg()`, `@param`) with positional parameters. - -The function: -1. Searches for named parameters using `named.IsParamFunc()` and `named.IsParamSign()` -2. Extracts parameter names and types -3. Replaces them with `ast.ParamRef` nodes -4. Returns a map of parameter positions to their metadata - -### Expand -`Expand(raw *ast.RawStmt, expected int) error` - -Expands `sqlc.slice()` parameters into the correct number of positional parameters. - -## How Parameter Rewriting Works - -### Step 1: Find Named Parameters -```go -refs := astutils.Search(raw.Stmt, func(node ast.Node) bool { - return named.IsParamFunc(node) || named.IsParamSign(node) -}) -``` - -### Step 2: Replace with ParamRef -```go -astutils.Apply(raw.Stmt, func(cr *astutils.Cursor) bool { - if named.IsParamFunc(cr.Node()) { - // Extract name from sqlc.arg(name) - call := cr.Node().(*ast.FuncCall) - name := extractName(call.Args) - - cr.Replace(&ast.ParamRef{ - Number: nextParam(), - Location: call.Location, - }) - } - return true -}, nil) -``` - -## Important: AST Node Requirements - -For parameter rewriting to work correctly, the AST must be walkable. This means: - -1. All node types must have cases in `astutils/walk.go` -2. All node types must have cases in `astutils/rewrite.go` -3. New container types (like `OnDuplicateKeyUpdate`) must be traversed - -### Example: OnDuplicateKeyUpdate - -MySQL's `ON DUPLICATE KEY UPDATE` clause can contain `sqlc.arg()`: -```sql -INSERT INTO t (a) VALUES (sqlc.arg(val)) -ON DUPLICATE KEY UPDATE a = sqlc.arg(new_val) -``` - -For the parameter in `ON DUPLICATE KEY UPDATE` to be found and replaced: - -1. `InsertStmt` in `rewrite.go` must traverse `OnDuplicateKeyUpdate`: -```go -case *ast.InsertStmt: - a.apply(n, "Relation", nil, n.Relation) - a.apply(n, "Cols", nil, n.Cols) - a.apply(n, "SelectStmt", nil, n.SelectStmt) - a.apply(n, "OnConflictClause", nil, n.OnConflictClause) - a.apply(n, "OnDuplicateKeyUpdate", nil, n.OnDuplicateKeyUpdate) // Critical! - a.apply(n, "ReturningList", nil, n.ReturningList) - a.apply(n, "WithClause", nil, n.WithClause) -``` - -2. `OnDuplicateKeyUpdate` must have its own case: -```go -case *ast.OnDuplicateKeyUpdate: - a.apply(n, "List", nil, n.List) -``` - -## Debugging Parameter Issues - -If a `sqlc.arg()` isn't being converted to `?`: - -1. Check that the containing node type has a case in `rewrite.go` -2. Check that the case traverses all child fields -3. Add debug logging to see if the node is being visited: -```go -case *ast.YourType: - fmt.Printf("Visiting YourType with fields: %+v\n", n) - a.apply(n, "ChildField", nil, n.ChildField) -``` - -## Parameter Output Format by Engine - -- PostgreSQL: `$1`, `$2`, `$3`, ... -- MySQL: `?`, `?`, `?`, ... -- SQLite: `?`, `?`, `?`, ... - -The format is determined by the `Dialect.Param()` method in each engine. diff --git a/internal/sql/rewrite/embeds.go b/internal/sql/rewrite/embeds.go deleted file mode 100644 index 596c03be89..0000000000 --- a/internal/sql/rewrite/embeds.go +++ /dev/null @@ -1,91 +0,0 @@ -package rewrite - -import ( - "fmt" - - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" -) - -// Embed is an instance of `sqlc.embed(param)` -type Embed struct { - Table *ast.TableName - param string - Node *ast.ColumnRef -} - -// Orig string to replace -func (e Embed) Orig() string { - return fmt.Sprintf("sqlc.embed(%s)", e.param) -} - -// EmbedSet is a set of Embed instances -type EmbedSet []*Embed - -// Find a matching embed by column ref -func (es EmbedSet) Find(node *ast.ColumnRef) (*Embed, bool) { - for _, e := range es { - if e.Node == node { - return e, true - } - } - return nil, false -} - -// Embeds rewrites `sqlc.embed(param)` to a `ast.ColumnRef` of form `param.*`. -// The compiler can make use of the returned `EmbedSet` while expanding the -// `param.*` column refs to produce the correct source edits. -func Embeds(raw *ast.RawStmt) (*ast.RawStmt, EmbedSet) { - var embeds []*Embed - - node := astutils.Apply(raw, func(cr *astutils.Cursor) bool { - node := cr.Node() - - switch { - case isEmbed(node): - fun := node.(*ast.FuncCall) - - if len(fun.Args.Items) == 0 { - return false - } - - param, _ := flatten(fun.Args) - - node := &ast.ColumnRef{ - Fields: &ast.List{ - Items: []ast.Node{ - &ast.String{Str: param}, - &ast.A_Star{}, - }, - }, - } - - embeds = append(embeds, &Embed{ - Table: &ast.TableName{Name: param}, - param: param, - Node: node, - }) - - cr.Replace(node) - return false - default: - return true - } - }, nil) - - return node.(*ast.RawStmt), embeds -} - -func isEmbed(node ast.Node) bool { - call, ok := node.(*ast.FuncCall) - if !ok { - return false - } - - if call.Func == nil { - return false - } - - isValid := call.Func.Schema == "sqlc" && call.Func.Name == "embed" - return isValid -} diff --git a/internal/sql/rewrite/parameters.go b/internal/sql/rewrite/parameters.go deleted file mode 100644 index 77ad5d2573..0000000000 --- a/internal/sql/rewrite/parameters.go +++ /dev/null @@ -1,205 +0,0 @@ -package rewrite - -import ( - "fmt" - "strings" - - "github.com/sqlc-dev/sqlc/internal/config" - "github.com/sqlc-dev/sqlc/internal/source" - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" - "github.com/sqlc-dev/sqlc/internal/sql/named" -) - -// Given an AST node, return the string representation of names -func flatten(root ast.Node) (string, bool) { - sw := &stringWalker{} - astutils.Walk(sw, root) - return sw.String, sw.IsConst -} - -type stringWalker struct { - String string - IsConst bool -} - -func (s *stringWalker) Visit(node ast.Node) astutils.Visitor { - if _, ok := node.(*ast.A_Const); ok { - s.IsConst = true - } - if n, ok := node.(*ast.String); ok { - s.String += n.Str - } - return s -} - -func isNamedParamSignCast(node ast.Node) bool { - expr, ok := node.(*ast.A_Expr) - if !ok { - return false - } - _, cast := expr.Rexpr.(*ast.TypeCast) - return astutils.Join(expr.Name, ".") == "@" && cast -} - -// paramFromFuncCall creates a param from sqlc.n?arg() calls return the -// parameter and whether the parameter name was specified a best guess as its -// "source" string representation (used for replacing this function call in the -// original SQL query) -func paramFromFuncCall(call *ast.FuncCall) (named.Param, string) { - paramName, isConst := flatten(call.Args) - - // origName keeps track of how the parameter was specified in the source SQL - origName := paramName - if isConst { - origName = fmt.Sprintf("'%s'", paramName) - } - - var param named.Param - switch call.Func.Name { - case "narg": - param = named.NewUserNullableParam(paramName) - case "slice": - param = named.NewSqlcSlice(paramName) - default: - param = named.NewParam(paramName) - } - - // TODO: This code assumes that sqlc.arg(name) / sqlc.narg(name) is on a single line - // with no extraneous spaces (or any non-significant tokens for that matter) - // except between the function name and argument - funcName := call.Func.Schema + "." + call.Func.Name - spaces := "" - if call.Args != nil && len(call.Args.Items) > 0 { - leftParen := call.Args.Items[0].Pos() - 1 - spaces = strings.Repeat(" ", leftParen-call.Location-len(funcName)) - } - origText := fmt.Sprintf("%s%s(%s)", funcName, spaces, origName) - return param, origText -} - -func NamedParameters(engine config.Engine, raw *ast.RawStmt, numbs map[int]bool, dollar bool) (*ast.RawStmt, *named.ParamSet, []source.Edit) { - foundFunc := astutils.Search(raw, named.IsParamFunc) - foundSign := astutils.Search(raw, named.IsParamSign) - hasNamedParameterSupport := engine != config.EngineMySQL - allParams := named.NewParamSet(numbs, hasNamedParameterSupport) - - if len(foundFunc.Items)+len(foundSign.Items) == 0 { - return raw, allParams, nil - } - - var edits []source.Edit - node := astutils.Apply(raw, func(cr *astutils.Cursor) bool { - node := cr.Node() - switch { - case named.IsParamFunc(node): - fun := node.(*ast.FuncCall) - param, origText := paramFromFuncCall(fun) - argn := allParams.Add(param) - cr.Replace(&ast.ParamRef{ - Number: argn, - Location: fun.Location, - }) - - var replace string - if engine == config.EngineGoogleSQL { - // GoogleSQL supports named parameters natively (and Spanner - // requires them), so keep the "@name" form rather than - // rewriting to a positional placeholder. - if param.IsSqlcSlice() { - replace = fmt.Sprintf(`/*SLICE:%s*/@%s`, param.Name(), param.Name()) - } else { - replace = fmt.Sprintf("@%s", param.Name()) - } - } else if engine == config.EngineMySQL || engine == config.EngineSQLite || !dollar { - if param.IsSqlcSlice() { - // This sequence is also replicated in internal/codegen/golang.Field - // since it's needed during template generation for replacement - replace = fmt.Sprintf(`/*SLICE:%s*/?`, param.Name()) - } else { - if engine == config.EngineSQLite { - replace = fmt.Sprintf("?%d", argn) - } else { - replace = "?" - } - } - } else { - replace = fmt.Sprintf("$%d", argn) - } - - edits = append(edits, source.Edit{ - Location: fun.Location - raw.StmtLocation, - Old: origText, - New: replace, - }) - return false - - case isNamedParamSignCast(node): - expr := node.(*ast.A_Expr) - cast := expr.Rexpr.(*ast.TypeCast) - paramName, _ := flatten(cast.Arg) - param := named.NewParam(paramName) - - argn := allParams.Add(param) - cast.Arg = &ast.ParamRef{ - Number: argn, - Location: expr.Location, - } - cr.Replace(cast) - - // TODO: This code assumes that @foo::bool is on a single line - var replace string - if engine == config.EngineGoogleSQL { - replace = fmt.Sprintf("@%s", paramName) - } else if engine == config.EngineMySQL || !dollar { - replace = "?" - } else if engine == config.EngineSQLite { - replace = fmt.Sprintf("?%d", argn) - } else { - replace = fmt.Sprintf("$%d", argn) - } - - edits = append(edits, source.Edit{ - Location: expr.Location - raw.StmtLocation, - Old: fmt.Sprintf("@%s", paramName), - New: replace, - }) - return false - - case named.IsParamSign(node): - expr := node.(*ast.A_Expr) - paramName, _ := flatten(expr.Rexpr) - param := named.NewParam(paramName) - - argn := allParams.Add(param) - cr.Replace(&ast.ParamRef{ - Number: argn, - Location: expr.Location, - }) - - // TODO: This code assumes that @foo is on a single line - var replace string - if engine == config.EngineGoogleSQL { - replace = fmt.Sprintf("@%s", paramName) - } else if engine == config.EngineMySQL || !dollar { - replace = "?" - } else if engine == config.EngineSQLite { - replace = fmt.Sprintf("?%d", argn) - } else { - replace = fmt.Sprintf("$%d", argn) - } - - edits = append(edits, source.Edit{ - Location: expr.Location - raw.StmtLocation, - Old: fmt.Sprintf("@%s", paramName), - New: replace, - }) - return false - - default: - return true - } - }, nil) - - return node.(*ast.RawStmt), allParams, edits -} diff --git a/internal/sql/validate/cmd.go b/internal/sql/validate/cmd.go index 66e849de6c..e9d9dd03cb 100644 --- a/internal/sql/validate/cmd.go +++ b/internal/sql/validate/cmd.go @@ -7,7 +7,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/astutils" - "github.com/sqlc-dev/sqlc/internal/sql/named" ) func validateCopyfrom(n ast.Node) error { @@ -39,10 +38,7 @@ func validateCopyfrom(n ast.Node) error { return nil } for _, v := range sublist.Items { - _, ok := v.(*ast.ParamRef) - ok = ok || named.IsParamFunc(v) - ok = ok || named.IsParamSign(v) - if !ok { + if _, ok := v.(*ast.ParamRef); !ok { return errors.New(":copyfrom doesn't support non-parameter values") } } @@ -50,13 +46,11 @@ func validateCopyfrom(n ast.Node) error { } func validateBatch(n ast.Node) error { - funcs := astutils.Search(n, named.IsParamFunc) - params := astutils.Search(n, named.IsParamSign) args := astutils.Search(n, func(n ast.Node) bool { _, ok := n.(*ast.ParamRef) return ok }) - if (len(params.Items) + len(funcs.Items) + len(args.Items)) == 0 { + if len(args.Items) == 0 { return errors.New(":batch* commands require parameters") } return nil diff --git a/internal/sql/validate/func_call.go b/internal/sql/validate/func_call.go index dad621eb12..c60cbaa04b 100644 --- a/internal/sql/validate/func_call.go +++ b/internal/sql/validate/func_call.go @@ -30,10 +30,6 @@ func (v *funcCallVisitor) Visit(node ast.Node) astutils.Visitor { return v } - if fn.Schema == "sqlc" { - return nil - } - fun, err := v.catalog.ResolveFuncCall(call) if fun != nil { return v diff --git a/internal/sql/validate/in.go b/internal/sql/validate/in.go deleted file mode 100644 index 56bcee125d..0000000000 --- a/internal/sql/validate/in.go +++ /dev/null @@ -1,86 +0,0 @@ -package validate - -import ( - "fmt" - - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" - "github.com/sqlc-dev/sqlc/internal/sql/catalog" - "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" -) - -type inVisitor struct { - catalog *catalog.Catalog - err error -} - -func (v *inVisitor) Visit(node ast.Node) astutils.Visitor { - if v.err != nil { - return nil - } - - in, ok := node.(*ast.In) - if !ok { - return v - } - - // Validate that sqlc.slice in an IN statement is the only arg, eg: - // id IN (sqlc.slice("ids")) -- GOOD - // id in (0, 1, sqlc.slice("ids")) -- BAD - - if len(in.List) <= 1 { - return v - } - - for _, n := range in.List { - call, ok := n.(*ast.FuncCall) - if !ok { - continue - } - fn := call.Func - if fn == nil { - continue - } - - if fn.Schema == "sqlc" && fn.Name == "slice" { - var inExpr, sliceArg string - - // determine inExpr - switch n := in.Expr.(type) { - case *ast.ColumnRef: - inExpr = n.Name - default: - inExpr = "..." - } - - // determine sliceArg - if len(call.Args.Items) == 1 { - switch n := call.Args.Items[0].(type) { - case *ast.A_Const: - if str, ok := n.Val.(*ast.String); ok { - sliceArg = "\"" + str.Str + "\"" - } else { - sliceArg = "?" - } - case *ast.ColumnRef: - sliceArg = n.Name - default: - // impossible, validate.FuncCall should have caught this - sliceArg = "..." - } - } - v.err = &sqlerr.Error{ - Message: fmt.Sprintf("expected '%s IN' expr to consist only of sqlc.slice(%s); eg ", inExpr, sliceArg), - Location: call.Pos(), - } - } - } - - return v -} - -func In(c *catalog.Catalog, n ast.Node) error { - visitor := inVisitor{catalog: c} - astutils.Walk(&visitor, n) - return visitor.err -} diff --git a/internal/sql/validate/param_ref.go b/internal/sql/validate/param_ref.go deleted file mode 100644 index ab9413f40f..0000000000 --- a/internal/sql/validate/param_ref.go +++ /dev/null @@ -1,48 +0,0 @@ -package validate - -import ( - "errors" - "fmt" - - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" - "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" -) - -func ParamRef(n ast.Node) (map[int]bool, bool, error) { - var allrefs []*ast.ParamRef - var dollar bool - var nodollar bool - // Find all parameter references - astutils.Walk(astutils.VisitorFunc(func(node ast.Node) { - switch n := node.(type) { - case *ast.ParamRef: - ref := node.(*ast.ParamRef) - if ref.Dollar { - dollar = true - } else { - nodollar = true - } - allrefs = append(allrefs, n) - } - }), n) - if dollar && nodollar { - return nil, false, errors.New("can not mix $1 format with ? format") - } - - seen := map[int]bool{} - for _, r := range allrefs { - if r.Number > 0 { - seen[r.Number] = true - } - } - for i := 1; i <= len(seen); i += 1 { - if _, ok := seen[i]; !ok { - return seen, !nodollar, &sqlerr.Error{ - Code: "42P18", - Message: fmt.Sprintf("could not determine data type of parameter $%d", i), - } - } - } - return seen, !nodollar, nil -} diff --git a/internal/sql/validate/param_style.go b/internal/sql/validate/param_style.go deleted file mode 100644 index 1182051d20..0000000000 --- a/internal/sql/validate/param_style.go +++ /dev/null @@ -1,68 +0,0 @@ -package validate - -import ( - "fmt" - - "github.com/sqlc-dev/sqlc/internal/sql/ast" - "github.com/sqlc-dev/sqlc/internal/sql/astutils" - "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" -) - -type sqlcFuncVisitor struct { - err error -} - -func (v *sqlcFuncVisitor) Visit(node ast.Node) astutils.Visitor { - if v.err != nil { - return nil - } - - call, ok := node.(*ast.FuncCall) - if !ok { - return v - } - fn := call.Func - if fn == nil { - return v - } - - // Custom validation for sqlc.arg, sqlc.narg and sqlc.slice - // TODO: Replace this once type-checking is implemented - if fn.Schema == "sqlc" { - if !(fn.Name == "arg" || fn.Name == "narg" || fn.Name == "slice" || fn.Name == "embed") { - v.err = sqlerr.FunctionNotFound("sqlc." + fn.Name) - return nil - } - - if len(call.Args.Items) != 1 { - v.err = &sqlerr.Error{ - Message: fmt.Sprintf("expected 1 parameter to sqlc.%s; got %d", fn.Name, len(call.Args.Items)), - Location: call.Pos(), - } - return nil - } - - switch n := call.Args.Items[0].(type) { - case *ast.A_Const: - case *ast.ColumnRef: - default: - v.err = &sqlerr.Error{ - Message: fmt.Sprintf("expected parameter to sqlc.%s to be string or reference; got %T", fn.Name, n), - Location: call.Pos(), - } - return nil - } - - // If we have sqlc.arg or sqlc.narg, there is no need to resolve the function call. - // It won't resolve anyway, sinc it is not a real function. - return nil - } - - return nil -} - -func SqlcFunctions(n ast.Node) error { - visitor := sqlcFuncVisitor{} - astutils.Walk(&visitor, n) - return visitor.err -} diff --git a/internal/sql/validate/slice.go b/internal/sql/validate/slice.go new file mode 100644 index 0000000000..cbd45ed9fe --- /dev/null +++ b/internal/sql/validate/slice.go @@ -0,0 +1,70 @@ +package validate + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/astutils" + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +type sliceVisitor struct { + // slices maps the location of a placeholder in the query text to the name + // of the sqlc.slice() parameter it was rewritten from. + slices map[int]string + err error +} + +func (v *sliceVisitor) Visit(node ast.Node) astutils.Visitor { + if v.err != nil { + return nil + } + + in, ok := node.(*ast.In) + if !ok { + return v + } + + // A slice expands to a comma-separated list at query time, so it has to be + // the only element of the IN expression: + // id IN (sqlc.slice("ids")) -- GOOD + // id in (0, 1, sqlc.slice("ids")) -- BAD + if len(in.List) <= 1 { + return v + } + + for _, n := range in.List { + ref, ok := n.(*ast.ParamRef) + if !ok { + continue + } + name, ok := v.slices[ref.Location] + if !ok { + continue + } + + inExpr := "..." + if col, ok := in.Expr.(*ast.ColumnRef); ok { + inExpr = col.Name + } + v.err = &sqlerr.Error{ + Message: fmt.Sprintf("expected '%s IN' expr to consist only of sqlc.slice(%s); eg ", inExpr, name), + Location: ref.Location, + } + return nil + } + + return v +} + +// Slice reports whether every sqlc.slice() parameter is the only element of the +// IN expression that contains it. The slices map is keyed by the location the +// preprocessor recorded for each rewritten slice placeholder. +func Slice(n ast.Node, slices map[int]string) error { + if len(slices) == 0 { + return nil + } + visitor := sliceVisitor{slices: slices} + astutils.Walk(&visitor, n) + return visitor.err +}