Repository: cloudspannerecosystem/memefish
Pull request #160 (2024-10-21 07:12:36)

Sort `Statement` and `DDL` types
This order follows the official documentation:

- https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language
- https://cloud.google.com/spanner/docs/reference/standard-sql/dml-syntax

If this rule can be enforced by a linter, it sounds really good.


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 1d8c21e..1ca6fcc 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -50,31 +50,36 @@ type Statement interface {
 	isStatement()
 }
 
+// The order of this list follows the official documentation:
+//
+// - https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language
+// - https://cloud.google.com/spanner/docs/reference/standard-sql/dml-syntax
+
 func (QueryStatement) isStatement()     {}
 func (CreateDatabase) isStatement()     {}
 func (CreateTable) isStatement()        {}
-func (CreateSequence) isStatement()     {}
+func (AlterTable) isStatement()         {}
+func (DropTable) isStatement()          {}
+func (CreateIndex) isStatement()        {}
+func (AlterIndex) isStatement()         {}
+func (DropIndex) isStatement()          {}
 func (CreateView) isStatement()         {}
 func (DropView) isStatement()           {}
-func (CreateIndex) isStatement()        {}
-func (CreateVectorIndex) isStatement()  {}
+func (CreateChangeStream) isStatement() {}
+func (AlterChangeStream) isStatement()  {}
+func (DropChangeStream) isStatement()   {}
 func (CreateRole) isStatement()         {}
-func (AlterTable) isStatement()         {}
-func (AlterIndex) isStatement()         {}
+func (DropRole) isStatement()           {}
+func (Grant) isStatement()              {}
+func (Revoke) isStatement()             {}
+func (CreateSequence) isStatement()     {}
 func (AlterSequence) isStatement()      {}
-func (DropTable) isStatement()          {}
-func (DropIndex) isStatement()          {}
-func (DropVectorIndex) isStatement()    {}
 func (DropSequence) isStatement()       {}
-func (DropRole) isStatement()           {}
+func (CreateVectorIndex) isStatement()  {}
+func (DropVectorIndex) isStatement()    {}
 func (Insert) isStatement()             {}
 func (Delete) isStatement()             {}
 func (Update) isStatement()             {}
-func (Grant) isStatement()              {}
-func (Revoke) isStatement()             {}
-func (CreateChangeStream) isStatement() {}
-func (AlterChangeStream) isStatement()  {}
-func (DropChangeStream) isStatement()   {}
 
 // QueryExpr represents set operator operands.
 type QueryExpr interface {
@@ -254,27 +259,31 @@ type DDL interface {
 	isDDL()
 }
 
+// The order of this list follows the official documentation:
+//
+// - https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language
+
 func (CreateDatabase) isDDL()     {}
 func (CreateTable) isDDL()        {}
-func (CreateView) isDDL()         {}
-func (DropView) isDDL()           {}
-func (CreateSequence) isDDL()     {}
 func (AlterTable) isDDL()         {}
 func (DropTable) isDDL()          {}
 func (CreateIndex) isDDL()        {}
-func (CreateVectorIndex) isDDL()  {}
 func (AlterIndex) isDDL()         {}
-func (AlterSequence) isDDL()      {}
 func (DropIndex) isDDL()          {}
-func (DropVectorIndex) isDDL()    {}
-func (DropSequence) isDDL()       {}
+func (CreateView) isDDL()         {}
+func (DropView) isDDL()           {}
+func (CreateChangeStream) isDDL() {}
+func (AlterChangeStream) isDDL()  {}
+func (DropChangeStream) isDDL()   {}
 func (CreateRole) isDDL()         {}
 func (DropRole) isDDL()           {}
 func (Grant) isDDL()              {}
 func (Revoke) isDDL()             {}
-func (CreateChangeStream) isDDL() {}
-func (AlterChangeStream) isDDL()  {}
-func (DropChangeStream) isDDL()   {}
+func (CreateSequence) isDDL()     {}
+func (AlterSequence) isDDL()      {}
+func (DropSequence) isDDL()       {}
+func (CreateVectorIndex) isDDL()  {}
+func (DropVectorIndex) isDDL()    {}
 
 // Constraint represents table constraint of CONSTARINT clause.
 type Constraint interface {
@@ -1009,14 +1018,14 @@ type IndexExpr struct {
 
 // CallExpr is function call expression node.
 //
-//		{{.Func | sql}}(
+//	{{.Func | sql}}(
 //		{{if .Distinct}}DISTINCT{{end}}
 //		{{.Args | sqlJoin ", "}}
 //		{{if len(.Args) > 0 && len(.NamedArgs) > 0}}, {{end}}
 //		{{.NamedArgs | sqlJoin ", "}}
-//	    {{.NullHandling | sqlOpt}}
-//	    {{.Having | sqlOpt}}
-//		)
+//		{{.NullHandling | sqlOpt}}
+//		{{.Having | sqlOpt}}
+//	)
 type CallExpr struct {
 	// pos = Func.pos
 	// end = Rparen + 1
@@ -1159,16 +1168,18 @@ type AtTimeZone struct {
 	Expr Expr
 }
 
-// CastExpr is CAST call expression node.
+// CastExpr is CAST/SAFE_CAST call expression node.
 //
-//	CAST({{.Expr | sql}} AS {{.Type | sql}})
+//	{{if .Safe}}SAFE_{{end}}CAST({{.Expr | sql}} AS {{.Type | sql}})
 type CastExpr struct {
 	// pos = Cast
 	// end = Rparen + 1
 
-	Cast   token.Pos // position of "CAST" keyword
+	Cast   token.Pos // position of "CAST" keyword or "SAFE_CAST" pseudo keyword
 	Rparen token.Pos // position of ")"
 
+	Safe bool
+
 	Expr Expr
 	Type Type
 }
diff --git a/ast/options.go b/ast/options.go
index 931c64c..5c3d698 100644
--- a/ast/options.go
+++ b/ast/options.go
@@ -23,11 +23,11 @@ func (o *Options) Field(name string) (expr Expr, found bool) {
 
 // FieldNotFound is returned Options.*Field methods.
 // It can be handled as a non-error.
-var FieldNotFound = errors.New("field not found")
+var ErrFieldNotFound = errors.New("field not found")
 
 // fieldTypeMismatch is only defined for test.
 // It is intentionally unexported.
-var fieldTypeMismatch = errors.New("type mismatched")
+var errFieldTypeMismatch = errors.New("type mismatched")
 
 // BoolField finds name in records, and return its value as *bool.
 // If Options doesn't have a record with name, it returns FieldNotFound error.
@@ -37,7 +37,7 @@ var fieldTypeMismatch = errors.New("type mismatched")
 func (o *Options) BoolField(name string) (*bool, error) {
 	v, ok := o.Field(name)
 	if !ok {
-		return nil, FieldNotFound
+		return nil, ErrFieldNotFound
 	}
 	switch v := v.(type) {
 	case *BoolLiteral:
@@ -45,7 +45,7 @@ func (o *Options) BoolField(name string) (*bool, error) {
 	case *NullLiteral:
 		return nil, nil
 	default:
-		return nil, fmt.Errorf("expect true, false or null, but got unknown type %T: %w", v, fieldTypeMismatch)
+		return nil, fmt.Errorf("expect true, false or null, but got unknown type %T: %w", v, errFieldTypeMismatch)
 	}
 }
 
@@ -57,7 +57,7 @@ func (o *Options) BoolField(name string) (*bool, error) {
 func (o *Options) IntegerField(name string) (*int64, error) {
 	v, ok := o.Field(name)
 	if !ok {
-		return nil, FieldNotFound
+		return nil, ErrFieldNotFound
 	}
 	switch v := v.(type) {
 	case *IntLiteral:
@@ -69,7 +69,7 @@ func (o *Options) IntegerField(name string) (*int64, error) {
 	case *NullLiteral:
 		return nil, nil
 	default:
-		return nil, fmt.Errorf("expect integer or null, but got unknown type %T: %w", v, fieldTypeMismatch)
+		return nil, fmt.Errorf("expect integer or null, but got unknown type %T: %w", v, errFieldTypeMismatch)
 	}
 }
 
@@ -81,7 +81,7 @@ func (o *Options) IntegerField(name string) (*int64, error) {
 func (o *Options) StringField(name string) (*string, error) {
 	v, ok := o.Field(name)
 	if !ok {
-		return nil, FieldNotFound
+		return nil, ErrFieldNotFound
 	}
 	switch v := v.(type) {
 	case *StringLiteral:
@@ -89,6 +89,6 @@ func (o *Options) StringField(name string) (*string, error) {
 	case *NullLiteral:
 		return nil, nil
 	default:
-		return nil, fmt.Errorf("expect string literal or null, but got unknown type %T: %w", v, fieldTypeMismatch)
+		return nil, fmt.Errorf("expect string literal or null, but got unknown type %T: %w", v, errFieldTypeMismatch)
 	}
 }
diff --git a/ast/sql.go b/ast/sql.go
index 34c16ec..68b1df3 100644
--- a/ast/sql.go
+++ b/ast/sql.go
@@ -555,7 +555,7 @@ func (a *AtTimeZone) SQL() string {
 }
 
 func (c *CastExpr) SQL() string {
-	return "CAST(" + c.Expr.SQL() + " AS " + c.Type.SQL() + ")"
+	return strOpt(c.Safe, "SAFE_") + "CAST(" + c.Expr.SQL() + " AS " + c.Type.SQL() + ")"
 }
 
 func (c *CaseExpr) SQL() string {
diff --git a/parser.go b/parser.go
index eefa17b..f047e4f 100644
--- a/parser.go
+++ b/parser.go
@@ -1415,6 +1415,10 @@ func (p *Parser) parseLit() ast.Expr {
 		return p.parseParenExpr()
 	case token.TokenIdent:
 		id := p.Token
+		switch {
+		case id.IsKeywordLike("SAFE_CAST"):
+			return p.parseCastExpr()
+		}
 		p.nextToken()
 		switch p.Token.Kind {
 		case "(":
@@ -1681,7 +1685,19 @@ func (p *Parser) parseCaseElse() *ast.CaseElse {
 }
 
 func (p *Parser) parseCastExpr() *ast.CastExpr {
-	cast := p.expect("CAST").Pos
+	if p.Token.Kind != "CAST" && !p.Token.IsKeywordLike("SAFE_CAST") {
+		panic(p.errorfAtToken(&p.Token, `expected CAST keyword or SAFE_CAST pseudo keyword, but: %v`, p.Token.Kind))
+	}
+
+	var cast token.Pos
+	var safe bool
+	if p.Token.Kind == "CAST" {
+		cast = p.expect("CAST").Pos
+	} else {
+		cast = p.expectKeywordLike("SAFE_CAST").Pos
+		safe = true
+	}
+
 	p.expect("(")
 	e := p.parseExpr()
 	p.expect("AS")
@@ -1690,6 +1706,7 @@ func (p *Parser) parseCastExpr() *ast.CastExpr {
 	return &ast.CastExpr{
 		Cast:   cast,
 		Rparen: rparen,
+		Safe:   safe,
 		Expr:   e,
 		Type:   t,
 	}


Test patch:
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 4c974d3..b95441c 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -8,21 +8,28 @@ func TestStatement(t *testing.T) {
 	Statement(&QueryStatement{}).isStatement()
 	Statement(&CreateDatabase{}).isStatement()
 	Statement(&CreateTable{}).isStatement()
-	Statement(&CreateView{}).isStatement()
-	Statement(&CreateIndex{}).isStatement()
-	Statement(&CreateSequence{}).isStatement()
-	Statement(&CreateRole{}).isStatement()
 	Statement(&AlterTable{}).isStatement()
-	Statement(&AlterIndex{}).isStatement()
 	Statement(&DropTable{}).isStatement()
+	Statement(&CreateIndex{}).isStatement()
+	Statement(&AlterIndex{}).isStatement()
 	Statement(&DropIndex{}).isStatement()
-	Statement(&DropVectorIndex{}).isStatement()
+	Statement(&CreateView{}).isStatement()
+	Statement(&DropView{}).isStatement()
+	Statement(&CreateChangeStream{}).isStatement()
+	Statement(&AlterChangeStream{}).isStatement()
+	Statement(&DropChangeStream{}).isStatement()
+	Statement(&CreateRole{}).isStatement()
 	Statement(&DropRole{}).isStatement()
+	Statement(&Grant{}).isStatement()
+	Statement(&Revoke{}).isStatement()
+	Statement(&CreateSequence{}).isStatement()
+	Statement(&AlterSequence{}).isStatement()
+	Statement(&DropSequence{}).isStatement()
+	Statement(&CreateVectorIndex{}).isStatement()
+	Statement(&DropVectorIndex{}).isStatement()
 	Statement(&Insert{}).isStatement()
 	Statement(&Delete{}).isStatement()
 	Statement(&Update{}).isStatement()
-	Statement(&Grant{}).isStatement()
-	Statement(&Revoke{}).isStatement()
 }
 
 func TestQueryExpr(t *testing.T) {
@@ -130,23 +137,25 @@ func TestStringValue(t *testing.T) {
 func TestDDL(t *testing.T) {
 	DDL(&CreateDatabase{}).isDDL()
 	DDL(&CreateTable{}).isDDL()
-	DDL(&CreateIndex{}).isDDL()
-	DDL(&CreateVectorIndex{}).isDDL()
-	DDL(&CreateSequence{}).isDDL()
-	DDL(&AlterSequence{}).isDDL()
-	DDL(&DropSequence{}).isDDL()
-	DDL(&CreateView{}).isDDL()
-	DDL(&DropView{}).isDDL()
 	DDL(&AlterTable{}).isDDL()
 	DDL(&DropTable{}).isDDL()
 	DDL(&CreateIndex{}).isDDL()
 	DDL(&AlterIndex{}).isDDL()
 	DDL(&DropIndex{}).isDDL()
-	DDL(&DropVectorIndex{}).isDDL()
+	DDL(&CreateView{}).isDDL()
+	DDL(&DropView{}).isDDL()
+	DDL(&CreateChangeStream{}).isDDL()
+	DDL(&AlterChangeStream{}).isDDL()
+	DDL(&DropChangeStream{}).isDDL()
 	DDL(&CreateRole{}).isDDL()
 	DDL(&DropRole{}).isDDL()
 	DDL(&Grant{}).isDDL()
 	DDL(&Revoke{}).isDDL()
+	DDL(&CreateSequence{}).isDDL()
+	DDL(&AlterSequence{}).isDDL()
+	DDL(&DropSequence{}).isDDL()
+	DDL(&CreateVectorIndex{}).isDDL()
+	DDL(&DropVectorIndex{}).isDDL()
 }
 
 func TestConstraint(t *testing.T) {
diff --git a/ast/options_test.go b/ast/options_test.go
index ad952d5..d0b5792 100644
--- a/ast/options_test.go
+++ b/ast/options_test.go
@@ -2,8 +2,9 @@ package ast
 
 import (
 	"errors"
-	"github.com/google/go-cmp/cmp"
 	"testing"
+
+	"github.com/google/go-cmp/cmp"
 )
 
 func TestOptions_BoolField(t *testing.T) {
@@ -53,7 +54,7 @@ func TestOptions_BoolField(t *testing.T) {
 			},
 			name:    "bool_option",
 			want:    nil,
-			wantErr: FieldNotFound,
+			wantErr: ErrFieldNotFound,
 		},
 		{
 			desc: "invalid type",
@@ -63,7 +64,7 @@ func TestOptions_BoolField(t *testing.T) {
 				},
 			},
 			name:    "string_option",
-			wantErr: fieldTypeMismatch,
+			wantErr: errFieldTypeMismatch,
 		},
 	}
 
@@ -123,7 +124,7 @@ func TestOptions_StringField(t *testing.T) {
 			},
 			name:    "string_field",
 			want:    nil,
-			wantErr: FieldNotFound,
+			wantErr: ErrFieldNotFound,
 		},
 		{
 			desc: "invalid value",
@@ -133,7 +134,7 @@ func TestOptions_StringField(t *testing.T) {
 				},
 			},
 			name:    "bool_option",
-			wantErr: fieldTypeMismatch,
+			wantErr: errFieldTypeMismatch,
 		},
 	}
 
@@ -193,7 +194,7 @@ func TestOptions_IntegerField(t *testing.T) {
 			},
 			name:    "integer_option",
 			want:    nil,
-			wantErr: FieldNotFound,
+			wantErr: ErrFieldNotFound,
 		},
 		{
 			desc: "invalid value",
@@ -203,7 +204,7 @@ func TestOptions_IntegerField(t *testing.T) {
 				},
 			},
 			name:    "bool_option",
-			wantErr: fieldTypeMismatch,
+			wantErr: errFieldTypeMismatch,
 		},
 	}
 
diff --git a/testdata/input/expr/conversion_functions_safe_cast.sql b/testdata/input/expr/conversion_functions_safe_cast.sql
new file mode 100644
index 0000000..a7ec0fe
--- /dev/null
+++ b/testdata/input/expr/conversion_functions_safe_cast.sql
@@ -0,0 +1,1 @@
+SAFE_CAST("apple" AS INT64)
\ No newline at end of file
diff --git a/testdata/result/expr/cast_as_typename.sql.txt b/testdata/result/expr/cast_as_typename.sql.txt
index ec1e5fe..3edb8be 100644
--- a/testdata/result/expr/cast_as_typename.sql.txt
+++ b/testdata/result/expr/cast_as_typename.sql.txt
@@ -4,6 +4,7 @@ CAST('order_number: "123"' AS examples.shipping.`Order`)
 &ast.CastExpr{
   Cast:   0,
   Rparen: 55,
+  Safe:   false,
   Expr:   &ast.StringLiteral{
     ValuePos: 5,
     ValueEnd: 26,
diff --git a/testdata/result/expr/conversion_functions_safe_cast.sql.txt b/testdata/result/expr/conversion_functions_safe_cast.sql.txt
new file mode 100644
index 0000000..8f9d1cb
--- /dev/null
+++ b/testdata/result/expr/conversion_functions_safe_cast.sql.txt
@@ -0,0 +1,20 @@
+--- conversion_functions_safe_cast.sql
+SAFE_CAST("apple" AS INT64)
+--- AST
+&ast.CastExpr{
+  Cast:   0,
+  Rparen: 26,
+  Safe:   true,
+  Expr:   &ast.StringLiteral{
+    ValuePos: 10,
+    ValueEnd: 17,
+    Value:    "apple",
+  },
+  Type: &ast.SimpleType{
+    NamePos: 21,
+    Name:    "INT64",
+  },
+}
+
+--- SQL
+SAFE_CAST("apple" AS INT64)
diff --git a/testdata/result/query/select_cast.sql.txt b/testdata/result/query/select_cast.sql.txt
index de64735..d5f6fb9 100644
--- a/testdata/result/query/select_cast.sql.txt
+++ b/testdata/result/query/select_cast.sql.txt
@@ -18,6 +18,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   7,
           Rparen: 22,
+          Safe:   false,
           Expr:   &ast.IntLiteral{
             ValuePos: 12,
             ValueEnd: 13,
@@ -34,6 +35,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   25,
           Rparen: 44,
+          Safe:   false,
           Expr:   &ast.FloatLiteral{
             ValuePos: 30,
             ValueEnd: 33,
@@ -49,6 +51,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   47,
           Rparen: 147,
+          Safe:   false,
           Expr:   &ast.StructLiteral{
             Struct: 0,
             Lparen: 52,
diff --git a/testdata/result/statement/select_cast.sql.txt b/testdata/result/statement/select_cast.sql.txt
index de64735..d5f6fb9 100644
--- a/testdata/result/statement/select_cast.sql.txt
+++ b/testdata/result/statement/select_cast.sql.txt
@@ -18,6 +18,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   7,
           Rparen: 22,
+          Safe:   false,
           Expr:   &ast.IntLiteral{
             ValuePos: 12,
             ValueEnd: 13,
@@ -34,6 +35,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   25,
           Rparen: 44,
+          Safe:   false,
           Expr:   &ast.FloatLiteral{
             ValuePos: 30,
             ValueEnd: 33,
@@ -49,6 +51,7 @@ limit cast(1 as INT64) offset cast(@foo as INT64)
         Expr: &ast.CastExpr{
           Cast:   47,
           Rparen: 147,
+          Safe:   false,
           Expr:   &ast.StructLiteral{
             Struct: 0,
             Lparen: 52,