Repository: cloudspannerecosystem/memefish
Pull request #164 (2024-10-21 16:43:18)

Implement SAFE_CAST in CastExpr
This PR implements `SAFE_CAST` as `Safe` flag in `CastExpr`.

part of #94 


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 1d8c21e..4d10747 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -1159,16 +1159,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/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/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,