Repository: cloudspannerecosystem/memefish (go)

Issue:
Support JSON literals
Detected in #115.

References

* https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#json_literals
* https://cloud.google.com/spanner/docs/working-with-json
* https://cloud.google.com/spanner/docs/reference/standard-sql/data-types#json_type

Pull request description:
Support JSON literals
This PR adds support of JSON literals.

Note: It adds `TestParseExpr()` and `testdata/{input,result}/expr` to simplify testdata of expressions.

fix #117 

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index bda55c2..4614f24 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -154,6 +154,7 @@ func (BytesLiteral) isExpr()     {}
 func (DateLiteral) isExpr()      {}
 func (TimestampLiteral) isExpr() {}
 func (NumericLiteral) isExpr()   {}
+func (JSONLiteral) isExpr()      {}
 
 // Arg represents argument of function call.
 type Arg interface {
@@ -1324,6 +1325,18 @@ type NumericLiteral struct {
 	Value *StringLiteral
 }
 
+// JSONLiteral is JSON literal node.
+//
+//	JSON {{.Value | sql}}
+type JSONLiteral struct {
+	// pos = JSON
+	// end = ValueEnd.end
+
+	JSON token.Pos // position of "JSON"
+
+	Value *StringLiteral
+}
+
 // ================================================================================
 //
 // Type
diff --git a/ast/const.go b/ast/const.go
index 7d1ae6f..9ea04b1 100644
--- a/ast/const.go
+++ b/ast/const.go
@@ -108,6 +108,7 @@ const (
 	DateTypeName      ScalarTypeName = "DATE"
 	TimestampTypeName ScalarTypeName = "TIMESTAMP"
 	NumericTypeName   ScalarTypeName = "NUMERIC"
+	JSONTypeName      ScalarTypeName = "JSON"
 )
 
 type OnDeleteAction string
diff --git a/ast/pos.go b/ast/pos.go
index 4e87770..4e3ebb3 100644
--- a/ast/pos.go
+++ b/ast/pos.go
@@ -437,6 +437,9 @@ func (t *TimestampLiteral) End() token.Pos { return t.Value.End() }
 func (t *NumericLiteral) Pos() token.Pos { return t.Numeric }
 func (t *NumericLiteral) End() token.Pos { return t.Value.End() }
 
+func (t *JSONLiteral) Pos() token.Pos { return t.JSON }
+func (t *JSONLiteral) End() token.Pos { return t.Value.End() }
+
 // ================================================================================
 //
 // Type
diff --git a/ast/sql.go b/ast/sql.go
index dce4c55..ad5e719 100644
--- a/ast/sql.go
+++ b/ast/sql.go
@@ -681,6 +681,10 @@ func (t *NumericLiteral) SQL() string {
 	return "NUMERIC " + t.Value.SQL()
 }
 
+func (t *JSONLiteral) SQL() string {
+	return "JSON " + t.Value.SQL()
+}
+
 // ================================================================================
 //
 // Type
diff --git a/parser.go b/parser.go
index 50edf82..b13a461 100644
--- a/parser.go
+++ b/parser.go
@@ -1424,6 +1424,9 @@ func (p *Parser) parseLit() ast.Expr {
 			if id.IsKeywordLike("NUMERIC") {
 				return p.parseNumericLiteral(id)
 			}
+			if id.IsKeywordLike("JSON") {
+				return p.parseJSONLiteral(id)
+			}
 		}
 		return &ast.Ident{
 			NamePos: id.Pos,
@@ -1820,6 +1823,14 @@ func (p *Parser) parseNumericLiteral(id token.Token) *ast.NumericLiteral {
 	}
 }
 
+func (p *Parser) parseJSONLiteral(id token.Token) *ast.JSONLiteral {
+	s := p.parseStringLiteral()
+	return &ast.JSONLiteral{
+		JSON:  id.Pos,
+		Value: s,
+	}
+}
+
 func (p *Parser) lookaheadSubQuery() bool {
 	lexer := p.Lexer.Clone()
 	defer func() {


Test patch:
diff --git a/parser_test.go b/parser_test.go
index ca732df..80a7328 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -153,6 +153,15 @@ func TestParseDML(t *testing.T) {
 	})
 }
 
+func TestParseExpr(t *testing.T) {
+	inputPath := "./testdata/input/expr"
+	resultPath := "./testdata/result/expr"
+
+	testParser(t, inputPath, resultPath, func(p *memefish.Parser) (ast.Node, error) {
+		return p.ParseExpr()
+	})
+}
+
 func TestParseStatement(t *testing.T) {
 	inputPaths := []string{
 		"./testdata/input/query",
diff --git a/testdata/input/expr/json_literal.sql b/testdata/input/expr/json_literal.sql
new file mode 100644
index 0000000..0c65982
--- /dev/null
+++ b/testdata/input/expr/json_literal.sql
@@ -0,0 +1,1 @@
+JSON '{"s": "foo", "n": 42}'
\ No newline at end of file
diff --git a/testdata/input/query/select_literals_all.sql b/testdata/input/query/select_literals_all.sql
index d2f11aa..3d211fd 100644
--- a/testdata/input/query/select_literals_all.sql
+++ b/testdata/input/query/select_literals_all.sql
@@ -40,6 +40,9 @@ lines''',
   TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles",
   TIMESTAMP'2014-09-27',
   TIMESTAMP('2014-09-27'),
+  JSON '1',
+  JSON "[1, 2]",
+  JSON'{}',
   NUMERIC '0',
   NUMERIC "0",
-  true, fals
\ No newline at end of file
+  true, false
\ No newline at end of file
diff --git a/testdata/result/expr/json_literal.sql.txt b/testdata/result/expr/json_literal.sql.txt
new file mode 100644
index 0000000..6d4efa7
--- /dev/null
+++ b/testdata/result/expr/json_literal.sql.txt
@@ -0,0 +1,14 @@
+--- json_literal.sql
+JSON '{"s": "foo", "n": 42}'
+--- AST
+&ast.JSONLiteral{
+  JSON:  0,
+  Value: &ast.StringLiteral{
+    ValuePos: 5,
+    ValueEnd: 28,
+    Value:    "{\"s\": \"foo\", \"n\": 42}",
+  },
+}
+
+--- SQL
+JSON "{\"s\": \"foo\", \"n\": 42}"
diff --git a/testdata/result/query/select_literals_all.sql.txt b/testdata/result/query/select_literals_all.sql.txt
index 7e9710c..53fcf56 100644
--- a/testdata/result/query/select_literals_all.sql.txt
+++ b/testdata/result/query/select_literals_all.sql.txt
@@ -41,9 +41,12 @@ lines''',
   TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles",
   TIMESTAMP'2014-09-27',
   TIMESTAMP('2014-09-27'),
+  JSON '1',
+  JSON "[1, 2]",
+  JSON'{}',
   NUMERIC '0',
   NUMERIC "0",
-  true, fals
+  true, false
 --- AST
 &ast.QueryStatement{
   Hint:  (*ast.Hint)(nil),
@@ -555,37 +558,66 @@ lines''',
           NamedArgs: []*ast.NamedArg(nil),
         },
       },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  708,
+          Value: &ast.StringLiteral{
+            ValuePos: 713,
+            ValueEnd: 716,
+            Value:    "1",
+          },
+        },
+      },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  720,
+          Value: &ast.StringLiteral{
+            ValuePos: 725,
+            ValueEnd: 733,
+            Value:    "[1, 2]",
+          },
+        },
+      },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  737,
+          Value: &ast.StringLiteral{
+            ValuePos: 741,
+            ValueEnd: 745,
+            Value:    "{}",
+          },
+        },
+      },
       &ast.ExprSelectItem{
         Expr: &ast.NumericLiteral{
-          Numeric: 708,
+          Numeric: 749,
           Value:   &ast.StringLiteral{
-            ValuePos: 716,
-            ValueEnd: 719,
+            ValuePos: 757,
+            ValueEnd: 760,
             Value:    "0",
           },
         },
       },
       &ast.ExprSelectItem{
         Expr: &ast.NumericLiteral{
-          Numeric: 723,
+          Numeric: 764,
           Value:   &ast.StringLiteral{
-            ValuePos: 731,
-            ValueEnd: 734,
+            ValuePos: 772,
+            ValueEnd: 775,
             Value:    "0",
           },
         },
       },
       &ast.ExprSelectItem{
         Expr: &ast.BoolLiteral{
-          ValuePos: 738,
+          ValuePos: 779,
           Value:    true,
         },
       },
       &ast.ExprSelectItem{
-        Expr: &ast.Ident{
-          NamePos: 744,
-          NameEnd: 748,
-          Name:    "fals",
+        Expr: &ast.BoolLiteral{
+          ValuePos: 785,
+          Value:    false,
         },
       },
     },
@@ -599,4 +631,4 @@ lines''',
 }
 
 --- SQL
-SELECT "abc", "it\'s", "it\'s", "Title: \"Boy\"", "abc", "it\'s", "Title:\"Boy\"", "two\nlines", "why\?", "abc+", "abc+", "abc+", "f\\(abc,(.*),def\\)", B"abc", B"abc", B"abc", B"abc+", B"abc+", B"abc", 123, 0xABC, -123, -0xABC, 123.456e-67, .1E4, 58., 4e2, ARRAY[1, 2, 3], ARRAY["x", "y", "xy"], ARRAY[1, 2, 3], ARRAY<STRING>["x", "y", "xy"], ARRAY<INT64>[], ARRAY(SELECT STRUCT(1, 2, 3)), ARRAY(SELECT STRUCT(1, 2, 3)), DATE "2014-09-27", DATE("2014-09-27"), TIMESTAMP "2014-09-27 12:30:00.45-08", TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles", TIMESTAMP "2014-09-27", TIMESTAMP("2014-09-27"), NUMERIC "0", NUMERIC "0", TRUE, fals
+SELECT "abc", "it\'s", "it\'s", "Title: \"Boy\"", "abc", "it\'s", "Title:\"Boy\"", "two\nlines", "why\?", "abc+", "abc+", "abc+", "f\\(abc,(.*),def\\)", B"abc", B"abc", B"abc", B"abc+", B"abc+", B"abc", 123, 0xABC, -123, -0xABC, 123.456e-67, .1E4, 58., 4e2, ARRAY[1, 2, 3], ARRAY["x", "y", "xy"], ARRAY[1, 2, 3], ARRAY<STRING>["x", "y", "xy"], ARRAY<INT64>[], ARRAY(SELECT STRUCT(1, 2, 3)), ARRAY(SELECT STRUCT(1, 2, 3)), DATE "2014-09-27", DATE("2014-09-27"), TIMESTAMP "2014-09-27 12:30:00.45-08", TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles", TIMESTAMP "2014-09-27", TIMESTAMP("2014-09-27"), JSON "1", JSON "[1, 2]", JSON "{}", NUMERIC "0", NUMERIC "0", TRUE, FALSE
diff --git a/testdata/result/statement/select_literals_all.sql.txt b/testdata/result/statement/select_literals_all.sql.txt
index 7e9710c..53fcf56 100644
--- a/testdata/result/statement/select_literals_all.sql.txt
+++ b/testdata/result/statement/select_literals_all.sql.txt
@@ -41,9 +41,12 @@ lines''',
   TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles",
   TIMESTAMP'2014-09-27',
   TIMESTAMP('2014-09-27'),
+  JSON '1',
+  JSON "[1, 2]",
+  JSON'{}',
   NUMERIC '0',
   NUMERIC "0",
-  true, fals
+  true, false
 --- AST
 &ast.QueryStatement{
   Hint:  (*ast.Hint)(nil),
@@ -555,37 +558,66 @@ lines''',
           NamedArgs: []*ast.NamedArg(nil),
         },
       },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  708,
+          Value: &ast.StringLiteral{
+            ValuePos: 713,
+            ValueEnd: 716,
+            Value:    "1",
+          },
+        },
+      },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  720,
+          Value: &ast.StringLiteral{
+            ValuePos: 725,
+            ValueEnd: 733,
+            Value:    "[1, 2]",
+          },
+        },
+      },
+      &ast.ExprSelectItem{
+        Expr: &ast.JSONLiteral{
+          JSON:  737,
+          Value: &ast.StringLiteral{
+            ValuePos: 741,
+            ValueEnd: 745,
+            Value:    "{}",
+          },
+        },
+      },
       &ast.ExprSelectItem{
         Expr: &ast.NumericLiteral{
-          Numeric: 708,
+          Numeric: 749,
           Value:   &ast.StringLiteral{
-            ValuePos: 716,
-            ValueEnd: 719,
+            ValuePos: 757,
+            ValueEnd: 760,
             Value:    "0",
           },
         },
       },
       &ast.ExprSelectItem{
         Expr: &ast.NumericLiteral{
-          Numeric: 723,
+          Numeric: 764,
           Value:   &ast.StringLiteral{
-            ValuePos: 731,
-            ValueEnd: 734,
+            ValuePos: 772,
+            ValueEnd: 775,
             Value:    "0",
           },
         },
       },
       &ast.ExprSelectItem{
         Expr: &ast.BoolLiteral{
-          ValuePos: 738,
+          ValuePos: 779,
           Value:    true,
         },
       },
       &ast.ExprSelectItem{
-        Expr: &ast.Ident{
-          NamePos: 744,
-          NameEnd: 748,
-          Name:    "fals",
+        Expr: &ast.BoolLiteral{
+          ValuePos: 785,
+          Value:    false,
         },
       },
     },
