spanner/spansql: parse foreign key constraints in CREATE TABLE
This defines the AST structures and parses named and unnamed constraints
consisting of a foreign key in a CREATE TABLE statement.
Future changes will add foreign key support in ALTER TABLE.
Change-Id: I98547e1d093892a394e44ae6b9cb03a26f9ff43b
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/52990
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Knut Olav Løite <koloite@gmail.com>
diff --git a/spanner/spansql/keywords.go b/spanner/spansql/keywords.go
index e7a8941..4dce1ca 100644
--- a/spanner/spansql/keywords.go
+++ b/spanner/spansql/keywords.go
@@ -123,6 +123,12 @@
"WINDOW": true,
"WITH": true,
"WITHIN": true,
+
+ // For new foreign key support; these aren't officially listed yet.
+ "CONSTRAINT": true,
+ "FOREIGN": true,
+ "KEY": true,
+ "REFERENCES": true,
}
// funcs is the set of reserved keywords that are functions.
diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go
index ff9cf1b..5e62851 100644
--- a/spanner/spansql/parser.go
+++ b/spanner/spansql/parser.go
@@ -947,7 +947,7 @@
/*
CREATE TABLE table_name(
- [column_def, ...] )
+ [column_def, ...] [ table_constraint, ...] )
primary_key [, cluster]
primary_key:
@@ -971,6 +971,15 @@
ct := &CreateTable{Name: tname, Position: pos}
err = p.parseCommaList(func(p *parser) *parseError {
+ if p.sniff("CONSTRAINT") || p.sniff("FOREIGN") {
+ tc, err := p.parseTableConstraint()
+ if err != nil {
+ return err
+ }
+ ct.Constraints = append(ct.Constraints, tc)
+ return nil
+ }
+
cd, err := p.parseColumnDef()
if err != nil {
return err
@@ -1322,6 +1331,78 @@
return kp, nil
}
+func (p *parser) parseTableConstraint() (TableConstraint, *parseError) {
+ debugf("parseTableConstraint: %v", p)
+
+ /*
+ table_constraint:
+ [ CONSTRAINT constraint_name ]
+ foreign_key
+ */
+
+ if p.eat("CONSTRAINT") {
+ pos := p.Pos()
+ // Named foreign key.
+ cname, err := p.parseTableOrIndexOrColumnName()
+ if err != nil {
+ return TableConstraint{}, err
+ }
+ fk, err := p.parseForeignKey()
+ if err != nil {
+ return TableConstraint{}, err
+ }
+ return TableConstraint{
+ Name: cname,
+ ForeignKey: fk,
+ Position: pos,
+ }, nil
+ }
+
+ // Unnamed foreign key.
+ fk, err := p.parseForeignKey()
+ if err != nil {
+ return TableConstraint{}, err
+ }
+ return TableConstraint{
+ ForeignKey: fk,
+ Position: fk.Position,
+ }, nil
+}
+
+func (p *parser) parseForeignKey() (ForeignKey, *parseError) {
+ debugf("parseForeignKey: %v", p)
+
+ /*
+ foreign_key:
+ FOREIGN KEY ( column_name [, ... ] ) REFERENCES ref_table ( ref_column [, ... ] )
+ */
+
+ if err := p.expect("FOREIGN"); err != nil {
+ return ForeignKey{}, err
+ }
+ fk := ForeignKey{Position: p.Pos()}
+ if err := p.expect("KEY"); err != nil {
+ return ForeignKey{}, err
+ }
+ var err *parseError
+ fk.Columns, err = p.parseColumnNameList()
+ if err != nil {
+ return ForeignKey{}, err
+ }
+ if err := p.expect("REFERENCES"); err != nil {
+ return ForeignKey{}, err
+ }
+ fk.RefTable, err = p.parseTableOrIndexOrColumnName()
+ if err != nil {
+ return ForeignKey{}, err
+ }
+ fk.RefColumns, err = p.parseColumnNameList()
+ if err != nil {
+ return ForeignKey{}, err
+ }
+ return fk, nil
+}
+
func (p *parser) parseColumnNameList() ([]string, *parseError) {
var list []string
err := p.parseCommaList(func(p *parser) *parseError {
diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go
index 998ca90..5c30791 100644
--- a/spanner/spansql/parser_test.go
+++ b/spanner/spansql/parser_test.go
@@ -248,7 +248,9 @@
) STORING (Count), INTERLEAVE IN SomeTable;
CREATE TABLE FooBarAux (
System STRING(MAX) NOT NULL,
+ CONSTRAINT Con1 FOREIGN KEY (System) REFERENCES FooBar (System),
RepoPath STRING(MAX) NOT NULL,
+ FOREIGN KEY (System, RepoPath) REFERENCES Stranger (Sys, RPath), -- unnamed foreign key
Author STRING(MAX) NOT NULL,
) PRIMARY KEY(System, RepoPath, Author),
INTERLEAVE IN PARENT FooBar ON DELETE CASCADE;
@@ -296,8 +298,29 @@
Name: "FooBarAux",
Columns: []ColumnDef{
{Name: "System", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(12)},
- {Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(13)},
- {Name: "Author", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(14)},
+ {Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(14)},
+ {Name: "Author", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(16)},
+ },
+ Constraints: []TableConstraint{
+ {
+ Name: "Con1",
+ ForeignKey: ForeignKey{
+ Columns: []string{"System"},
+ RefTable: "FooBar",
+ RefColumns: []string{"System"},
+ Position: line(13),
+ },
+ Position: line(13),
+ },
+ {
+ ForeignKey: ForeignKey{
+ Columns: []string{"System", "RepoPath"},
+ RefTable: "Stranger",
+ RefColumns: []string{"Sys", "RPath"},
+ Position: line(15),
+ },
+ Position: line(15),
+ },
},
PrimaryKey: []KeyPart{
{Column: "System"},
@@ -312,18 +335,18 @@
},
&AlterTable{
Name: "FooBar",
- Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(18)}},
- Position: line(18),
+ Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(20)}},
+ Position: line(20),
},
&AlterTable{
Name: "FooBar",
Alteration: DropColumn{Name: "TZ"},
- Position: line(19),
+ Position: line(21),
},
&AlterTable{
Name: "FooBar",
Alteration: SetOnDelete{Action: NoActionOnDelete},
- Position: line(20),
+ Position: line(22),
},
&AlterTable{
Name: "FooBar",
@@ -331,21 +354,21 @@
Name: "Author",
Type: Type{Base: String, Len: MaxLen},
NotNull: true,
- Position: line(21),
+ Position: line(23),
}},
- Position: line(21),
+ Position: line(23),
},
- &DropIndex{Name: "MyFirstIndex", Position: line(23)},
- &DropTable{Name: "FooBar", Position: line(24)},
+ &DropIndex{Name: "MyFirstIndex", Position: line(25)},
+ &DropTable{Name: "FooBar", Position: line(26)},
&CreateTable{
Name: "NonScalars",
Columns: []ColumnDef{
- {Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(29)},
- {Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(30)},
- {Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(31)},
+ {Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(31)},
+ {Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(32)},
+ {Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(33)},
},
PrimaryKey: []KeyPart{{Column: "Dummy"}},
- Position: line(28),
+ Position: line(30),
},
}, Comments: []*Comment{
{Marker: "#", Start: line(2), End: line(2),
@@ -354,11 +377,13 @@
Text: []string{"This is another comment."}},
{Marker: "/*", Start: line(4), End: line(5),
Text: []string{" This is a", "\t\t\t\t\t\t * multiline comment."}},
- {Marker: "--", Isolated: true, Start: line(26), End: line(27),
+ {Marker: "--", Start: line(15), End: line(15),
+ Text: []string{"unnamed foreign key"}},
+ {Marker: "--", Isolated: true, Start: line(28), End: line(29),
Text: []string{"This table has some commentary", "that spans multiple lines."}},
// These comments shouldn't get combined:
- {Marker: "--", Start: line(29), End: line(29), Text: []string{"dummy comment"}},
- {Marker: "--", Start: line(30), End: line(30), Text: []string{"comment on ids"}},
+ {Marker: "--", Start: line(31), End: line(31), Text: []string{"dummy comment"}},
+ {Marker: "--", Start: line(32), End: line(32), Text: []string{"comment on ids"}},
}}},
// No trailing comma:
{`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{
diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go
index f5ffc45..5cbfcfb 100644
--- a/spanner/spansql/sql.go
+++ b/spanner/spansql/sql.go
@@ -29,6 +29,9 @@
for _, c := range ct.Columns {
str += " " + c.SQL() + ",\n"
}
+ for _, tc := range ct.Constraints {
+ str += " " + tc.SQL() + ",\n"
+ }
str += ") PRIMARY KEY("
for i, c := range ct.PrimaryKey {
if i > 0 {
@@ -127,6 +130,24 @@
return str
}
+func (tc TableConstraint) SQL() string {
+ var str string
+ if tc.Name != "" {
+ str += "CONSTRAINT " + tc.Name
+ }
+ str += tc.ForeignKey.SQL()
+ return str
+}
+
+func (fk ForeignKey) SQL() string {
+ str := "FOREIGN KEY ("
+ str += strings.Join(fk.Columns, ", ")
+ str += ") REFERENCES " + fk.RefTable + " ("
+ str += strings.Join(fk.RefColumns, ", ")
+ str += ")"
+ return str
+}
+
func (t Type) SQL() string {
str := t.Base.SQL()
if t.Base == String || t.Base == Bytes {
diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go
index 8af40e3..5c915b4 100644
--- a/spanner/spansql/types.go
+++ b/spanner/spansql/types.go
@@ -29,10 +29,11 @@
// CreateTable represents a CREATE TABLE statement.
// https://cloud.google.com/spanner/docs/data-definition-language#create_table
type CreateTable struct {
- Name string
- Columns []ColumnDef
- PrimaryKey []KeyPart
- Interleave *Interleave
+ Name string
+ Columns []ColumnDef
+ Constraints []TableConstraint
+ PrimaryKey []KeyPart
+ Interleave *Interleave
Position Position // position of the "CREATE" token
}
@@ -45,9 +46,27 @@
// Mutate in place.
ct.Columns[i].clearOffset()
}
+ for i := range ct.Constraints {
+ // Mutate in place.
+ ct.Constraints[i].clearOffset()
+ }
ct.Position.Offset = 0
}
+// TableConstraint represents a constraint on a table.
+type TableConstraint struct {
+ Name string // may be empty
+ ForeignKey ForeignKey
+
+ Position Position // position of the "CONSTRAINT" or "FOREIGN" token
+}
+
+func (tc TableConstraint) Pos() Position { return tc.Position }
+func (tc *TableConstraint) clearOffset() {
+ tc.Position.Offset = 0
+ tc.ForeignKey.clearOffset()
+}
+
// Interleave represents an interleave clause of a CREATE TABLE statement.
type Interleave struct {
Parent string
@@ -181,6 +200,19 @@
func (cd ColumnDef) Pos() Position { return cd.Position }
func (cd *ColumnDef) clearOffset() { cd.Position.Offset = 0 }
+// ForeignKey represents a foreign key definition as part of a CREATE TABLE
+// or ALTER TABLE statement.
+type ForeignKey struct {
+ Columns []string
+ RefTable string
+ RefColumns []string
+
+ Position Position // position of the "FOREIGN" token
+}
+
+func (fk ForeignKey) Pos() Position { return fk.Position }
+func (fk *ForeignKey) clearOffset() { fk.Position.Offset = 0 }
+
// Type represents a column type.
type Type struct {
Array bool