spanner/spansql: fix parsing of table constraints

"CONSTRAINT" and friends are not reserved keywords (verified
internally), and the parsing ambiguity resulting from this must be
resolved with token lookahead.

Change-Id: Ie321319158536d8f0f346a5717fdbca9b407e921
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/53091
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 4dce1ca..e7a8941 100644
--- a/spanner/spansql/keywords.go
+++ b/spanner/spansql/keywords.go
@@ -123,12 +123,6 @@
 	"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 e1ec959..308073f 100644
--- a/spanner/spansql/parser.go
+++ b/spanner/spansql/parser.go
@@ -971,7 +971,7 @@
 
 	ct := &CreateTable{Name: tname, Position: pos}
 	err = p.parseCommaList(func(p *parser) *parseError {
-		if p.sniff("CONSTRAINT") || p.sniff("FOREIGN") {
+		if p.sniffTableConstraint() {
 			tc, err := p.parseTableConstraint()
 			if err != nil {
 				return err
@@ -1030,6 +1030,34 @@
 	return ct, nil
 }
 
+func (p *parser) sniffTableConstraint() bool {
+	// Unfortunately the Cloud Spanner grammar is LL(3) because
+	//	CONSTRAINT BOOL
+	// could be the start of a declaration of a column called "CONSTRAINT" of boolean type,
+	// or it could be the start of a foreign key constraint called "BOOL".
+	// We have to sniff up to the third token to see what production it is.
+	// If we have "FOREIGN" and "KEY", this is an unnamed table constraint.
+	// If we have "CONSTRAINT", an identifier and "FOREIGN", this is a table constraint.
+	// Otherwise, this is a column definition.
+
+	if p.sniff("FOREIGN", "KEY") {
+		return true
+	}
+
+	// Store parser state, and peek ahead.
+	// Restore on the way out.
+	orig := *p
+	defer func() { *p = orig }()
+
+	if !p.eat("CONSTRAINT") {
+		return false
+	}
+	if _, err := p.parseTableOrIndexOrColumnName(); err != nil {
+		return false
+	}
+	return p.eat("FOREIGN")
+}
+
 func (p *parser) parseCreateIndex() (*CreateIndex, *parseError) {
 	debugf("parseCreateIndex: %v", p)
 
diff --git a/spanner/spansql/parser_test.go b/spanner/spansql/parser_test.go
index 2c3d690..af3be98 100644
--- a/spanner/spansql/parser_test.go
+++ b/spanner/spansql/parser_test.go
@@ -253,6 +253,7 @@
 			RepoPath STRING(MAX) NOT NULL,
 			FOREIGN KEY (System, RepoPath) REFERENCES Stranger (Sys, RPath), -- unnamed foreign key
 			Author STRING(MAX) NOT NULL,
+			CONSTRAINT BOOL,  -- not a constraint
 		) PRIMARY KEY(System, RepoPath, Author),
 		  INTERLEAVE IN PARENT FooBar ON DELETE CASCADE;
 
@@ -303,6 +304,7 @@
 					{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(14)},
 					{Name: "Author", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(16)},
+					{Name: "CONSTRAINT", Type: Type{Base: Bool}, Position: line(17)},
 				},
 				Constraints: []TableConstraint{
 					{
@@ -338,13 +340,13 @@
 			},
 			&AlterTable{
 				Name:       "FooBar",
-				Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(20)}},
-				Position:   line(20),
+				Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(21)}},
+				Position:   line(21),
 			},
 			&AlterTable{
 				Name:       "FooBar",
 				Alteration: DropColumn{Name: "TZ"},
-				Position:   line(21),
+				Position:   line(22),
 			},
 			&AlterTable{
 				Name: "FooBar",
@@ -354,21 +356,21 @@
 						Columns:    []string{"RepoPath"},
 						RefTable:   "Repos",
 						RefColumns: []string{"RPath"},
-						Position:   line(22),
+						Position:   line(23),
 					},
-					Position: line(22),
+					Position: line(23),
 				}},
-				Position: line(22),
+				Position: line(23),
 			},
 			&AlterTable{
 				Name:       "FooBar",
 				Alteration: DropConstraint{Name: "Con3"},
-				Position:   line(23),
+				Position:   line(24),
 			},
 			&AlterTable{
 				Name:       "FooBar",
 				Alteration: SetOnDelete{Action: NoActionOnDelete},
-				Position:   line(24),
+				Position:   line(25),
 			},
 			&AlterTable{
 				Name: "FooBar",
@@ -376,21 +378,21 @@
 					Name:     "Author",
 					Type:     Type{Base: String, Len: MaxLen},
 					NotNull:  true,
-					Position: line(25),
+					Position: line(26),
 				}},
-				Position: line(25),
+				Position: line(26),
 			},
-			&DropIndex{Name: "MyFirstIndex", Position: line(27)},
-			&DropTable{Name: "FooBar", Position: line(28)},
+			&DropIndex{Name: "MyFirstIndex", Position: line(28)},
+			&DropTable{Name: "FooBar", Position: line(29)},
 			&CreateTable{
 				Name: "NonScalars",
 				Columns: []ColumnDef{
-					{Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(33)},
-					{Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(34)},
-					{Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(35)},
+					{Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(34)},
+					{Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(35)},
+					{Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(36)},
 				},
 				PrimaryKey: []KeyPart{{Column: "Dummy"}},
-				Position:   line(32),
+				Position:   line(33),
 			},
 		}, Comments: []*Comment{
 			{Marker: "#", Start: line(2), End: line(2),
@@ -401,11 +403,13 @@
 				Text: []string{" This is a", "\t\t\t\t\t\t  * multiline comment."}},
 			{Marker: "--", Start: line(15), End: line(15),
 				Text: []string{"unnamed foreign key"}},
-			{Marker: "--", Isolated: true, Start: line(30), End: line(31),
+			{Marker: "--", Start: line(17), End: line(17),
+				Text: []string{"not a constraint"}},
+			{Marker: "--", Isolated: true, Start: line(31), End: line(32),
 				Text: []string{"This table has some commentary", "that spans multiple lines."}},
 			// These comments shouldn't get combined:
-			{Marker: "--", Start: line(33), End: line(33), Text: []string{"dummy comment"}},
-			{Marker: "--", Start: line(34), End: line(34), Text: []string{"comment on ids"}},
+			{Marker: "--", Start: line(34), End: line(34), Text: []string{"dummy comment"}},
+			{Marker: "--", Start: line(35), End: line(35), Text: []string{"comment on ids"}},
 		}}},
 		// No trailing comma:
 		{`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{