Skip to content
Open
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
7 changes: 6 additions & 1 deletion spanner/spansql/keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ func init() {
funcArgParsers["TOKENIZE_NGRAMS"] = tokenDefinitionArgParser
funcArgParsers["TOKENIZE_NUMBER"] = tokenDefinitionArgParser
funcArgParsers["TOKENIZE_SUBSTRING"] = tokenDefinitionArgParser
// Special case for array functions with lambda arguments
funcArgParsers["ARRAY_TRANSFORM"] = lambdaArgParser
funcArgParsers["ARRAY_FILTER"] = lambdaArgParser
funcArgParsers["ARRAY_INCLUDES"] = lambdaArgParser
}

var funcNames = []string{
Expand Down Expand Up @@ -234,6 +238,7 @@ var funcNames = []string{
"RTRIM",
"SAFE_CONVERT_BYTES_TO_STRING",
"SPLIT",
"SPLIT_SUBSTR",
"STARTS_WITH",
"STRPOS",
"SUBSTR",
Expand All @@ -256,7 +261,7 @@ var funcNames = []string{
"ARRAY_CONCAT",
"ARRAY_FIRST", "ARRAY_INCLUDES", "ARRAY_INCLUDES_ALL", "ARRAY_INCLUDES_ANY", "ARRAY_LAST",
"ARRAY_LENGTH",
"ARRAY_MAX", "ARRAY_MIN", "ARRAY_REVERSE", "ARRAY_SLICE", "ARRAY_TRANSFORM",
"ARRAY_MAX", "ARRAY_MIN", "ARRAY_REVERSE", "ARRAY_SLICE", "ARRAY_TRANSFORM", "ARRAY_FILTER",
"ARRAY_TO_STRING",
"GENERATE_ARRAY", "GENERATE_DATE_ARRAY",
"OFFSET", "ORDINAL",
Expand Down
38 changes: 38 additions & 0 deletions spanner/spansql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4245,6 +4245,44 @@ var tokenDefinitionArgParser = func(p *parser) (Expr, *parseError) {
return p.parseExpr()
}

var lambdaArgParser = func(p *parser) (Expr, *parseError) {
var key Expr
if p.sniff("(") {
defs, err := p.parseParenExprList()
if err != nil {
return nil, err
}
key = Paren{Expr: ExprList(defs)}
} else if p.sniffAhead(1, "-", ">") {
// special case to handle `id -> ...` syntax,
// otherwise it gets parsed as a binary operation
// and parser expects another identifier, resulting in an error
column, err := p.parseLit()
if err != nil {
return nil, err
}
key = column
} else {
column, err := p.parseExpr()
if err != nil {
return nil, err
}
key = column
}

if p.eat("-", ">") {
expr, err := p.parseExpr()
if err != nil {
return nil, err
}
return Lambda{
Key: key,
Value: expr,
}, nil
}
return key, nil
}

func (p *parser) parseAggregateFunc() (Func, *parseError) {
tok := p.next()
if tok.err != nil {
Expand Down
66 changes: 66 additions & 0 deletions spanner/spansql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,72 @@ func TestParseExpr(t *testing.T) {
},
},
},

// Lambda expressions
{
`ARRAY_TRANSFORM(items, (item, idx) -> SPLIT_SUBSTR(item, "@", 1, 1))`,
Func{Name: "ARRAY_TRANSFORM", Args: []Expr{ID("items"), Lambda{
Key: Paren{Expr: ExprList{ID("item"), ID("idx")}},
Value: Func{Name: "SPLIT_SUBSTR", Args: []Expr{ID("item"), StringLiteral("@"), IntegerLiteral(1), IntegerLiteral(1)}},
}}},
},
{
`id`,
ID("id"),
},
{
`ARRAY_TRANSFORM(items, item -> SPLIT_SUBSTR(item, "@", 1, 1))`,
Func{Name: "ARRAY_TRANSFORM", Args: []Expr{ID("items"), Lambda{
Key: ID("item"),
Value: Func{Name: "SPLIT_SUBSTR", Args: []Expr{ID("item"), StringLiteral("@"), IntegerLiteral(1), IntegerLiteral(1)}},
}}},
},
{
`ARRAY_FILTER(items, (item, idx) -> item)`,
Func{Name: "ARRAY_FILTER", Args: []Expr{ID("items"), Lambda{
Key: Paren{Expr: ExprList{ID("item"), ID("idx")}},
Value: ID("item"),
}}},
},
{
`ARRAY_FILTER(items, item -> STARTS_WITH(item, "secret"))`,
Func{Name: "ARRAY_FILTER", Args: []Expr{ID("items"), Lambda{
Key: ID("item"),
Value: Func{Name: "STARTS_WITH", Args: []Expr{ID("item"), StringLiteral("secret")}},
}}},
},
{
`ARRAY_INCLUDES(items, (item, idx) -> item)`,
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), Lambda{
Key: Paren{Expr: ExprList{ID("item"), ID("idx")}},
Value: ID("item"),
}}},
},
{
`ARRAY_INCLUDES(items, item -> STARTS_WITH(item, "secret"))`,
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), Lambda{
Key: ID("item"),
Value: Func{Name: "STARTS_WITH", Args: []Expr{ID("item"), StringLiteral("secret")}},
}}},
},
// Check that ARRAY_INCLUDES still accepts a search_value
{
`ARRAY_INCLUDES(items, "item")`,
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), StringLiteral("item")}},
},
// ARRAY_INCLUDES with a binary expression as search_value
{
`ARRAY_INCLUDES(items, 1 + 1)`,
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), ArithOp{Op: Add, LHS: IntegerLiteral(1), RHS: IntegerLiteral(1)}}},
},
// ARRAY_FILTER with a binary expression in the lambda body
{
`ARRAY_FILTER(items, item -> item > 5)`,
Func{Name: "ARRAY_FILTER", Args: []Expr{ID("items"), Lambda{
Key: ID("item"),
Value: ComparisonOp{Op: Gt, LHS: ID("item"), RHS: IntegerLiteral(5)},
}}},
},
}
for _, test := range tests {
p := newParser("test-file", test.in)
Expand Down
12 changes: 12 additions & 0 deletions spanner/spansql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,18 @@ func (a Array) addSQL(sb *strings.Builder) {
sb.WriteString("]")
}

func (l ExprList) SQL() string { return buildSQL(l) }
func (l ExprList) addSQL(sb *strings.Builder) {
addExprList(sb, l, ", ")
}

func (l Lambda) SQL() string { return buildSQL(l) }
func (l Lambda) addSQL(sb *strings.Builder) {
l.Key.addSQL(sb)
sb.WriteString(" -> ")
l.Value.addSQL(sb)
}

func (sl StructLiteral) SQL() string { return buildSQL(sl) }
func (sl StructLiteral) addSQL(sb *strings.Builder) {
sb.WriteString("STRUCT")
Expand Down
23 changes: 23 additions & 0 deletions spanner/spansql/sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2240,6 +2240,29 @@ INNER JOIN t2 ON t1.id = t2.t1_id
GROUP BY t1.id, t2.name`,
reparseQuery,
},
// Multiple ARRAY function with lambda expressions + ARRAY_INCLUDES with regular search_value
{
Query{
Select: Select{
List: []Expr{
Func{Name: "ARRAY_TRANSFORM", Args: []Expr{ID("items"), Lambda{Key: Paren{Expr: ExprList{ID("item"), ID("idx")}}, Value: Func{Name: "SPLIT", Args: []Expr{ID("item"), StringLiteral(",")}}}}},
Func{Name: "ARRAY_FILTER", Args: []Expr{ID("items"), Lambda{Key: ID("item"), Value: Func{Name: "STARTS_WITH", Args: []Expr{ID("item"), StringLiteral("secret")}}}}},
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), Lambda{Key: ID("item"), Value: Func{Name: "STARTS_WITH", Args: []Expr{ID("item"), StringLiteral("secret")}}}}},
Func{Name: "ARRAY_INCLUDES", Args: []Expr{ID("items"), StringLiteral("secret")}},
},
From: []SelectFrom{SelectFromTable{
Table: "orders",
}},
},
},
`SELECT
ARRAY_TRANSFORM(items, (item, idx) -> SPLIT(item, ",")),
ARRAY_FILTER(items, item -> STARTS_WITH(item, "secret")),
ARRAY_INCLUDES(items, item -> STARTS_WITH(item, "secret")),
ARRAY_INCLUDES(items, "secret")
FROM orders`,
reparseQuery,
},
}
for _, test := range tests {
sql := test.data.SQL()
Expand Down
13 changes: 13 additions & 0 deletions spanner/spansql/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,19 @@ type Array []Expr

func (Array) isExpr() {}

// ExprList represents a comma-separated list of expressions
type ExprList []Expr

func (ExprList) isExpr() {}

// Lambda represents a lambda expression https://docs.cloud.google.com/spanner/docs/reference/standard-sql/functions-reference#lambdas
type Lambda struct {
Key Expr
Value Expr
}

func (Lambda) isExpr() {}

// StructLiteral represents a struct literal.
// https://cloud.google.com/spanner/docs/query-syntax#struct_type
type StructLiteral struct {
Expand Down
Loading