diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index b2feb3fe145..8686e9f0335 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -982,7 +982,7 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { /* statement: - { create_database | create_table | create_index | alter_table | drop_table | drop_index | create_change_stream | alter_change_stream | drop_change_stream } + { create_database | create_table | create_index | alter_table | drop_table | rename_table | drop_index | create_change_stream | alter_change_stream | drop_change_stream } */ // TODO: support create_database @@ -1069,6 +1069,9 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { } return &DropSequence{Name: name, IfExists: ifExists, Position: pos}, nil } + } else if p.sniff("RENAME", "TABLE") { + a, err := p.parseRenameTable() + return a, err } else if p.sniff("ALTER", "DATABASE") { a, err := p.parseAlterDatabase() return a, err @@ -1106,9 +1109,12 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { /* CREATE TABLE [ IF NOT EXISTS ] table_name( - [column_def, ...] [ table_constraint, ...] ) + [column_def, ...] [ table_constraint, ...] [ synonym ] ) primary_key [, cluster] + synonym: + SYNONYM (name) + primary_key: PRIMARY KEY ( [key_part, ...] ) @@ -1143,6 +1149,15 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { return nil } + if p.sniffTableSynonym() { + ts, err := p.parseTableSynonym() + if err != nil { + return err + } + ct.Synonym = ts + return nil + } + cd, err := p.parseColumnDef() if err != nil { return err @@ -1228,6 +1243,35 @@ func (p *parser) sniffTableConstraint() bool { return p.sniff("FOREIGN") || p.sniff("CHECK") } +func (p *parser) sniffTableSynonym() bool { + return p.sniff("SYNONYM") +} + +func (p *parser) parseTableSynonym() (ID, *parseError) { + debugf("parseTableSynonym: %v", p) + + /* + table_synonym: + SYNONYM ( name ) + */ + + if err := p.expect("SYNONYM"); err != nil { + return "", err + } + if err := p.expect("("); err != nil { + return "", err + } + name, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return "", err + } + if err := p.expect(")"); err != nil { + return "", err + } + + return name, nil +} + func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) { debugf("parseCreateIndex: %v", p) @@ -1580,7 +1624,10 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { | DROP [ COLUMN ] column_name | ADD table_constraint | DROP CONSTRAINT constraint_name - | SET ON DELETE { CASCADE | NO ACTION } } + | SET ON DELETE { CASCADE | NO ACTION } + | ADD SYNONYM synonym_name + | DROP SYNONYM synonym_name + | RENAME TO new_table_name } table_column_alteration: ALTER [ COLUMN ] column_name { { scalar_type | array_type } [NOT NULL] | SET options_def } @@ -1625,6 +1672,16 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { return a, nil } + // TODO: "COLUMN" is optional. A column named SYNONYM is allowed. + if p.eat("SYNONYM") { + synonym, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + a.Alteration = AddSynonym{Name: synonym} + return a, nil + } + // TODO: "COLUMN" is optional. if err := p.expect("COLUMN"); err != nil { return nil, err @@ -1654,6 +1711,16 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { return a, nil } + // TODO: "COLUMN" is optional. A column named SYNONYM is allowed. + if p.eat("SYNONYM") { + synonym, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + a.Alteration = DropSynonym{Name: synonym} + return a, nil + } + // TODO: "COLUMN" is optional. if err := p.expect("COLUMN"); err != nil { return nil, err @@ -1704,10 +1771,78 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { a.Alteration = ReplaceRowDeletionPolicy{RowDeletionPolicy: rdp} return a, nil } + case tok.caseEqual("RENAME"): + if p.eat("TO") { + newName, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + rt := RenameTo{ToName: newName} + if p.eat(",", "ADD", "SYNONYM") { + synonym, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + rt.Synonym = synonym + } + a.Alteration = rt + return a, nil + } } return a, nil } +func (p *parser) parseRenameTable() (*RenameTable, *parseError) { + debugf("parseRenameTable: %v", p) + + /* + RENAME TABLE table_name TO new_name [, table_name2 TO new_name2, ...] + */ + + if err := p.expect("RENAME"); err != nil { + return nil, err + } + pos := p.Pos() + if err := p.expect("TABLE"); err != nil { + return nil, err + } + rt := &RenameTable{ + Position: pos, + } + + var renameOps []TableRenameOp + for { + fromName, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + if err := p.expect("TO"); err != nil { + return nil, err + } + toName, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + renameOps = append(renameOps, TableRenameOp{FromName: fromName, ToName: toName}) + + tok := p.next() + if tok.err != nil { + if tok.err == eof { + break + } + return nil, tok.err + } else if tok.value == "," { + continue + } else if tok.value == ";" { + break + } else { + return nil, p.errorf("unexpected token %q", tok.value) + } + } + rt.TableRenameOps = renameOps + return rt, nil +} + func (p *parser) parseAlterDatabase() (*AlterDatabase, *parseError) { debugf("parseAlterDatabase: %v", p) diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index 6d534abcb07..62430d98fdd 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -714,6 +714,33 @@ func TestParseDDL(t *testing.T) { ); DROP SEQUENCE MySequence; + -- Table with a synonym. + CREATE TABLE TableWithSynonym ( + Name STRING(MAX) NOT NULL, + SYNONYM(AnotherName), + ) PRIMARY KEY (Name); + + ALTER TABLE TableWithSynonym DROP SYNONYM AnotherName; + ALTER TABLE TableWithSynonym ADD SYNONYM YetAnotherName; + + -- Table rename. + CREATE TABLE OldName ( + Name STRING(MAX) NOT NULL, + ) PRIMARY KEY (Name); + + ALTER TABLE OldName RENAME TO NewName; + ALTER TABLE NewName RENAME TO OldName, ADD SYNONYM NewName; + + -- Table rename chain. + CREATE TABLE Table1 ( + Name STRING(MAX) NOT NULL, + ) PRIMARY KEY (Name); + CREATE TABLE Table2 ( + Name STRING(MAX) NOT NULL, + ) PRIMARY KEY (Name); + + RENAME TABLE Table1 TO temp, Table2 TO Table1, temp TO Table2; + -- Trailing comment at end of file. `, &DDL{Filename: "filename", List: []DDLStmt{ &CreateTable{ @@ -1164,6 +1191,77 @@ func TestParseDDL(t *testing.T) { Position: line(114), }, &DropSequence{Name: "MySequence", Position: line(120)}, + + &CreateTable{ + Name: "TableWithSynonym", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(124)}, + }, + Synonym: "AnotherName", + PrimaryKey: []KeyPart{{Column: "Name"}}, + Position: line(123), + }, + &AlterTable{ + Name: "TableWithSynonym", + Alteration: DropSynonym{ + Name: "AnotherName", + }, + Position: line(128), + }, + &AlterTable{ + Name: "TableWithSynonym", + Alteration: AddSynonym{ + Name: "YetAnotherName", + }, + Position: line(129), + }, + &CreateTable{ + Name: "OldName", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(133)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + Position: line(132), + }, + &AlterTable{ + Name: "OldName", + Alteration: RenameTo{ + ToName: "NewName", + }, + Position: line(136), + }, + &AlterTable{ + Name: "NewName", + Alteration: RenameTo{ + ToName: "OldName", + Synonym: "NewName", + }, + Position: line(137), + }, + &CreateTable{ + Name: "Table1", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(141)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + Position: line(140), + }, + &CreateTable{ + Name: "Table2", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(144)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + Position: line(143), + }, + &RenameTable{ + TableRenameOps: []TableRenameOp{ + {FromName: "Table1", ToName: "temp"}, + {FromName: "Table2", ToName: "Table1"}, + {FromName: "temp", ToName: "Table2"}, + }, + Position: line(147), + }, }, Comments: []*Comment{ { Marker: "#", Start: line(2), End: line(2), @@ -1197,9 +1295,12 @@ func TestParseDDL(t *testing.T) { {Marker: "--", Isolated: true, Start: line(43), End: line(43), Text: []string{"Table with generated column."}}, {Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Table with row deletion policy."}}, {Marker: "--", Isolated: true, Start: line(75), End: line(75), Text: []string{"Table has a column with a default value."}}, + {Marker: "--", Isolated: true, Start: line(122), End: line(122), Text: []string{"Table with a synonym."}}, + {Marker: "--", Isolated: true, Start: line(131), End: line(131), Text: []string{"Table rename."}}, + {Marker: "--", Isolated: true, Start: line(139), End: line(139), Text: []string{"Table rename chain."}}, // Comment after everything else. - {Marker: "--", Isolated: true, Start: line(122), End: line(122), Text: []string{"Trailing comment at end of file."}}, + {Marker: "--", Isolated: true, Start: line(149), End: line(149), Text: []string{"Trailing comment at end of file."}}, }}}, // 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 687a18f58b4..577a45e2ef7 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -49,6 +49,9 @@ func (ct CreateTable) SQL() string { for _, tc := range ct.Constraints { str += " " + tc.SQL() + ",\n" } + if len(ct.Synonym) > 0 { + str += " SYNONYM(" + ct.Synonym.SQL() + "),\n" + } str += ") PRIMARY KEY(" for i, c := range ct.PrimaryKey { if i > 0 { @@ -306,6 +309,22 @@ func (dc DropConstraint) SQL() string { return "DROP CONSTRAINT " + dc.Name.SQL() } +func (rt RenameTo) SQL() string { + str := "RENAME TO " + rt.ToName.SQL() + if len(rt.Synonym) > 0 { + str += ", ADD SYNONYM " + rt.Synonym.SQL() + } + return str +} + +func (as AddSynonym) SQL() string { + return "ADD SYNONYM " + as.Name.SQL() +} + +func (ds DropSynonym) SQL() string { + return "DROP SYNONYM " + ds.Name.SQL() +} + func (sod SetOnDelete) SQL() string { return "SET ON DELETE " + sod.Action.SQL() } @@ -373,6 +392,17 @@ func (co ColumnOptions) SQL() string { return str } +func (rt RenameTable) SQL() string { + str := "RENAME TABLE " + for i, op := range rt.TableRenameOps { + if i > 0 { + str += ", " + } + str += op.FromName.SQL() + " TO " + op.ToName.SQL() + } + return str +} + func (ad AlterDatabase) SQL() string { return "ALTER DATABASE " + ad.Name.SQL() + " " + ad.Alteration.SQL() } diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index 645190b316b..805f28b8352 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -168,6 +168,22 @@ func TestSQL(t *testing.T) { ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ))`, reparseDDL, }, + { + &CreateTable{ + Name: "WithSynonym", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + Synonym: "AnotherName", + Position: line(1), + }, + `CREATE TABLE WithSynonym ( + Name STRING(MAX) NOT NULL, + SYNONYM(AnotherName), +) PRIMARY KEY(Name)`, + reparseDDL, + }, { &DropTable{ Name: "Ta", @@ -494,6 +510,52 @@ func TestSQL(t *testing.T) { "ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ))", reparseDDL, }, + { + &AlterTable{ + Name: "Ta", + Alteration: AddSynonym{ + Name: "Syn", + }, + Position: line(1), + }, + "ALTER TABLE Ta ADD SYNONYM Syn", + reparseDDL, + }, + { + &AlterTable{ + Name: "Ta", + Alteration: DropSynonym{ + Name: "Syn", + }, + Position: line(1), + }, + "ALTER TABLE Ta DROP SYNONYM Syn", + reparseDDL, + }, + { + &AlterTable{ + Name: "Ta", + Alteration: RenameTo{ + ToName: "Tb", + Synonym: "Syn", + }, + Position: line(1), + }, + "ALTER TABLE Ta RENAME TO Tb, ADD SYNONYM Syn", + reparseDDL, + }, + { + &RenameTable{ + TableRenameOps: []TableRenameOp{ + {FromName: "Ta", ToName: "tmp"}, + {FromName: "Tb", ToName: "Ta"}, + {FromName: "tmp", ToName: "Tb"}, + }, + Position: line(1), + }, + "RENAME TABLE Ta TO tmp, Tb TO Ta, tmp TO Tb", + reparseDDL, + }, { &AlterDatabase{ Name: "dbname", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index ea4016ae0f7..481d83f10f2 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -40,6 +40,7 @@ type CreateTable struct { PrimaryKey []KeyPart Interleave *Interleave RowDeletionPolicy *RowDeletionPolicy + Synonym ID // may be empty Position Position // position of the "CREATE" token } @@ -278,8 +279,9 @@ func (at *AlterTable) clearOffset() { } // TableAlteration is satisfied by AddColumn, DropColumn, AddConstraint, -// DropConstraint, SetOnDelete and AlterColumn, -// AddRowDeletionPolicy, ReplaceRowDeletionPolicy, DropRowDeletionPolicy. +// DropConstraint, SetOnDelete, AlterColumn, +// AddRowDeletionPolicy, ReplaceRowDeletionPolicy, DropRowDeletionPolicy, +// RenameTo, AddSynonym, and DropSynonym. type TableAlteration interface { isTableAlteration() SQL() string @@ -294,6 +296,9 @@ func (AlterColumn) isTableAlteration() {} func (AddRowDeletionPolicy) isTableAlteration() {} func (ReplaceRowDeletionPolicy) isTableAlteration() {} func (DropRowDeletionPolicy) isTableAlteration() {} +func (RenameTo) isTableAlteration() {} +func (AddSynonym) isTableAlteration() {} +func (DropSynonym) isTableAlteration() {} type ( AddColumn struct { @@ -348,6 +353,32 @@ const ( CascadeOnDelete ) +type ( + RenameTo struct { + ToName ID + Synonym ID // may be empty + } + AddSynonym struct{ Name ID } + DropSynonym struct{ Name ID } +) + +// RenameTable represents a RENAME TABLE statement. +type RenameTable struct { + TableRenameOps []TableRenameOp + + Position Position // position of the "RENAME" token +} + +type TableRenameOp struct { + FromName ID + ToName ID +} + +func (rt *RenameTable) String() string { return fmt.Sprintf("%#v", rt) } +func (*RenameTable) isDDLStmt() {} +func (rt *RenameTable) Pos() Position { return rt.Position } +func (rt *RenameTable) clearOffset() { rt.Position.Offset = 0 } + // AlterDatabase represents an ALTER DATABASE statement. // https://cloud.google.com/spanner/docs/data-definition-language#alter-database type AlterDatabase struct {