From 3d6c6c7873e1b75e8b492ede2e561411dc40536a Mon Sep 17 00:00:00 2001 From: Takeshi Nakata Date: Tue, 27 Jul 2021 16:52:41 +0900 Subject: [PATCH] feat(spanner/spansql): add ROW DELETION POLICY parsing (#4496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(spanner/spansql): modify expect functions to allow ...string * feat(spanner/spansql): add ROW DELETION POLICY parse. This enables to parse `ROW DELETION POLICY` at `CREATE TABLE`, `ALTER TABLE` clauses. * fix(spanner/spansql): add empty line between functions * fix(spanner/spansql): eat `ROW DELETION POLICY` first Co-authored-by: Knut Olav Løite --- spanner/spansql/parser.go | 78 +++++++++++++++++++++++++++++++--- spanner/spansql/parser_test.go | 52 ++++++++++++++++++++++- spanner/spansql/sql.go | 18 ++++++++ spanner/spansql/sql_test.go | 58 +++++++++++++++++++++++++ spanner/spansql/types.go | 38 +++++++++++------ 5 files changed, 224 insertions(+), 20 deletions(-) diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index a575d781069..44fdfc7d82f 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -933,13 +933,15 @@ func (p *parser) eat(want ...string) bool { return true } -func (p *parser) expect(want string) *parseError { - tok := p.next() - if tok.err != nil { - return tok.err - } - if !tok.caseEqual(want) { - return p.errorf("got %q while expecting %q", tok.value, want) +func (p *parser) expect(want ...string) *parseError { + for _, w := range want { + tok := p.next() + if tok.err != nil { + return tok.err + } + if !tok.caseEqual(w) { + return p.errorf("got %q while expecting %q", tok.value, w) + } } return nil } @@ -1082,6 +1084,13 @@ func (p *parser) parseCreateTable() (*CreateTable, *parseError) { ct.Interleave.OnDelete = od } } + if p.eat(",", "ROW", "DELETION", "POLICY") { + rdp, err := p.parseRowDeletionPolicy() + if err != nil { + return nil, err + } + ct.RowDeletionPolicy = &rdp + } return ct, nil } @@ -1236,6 +1245,15 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { return a, nil } + if p.eat("ROW", "DELETION", "POLICY") { + rdp, err := p.parseRowDeletionPolicy() + if err != nil { + return nil, err + } + a.Alteration = AddRowDeletionPolicy{RowDeletionPolicy: rdp} + return a, nil + } + // TODO: "COLUMN" is optional. if err := p.expect("COLUMN"); err != nil { return nil, err @@ -1256,6 +1274,11 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { return a, nil } + if p.eat("ROW", "DELETION", "POLICY") { + a.Alteration = DropRowDeletionPolicy{} + return a, nil + } + // TODO: "COLUMN" is optional. if err := p.expect("COLUMN"); err != nil { return nil, err @@ -1297,7 +1320,17 @@ func (p *parser) parseAlterTable() (*AlterTable, *parseError) { Alteration: ca, } return a, nil + case tok.caseEqual("REPLACE"): + if p.eat("ROW", "DELETION", "POLICY") { + rdp, err := p.parseRowDeletionPolicy() + if err != nil { + return nil, err + } + a.Alteration = ReplaceRowDeletionPolicy{RowDeletionPolicy: rdp} + return a, nil + } } + return a, nil } func (p *parser) parseAlterDatabase() (*AlterDatabase, *parseError) { @@ -2963,6 +2996,37 @@ func (p *parser) parseOnDelete() (OnDelete, *parseError) { return NoActionOnDelete, nil } +func (p *parser) parseRowDeletionPolicy() (RowDeletionPolicy, *parseError) { + if err := p.expect("(", "OLDER_THAN", "("); err != nil { + return RowDeletionPolicy{}, err + } + cname, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return RowDeletionPolicy{}, err + } + if err := p.expect(",", "INTERVAL"); err != nil { + return RowDeletionPolicy{}, err + } + tok := p.next() + if tok.err != nil { + return RowDeletionPolicy{}, tok.err + } + if tok.typ != int64Token { + return RowDeletionPolicy{}, p.errorf("got %q, expected int64 token", tok.value) + } + n, serr := strconv.ParseInt(tok.value, tok.int64Base, 64) + if serr != nil { + return RowDeletionPolicy{}, p.errorf("%v", serr) + } + if err := p.expect("DAY", ")", ")"); err != nil { + return RowDeletionPolicy{}, err + } + return RowDeletionPolicy{ + Column: cname, + NumDays: n, + }, nil +} + // parseCommaList parses a comma-separated list enclosed by bra and ket, // delegating to f for the individual element parsing. // Only invoke this with symbols as bra/ket; they are matched literally, not case insensitively. diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index 5bbe8276c5d..8f43aba701e 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -477,6 +477,17 @@ func TestParseDDL(t *testing.T) { NameLen INT64 AS (char_length(Name)) STORED, ) PRIMARY KEY (Name); + -- Table with row deletion policy. + CREATE TABLE WithRowDeletionPolicy ( + Name STRING(MAX) NOT NULL, + DelTimestamp TIMESTAMP NOT NULL, + ) PRIMARY KEY (Name) + , ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY )); + + ALTER TABLE WithRowDeletionPolicy DROP ROW DELETION POLICY; + ALTER TABLE WithRowDeletionPolicy ADD ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY )); + ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY )); + -- Trailing comment at end of file. `, &DDL{Filename: "filename", List: []DDLStmt{ &CreateTable{ @@ -628,6 +639,44 @@ func TestParseDDL(t *testing.T) { PrimaryKey: []KeyPart{{Column: "Name"}}, Position: line(44), }, + &CreateTable{ + Name: "WithRowDeletionPolicy", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(51)}, + {Name: "DelTimestamp", Type: Type{Base: Timestamp}, NotNull: true, Position: line(52)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + RowDeletionPolicy: &RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + Position: line(50), + }, + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: DropRowDeletionPolicy{}, + Position: line(56), + }, + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: AddRowDeletionPolicy{ + RowDeletionPolicy: RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + }, + Position: line(57), + }, + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: ReplaceRowDeletionPolicy{ + RowDeletionPolicy: RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + }, + Position: line(58), + }, }, Comments: []*Comment{ {Marker: "#", Start: line(2), End: line(2), Text: []string{"This is a comment."}}, @@ -647,9 +696,10 @@ func TestParseDDL(t *testing.T) { {Marker: "--", Isolated: true, Start: line(38), End: line(38), Text: []string{"leading multi comment immediately after inline comment"}}, {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."}}, // Comment after everything else. - {Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Trailing comment at end of file."}}, + {Marker: "--", Isolated: true, Start: line(60), End: line(60), 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 e50eca5fb4e..c7c03898f40 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -56,6 +56,9 @@ func (ct CreateTable) SQL() string { if il := ct.Interleave; il != nil { str += ",\n INTERLEAVE IN PARENT " + il.Parent.SQL() + " ON DELETE " + il.OnDelete.SQL() } + if rdp := ct.RowDeletionPolicy; rdp != nil { + str += ",\n " + rdp.SQL() + } return str } @@ -130,6 +133,17 @@ func (ac AlterColumn) SQL() string { return "ALTER COLUMN " + ac.Name.SQL() + " " + ac.Alteration.SQL() } +func (ardp AddRowDeletionPolicy) SQL() string { + return "ADD " + ardp.RowDeletionPolicy.SQL() +} + +func (rrdp ReplaceRowDeletionPolicy) SQL() string { + return "REPLACE " + rrdp.RowDeletionPolicy.SQL() +} + +func (drdp DropRowDeletionPolicy) SQL() string { + return "DROP ROW DELETION POLICY" +} func (sct SetColumnType) SQL() string { str := sct.Type.SQL() if sct.NotNull { @@ -246,6 +260,10 @@ func (tc TableConstraint) SQL() string { return str } +func (rdp RowDeletionPolicy) SQL() string { + return "ROW DELETION POLICY ( OLDER_THAN ( " + rdp.Column.SQL() + ", INTERVAL " + strconv.FormatInt(rdp.NumDays, 10) + " DAY ))" +} + func (fk ForeignKey) SQL() string { str := "FOREIGN KEY (" + idList(fk.Columns, ", ") str += ") REFERENCES " + fk.RefTable.SQL() + " (" diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index 74527149ab3..ddf6190c9ca 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -135,6 +135,27 @@ func TestSQL(t *testing.T) { INTERLEAVE IN PARENT Ta ON DELETE CASCADE`, reparseDDL, }, + { + &CreateTable{ + Name: "WithRowDeletionPolicy", + Columns: []ColumnDef{ + {Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)}, + {Name: "DelTimestamp", Type: Type{Base: Timestamp}, NotNull: true, Position: line(3)}, + }, + PrimaryKey: []KeyPart{{Column: "Name"}}, + RowDeletionPolicy: &RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + Position: line(1), + }, + `CREATE TABLE WithRowDeletionPolicy ( + Name STRING(MAX) NOT NULL, + DelTimestamp TIMESTAMP NOT NULL, +) PRIMARY KEY(Name), + ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ))`, + reparseDDL, + }, { &DropTable{ Name: "Ta", @@ -230,6 +251,43 @@ func TestSQL(t *testing.T) { "ALTER TABLE Ta ALTER COLUMN Ci SET OPTIONS (allow_commit_timestamp = null)", reparseDDL, }, + { + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: DropRowDeletionPolicy{}, + Position: line(1), + }, + "ALTER TABLE WithRowDeletionPolicy DROP ROW DELETION POLICY", + reparseDDL, + }, + { + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: AddRowDeletionPolicy{ + RowDeletionPolicy: RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + }, + Position: line(1), + }, + "ALTER TABLE WithRowDeletionPolicy ADD ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ))", + reparseDDL, + }, + { + &AlterTable{ + Name: "WithRowDeletionPolicy", + Alteration: ReplaceRowDeletionPolicy{ + RowDeletionPolicy: RowDeletionPolicy{ + Column: ID("DelTimestamp"), + NumDays: 30, + }, + }, + Position: line(1), + }, + "ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ))", + reparseDDL, + }, { &AlterDatabase{ Name: "dbname", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index e7d622f6d5a..87b9c14482d 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -33,11 +33,12 @@ import ( // CreateTable represents a CREATE TABLE statement. // https://cloud.google.com/spanner/docs/data-definition-language#create_table type CreateTable struct { - Name ID - Columns []ColumnDef - Constraints []TableConstraint - PrimaryKey []KeyPart - Interleave *Interleave + Name ID + Columns []ColumnDef + Constraints []TableConstraint + PrimaryKey []KeyPart + Interleave *Interleave + RowDeletionPolicy *RowDeletionPolicy Position Position // position of the "CREATE" token } @@ -90,6 +91,12 @@ type Interleave struct { OnDelete OnDelete } +// RowDeletionPolicy represents an row deletion policy clause of a CREATE, ALTER TABLE statement. +type RowDeletionPolicy struct { + Column ID + NumDays int64 +} + // CreateIndex represents a CREATE INDEX statement. // https://cloud.google.com/spanner/docs/data-definition-language#create-index type CreateIndex struct { @@ -162,18 +169,22 @@ func (at *AlterTable) clearOffset() { } // TableAlteration is satisfied by AddColumn, DropColumn, AddConstraint, -// DropConstraint, SetOnDelete and AlterColumn. +// DropConstraint, SetOnDelete and AlterColumn, +// AddRowDeletionPolicy, ReplaceRowDeletionPolicy, DropRowDeletionPolicy. type TableAlteration interface { isTableAlteration() SQL() string } -func (AddColumn) isTableAlteration() {} -func (DropColumn) isTableAlteration() {} -func (AddConstraint) isTableAlteration() {} -func (DropConstraint) isTableAlteration() {} -func (SetOnDelete) isTableAlteration() {} -func (AlterColumn) isTableAlteration() {} +func (AddColumn) isTableAlteration() {} +func (DropColumn) isTableAlteration() {} +func (AddConstraint) isTableAlteration() {} +func (DropConstraint) isTableAlteration() {} +func (SetOnDelete) isTableAlteration() {} +func (AlterColumn) isTableAlteration() {} +func (AddRowDeletionPolicy) isTableAlteration() {} +func (ReplaceRowDeletionPolicy) isTableAlteration() {} +func (DropRowDeletionPolicy) isTableAlteration() {} type AddColumn struct{ Def ColumnDef } type DropColumn struct{ Name ID } @@ -184,6 +195,9 @@ type AlterColumn struct { Name ID Alteration ColumnAlteration } +type AddRowDeletionPolicy struct{ RowDeletionPolicy RowDeletionPolicy } +type ReplaceRowDeletionPolicy struct{ RowDeletionPolicy RowDeletionPolicy } +type DropRowDeletionPolicy struct{} // ColumnAlteration is satisfied by SetColumnType and SetColumnOptions. type ColumnAlteration interface {