Skip to content

Commit 2f9b49c

Browse files
committed
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.
1 parent 3ad8b20 commit 2f9b49c

7 files changed

Lines changed: 75 additions & 5 deletions

File tree

‎internal/compiler/find_params.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,27 @@ func (l *limitOffset) Pos() int {
5252
return 0
5353
}
5454

55+
// likeEscape is the parent of a parameter in the ESCAPE clause of a pattern
56+
// match. It is a string whatever the matched column is, so it must not be
57+
// typed after the operands as the rest of the A_Expr is.
58+
type likeEscape struct {
59+
}
60+
61+
func (l *likeEscape) Pos() int {
62+
return 0
63+
}
64+
5565
func (p paramSearch) Visit(node ast.Node) astutils.Visitor {
5666
switch n := node.(type) {
5767

5868
case *ast.A_Expr:
5969
p.parent = node
70+
if ref, ok := n.Escape.(*ast.ParamRef); ok {
71+
if _, found := p.seen[ref.Location]; !found {
72+
*p.refs = append(*p.refs, paramRef{parent: &likeEscape{}, ref: ref, rv: p.rangeVar})
73+
p.seen[ref.Location] = struct{}{}
74+
}
75+
}
6076

6177
case *ast.BetweenExpr:
6278
p.parent = node

‎internal/compiler/resolve.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar,
141141
},
142142
})
143143

144+
case *likeEscape:
145+
defaultP := named.NewInferredParam("escape", true)
146+
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
147+
a = append(a, Parameter{
148+
Number: ref.ref.Number,
149+
Column: &Column{
150+
Name: p.Name(),
151+
DataType: "text",
152+
NotNull: p.NotNull(),
153+
IsNamedParam: isNamed,
154+
},
155+
})
156+
144157
case *ast.A_Expr:
145158
// TODO: While this works for a wide range of simple expressions,
146159
// more complicated expressions will cause this logic to fail.

‎internal/core/analyzer/expr.go‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,36 @@ func (a *analyzer) typeAExpr(e *ast.A_Expr) (exprType, error) {
331331
if err != nil {
332332
return exprType{}, err
333333
}
334+
escapeT, err := a.typeEscape(e.Escape)
335+
if err != nil {
336+
return exprType{}, err
337+
}
334338
// An operator's result is NULL when an operand is, except for IS and
335339
// IS NOT, which test for NULL rather than propagate it: x IS NULL and
336340
// x IS y are never NULL, whatever x and y are.
337341
return exprType{
338342
typeOID: overload.ResultTypeOID,
339-
nullable: (leftT.nullable || rightT.nullable) && !isNullTest(opName),
343+
nullable: (leftT.nullable || rightT.nullable || escapeT.nullable) && !isNullTest(opName),
340344
}, nil
341345
}
342346

347+
// typeEscape types the ESCAPE operand of a pattern match. The escape is a
348+
// one-character string whatever is being matched, so a placeholder there is
349+
// text, named after the clause rather than after the matched column.
350+
func (a *analyzer) typeEscape(n ast.Node) (exprType, error) {
351+
if n == nil {
352+
return exprType{}, nil
353+
}
354+
if pr, ok := n.(*ast.ParamRef); ok {
355+
a.inferParam(pr, a.namedType("text"))
356+
if cur := a.params[pr.Number]; cur.Name == "" && cur.Source == nil {
357+
cur.Name = "escape"
358+
a.params[pr.Number] = cur
359+
}
360+
}
361+
return a.typeExpr(n)
362+
}
363+
343364
// castParamRef is the placeholder a cast wraps, as ClickHouse's {p:UInt64}
344365
// is written, or nil. The cast has typed it already; what it is compared
345366
// with still names it and says which column it stands in for.

‎internal/engine/sqlite/convert.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ func (c *cc) convertLikeExpr(n *meyer.LikeExpr) ast.Node {
10641064
Name: &ast.List{Items: []ast.Node{&ast.String{Str: name}}},
10651065
Lexpr: c.convert(n.X),
10661066
Rexpr: c.convert(n.Y),
1067+
Escape: c.convertExpr(n.Escape),
10671068
Location: n.Pos(),
10681069
}
10691070
}

‎internal/sql/ast/a_expr.go‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ type A_Expr struct {
1717
// engine that accepts more than one spelling of the same operator
1818
// (SQLite's != for <>, == for =), that is the author's spelling, which
1919
// is how the printer preserves it.
20-
Name *List `json:"name,omitempty"`
21-
Lexpr Node `json:"lexpr,omitempty"`
22-
Rexpr Node `json:"rexpr,omitempty"`
23-
Location int `json:"location"`
20+
Name *List `json:"name,omitempty"`
21+
Lexpr Node `json:"lexpr,omitempty"`
22+
Rexpr Node `json:"rexpr,omitempty"`
23+
// Escape is the ESCAPE operand of a pattern match, as SQLite writes
24+
// "x LIKE p ESCAPE e". pg_query folds the escape into the pattern as
25+
// like_escape(p, e) instead, so PostgreSQL never sets it.
26+
Escape Node `json:"escape,omitempty"`
27+
Location int `json:"location"`
2428
}
2529

2630
func (n *A_Expr) Pos() int {
@@ -119,6 +123,7 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) {
119123
buf.WriteString(" LIKE ")
120124
}
121125
buf.astFormat(n.Rexpr, d)
126+
n.formatEscape(buf, d)
122127
case A_Expr_Kind_ILIKE:
123128
buf.astFormat(n.Lexpr, d)
124129
if n.negated() {
@@ -127,6 +132,7 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) {
127132
buf.WriteString(" ILIKE ")
128133
}
129134
buf.astFormat(n.Rexpr, d)
135+
n.formatEscape(buf, d)
130136
case A_Expr_Kind_SIMILAR:
131137
buf.astFormat(n.Lexpr, d)
132138
buf.WriteString(" SIMILAR TO ")
@@ -174,5 +180,14 @@ func (n *A_Expr) Format(buf *TrackedBuffer, d format.Dialect) {
174180
}
175181
buf.astFormat(n.Rexpr, d)
176182
}
183+
n.formatEscape(buf, d)
184+
}
185+
}
186+
187+
// formatEscape prints the ESCAPE clause of a pattern match, when it has one.
188+
func (n *A_Expr) formatEscape(buf *TrackedBuffer, d format.Dialect) {
189+
if set(n.Escape) {
190+
buf.WriteString(" ESCAPE ")
191+
buf.astFormat(n.Escape, d)
177192
}
178193
}

‎internal/sql/astutils/rewrite.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func (a *application) apply(parent ast.Node, name string, iter *iterator, n ast.
235235
a.apply(n, "Name", nil, n.Name)
236236
a.apply(n, "Lexpr", nil, n.Lexpr)
237237
a.apply(n, "Rexpr", nil, n.Rexpr)
238+
a.apply(n, "Escape", nil, n.Escape)
238239

239240
case *ast.A_Indices:
240241
a.apply(n, "Lidx", nil, n.Lidx)

‎internal/sql/astutils/walk.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ func Walk(f Visitor, node ast.Node) {
174174
if n.Rexpr != nil {
175175
Walk(f, n.Rexpr)
176176
}
177+
if n.Escape != nil {
178+
Walk(f, n.Escape)
179+
}
177180

178181
case *ast.A_Indices:
179182
if n.Lidx != nil {

0 commit comments

Comments
 (0)