Skip to content

Commit b4a6f4c

Browse files
authored
feat(spanner/spansql): support struct literal (#13766)
SpanSQL does not currently support STRUCT type. This PR aims to add support for this with adequate tests for subqueries as well. Closes #13765
1 parent 209126b commit b4a6f4c

File tree

4 files changed

+458
-6
lines changed

4 files changed

+458
-6
lines changed

spanner/spansql/parser.go

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3683,10 +3683,17 @@ func (p *parser) parseSelect() (Select, *parseError) {
36833683

36843684
var sel Select
36853685

3686-
if p.eat("ALL") {
3687-
// Nothing to do; this is the default.
3688-
} else if p.eat("DISTINCT") {
3686+
// Handle AS STRUCT, DISTINCT, and ALL, which can appear in flexible order.
3687+
if p.eat("AS", "STRUCT") {
3688+
sel.AsStruct = true
3689+
}
3690+
if p.eat("DISTINCT") {
36893691
sel.Distinct = true
3692+
} else if p.eat("ALL") {
3693+
// This is the default, just consume the token.
3694+
}
3695+
if !sel.AsStruct && p.eat("AS", "STRUCT") {
3696+
sel.AsStruct = true
36903697
}
36913698

36923699
// Read expressions for the SELECT list.
@@ -4685,6 +4692,17 @@ func (p *parser) parseLit() (Expr, *parseError) {
46854692
return Paren{Expr: e}, nil
46864693
}
46874694

4695+
// Handle ARRAY and STRUCT before generic function handling
4696+
// because they have special syntax (ARRAY can have subqueries, STRUCT can have types)
4697+
if tok.caseEqual("ARRAY") && (p.sniff("(") || p.sniff("[") || p.sniff("<")) {
4698+
p.back()
4699+
return p.parseArrayOrArraySubquery()
4700+
}
4701+
if tok.caseEqual("STRUCT") && (p.sniff("(") || p.sniff("<")) {
4702+
p.back()
4703+
return p.parseStructLit()
4704+
}
4705+
46884706
// If the literal was an identifier, and there's an open paren next,
46894707
// this is a function invocation.
46904708
// The `funcs` map is keyed by upper case strings.
@@ -4746,7 +4764,7 @@ func (p *parser) parseLit() (Expr, *parseError) {
47464764
switch {
47474765
case tok.caseEqual("ARRAY") || tok.value == "[":
47484766
p.back()
4749-
return p.parseArrayLit()
4767+
return p.parseArrayOrArraySubquery()
47504768
case tok.caseEqual("DATE"):
47514769
if p.sniffTokenType(stringToken) {
47524770
p.back()
@@ -4762,10 +4780,11 @@ func (p *parser) parseLit() (Expr, *parseError) {
47624780
p.back()
47634781
return p.parseJSONLit()
47644782
}
4783+
case tok.caseEqual("STRUCT"):
4784+
p.back()
4785+
return p.parseStructLit()
47654786
}
47664787

4767-
// TODO: struct literals
4768-
47694788
// Try a parameter.
47704789
// TODO: check character sets.
47714790
if strings.HasPrefix(tok.value, "@") {
@@ -4941,6 +4960,67 @@ func (p *parser) parseNullIfExpr() (NullIf, *parseError) {
49414960
return NullIf{Expr: expr, ExprToMatch: exprToMatch}, nil
49424961
}
49434962

4963+
func (p *parser) parseArrayOrArraySubquery() (Expr, *parseError) {
4964+
// ARRAY can be followed by:
4965+
// - [...] for array literals
4966+
// - (SELECT ...) for array subqueries
4967+
// - <type>[...] for typed array literals
4968+
4969+
if p.sniff("[") {
4970+
// Handle [...] without ARRAY keyword
4971+
return p.parseArrayLit()
4972+
}
4973+
4974+
if !p.eat("ARRAY") {
4975+
return nil, p.errorf("expected ARRAY or [")
4976+
}
4977+
4978+
// After ARRAY, check what follows
4979+
// Skip any <type> specification if present
4980+
if p.eat("<") {
4981+
// This is a typed array literal ARRAY<type>[...]
4982+
// Skip the type specification
4983+
depth := 1
4984+
for depth > 0 && !p.done {
4985+
tok := p.next()
4986+
if tok.err != nil {
4987+
return nil, tok.err
4988+
}
4989+
if tok.value == "<" {
4990+
depth++
4991+
} else if tok.value == ">" {
4992+
depth--
4993+
}
4994+
}
4995+
}
4996+
4997+
// Now check for [ or (
4998+
if p.eat("[") {
4999+
// It's an array literal ARRAY[...] or ARRAY<type>[...]
5000+
// parseArrayLit expects to be positioned after ARRAY and will consume [...]
5001+
p.back() // Put [ back so parseArrayLit can consume it
5002+
return p.parseArrayLit()
5003+
} else if p.eat("(") {
5004+
// Check if it's a subquery: ARRAY(SELECT ...)
5005+
if p.sniff("SELECT") {
5006+
// It's an ARRAY subquery
5007+
q, err := p.parseQuery()
5008+
if err != nil {
5009+
return nil, err
5010+
}
5011+
if err := p.expect(")"); err != nil {
5012+
return nil, err
5013+
}
5014+
return ArraySubquery{Query: q}, nil
5015+
}
5016+
5017+
// Not supported: ARRAY(expr, expr, ...)
5018+
return nil, p.errorf("ARRAY(...) expression lists are not supported, use ARRAY[...] instead")
5019+
}
5020+
5021+
return nil, p.errorf("expected [ or ( after ARRAY")
5022+
}
5023+
49445024
func (p *parser) parseArrayLit() (Array, *parseError) {
49455025
// ARRAY keyword is optional.
49465026
// TODO: If it is present, consume any <T> after it.
@@ -4959,6 +5039,43 @@ func (p *parser) parseArrayLit() (Array, *parseError) {
49595039
return arr, err
49605040
}
49615041

5042+
func (p *parser) parseStructLit() (StructLiteral, *parseError) {
5043+
if err := p.expect("STRUCT"); err != nil {
5044+
return StructLiteral{}, err
5045+
}
5046+
5047+
var sl StructLiteral
5048+
5049+
// Check for typed struct: STRUCT<type1, type2, ...>
5050+
if p.eat("<") {
5051+
for {
5052+
typ, err := p.parseType()
5053+
if err != nil {
5054+
return StructLiteral{}, err
5055+
}
5056+
sl.FieldTypes = append(sl.FieldTypes, typ)
5057+
5058+
if !p.eat(",") {
5059+
break
5060+
}
5061+
}
5062+
if err := p.expect(">"); err != nil {
5063+
return StructLiteral{}, err
5064+
}
5065+
}
5066+
5067+
// Parse the field values
5068+
err := p.parseCommaList("(", ")", func(p *parser) *parseError {
5069+
e, err := p.parseExpr()
5070+
if err != nil {
5071+
return err
5072+
}
5073+
sl.Fields = append(sl.Fields, e)
5074+
return nil
5075+
})
5076+
return sl, err
5077+
}
5078+
49625079
// TODO: There should be exported Parse{Date,Timestamp}Literal package-level funcs
49635080
// to support spannertest coercing plain string literals when used in a typed context.
49645081
// Those should wrap parseDateLit and parseTimestampLit below.

spanner/spansql/sql.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,9 @@ func (sel Select) addSQL(sb *strings.Builder) {
873873
if sel.Distinct {
874874
sb.WriteString(" DISTINCT")
875875
}
876+
if sel.AsStruct {
877+
sb.WriteString(" AS STRUCT")
878+
}
876879

877880
// SELECT list with aliases
878881
for i, e := range sel.List {
@@ -1091,6 +1094,13 @@ func (ss ScalarSubquery) addSQL(sb *strings.Builder) {
10911094
sb.WriteString(")")
10921095
}
10931096

1097+
func (as ArraySubquery) SQL() string { return buildSQL(as) }
1098+
func (as ArraySubquery) addSQL(sb *strings.Builder) {
1099+
sb.WriteString("ARRAY(")
1100+
as.Query.addSQL(sb)
1101+
sb.WriteString(")")
1102+
}
1103+
10941104
func (io InOp) SQL() string { return buildSQL(io) }
10951105
func (io InOp) addSQL(sb *strings.Builder) {
10961106
io.LHS.addSQL(sb)
@@ -1232,6 +1242,24 @@ func (a Array) addSQL(sb *strings.Builder) {
12321242
sb.WriteString("]")
12331243
}
12341244

1245+
func (sl StructLiteral) SQL() string { return buildSQL(sl) }
1246+
func (sl StructLiteral) addSQL(sb *strings.Builder) {
1247+
sb.WriteString("STRUCT")
1248+
if len(sl.FieldTypes) > 0 {
1249+
sb.WriteString("<")
1250+
for i, typ := range sl.FieldTypes {
1251+
if i > 0 {
1252+
sb.WriteString(", ")
1253+
}
1254+
sb.WriteString(typ.SQL())
1255+
}
1256+
sb.WriteString(">")
1257+
}
1258+
sb.WriteString("(")
1259+
addExprList(sb, sl.Fields, ", ")
1260+
sb.WriteString(")")
1261+
}
1262+
12351263
func (id ID) SQL() string { return buildSQL(id) }
12361264
func (id ID) addSQL(sb *strings.Builder) {
12371265
// https://cloud.google.com/spanner/docs/lexical#identifiers

0 commit comments

Comments
 (0)