@@ -599,4 +631,4 @@ lines''',
 }
 
 --- SQL
-SELECT "abc", "it\'s", "it\'s", "Title: \"Boy\"", "abc", "it\'s", "Title:\"Boy\"", "two\nlines", "why\?", "abc+", "abc+", "abc+", "f\\(abc,(.*),def\\)", B"abc", B"abc", B"abc", B"abc+", B"abc+", B"abc", 123, 0xABC, -123, -0xABC, 123.456e-67, .1E4, 58., 4e2, ARRAY[1, 2, 3], ARRAY["x", "y", "xy"], ARRAY[1, 2, 3], ARRAY<STRING>["x", "y", "xy"], ARRAY<INT64>[], ARRAY(SELECT STRUCT(1, 2, 3)), ARRAY(SELECT STRUCT(1, 2, 3)), DATE "2014-09-27", DATE("2014-09-27"), TIMESTAMP "2014-09-27 12:30:00.45-08", TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles", TIMESTAMP "2014-09-27", TIMESTAMP("2014-09-27"), NUMERIC "0", NUMERIC "0", TRUE, fals
+SELECT "abc", "it\'s", "it\'s", "Title: \"Boy\"", "abc", "it\'s", "Title:\"Boy\"", "two\nlines", "why\?", "abc+", "abc+", "abc+", "f\\(abc,(.*),def\\)", B"abc", B"abc", B"abc", B"abc+", B"abc+", B"abc", 123, 0xABC, -123, -0xABC, 123.456e-67, .1E4, 58., 4e2, ARRAY[1, 2, 3], ARRAY["x", "y", "xy"], ARRAY[1, 2, 3], ARRAY<STRING>["x", "y", "xy"], ARRAY<INT64>[], ARRAY(SELECT STRUCT(1, 2, 3)), ARRAY(SELECT STRUCT(1, 2, 3)), DATE "2014-09-27", DATE("2014-09-27"), TIMESTAMP "2014-09-27 12:30:00.45-08", TIMESTAMP "2014-09-27 12:30:00.45 America/Los_Angeles", TIMESTAMP "2014-09-27", TIMESTAMP("2014-09-27"), JSON "1", JSON "[1, 2]", JSON "{}", NUMERIC "0", NUMERIC "0", TRUE, FALSE