From 2f9b49ca167f63c20ffcfda02f70863a9aa3f06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sch=C3=B6ne?= Date: Tue, 3 Nov 2026 15:28:34 +0100 Subject: [PATCH 1/2] fix: Parse ESCAPE clause parameters Added a escape field to the AST expression struct. This field now stores the node for the ESCAPE clause and gets walked. The SQLite converter fills in the field. Both the default and core analyzer got a new case for recording the ESCAPE parameters. --- internal/compiler/find_params.go | 16 ++++++++++++++++ internal/compiler/resolve.go | 13 +++++++++++++ internal/core/analyzer/expr.go | 23 ++++++++++++++++++++++- internal/engine/sqlite/convert.go | 1 + internal/sql/ast/a_expr.go | 23 +++++++++++++++++++---- internal/sql/astutils/rewrite.go | 1 + internal/sql/astutils/walk.go | 3 +++ 7 files changed, 75 insertions(+), 5 deletions(-) diff --git a/internal/compiler/find_params.go b/internal/compiler/find_params.go index 8199addd33..9221724e63 100644 --- a/internal/compiler/find_params.go +++ b/internal/compiler/find_params.go @@ -52,11 +52,27 @@ func (l *limitOffset) Pos() int { return 0 } +// likeEscape is the parent of a parameter in the ESCAPE clause of a pattern +// match. It is a string whatever the matched column is, so it must not be +// typed after the operands as the rest of the A_Expr is. +type likeEscape struct { +} + +func (l *likeEscape) Pos() int { + return 0 +} + func (p paramSearch) Visit(node ast.Node) astutils.Visitor { switch n := node.(type) { case *ast.A_Expr: p.parent = node + if ref, ok := n.Escape.(*ast.ParamRef); ok { + if _, found := p.seen[ref.Location]; !found { + *p.refs = append(*p.refs, paramRef{parent: &likeEscape{}, ref: ref, rv: p.rangeVar}) + p.seen[ref.Location] = struct{}{} + } + } case *ast.BetweenExpr: p.parent = node diff --git a/internal/compiler/resolve.go b/internal/compiler/resolve.go index f47a01d7a8..e862c5d261 100644 --- a/internal/compiler/resolve.go +++ b/internal/compiler/resolve.go @@ -141,6 +141,19 @@ func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, }, }) + case *likeEscape: + defaultP := named.NewInferredParam("escape", true) + p, isNamed := params.FetchMerge(ref.ref.Number, defaultP) + a = append(a, Parameter{ + Number: ref.ref.Number, + Column: &Column{ + Name: p.Name(), + DataType: "text", + NotNull: p.NotNull(), + IsNamedParam: isNamed, + }, + }) + case *ast.A_Expr: // TODO: While this works for a wide range of simple expressions, // more complicated expressions will cause this logic to fail. diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index 62a69b273d..0e031ccf0d 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -331,15 +331,36 @@ func (a *analyzer) typeAExpr(e *ast.A_Expr) (exprType, error) { if err != nil { return exprType{}, err } + escapeT, err := a.typeEscape(e.Escape) + if err != nil { + return exprType{}, err + } // An operator's result is NULL when an operand is, except for IS and // IS NOT, which test for NULL rather than propagate it: x IS NULL and // x IS y are never NULL, whatever x and y are. return exprType{ typeOID: overload.ResultTypeOID, - nullable: (leftT.nullable || rightT.nullable) && !isNullTest(opName), + nullable: (leftT.nullable || rightT.nullable || escapeT.nullable) && !isNullTest(opName), }, nil } +// typeEscape types the ESCAPE operand of a pattern match. The escape is a +// one-character string whatever is being matched, so a placeholder there is +// text, named after the clause rather than after the matched column. +func (a *analyzer) typeEscape(n ast.Node) (exprType, error) { + if n == nil { + return exprType{}, nil + } + if pr, ok := n.(*ast.ParamRef); ok { + a.inferParam(pr, a.namedType("text")) + if cur := a.params[pr.Number]; cur.Name == "" && cur.Source == nil { + cur.Name = "escape" + a.params[pr.Number] = cur + } + } + return a.typeExpr(n) +} + // castParamRef is the placeholder a cast wraps, as ClickHouse's {p:UInt64} // is written, or nil. The cast has typed it already; what it is compared // with still names it and says which column it stands in for. diff --git a/internal/engine/sqlite/convert.go b/internal/engine/sqlite/convert.go index dad36d4d1e..c71865541d 100644 --- a/internal/engine/sqlite/convert.go +++ b/internal/engine/sqlite/convert.go @@ -1064,6 +1064,7 @@ func (c *cc) convertLikeExpr(n *meyer.LikeExpr) ast.Node { Name: &ast.List{Items: []ast.Node{&ast.String{Str: name}}}, Lexpr: c.convert(n.X), Rexpr: c.convert(n.Y), + Escape: c.convertExpr(n.Escape), Location: n.Pos(), } } diff --git a/internal/sql/ast/a_expr.go b/internal/sql/ast/a_expr.go index 1327467cf5..d01c64a154 100644 --- a/internal/sql/ast/a_expr.go +++ b/internal/sql/ast/a_expr.go @@ -17,10 +17,14 @@ type A_Expr struct { // engine that accepts more than one spelling of the same operator // (SQLite's != for <>, == for =), that is the author's spelling, which // is how the printer preserves it. - Name *List `json:"name,omitempty"` - Lexpr Node `json:"lexpr,omitempty"` - Rexpr Node `json:"rexpr,omitempty"` - Location int `json:"location"` + Name *List `json:"name,omitempty"` + Lexpr Node `json:"lexpr,omitempty"` + Rexpr Node `json:"rexpr,omitempty"` + // Escape is the ESCAPE operand of a pattern match, as SQLite writes + // "x LIKE p ESCAPE e". pg_query folds the escape into the pattern as + // like_escape(p, e) instead, so PostgreSQL never sets it. + Escape Node `json:"escape,omitempty"` + Location int `json:"location"` } func (n *A_Expr) Pos() int { @@ -119,6 +123,7 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) { buf.WriteString(" LIKE ") } buf.astFormat(n.Rexpr, d) + n.formatEscape(buf, d) case A_Expr_Kind_ILIKE: buf.astFormat(n.Lexpr, d) if n.negated() { @@ -127,6 +132,7 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) { buf.WriteString(" ILIKE ") } buf.astFormat(n.Rexpr, d) + n.formatEscape(buf, d) case A_Expr_Kind_SIMILAR: buf.astFormat(n.Lexpr, d) buf.WriteString(" SIMILAR TO ") @@ -174,5 +180,14 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) { } buf.astFormat(n.Rexpr, d) } + n.formatEscape(buf, d) + } +} + +// formatEscape prints the ESCAPE clause of a pattern match, when it has one. +func (n *A_Expr) formatEscape(buf *TrackedBuffer, d format.Dialect) { + if set(n.Escape) { + buf.WriteString(" ESCAPE ") + buf.astFormat(n.Escape, d) } } diff --git a/internal/sql/astutils/rewrite.go b/internal/sql/astutils/rewrite.go index 25b274a16c..9cf4fe0658 100644 --- a/internal/sql/astutils/rewrite.go +++ b/internal/sql/astutils/rewrite.go @@ -235,6 +235,7 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast. a.apply(n, "Name", nil, n.Name) a.apply(n, "Lexpr", nil, n.Lexpr) a.apply(n, "Rexpr", nil, n.Rexpr) + a.apply(n, "Escape", nil, n.Escape) case *ast.A_Indices: a.apply(n, "Lidx", nil, n.Lidx) diff --git a/internal/sql/astutils/walk.go b/internal/sql/astutils/walk.go index 6d5e80bdc3..a9c2c438ab 100644 --- a/internal/sql/astutils/walk.go +++ b/internal/sql/astutils/walk.go @@ -174,6 +174,9 @@ func Walk(f Visitor, node ast.Node) { if n.Rexpr != nil { Walk(f, n.Rexpr) } + if n.Escape != nil { + Walk(f, n.Escape) + } case *ast.A_Indices: if n.Lidx != nil { From b4f74eeeb6ffaa100fde6ef87e99ba710431941a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sch=C3=B6ne?= Date: Tue, 3 Nov 2026 15:30:44 +0100 Subject: [PATCH 2/2] test: LIKE ESCAPE end-to-end tests with multiple paramters Added end-to-end tests with different combinations of parameters inside the ESCAPE clause. like_escape uses the default analyzer while like_escape_core uses the core analyzer. Both tests contain the same test cases but use the different analyzers. --- .../testdata/like_escape/sqlite/go/db.go | 31 +++ .../testdata/like_escape/sqlite/go/models.go | 11 + .../like_escape/sqlite/go/query.sql.go | 193 ++++++++++++++++++ .../testdata/like_escape/sqlite/query.sql | 17 ++ .../testdata/like_escape/sqlite/schema.sql | 1 + .../testdata/like_escape/sqlite/sqlc.json | 12 ++ .../like_escape_core/sqlite/exec.json | 6 + .../testdata/like_escape_core/sqlite/go/db.go | 31 +++ .../like_escape_core/sqlite/go/models.go | 11 + .../like_escape_core/sqlite/go/query.sql.go | 193 ++++++++++++++++++ .../like_escape_core/sqlite/query.sql | 17 ++ .../like_escape_core/sqlite/schema.sql | 1 + .../like_escape_core/sqlite/sqlc.json | 12 ++ 13 files changed, 536 insertions(+) create mode 100644 internal/endtoend/testdata/like_escape/sqlite/go/db.go create mode 100644 internal/endtoend/testdata/like_escape/sqlite/go/models.go create mode 100644 internal/endtoend/testdata/like_escape/sqlite/go/query.sql.go create mode 100644 internal/endtoend/testdata/like_escape/sqlite/query.sql create mode 100644 internal/endtoend/testdata/like_escape/sqlite/schema.sql create mode 100644 internal/endtoend/testdata/like_escape/sqlite/sqlc.json create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/exec.json create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/go/db.go create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/go/models.go create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/go/query.sql.go create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/query.sql create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/schema.sql create mode 100644 internal/endtoend/testdata/like_escape_core/sqlite/sqlc.json diff --git a/internal/endtoend/testdata/like_escape/sqlite/go/db.go b/internal/endtoend/testdata/like_escape/sqlite/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/like_escape/sqlite/go/models.go b/internal/endtoend/testdata/like_escape/sqlite/go/models.go new file mode 100644 index 0000000000..bd18801202 --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/go/models.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type T struct { + ID int64 + Name string + Score int64 +} diff --git a/internal/endtoend/testdata/like_escape/sqlite/go/query.sql.go b/internal/endtoend/testdata/like_escape/sqlite/go/query.sql.go new file mode 100644 index 0000000000..6dae781c1c --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/go/query.sql.go @@ -0,0 +1,193 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const searchEscapeOnly = `-- name: SearchEscapeOnly :many +SELECT id FROM t WHERE name LIKE 'a!%%' ESCAPE ? +` + +func (q *Queries) SearchEscapeOnly(ctx context.Context, escape string) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchEscapeOnly, escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchLiteralEscape = `-- name: SearchLiteralEscape :many +SELECT id FROM t WHERE name LIKE ? ESCAPE '!' +` + +func (q *Queries) SearchLiteralEscape(ctx context.Context, name string) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchLiteralEscape, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchName = `-- name: SearchName :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? +` + +type SearchNameParams struct { + Name string + Escape string +} + +func (q *Queries) SearchName(ctx context.Context, arg SearchNameParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchName, arg.Name, arg.Escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchNameNamed = `-- name: SearchNameNamed :many +SELECT id FROM t WHERE name LIKE ?1 ESCAPE ?2 +` + +type SearchNameNamedParams struct { + Pattern string + Esc string +} + +func (q *Queries) SearchNameNamed(ctx context.Context, arg SearchNameNamedParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchNameNamed, arg.Pattern, arg.Esc) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchNotLike = `-- name: SearchNotLike :many +SELECT id FROM t WHERE name NOT LIKE ? ESCAPE ? +` + +type SearchNotLikeParams struct { + Name string + Escape string +} + +func (q *Queries) SearchNotLike(ctx context.Context, arg SearchNotLikeParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchNotLike, arg.Name, arg.Escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchThenWhere = `-- name: SearchThenWhere :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? AND score > ? +` + +type SearchThenWhereParams struct { + Name string + Escape string + Score int64 +} + +func (q *Queries) SearchThenWhere(ctx context.Context, arg SearchThenWhereParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchThenWhere, arg.Name, arg.Escape, arg.Score) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/like_escape/sqlite/query.sql b/internal/endtoend/testdata/like_escape/sqlite/query.sql new file mode 100644 index 0000000000..b323eed2e5 --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/query.sql @@ -0,0 +1,17 @@ +-- name: SearchName :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ?; + +-- name: SearchNameNamed :many +SELECT id FROM t WHERE name LIKE @pattern ESCAPE @esc; + +-- name: SearchNotLike :many +SELECT id FROM t WHERE name NOT LIKE ? ESCAPE ?; + +-- name: SearchEscapeOnly :many +SELECT id FROM t WHERE name LIKE 'a!%%' ESCAPE ?; + +-- name: SearchLiteralEscape :many +SELECT id FROM t WHERE name LIKE ? ESCAPE '!'; + +-- name: SearchThenWhere :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? AND score > ?; diff --git a/internal/endtoend/testdata/like_escape/sqlite/schema.sql b/internal/endtoend/testdata/like_escape/sqlite/schema.sql new file mode 100644 index 0000000000..f3d6e28b7e --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/schema.sql @@ -0,0 +1 @@ +CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, score INTEGER NOT NULL); diff --git a/internal/endtoend/testdata/like_escape/sqlite/sqlc.json b/internal/endtoend/testdata/like_escape/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/like_escape/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/exec.json b/internal/endtoend/testdata/like_escape_core/sqlite/exec.json new file mode 100644 index 0000000000..8da46d61b5 --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/exec.json @@ -0,0 +1,6 @@ +{ + "command": "generate", + "env": { + "SQLCEXPERIMENT": "coreanalyzer" + } +} diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/go/db.go b/internal/endtoend/testdata/like_escape_core/sqlite/go/db.go new file mode 100644 index 0000000000..32faa9f2b8 --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/go/models.go b/internal/endtoend/testdata/like_escape_core/sqlite/go/models.go new file mode 100644 index 0000000000..bd18801202 --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/go/models.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +type T struct { + ID int64 + Name string + Score int64 +} diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/go/query.sql.go b/internal/endtoend/testdata/like_escape_core/sqlite/go/query.sql.go new file mode 100644 index 0000000000..6dae781c1c --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/go/query.sql.go @@ -0,0 +1,193 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const searchEscapeOnly = `-- name: SearchEscapeOnly :many +SELECT id FROM t WHERE name LIKE 'a!%%' ESCAPE ? +` + +func (q *Queries) SearchEscapeOnly(ctx context.Context, escape string) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchEscapeOnly, escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchLiteralEscape = `-- name: SearchLiteralEscape :many +SELECT id FROM t WHERE name LIKE ? ESCAPE '!' +` + +func (q *Queries) SearchLiteralEscape(ctx context.Context, name string) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchLiteralEscape, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchName = `-- name: SearchName :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? +` + +type SearchNameParams struct { + Name string + Escape string +} + +func (q *Queries) SearchName(ctx context.Context, arg SearchNameParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchName, arg.Name, arg.Escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchNameNamed = `-- name: SearchNameNamed :many +SELECT id FROM t WHERE name LIKE ?1 ESCAPE ?2 +` + +type SearchNameNamedParams struct { + Pattern string + Esc string +} + +func (q *Queries) SearchNameNamed(ctx context.Context, arg SearchNameNamedParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchNameNamed, arg.Pattern, arg.Esc) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchNotLike = `-- name: SearchNotLike :many +SELECT id FROM t WHERE name NOT LIKE ? ESCAPE ? +` + +type SearchNotLikeParams struct { + Name string + Escape string +} + +func (q *Queries) SearchNotLike(ctx context.Context, arg SearchNotLikeParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchNotLike, arg.Name, arg.Escape) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const searchThenWhere = `-- name: SearchThenWhere :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? AND score > ? +` + +type SearchThenWhereParams struct { + Name string + Escape string + Score int64 +} + +func (q *Queries) SearchThenWhere(ctx context.Context, arg SearchThenWhereParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, searchThenWhere, arg.Name, arg.Escape, arg.Score) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/query.sql b/internal/endtoend/testdata/like_escape_core/sqlite/query.sql new file mode 100644 index 0000000000..b323eed2e5 --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/query.sql @@ -0,0 +1,17 @@ +-- name: SearchName :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ?; + +-- name: SearchNameNamed :many +SELECT id FROM t WHERE name LIKE @pattern ESCAPE @esc; + +-- name: SearchNotLike :many +SELECT id FROM t WHERE name NOT LIKE ? ESCAPE ?; + +-- name: SearchEscapeOnly :many +SELECT id FROM t WHERE name LIKE 'a!%%' ESCAPE ?; + +-- name: SearchLiteralEscape :many +SELECT id FROM t WHERE name LIKE ? ESCAPE '!'; + +-- name: SearchThenWhere :many +SELECT id FROM t WHERE name LIKE ? ESCAPE ? AND score > ?; diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/schema.sql b/internal/endtoend/testdata/like_escape_core/sqlite/schema.sql new file mode 100644 index 0000000000..f3d6e28b7e --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/schema.sql @@ -0,0 +1 @@ +CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL, score INTEGER NOT NULL); diff --git a/internal/endtoend/testdata/like_escape_core/sqlite/sqlc.json b/internal/endtoend/testdata/like_escape_core/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/like_escape_core/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +}