Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions snowflake/ast/parsenodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,15 +802,19 @@ var _ Node = (*SelectStmt)(nil)
// For expressions: Expr is set, Star is false.
// For star: Star is true, Expr may be a qualifier (table.*) or nil (bare *).
//
// Exclude / Replace / Rename carry the Snowflake star column-transforms that
// may follow a `*` or `tbl.*`: EXCLUDE drops the named columns; REPLACE
// substitutes an expression for a column while keeping its name; RENAME
// aliases columns. Any combination may appear together, in documented order
// (EXCLUDE, then REPLACE, then RENAME). They are only valid on a star target.
// Ilike / Exclude / Replace / Rename carry the Snowflake star
// column-transforms that may follow a `*` or `tbl.*`: ILIKE keeps only the
// columns whose names match the pattern; EXCLUDE drops the named columns;
// REPLACE substitutes an expression for a column while keeping its name;
// RENAME aliases columns. They appear in documented order (ILIKE, then
// EXCLUDE, then REPLACE, then RENAME) and are only valid on a star target.
// The docs additionally forbid combining ILIKE with EXCLUDE; the parser
// over-accepts that combination (semantic validation is a later layer's job).
type SelectTarget struct {
Expr Node // expression; nil for bare *
Alias Ident // AS alias; zero Ident if absent
Star bool // true for * or qualifier.*
Ilike *Literal // ILIKE '<pattern>' string literal; nil if absent
Exclude []Ident // EXCLUDE columns; nil if absent
Replace []StarReplace // REPLACE expr AS col pairs; nil if absent
Rename []StarRename // RENAME col AS alias pairs; nil if absent
Expand Down
6 changes: 6 additions & 0 deletions snowflake/ast/walk_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ func TestWalkCoverage_SelectClauses(t *testing.T) {
sql: "SELECT * REPLACE (UPPER(ra) AS ssn, rb || rc AS dept) FROM t",
cols: []string{"RA", "RB", "RC"},
},
{
name: "SelectTarget.Ilike star-transform pattern",
sql: "SELECT * ILIKE '%id%' REPLACE (UPPER(ra) AS ssn) FROM t",
cols: []string{"RA"},
lits: []string{"%id%"},
},
{
name: "SelectStmt.With CTE body",
sql: "WITH c AS (SELECT ca FROM t) SELECT * FROM c",
Expand Down
3 changes: 3 additions & 0 deletions snowflake/ast/walk_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions snowflake/deparse/deparse_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ func (w *writer) writeSelectTarget(t *ast.SelectTarget) error {
} else {
w.buf.WriteByte('*')
}
// ILIKE 'pattern'
if t.Ilike != nil {
w.buf.WriteString(" ILIKE")
if err := w.writeLiteral(t.Ilike); err != nil {
return err
}
}
// EXCLUDE (col, ...)
if len(t.Exclude) > 0 {
w.buf.WriteString(" EXCLUDE (")
Expand Down
23 changes: 23 additions & 0 deletions snowflake/deparse/deparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ func TestDeparse_Select_RenameStarList(t *testing.T) {
assertRoundTrip(t, `SELECT * RENAME (a AS b, c AS d) FROM t`)
}

func TestDeparse_Select_IlikeStar(t *testing.T) {
assertRoundTrip(t, `SELECT * ILIKE '%id%' FROM employee_table`)
}

func TestDeparse_Select_QualifiedStarIlike(t *testing.T) {
assertRoundTrip(t, `SELECT t.* ILIKE '%id%' FROM t`)
}

func TestDeparse_Select_IlikeRenameStar(t *testing.T) {
// Corpus official/select/example_12 shape.
assertRoundTrip(t, `SELECT * ILIKE '%id%' RENAME (department_id AS department) FROM employee_table`)
}

func TestDeparse_Select_IlikeReplaceStar(t *testing.T) {
// Corpus official/select/example_15 shape.
assertRoundTrip(t, `SELECT * ILIKE '%id%' REPLACE ('DEPT-' || department_id AS department_id) FROM employee_table`)
}

func TestDeparse_Select_IlikeReplaceRenameStar(t *testing.T) {
// ILIKE first, then REPLACE, then RENAME — the documented order.
assertRoundTrip(t, `SELECT * ILIKE 'col%' REPLACE (UPPER(a) AS a) RENAME (a AS b) FROM t`)
}

func TestDeparse_Select_ReplaceStar(t *testing.T) {
assertRoundTrip(t, `SELECT * REPLACE (UPPER(SSN) AS SSN) FROM T`)
}
Expand Down
44 changes: 35 additions & 9 deletions snowflake/parser/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ func (p *Parser) selectListTerminator() bool {
}

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

// Star ILIKE transform: ILIKE is an infix operator, so for
// `* ILIKE '<pattern>'` (and `tbl.* ILIKE ...`) the expression parser has
// already bound the star into LikeExpr(StarExpr, pattern) before this
// function can see it. A star is not a scalar operand, so in a SELECT
// list that shape is unambiguously Snowflake's star ILIKE
// column-transform, not a boolean expression — unwrap it here, at the
// select-target boundary, so other expression contexts are untouched.
// The unwrap keys on the exact documented shape (plain ILIKE with a
// string-literal pattern); NOT/ANY/ESCAPE variants and non-literal
// patterns are not transforms and stay expressions.
if like, ok := expr.(*ast.LikeExpr); ok &&
like.Op == ast.LikeOpILike && !like.Not && !like.Any && like.Escape == nil {
if _, isStar := like.Expr.(*ast.StarExpr); isStar {
if pat, isLit := like.Pattern.(*ast.Literal); isLit && pat.Kind == ast.LitString {
expr = like.Expr
target.Ilike = pat
}
}
}

// Check if the expression is a star (* or qualifier.*)
if _, ok := expr.(*ast.StarExpr); ok {
target.Star = true
target.Expr = expr

// Star column-transforms, in Snowflake's documented order: EXCLUDE,
// then REPLACE, then RENAME (any subset may be present).
// Star column-transforms, in Snowflake's documented order: ILIKE
// (unwrapped above), then EXCLUDE, then REPLACE, then RENAME (any
// subset may be present; the docs forbid combining ILIKE with
// EXCLUDE, which the parser over-accepts — the combination is
// positionally in order and parses soundly).
// ILIKE '<pattern>'
// EXCLUDE <col> | EXCLUDE (<col>, ...)
// REPLACE (<expr> AS <col>, ...)
// RENAME <col> AS <alias> | RENAME (<col> AS <alias>, ...)
Expand All @@ -537,15 +561,17 @@ func (p *Parser) parseSelectTarget() (*ast.SelectTarget, error) {
}
}
// A transform keyword still pending here is out of documented order
// (e.g. `* RENAME (...) REPLACE (...)`). parseSingle ignores tokens
// after a completed statement, so without this check the rest of the
// statement — including FROM — would be dropped silently. Fail loudly
// instead.
// (e.g. `* RENAME (...) REPLACE (...)`, or ILIKE after any other
// transform — ILIKE must come first, and in first position it was
// already consumed by the expression parse and unwrapped above).
// parseSingle ignores tokens after a completed statement, so without
// this check the rest of the statement — including FROM — would be
// dropped silently. Fail loudly instead.
switch p.cur.Type {
case kwEXCLUDE, kwREPLACE, kwRENAME:
case kwILIKE, kwEXCLUDE, kwREPLACE, kwRENAME:
return nil, &ParseError{
Loc: p.cur.Loc,
Msg: "star column-transforms must appear in EXCLUDE, REPLACE, RENAME order",
Msg: "star column-transforms must appear in ILIKE/EXCLUDE, REPLACE, RENAME order",
}
}
} else {
Expand Down
Loading
Loading