From aad3d76add3b761cefe351e5a69b5db4ce339e51 Mon Sep 17 00:00:00 2001 From: apstndb <803393+apstndb@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:11:05 +0900 Subject: [PATCH] feat: support anonymous PRIMARY KEY as table constraint --- ast/ast.go | 17 ++++ ast/pos.go | 8 ++ ast/sql.go | 4 + ast/walk_internal.go | 3 + parser.go | 21 +++++ .../create_table_anonymous_primary_key.sql | 5 ++ ...create_table_anonymous_primary_key.sql.txt | 84 +++++++++++++++++++ 7 files changed, 142 insertions(+) create mode 100644 testdata/input/ddl/create_table_anonymous_primary_key.sql create mode 100644 testdata/result/ddl/create_table_anonymous_primary_key.sql.txt diff --git a/ast/ast.go b/ast/ast.go index 243c9f6a..d22e1fb9 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -426,6 +426,8 @@ type Constraint interface { func (ForeignKey) isConstraint() {} func (Check) isConstraint() {} +func (TablePrimaryKey) isConstraint() {} + // TableAlteration represents ALTER TABLE action. type TableAlteration interface { @@ -2659,6 +2661,21 @@ type TableConstraint struct { Constraint Constraint } +// TablePrimaryKey is primary key constraint in CREATE TABLE. +// Note: While it implements Constraint interface to be stored in TableConstraints, +// it is only used for anonymous style as Spanner doesn't support named primary keys. +// +// PRIMARY KEY ({{.Columns | sqlJoin ", "}}) +type TablePrimaryKey struct { + // pos = Primary + // end = Rparen + 1 + + Primary token.Pos // position of "PRIMARY" keyword + Rparen token.Pos // position of ")" after columns + + Columns []*IndexKey +} + // ForeignKey is foreign key specifier in CREATE TABLE and ALTER TABLE. // // FOREIGN KEY ({{.ColumnNames | sqlJoin ","}}) REFERENCES {{.ReferenceTable}} ({{.ReferenceColumns | sqlJoin ","}}) diff --git a/ast/pos.go b/ast/pos.go index e8ccbc1d..be262d18 100644 --- a/ast/pos.go +++ b/ast/pos.go @@ -1214,6 +1214,14 @@ func (t *TableConstraint) End() token.Pos { return nodeEnd(wrapNode(t.Constraint)) } +func (t *TablePrimaryKey) Pos() token.Pos { + return t.Primary +} + +func (t *TablePrimaryKey) End() token.Pos { + return posAdd(t.Rparen, 1) +} + func (f *ForeignKey) Pos() token.Pos { return f.Foreign } diff --git a/ast/sql.go b/ast/sql.go index dd365134..e52b7362 100644 --- a/ast/sql.go +++ b/ast/sql.go @@ -869,6 +869,10 @@ func (c *TableConstraint) SQL() string { return sqlOpt("CONSTRAINT ", c.Name, " ") + c.Constraint.SQL() } +func (p *TablePrimaryKey) SQL() string { + return "PRIMARY KEY (" + sqlJoin(p.Columns, ", ") + ")" +} + func (f *ForeignKey) SQL() string { return "FOREIGN KEY (" + sqlJoin(f.Columns, ", ") + ") " + "REFERENCES " + f.ReferenceTable.SQL() + " (" + diff --git a/ast/walk_internal.go b/ast/walk_internal.go index fce47748..b9cd57b8 100644 --- a/ast/walk_internal.go +++ b/ast/walk_internal.go @@ -557,6 +557,9 @@ func walkInternal(node Node, v Visitor, stack []*stackItem) []*stackItem { stack = append(stack, &stackItem{node: wrapNode(n.Constraint), visitor: v.Field("Constraint")}) stack = append(stack, &stackItem{node: wrapNode(n.Name), visitor: v.Field("Name")}) + case *TablePrimaryKey: + stack = append(stack, &stackItem{nodes: wrapNodes(n.Columns), visitor: v.Field("Columns")}) + case *ForeignKey: stack = append(stack, &stackItem{nodes: wrapNodes(n.ReferenceColumns), visitor: v.Field("ReferenceColumns")}) stack = append(stack, &stackItem{node: wrapNode(n.ReferenceTable), visitor: v.Field("ReferenceTable")}) diff --git a/parser.go b/parser.go index 9fd1c9b9..f5c62015 100644 --- a/parser.go +++ b/parser.go @@ -3292,6 +3292,12 @@ func (p *Parser) parseCreateTable(pos token.Pos) *ast.CreateTable { ConstraintPos: token.InvalidPos, Constraint: c, }) + case p.Token.IsKeywordLike("PRIMARY"): + pk := p.parseTablePrimaryKey() + constraints = append(constraints, &ast.TableConstraint{ + ConstraintPos: token.InvalidPos, + Constraint: pk, + }) case p.Token.IsKeywordLike("SYNONYM"): synonym := p.parseSynonym() synonyms = append(synonyms, synonym) @@ -3498,6 +3504,21 @@ func (p *Parser) parseConstraint() *ast.TableConstraint { } } +func (p *Parser) parseTablePrimaryKey() *ast.TablePrimaryKey { + pos := p.expectKeywordLike("PRIMARY").Pos + p.expectKeywordLike("KEY") + + p.expect("(") + keys := parseCommaSeparatedList(p, p.parseIndexKey) + rparen := p.expect(")").Pos + + return &ast.TablePrimaryKey{ + Primary: pos, + Rparen: rparen, + Columns: keys, + } +} + func (p *Parser) parseForeignKey() *ast.ForeignKey { pos := p.expectKeywordLike("FOREIGN").Pos p.expectKeywordLike("KEY") diff --git a/testdata/input/ddl/create_table_anonymous_primary_key.sql b/testdata/input/ddl/create_table_anonymous_primary_key.sql new file mode 100644 index 00000000..aa41f69e --- /dev/null +++ b/testdata/input/ddl/create_table_anonymous_primary_key.sql @@ -0,0 +1,5 @@ +CREATE TABLE PKsTable( + PK1 INT64, + PK2 INT64, + PRIMARY KEY (PK1, PK2) +) \ No newline at end of file diff --git a/testdata/result/ddl/create_table_anonymous_primary_key.sql.txt b/testdata/result/ddl/create_table_anonymous_primary_key.sql.txt new file mode 100644 index 00000000..2c77d713 --- /dev/null +++ b/testdata/result/ddl/create_table_anonymous_primary_key.sql.txt @@ -0,0 +1,84 @@ +--- create_table_anonymous_primary_key.sql +CREATE TABLE PKsTable( + PK1 INT64, + PK2 INT64, + PRIMARY KEY (PK1, PK2) +) +--- AST +&ast.CreateTable{ + Rparen: 74, + PrimaryKeyRparen: -1, + Name: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 13, + NameEnd: 21, + Name: "PKsTable", + }, + }, + }, + Columns: []*ast.ColumnDef{ + &ast.ColumnDef{ + Null: -1, + Key: -1, + Name: &ast.Ident{ + NamePos: 25, + NameEnd: 28, + Name: "PK1", + }, + Type: &ast.ScalarSchemaType{ + NamePos: 29, + Name: "INT64", + }, + Hidden: -1, + }, + &ast.ColumnDef{ + Null: -1, + Key: -1, + Name: &ast.Ident{ + NamePos: 38, + NameEnd: 41, + Name: "PK2", + }, + Type: &ast.ScalarSchemaType{ + NamePos: 42, + Name: "INT64", + }, + Hidden: -1, + }, + }, + TableConstraints: []*ast.TableConstraint{ + &ast.TableConstraint{ + ConstraintPos: -1, + Constraint: &ast.TablePrimaryKey{ + Primary: 51, + Rparen: 72, + Columns: []*ast.IndexKey{ + &ast.IndexKey{ + DirPos: -1, + Name: &ast.Ident{ + NamePos: 64, + NameEnd: 67, + Name: "PK1", + }, + }, + &ast.IndexKey{ + DirPos: -1, + Name: &ast.Ident{ + NamePos: 69, + NameEnd: 72, + Name: "PK2", + }, + }, + }, + }, + }, + }, +} + +--- SQL +CREATE TABLE PKsTable ( + PK1 INT64, + PK2 INT64, + PRIMARY KEY (PK1, PK2) +)