From ccd020598921f1b5550587c95b4ceddf580705bb Mon Sep 17 00:00:00 2001 From: TOGASHI Tomoki Date: Fri, 25 Aug 2023 13:35:21 +0900 Subject: [PATCH] feat(spanner/spansql): add support for SEQUENCE statements (#8481) * feat(spanner/spansql): add support for SEQUENCE statements * feat(spanner/spansql): remove using generics for CI env --------- Co-authored-by: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> Co-authored-by: rahul2393 --- spanner/spansql/parser.go | 169 +++++++++++++++++++++++++++++++++ spanner/spansql/parser_test.go | 71 +++++++++++++- spanner/spansql/sql.go | 55 +++++++++++ spanner/spansql/sql_test.go | 81 ++++++++++++++++ spanner/spansql/types.go | 60 ++++++++++++ 5 files changed, 435 insertions(+), 1 deletion(-) diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index f258cdfb0494..eebf39d644a2 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -1058,6 +1058,16 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { return nil, err } return &DropChangeStream{Name: name, Position: pos}, nil + case tok.caseEqual("SEQUENCE"): + var ifExists bool + if p.eat("IF", "EXISTS") { + ifExists = true + } + name, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + return &DropSequence{Name: name, IfExists: ifExists, Position: pos}, nil } } else if p.sniff("ALTER", "DATABASE") { a, err := p.parseAlterDatabase() @@ -1080,6 +1090,12 @@ func (p *parser) parseDDLStmt() (DDLStmt, *parseError) { } else if p.sniff("ALTER", "INDEX") { ai, err := p.parseAlterIndex() return ai, err + } else if p.sniff("CREATE", "SEQUENCE") { + cs, err := p.parseCreateSequence() + return cs, err + } else if p.sniff("ALTER", "SEQUENCE") { + as, err := p.parseAlterSequence() + return as, err } return nil, p.errorf("unknown DDL statement") @@ -2673,6 +2689,159 @@ func (p *parser) parseAlterIndex() (*AlterIndex, *parseError) { return nil, p.errorf("got %q, expected ADD or DROP", tok.value) } +func (p *parser) parseCreateSequence() (*CreateSequence, *parseError) { + debugf("parseCreateSequence: %v", p) + + /* + CREATE SEQUENCE + [ IF NOT EXISTS ] sequence_name + [ OPTIONS ( sequence_options ) ] + */ + + if err := p.expect("CREATE"); err != nil { + return nil, err + } + pos := p.Pos() + if err := p.expect("SEQUENCE"); err != nil { + return nil, err + } + var ifNotExists bool + if p.eat("IF", "NOT", "EXISTS") { + ifNotExists = true + } + sname, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + + cs := &CreateSequence{Name: sname, IfNotExists: ifNotExists, Position: pos} + + if p.sniff("OPTIONS") { + cs.Options, err = p.parseSequenceOptions() + if err != nil { + return nil, err + } + } + + return cs, nil +} + +func (p *parser) parseAlterSequence() (*AlterSequence, *parseError) { + debugf("parseAlterSequence: %v", p) + + /* + ALTER SEQUENCE sequence_name + SET OPTIONS sequence_options + */ + + if err := p.expect("ALTER"); err != nil { + return nil, err + } + pos := p.Pos() + if err := p.expect("SEQUENCE"); err != nil { + return nil, err + } + sname, err := p.parseTableOrIndexOrColumnName() + if err != nil { + return nil, err + } + + as := &AlterSequence{Name: sname, Position: pos} + + tok := p.next() + if tok.err != nil { + return nil, tok.err + } + switch { + default: + return nil, p.errorf("got %q, expected SET", tok.value) + case tok.caseEqual("SET"): + options, err := p.parseSequenceOptions() + if err != nil { + return nil, err + } + as.Alteration = SetSequenceOptions{Options: options} + return as, nil + } +} + +func (p *parser) parseSequenceOptions() (SequenceOptions, *parseError) { + debugf("parseSequenceOptions: %v", p) + + if err := p.expect("OPTIONS", "("); err != nil { + return SequenceOptions{}, err + } + + // We ignore case for the key (because it is easier) but not the value. + var so SequenceOptions + for { + if p.eat("sequence_kind", "=") { + tok := p.next() + if tok.err != nil { + return SequenceOptions{}, tok.err + } + if tok.typ != stringToken { + return SequenceOptions{}, p.errorf("invalid sequence_kind value: %v", tok.value) + } + sequenceKind := tok.string + so.SequenceKind = &sequenceKind + } else if p.eat("skip_range_min", "=") { + tok := p.next() + if tok.err != nil { + return SequenceOptions{}, tok.err + } + if tok.typ != int64Token { + return SequenceOptions{}, p.errorf("invalid skip_range_min value: %v", tok.value) + } + value, err := strconv.Atoi(tok.value) + if err != nil { + return SequenceOptions{}, p.errorf("invalid skip_range_min value: %v", tok.value) + } + so.SkipRangeMin = &value + } else if p.eat("skip_range_max", "=") { + tok := p.next() + if tok.err != nil { + return SequenceOptions{}, tok.err + } + if tok.typ != int64Token { + return SequenceOptions{}, p.errorf("invalid skip_range_max value: %v", tok.value) + } + value, err := strconv.Atoi(tok.value) + if err != nil { + return SequenceOptions{}, p.errorf("invalid skip_range_max value: %v", tok.value) + } + so.SkipRangeMax = &value + } else if p.eat("start_with_counter", "=") { + tok := p.next() + if tok.err != nil { + return SequenceOptions{}, tok.err + } + if tok.typ != int64Token { + return SequenceOptions{}, p.errorf("invalid start_with_counter value: %v", tok.value) + } + value, err := strconv.Atoi(tok.value) + if err != nil { + return SequenceOptions{}, p.errorf("invalid start_with_counter value: %v", tok.value) + } + so.StartWithCounter = &value + } else { + tok := p.next() + return SequenceOptions{}, p.errorf("unknown sequence option: %v", tok.value) + } + if p.sniff(")") { + break + } + if !p.eat(",") { + return SequenceOptions{}, p.errorf("missing ',' in options list") + } + } + if err := p.expect(")"); err != nil { + return SequenceOptions{}, err + } + + return so, nil +} + var baseTypes = map[string]TypeBase{ "BOOL": Bool, "INT64": Int64, diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go index 9214fbc10f58..81147369cd00 100644 --- a/spanner/spansql/parser_test.go +++ b/spanner/spansql/parser_test.go @@ -693,6 +693,20 @@ func TestParseDDL(t *testing.T) { ALTER INDEX MyFirstIndex ADD STORED COLUMN UpdatedAt; ALTER INDEX MyFirstIndex DROP STORED COLUMN UpdatedAt; + CREATE SEQUENCE MySequence OPTIONS ( + sequence_kind='bit_reversed_positive', + skip_range_min = 1, + skip_range_max = 1000, + start_with_counter = 50 + ); + ALTER SEQUENCE MySequence SET OPTIONS ( + sequence_kind='bit_reversed_positive', + skip_range_min = 1, + skip_range_max = 1000, + start_with_counter = 50 + ); + DROP SEQUENCE MySequence; + -- Trailing comment at end of file. `, &DDL{Filename: "filename", List: []DDLStmt{ &CreateTable{ @@ -1119,6 +1133,29 @@ func TestParseDDL(t *testing.T) { Alteration: DropStoredColumn{Name: "UpdatedAt"}, Position: line(106), }, + &CreateSequence{ + Name: "MySequence", + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_positive"), + SkipRangeMin: intAddr(1), + SkipRangeMax: intAddr(1000), + StartWithCounter: intAddr(50), + }, + Position: line(108), + }, + &AlterSequence{ + Name: "MySequence", + Alteration: SetSequenceOptions{ + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_positive"), + SkipRangeMin: intAddr(1), + SkipRangeMax: intAddr(1000), + StartWithCounter: intAddr(50), + }, + }, + Position: line(114), + }, + &DropSequence{Name: "MySequence", Position: line(120)}, }, Comments: []*Comment{ { Marker: "#", Start: line(2), End: line(2), @@ -1154,7 +1191,7 @@ func TestParseDDL(t *testing.T) { {Marker: "--", Isolated: true, Start: line(75), End: line(75), Text: []string{"Table has a column with a default value."}}, // Comment after everything else. - {Marker: "--", Isolated: true, Start: line(108), End: line(108), Text: []string{"Trailing comment at end of file."}}, + {Marker: "--", Isolated: true, Start: line(122), End: line(122), Text: []string{"Trailing comment at end of file."}}, }}}, // No trailing comma: {`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{ @@ -1610,6 +1647,38 @@ func TestParseDDL(t *testing.T) { }, }, }, + { + `CREATE SEQUENCE IF NOT EXISTS sname OPTIONS (sequence_kind='bit_reversed_positive'); + ALTER SEQUENCE sname SET OPTIONS (start_with_counter=1); + DROP SEQUENCE IF EXISTS sname;`, + &DDL{ + Filename: "filename", + List: []DDLStmt{ + &CreateSequence{ + Name: "sname", + IfNotExists: true, + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_positive"), + }, + Position: line(1), + }, + &AlterSequence{ + Name: "sname", + Alteration: SetSequenceOptions{ + Options: SequenceOptions{ + StartWithCounter: intAddr(1), + }, + }, + Position: line(2), + }, + &DropSequence{ + Name: "sname", + IfExists: true, + Position: line(3), + }, + }, + }, + }, } for _, test := range tests { got, err := ParseDDL("filename", test.in) diff --git a/spanner/spansql/sql.go b/spanner/spansql/sql.go index 92e8ae05af8a..fb0f9c8ce45c 100644 --- a/spanner/spansql/sql.go +++ b/spanner/spansql/sql.go @@ -459,6 +459,61 @@ func (dsc DropStoredColumn) SQL() string { return "DROP STORED COLUMN " + dsc.Name.SQL() } +func (cs CreateSequence) SQL() string { + str := "CREATE SEQUENCE " + if cs.IfNotExists { + str += "IF NOT EXISTS " + } + return str + cs.Name.SQL() + " " + cs.Options.SQL() +} + +func (as AlterSequence) SQL() string { + return "ALTER SEQUENCE " + as.Name.SQL() + " " + as.Alteration.SQL() +} + +func (sa SetSequenceOptions) SQL() string { + return "SET " + sa.Options.SQL() +} + +func (so SequenceOptions) SQL() string { + str := "OPTIONS (" + hasOpt := false + if so.SequenceKind != nil { + hasOpt = true + str += fmt.Sprintf("sequence_kind='%s'", *so.SequenceKind) + } + if so.SkipRangeMin != nil { + if hasOpt { + str += ", " + } + hasOpt = true + str += fmt.Sprintf("skip_range_min=%v", *so.SkipRangeMin) + } + if so.SkipRangeMax != nil { + if hasOpt { + str += ", " + } + hasOpt = true + str += fmt.Sprintf("skip_range_max=%v", *so.SkipRangeMax) + } + if so.StartWithCounter != nil { + if hasOpt { + str += ", " + } + hasOpt = true + str += fmt.Sprintf("start_with_counter=%v", *so.StartWithCounter) + } + return str + ")" +} + +func (do DropSequence) SQL() string { + str := "DROP SEQUENCE " + if do.IfExists { + str += "IF EXISTS " + } + return str + do.Name.SQL() +} + func (d *Delete) SQL() string { return "DELETE FROM " + d.Table.SQL() + " WHERE " + d.Where.SQL() } diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index c43fc12d025d..ea31ae0fd169 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -28,6 +28,14 @@ func boolAddr(b bool) *bool { return &b } +func stringAddr(s string) *string { + return &s +} + +func intAddr(i int) *int { + return &i +} + func TestSQL(t *testing.T) { reparseDDL := func(s string) (interface{}, error) { ddl, err := ParseDDLStmt(s) @@ -758,6 +766,79 @@ func TestSQL(t *testing.T) { `ALTER TABLE tname1 ADD CONSTRAINT con1 FOREIGN KEY (cname2) REFERENCES tname2 (cname3) ON DELETE CASCADE`, reparseDDL, }, + { + &CreateSequence{ + Name: "sname", + IfNotExists: true, + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_sequence"), + SkipRangeMin: intAddr(1), + SkipRangeMax: intAddr(1234567), + StartWithCounter: intAddr(50), + }, + Position: line(1), + }, + `CREATE SEQUENCE IF NOT EXISTS sname OPTIONS (sequence_kind='bit_reversed_sequence', skip_range_min=1, skip_range_max=1234567, start_with_counter=50)`, + reparseDDL, + }, + { + &CreateSequence{ + Name: "sname", + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_sequence"), + }, + Position: line(1), + }, + `CREATE SEQUENCE sname OPTIONS (sequence_kind='bit_reversed_sequence')`, + reparseDDL, + }, + { + &AlterSequence{ + Name: "sname", + Alteration: SetSequenceOptions{ + Options: SequenceOptions{ + SequenceKind: stringAddr("bit_reversed_sequence"), + SkipRangeMin: intAddr(1), + SkipRangeMax: intAddr(1234567), + StartWithCounter: intAddr(50), + }, + }, + Position: line(1), + }, + `ALTER SEQUENCE sname SET OPTIONS (sequence_kind='bit_reversed_sequence', skip_range_min=1, skip_range_max=1234567, start_with_counter=50)`, + reparseDDL, + }, + { + &AlterSequence{ + Name: "sname", + Alteration: SetSequenceOptions{ + Options: SequenceOptions{ + StartWithCounter: intAddr(1), + }, + }, + Position: line(1), + }, + `ALTER SEQUENCE sname SET OPTIONS (start_with_counter=1)`, + reparseDDL, + }, + { + &DropSequence{ + Name: "sname", + IfExists: true, + Position: line(1), + }, + `DROP SEQUENCE IF EXISTS sname`, + reparseDDL, + }, + { + &DropSequence{ + Name: "sname", + IfExists: false, + Position: line(1), + }, + `DROP SEQUENCE sname`, + reparseDDL, + }, { &Insert{ Table: "Singers", diff --git a/spanner/spansql/types.go b/spanner/spansql/types.go index 60084f258cb7..91afef941602 100644 --- a/spanner/spansql/types.go +++ b/spanner/spansql/types.go @@ -1268,3 +1268,63 @@ type ( AddStoredColumn struct{ Name ID } DropStoredColumn struct{ Name ID } ) + +// CreateSequence represents an ALTER SEQUENCE statement. +// https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language#create-sequence +type CreateSequence struct { + Name ID + IfNotExists bool + Options SequenceOptions + + Position Position +} + +func (cs *CreateSequence) String() string { return fmt.Sprintf("%#v", cs) } +func (*CreateSequence) isDDLStmt() {} +func (cs *CreateSequence) Pos() Position { return cs.Position } +func (cs *CreateSequence) clearOffset() { cs.Position.Offset = 0 } + +// AlterSequence represents an ALTER SEQUENCE statement. +// https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language#alter-sequence +type AlterSequence struct { + Name ID + Alteration SequenceAlteration + + Position Position +} + +func (as *AlterSequence) String() string { return fmt.Sprintf("%#v", as) } +func (*AlterSequence) isDDLStmt() {} +func (as *AlterSequence) Pos() Position { return as.Position } +func (as *AlterSequence) clearOffset() { as.Position.Offset = 0 } + +type SequenceAlteration interface { + isSequenceAlteration() + SQL() string +} + +type SetSequenceOptions struct{ Options SequenceOptions } + +func (SetSequenceOptions) isSequenceAlteration() {} + +// SequenceOptions represents options on a sequence as part of a CREATE SEQUENCE and ALTER SEQUENCE statement. +type SequenceOptions struct { + SequenceKind *string + SkipRangeMin *int + SkipRangeMax *int + StartWithCounter *int +} + +// DropSequence represents a DROP SEQUENCE statement. +// https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language#drop-sequence +type DropSequence struct { + Name ID + IfExists bool + + Position Position +} + +func (ds *DropSequence) String() string { return fmt.Sprintf("%#v", ds) } +func (*DropSequence) isDDLStmt() {} +func (ds *DropSequence) Pos() Position { return ds.Position } +func (ds *DropSequence) clearOffset() { ds.Position.Offset = 0 }