Skip to content

Commit 124fa5c

Browse files
authored
feat(snowflake): parse star ILIKE transform (was mis-parsed as LikeExpr, dropping FROM) (#302)
1 parent 975ec85 commit 124fa5c

7 files changed

Lines changed: 318 additions & 14 deletions

File tree

snowflake/ast/parsenodes.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -802,15 +802,19 @@ var _ Node = (*SelectStmt)(nil)
802802
// For expressions: Expr is set, Star is false.
803803
// For star: Star is true, Expr may be a qualifier (table.*) or nil (bare *).
804804
//
805-
// Exclude / Replace / Rename carry the Snowflake star column-transforms that
806-
// may follow a `*` or `tbl.*`: EXCLUDE drops the named columns; REPLACE
807-
// substitutes an expression for a column while keeping its name; RENAME
808-
// aliases columns. Any combination may appear together, in documented order
809-
// (EXCLUDE, then REPLACE, then RENAME). They are only valid on a star target.
805+
// Ilike / Exclude / Replace / Rename carry the Snowflake star
806+
// column-transforms that may follow a `*` or `tbl.*`: ILIKE keeps only the
807+
// columns whose names match the pattern; EXCLUDE drops the named columns;
808+
// REPLACE substitutes an expression for a column while keeping its name;
809+
// RENAME aliases columns. They appear in documented order (ILIKE, then
810+
// EXCLUDE, then REPLACE, then RENAME) and are only valid on a star target.
811+
// The docs additionally forbid combining ILIKE with EXCLUDE; the parser
812+
// over-accepts that combination (semantic validation is a later layer's job).
810813
type SelectTarget struct {
811814
Expr Node // expression; nil for bare *
812815
Alias Ident // AS alias; zero Ident if absent
813816
Star bool // true for * or qualifier.*
817+
Ilike *Literal // ILIKE '<pattern>' string literal; nil if absent
814818
Exclude []Ident // EXCLUDE columns; nil if absent
815819
Replace []StarReplace // REPLACE expr AS col pairs; nil if absent
816820
Rename []StarRename // RENAME col AS alias pairs; nil if absent

snowflake/ast/walk_coverage_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ func TestWalkCoverage_SelectClauses(t *testing.T) {
172172
sql: "SELECT * REPLACE (UPPER(ra) AS ssn, rb || rc AS dept) FROM t",
173173
cols: []string{"RA", "RB", "RC"},
174174
},
175+
{
176+
name: "SelectTarget.Ilike star-transform pattern",
177+
sql: "SELECT * ILIKE '%id%' REPLACE (UPPER(ra) AS ssn) FROM t",
178+
cols: []string{"RA"},
179+
lits: []string{"%id%"},
180+
},
175181
{
176182
name: "SelectStmt.With CTE body",
177183
sql: "WITH c AS (SELECT ca FROM t) SELECT * FROM c",

snowflake/ast/walk_generated.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

snowflake/deparse/deparse_select.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,13 @@ func (w *writer) writeSelectTarget(t *ast.SelectTarget) error {
238238
} else {
239239
w.buf.WriteByte('*')
240240
}
241+
// ILIKE 'pattern'
242+
if t.Ilike != nil {
243+
w.buf.WriteString(" ILIKE")
244+
if err := w.writeLiteral(t.Ilike); err != nil {
245+
return err
246+
}
247+
}
241248
// EXCLUDE (col, ...)
242249
if len(t.Exclude) > 0 {
243250
w.buf.WriteString(" EXCLUDE (")

snowflake/deparse/deparse_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,29 @@ func TestDeparse_Select_RenameStarList(t *testing.T) {
134134
assertRoundTrip(t, `SELECT * RENAME (a AS b, c AS d) FROM t`)
135135
}
136136

137+
func TestDeparse_Select_IlikeStar(t *testing.T) {
138+
assertRoundTrip(t, `SELECT * ILIKE '%id%' FROM employee_table`)
139+
}
140+
141+
func TestDeparse_Select_QualifiedStarIlike(t *testing.T) {
142+
assertRoundTrip(t, `SELECT t.* ILIKE '%id%' FROM t`)
143+
}
144+
145+
func TestDeparse_Select_IlikeRenameStar(t *testing.T) {
146+
// Corpus official/select/example_12 shape.
147+
assertRoundTrip(t, `SELECT * ILIKE '%id%' RENAME (department_id AS department) FROM employee_table`)
148+
}
149+
150+
func TestDeparse_Select_IlikeReplaceStar(t *testing.T) {
151+
// Corpus official/select/example_15 shape.
152+
assertRoundTrip(t, `SELECT * ILIKE '%id%' REPLACE ('DEPT-' || department_id AS department_id) FROM employee_table`)
153+
}
154+
155+
func TestDeparse_Select_IlikeReplaceRenameStar(t *testing.T) {
156+
// ILIKE first, then REPLACE, then RENAME — the documented order.
157+
assertRoundTrip(t, `SELECT * ILIKE 'col%' REPLACE (UPPER(a) AS a) RENAME (a AS b) FROM t`)
158+
}
159+
137160
func TestDeparse_Select_ReplaceStar(t *testing.T) {
138161
assertRoundTrip(t, `SELECT * REPLACE (UPPER(SSN) AS SSN) FROM T`)
139162
}

snowflake/parser/select.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ func (p *Parser) selectListTerminator() bool {
490490
}
491491

492492
// parseSelectTarget parses one item in the SELECT list:
493-
// - * [EXCLUDE (col, ...)] [REPLACE (expr AS col, ...)] [RENAME (col AS alias, ...)]
493+
// - * [ILIKE 'pattern'] [EXCLUDE (col, ...)] [REPLACE (expr AS col, ...)] [RENAME (col AS alias, ...)]
494494
// - expr [AS alias]
495495
//
496496
// The expression parser already handles * (StarExpr) and qualifier.*
@@ -508,13 +508,37 @@ func (p *Parser) parseSelectTarget() (*ast.SelectTarget, error) {
508508
Loc: ast.Loc{Start: startLoc.Start},
509509
}
510510

511+
// Star ILIKE transform: ILIKE is an infix operator, so for
512+
// `* ILIKE '<pattern>'` (and `tbl.* ILIKE ...`) the expression parser has
513+
// already bound the star into LikeExpr(StarExpr, pattern) before this
514+
// function can see it. A star is not a scalar operand, so in a SELECT
515+
// list that shape is unambiguously Snowflake's star ILIKE
516+
// column-transform, not a boolean expression — unwrap it here, at the
517+
// select-target boundary, so other expression contexts are untouched.
518+
// The unwrap keys on the exact documented shape (plain ILIKE with a
519+
// string-literal pattern); NOT/ANY/ESCAPE variants and non-literal
520+
// patterns are not transforms and stay expressions.
521+
if like, ok := expr.(*ast.LikeExpr); ok &&
522+
like.Op == ast.LikeOpILike && !like.Not && !like.Any && like.Escape == nil {
523+
if _, isStar := like.Expr.(*ast.StarExpr); isStar {
524+
if pat, isLit := like.Pattern.(*ast.Literal); isLit && pat.Kind == ast.LitString {
525+
expr = like.Expr
526+
target.Ilike = pat
527+
}
528+
}
529+
}
530+
511531
// Check if the expression is a star (* or qualifier.*)
512532
if _, ok := expr.(*ast.StarExpr); ok {
513533
target.Star = true
514534
target.Expr = expr
515535

516-
// Star column-transforms, in Snowflake's documented order: EXCLUDE,
517-
// then REPLACE, then RENAME (any subset may be present).
536+
// Star column-transforms, in Snowflake's documented order: ILIKE
537+
// (unwrapped above), then EXCLUDE, then REPLACE, then RENAME (any
538+
// subset may be present; the docs forbid combining ILIKE with
539+
// EXCLUDE, which the parser over-accepts — the combination is
540+
// positionally in order and parses soundly).
541+
// ILIKE '<pattern>'
518542
// EXCLUDE <col> | EXCLUDE (<col>, ...)
519543
// REPLACE (<expr> AS <col>, ...)
520544
// RENAME <col> AS <alias> | RENAME (<col> AS <alias>, ...)
@@ -537,15 +561,17 @@ func (p *Parser) parseSelectTarget() (*ast.SelectTarget, error) {
537561
}
538562
}
539563
// A transform keyword still pending here is out of documented order
540-
// (e.g. `* RENAME (...) REPLACE (...)`). parseSingle ignores tokens
541-
// after a completed statement, so without this check the rest of the
542-
// statement — including FROM — would be dropped silently. Fail loudly
543-
// instead.
564+
// (e.g. `* RENAME (...) REPLACE (...)`, or ILIKE after any other
565+
// transform — ILIKE must come first, and in first position it was
566+
// already consumed by the expression parse and unwrapped above).
567+
// parseSingle ignores tokens after a completed statement, so without
568+
// this check the rest of the statement — including FROM — would be
569+
// dropped silently. Fail loudly instead.
544570
switch p.cur.Type {
545-
case kwEXCLUDE, kwREPLACE, kwRENAME:
571+
case kwILIKE, kwEXCLUDE, kwREPLACE, kwRENAME:
546572
return nil, &ParseError{
547573
Loc: p.cur.Loc,
548-
Msg: "star column-transforms must appear in EXCLUDE, REPLACE, RENAME order",
574+
Msg: "star column-transforms must appear in ILIKE/EXCLUDE, REPLACE, RENAME order",
549575
}
550576
}
551577
} else {

0 commit comments

Comments
 (0)