Skip to content

fix(sqlite): keep parameter in LIKE ... ESCAPE ? - #4675

Open
manuelschoene wants to merge 2 commits into
sqlc-dev:mainfrom
manuelschoene:fix/sqlite-escape-parameter
Open

manuelschoene wants to merge 2 commits into
sqlc-dev:mainfrom
manuelschoene:fix/sqlite-escape-parameter

Conversation

@manuelschoene

Copy link
Copy Markdown

Fixes #4668

Problem

With the SQLite engine, a parameter in the ESCAPE clause of LIKE is dropped from the generated code. The query text keeps every placeholder, but the generated method passes one argument too few:

-- name: SearchName :many
SELECT id FROM t WHERE name LIKE ? ESCAPE ?;
// before
func (q *Queries) SearchName(ctx context.Context, name string) ([]int64, error) {
	rows, err := q.db.QueryContext(ctx, searchName, name)

What happens at runtime depends on how the parameters are written:

  • ? placeholders: the query fails with sql: expected 2 arguments, got 1.
  • Named parameters (@pattern ESCAPE @esc, sqlc.arg(...)): these are rewritten to ?1/?2, and the driver does not count arguments for those. ?2 is bound to NULL, LIKE … ESCAPE NULL evaluates to NULL, and the query silently returns no rows.
  • Parameters after the escape (… ESCAPE ? AND score > ?): the arguments shift, so score is bound in the ESCAPE position.

Both the default analyzer and the core analyzer (SQLCEXPERIMENT=coreanalyzer) are affected. An escape given as a literal (ESCAPE '!') already worked.

Cause

The SQLite parser reads the escape into LikeExpr.Escape, but convertLikeExpr (internal/engine/sqlite/convert.go) only converted the two operands. ast.A_Expr had no field for an escape, so the parameter was gone before parameter discovery ran.

Changes

  • internal/sql/ast/a_expr.go: add A_Expr.Escape, and print ESCAPE <expr> for the pattern-match operators in Format. The PostgreSQL engine never sets the field, because pg_query folds the escape into the pattern as like_escape(p, e).
  • internal/engine/sqlite/convert.go: convert n.Escape into the new field.
  • internal/sql/astutils/walk.go, rewrite.go: visit Escape, so parameter discovery and named-parameter rewriting reach it.
  • internal/compiler/find_params.go, resolve.go (default analyzer): record an escape parameter with its own parent type. Otherwise it would be typed and named after the matched column. It is now always text, named escape unless the user names it.
  • internal/core/analyzer/expr.go (core analyzer): type the escape operand the same way (text, named escape by default), and count it towards the nullability of the result.

After the fix:

type SearchNameParams struct {
	Name   string
	Escape string
}

type SearchThenWhereParams struct {
	Name   string
	Escape string
	Score  int64
}

Tests

Two new end-to-end cases share the same schema and queries:

  • internal/endtoend/testdata/like_escape/sqlite/ covers the default analyzer.
  • internal/endtoend/testdata/like_escape_core/sqlite/ covers the core analyzer; its exec.json sets SQLCEXPERIMENT=coreanalyzer.

Both paths generate identical code. The cases cover:

  • positional ? parameters;
  • named parameters (@pattern ESCAPE @esc);
  • NOT LIKE;
  • a parameter only in the ESCAPE position;
  • a literal escape with a parameter pattern;
  • a parameter after the escape (… ESCAPE ? AND score > ?).

I ran the test suite without databases (go test ./...), and it passes. I did not run the tests that need a live PostgreSQL or MySQL server (the managed-db context and the examples). The change only affects the SQLite converter and the shared AST code, and the PostgreSQL and MySQL cases still generate unchanged under base.

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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite: parameter in LIKE ... ESCAPE ? is dropped from generated code

1 participant