Repository: cloudspannerecosystem/memefish (go)

Issue:
Need to maintain keyword list
I think the most important characteristic of "parse-unparse" is that it never changes the semantics of the query, but "parse-unparse" can break the semantics of the query.

`(*ast.Ident).SQL()` quote identifiers only when the identifier is keyword using [`QuoteSQLIdent`](https://github.com/cloudspannerecosystem/memefish/blob/9ec8ab98a6d7c7bcdbf61ac899e23b6d76f97f05/token/quote.go#L40-L52).

```
$ go run ./tools/parse --mode query 'SELECT 1 AS `SELECT`'
...
--- SQL
SELECT 1 AS `SELECT`
```

There are bad cases.

```
$ go run ./tools/parse --mode query 'SELECT 1 AS `IF`'
--- SQL
SELECT 1 AS IF
```

It is not valid query.

```
spanner> SELECT 1 AS IF;
ERROR: spanner: code = "InvalidArgument", desc = "Syntax error: Unexpected keyword IF [at 1:13]\\nSELECT 1 AS IF\\n            ^"
```

So, memefish must maintain `token.Keywords` in keywords.go.

It seems that the current diff is `GRAPH_TABLE` and `IF`.

```
--- keywords-in-go.txt  2024-09-28 00:12:47
+++ keywords.txt        2024-09-28 00:01:00
@@ -34,11 +34,13 @@
 FOR
 FROM
 FULL
+GRAPH_TABLE
 GROUP
 GROUPING
 GROUPS
 HASH
 HAVING
+IF
 IGNORE
 IN
 INNER
```

Pull request:
Sync keyword list and implements IfExpr
This PR makes `IF` and `GRAPH_TABLE` treated as keywords.

* To achieve it, `IF` conditional expression is now `IfExpr`, not `CallExpr`.

fixes #116 

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 6956f65..e2034aa 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -64,6 +64,7 @@ func (CreateSchema) isStatement()       {}
 func (DropSchema) isStatement()         {}
 func (CreateDatabase) isStatement()     {}
 func (AlterDatabase) isStatement()      {}
+func (CreatePlacement) isStatement()    {}
 func (CreateProtoBundle) isStatement()  {}
 func (AlterProtoBundle) isStatement()   {}
 func (DropProtoBundle) isStatement()    {}
@@ -169,6 +170,7 @@ func (CountStarExpr) isExpr()         {}
 func (CastExpr) isExpr()              {}
 func (ExtractExpr) isExpr()           {}
 func (CaseExpr) isExpr()              {}
+func (IfExpr) isExpr()                {}
 func (ParenExpr) isExpr()             {}
 func (ScalarSubQuery) isExpr()        {}
 func (ArraySubQuery) isExpr()         {}
@@ -307,6 +309,7 @@ func (CreateSchema) isDDL()       {}
 func (DropSchema) isDDL()         {}
 func (CreateDatabase) isDDL()     {}
 func (AlterDatabase) isDDL()      {}
+func (CreatePlacement) isDDL()    {}
 func (CreateProtoBundle) isDDL()  {}
 func (AlterProtoBundle) isDDL()   {}
 func (DropProtoBundle) isDDL()    {}
@@ -1305,6 +1308,22 @@ type CaseElse struct {
 	Expr Expr
 }
 
+// IfExpr is IF conditional expression.
+// Because IF is SQL keyword, it can't be a normal CallExpr.
+//
+//	IF({{.Expr | sql}}, {{.TrueResult | sql}}, {{.ElseResult | sql}})
+type IfExpr struct {
+	// pos = If
+	// end = Rparen + 1
+
+	If     token.Pos // position of "IF" keyword
+	Rparen token.Pos // position of ")"
+
+	Expr       Expr
+	TrueResult Expr
+	ElseResult Expr
+}
+
 // ParenExpr is parenthesized expression node.
 //
 //	({{. | sql}})
@@ -1827,6 +1846,19 @@ type AlterDatabase struct {
 	Options *Options
 }
 
+// CreatePlacement is CREATE PLACEMENT statement node.
+//
+//	CREATE PLACEMENT {{.Name | sql}} {{.Options | sqlOpt}}
+type CreatePlacement struct {
+	// pos = Create
+	// end = (Options ?? Name).end
+
+	Create token.Pos // position of "CREATE" keyword
+
+	Name    *Ident
+	Options *Options // optional
+}
+
 // ================================================================================
 //
 // PROTO BUNDLE statements
diff --git a/ast/pos.go b/ast/pos.go
index af1b319..fcc4e93 100644
--- a/ast/pos.go
+++ b/ast/pos.go
@@ -510,6 +510,14 @@ func (c *CaseElse) End() token.Pos {
 	return nodeEnd(wrapNode(c.Expr))
 }
 
+func (i *IfExpr) Pos() token.Pos {
+	return i.If
+}
+
+func (i *IfExpr) End() token.Pos {
+	return posAdd(i.Rparen, 1)
+}
+
 func (p *ParenExpr) Pos() token.Pos {
 	return p.Lparen
 }
@@ -822,6 +830,14 @@ func (a *AlterDatabase) End() token.Pos {
 	return nodeEnd(wrapNode(a.Name))
 }
 
+func (c *CreatePlacement) Pos() token.Pos {
+	return c.Create
+}
+
+func (c *CreatePlacement) End() token.Pos {
+	return nodeEnd(nodeChoice(wrapNode(c.Options), wrapNode(c.Name)))
+}
+
 func (p *ProtoBundleTypes) Pos() token.Pos {
 	return p.Lparen
 }
diff --git a/ast/sql.go b/ast/sql.go
index 242b653..0177a79 100644
--- a/ast/sql.go
+++ b/ast/sql.go
@@ -77,7 +77,7 @@ const (
 
 func exprPrec(e Expr) prec {
 	switch e := e.(type) {
-	case *CallExpr, *CountStarExpr, *CastExpr, *ExtractExpr, *CaseExpr, *ParenExpr, *ScalarSubQuery, *ArraySubQuery, *ExistsSubQuery, *Param, *Ident, *Path, *ArrayLiteral, *TupleStructLiteral, *TypedStructLiteral, *TypelessStructLiteral, *NullLiteral, *BoolLiteral, *IntLiteral, *FloatLiteral, *StringLiteral, *BytesLiteral, *DateLiteral, *TimestampLiteral, *NumericLiteral:
+	case *CallExpr, *CountStarExpr, *CastExpr, *ExtractExpr, *CaseExpr, *IfExpr, *ParenExpr, *ScalarSubQuery, *ArraySubQuery, *ExistsSubQuery, *Param, *Ident, *Path, *ArrayLiteral, *TupleStructLiteral, *TypedStructLiteral, *TypelessStructLiteral, *NullLiteral, *BoolLiteral, *IntLiteral, *FloatLiteral, *StringLiteral, *BytesLiteral, *DateLiteral, *TimestampLiteral, *NumericLiteral:
 		return precLit
 	case *IndexExpr, *SelectorExpr:
 		return precSelector
@@ -576,6 +576,10 @@ func (c *CaseElse) SQL() string {
 	return "ELSE " + c.Expr.SQL()
 }
 
+func (i *IfExpr) SQL() string {
+	return "IF(" + i.Expr.SQL() + ", " + i.TrueResult.SQL() + ", " + i.ElseResult.SQL() + ")"
+}
+
 func (p *ParenExpr) SQL() string {
 	return "(" + p.Expr.SQL() + ")"
 }
@@ -794,6 +798,10 @@ func (d *AlterDatabase) SQL() string {
 	return "ALTER DATABASE " + d.Name.SQL() + " SET " + d.Options.SQL()
 }
 
+func (c *CreatePlacement) SQL() string {
+	return "CREATE PLACEMENT " + c.Name.SQL() + sqlOpt(" ", c.Options, " ")
+}
+
 func (p *ProtoBundleTypes) SQL() string { return "(" + sqlJoin(p.Types, ", ") + ")" }
 
 func (b *CreateProtoBundle) SQL() string { return "CREATE PROTO BUNDLE " + b.Types.SQL() }
diff --git a/parser.go b/parser.go
index 708db3b..a9fb27b 100644
--- a/parser.go
+++ b/parser.go
@@ -1425,6 +1425,8 @@ func (p *Parser) parseLit() ast.Expr {
 		return p.parseParam()
 	case "CASE":
 		return p.parseCaseExpr()
+	case "IF":
+		return p.parseIfExpr()
 	case "CAST":
 		return p.parseCastExpr()
 	case "EXISTS":
@@ -1715,6 +1717,25 @@ func (p *Parser) parseCaseElse() *ast.CaseElse {
 	}
 }
 
+func (p *Parser) parseIfExpr() *ast.IfExpr {
+	pos := p.expect("IF").Pos
+	p.expect("(")
+	expr := p.parseExpr()
+	p.expect(",")
+	trueResult := p.parseExpr()
+	p.expect(",")
+	elseResult := p.parseExpr()
+	rparen := p.expect(")").Pos
+
+	return &ast.IfExpr{
+		If:         pos,
+		Rparen:     rparen,
+		Expr:       expr,
+		TrueResult: trueResult,
+		ElseResult: elseResult,
+	}
+}
+
 func (p *Parser) parseCastExpr() *ast.CastExpr {
 	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))
@@ -2304,6 +2325,8 @@ func (p *Parser) parseDDL() ast.DDL {
 			return p.parseCreateSchema(pos)
 		case p.Token.IsKeywordLike("DATABASE"):
 			return p.parseCreateDatabase(pos)
+		case p.Token.IsKeywordLike("PLACEMENT"):
+			return p.parseCreatePlacement(pos)
 		case p.Token.Kind == "PROTO":
 			return p.parseCreateProtoBundle(pos)
 		case p.Token.IsKeywordLike("TABLE"):
@@ -2411,6 +2434,7 @@ func (p *Parser) parseDropSchema(pos token.Pos) *ast.DropSchema {
 func (p *Parser) parseCreateDatabase(pos token.Pos) *ast.CreateDatabase {
 	p.expectKeywordLike("DATABASE")
 	name := p.parseIdent()
+
 	return &ast.CreateDatabase{
 		Create: pos,
 		Name:   name,
@@ -2430,6 +2454,18 @@ func (p *Parser) parseAlterDatabase(pos token.Pos) *ast.AlterDatabase {
 	}
 }
 
+func (p *Parser) parseCreatePlacement(pos token.Pos) *ast.CreatePlacement {
+	p.expectKeywordLike("PLACEMENT")
+	name := p.parseIdent()
+	options := p.parseOptions()
+
+	return &ast.CreatePlacement{
+		Create:  pos,
+		Name:    name,
+		Options: options,
+	}
+}
+
 func (p *Parser) parseProtoBundleTypes() *ast.ProtoBundleTypes {
 	lparen := p.expect("(").Pos
 	types := parseCommaSeparatedList(p, p.parseNamedType)
@@ -3803,7 +3839,7 @@ func (p *Parser) parseScalarSchemaType() ast.SchemaType {
 }
 
 func (p *Parser) parseIfNotExists() bool {
-	if p.Token.IsKeywordLike("IF") {
+	if p.Token.Kind == "IF" {
 		p.nextToken()
 		p.expect("NOT")
 		p.expect("EXISTS")
@@ -3813,7 +3849,7 @@ func (p *Parser) parseIfNotExists() bool {
 }
 
 func (p *Parser) parseIfExists() bool {
-	if p.Token.IsKeywordLike("IF") {
+	if p.Token.Kind == "IF" {
 		p.nextToken()
 		p.expect("EXISTS")
 		return true
diff --git a/token/keywords.go b/token/keywords.go
index 622db3a..a0caa7c 100644
--- a/token/keywords.go
+++ b/token/keywords.go
@@ -41,12 +41,14 @@ var Keywords = []TokenKind{
 	"FOR",
 	"FROM",
 	"FULL",
+	"GRAPH_TABLE",
 	"GROUP",
 	"GROUPING",
 	"GROUPS",
 	"HASH",
 	"HAVING",
 	"IGNORE",
+	"IF",
 	"IN",
 	"INNER",
 	"INTERSECT",


Test patch:
diff --git a/testdata/input/ddl/create_placement.sql b/testdata/input/ddl/create_placement.sql
new file mode 100644
index 0000000..5b19cd3
--- /dev/null
+++ b/testdata/input/ddl/create_placement.sql
@@ -0,0 +1,1 @@
+CREATE PLACEMENT `europeplacement` OPTIONS (instance_partition="europe-partition")
\ No newline at end of file
diff --git a/testdata/input/expr/edge_case_backquote_keyword.sql b/testdata/input/expr/edge_case_backquote_keyword.sql
new file mode 100644
index 0000000..b69184f
--- /dev/null
+++ b/testdata/input/expr/edge_case_backquote_keyword.sql
@@ -0,0 +1,1 @@
+`IF` + `GRAPH_TABLE`
\ No newline at end of file
diff --git a/testdata/input/expr/edge_case_backquote_non_keyword.sql b/testdata/input/expr/edge_case_backquote_non_keyword.sql
new file mode 100644
index 0000000..f410a5f
--- /dev/null
+++ b/testdata/input/expr/edge_case_backquote_non_keyword.sql
@@ -0,0 +1,1 @@
+`DATABASE` + `SCHEMA`
\ No newline at end of file
diff --git a/testdata/result/ddl/alter_table_add_column_with_if_expression.sql.txt b/testdata/result/ddl/alter_table_add_column_with_if_expression.sql.txt
index 9188ae2..580c5d1 100644
--- a/testdata/result/ddl/alter_table_add_column_with_if_expression.sql.txt
+++ b/testdata/result/ddl/alter_table_add_column_with_if_expression.sql.txt
@@ -32,92 +32,76 @@ ALTER TABLE foo ADD COLUMN expired_at TIMESTAMP AS (IF (status != "OPEN" AND sta
         As:     48,
         Stored: 153,
         Rparen: 151,
-        Expr:   &ast.CallExpr{
+        Expr:   &ast.IfExpr{
+          If:     52,
           Rparen: 150,
-          Func:   &ast.Ident{
-            NamePos: 52,
-            NameEnd: 54,
-            Name:    "IF",
+          Expr:   &ast.BinaryExpr{
+            Op:   "AND",
+            Left: &ast.BinaryExpr{
+              Op:   "!=",
+              Left: &ast.Ident{
+                NamePos: 56,
+                NameEnd: 62,
+                Name:    "status",
+              },
+              Right: &ast.StringLiteral{
+                ValuePos: 66,
+                ValueEnd: 72,
+                Value:    "OPEN",
+              },
+            },
+            Right: &ast.BinaryExpr{
+              Op:   "!=",
+              Left: &ast.Ident{
+                NamePos: 77,
+                NameEnd: 83,
+                Name:    "status",
+              },
+              Right: &ast.StringLiteral{
+                ValuePos: 87,
+                ValueEnd: 99,
+                Value:    "SCHEDULING",
+              },
+            },
           },
-          Distinct: false,
-          Args:     []ast.Arg{
-            &ast.ExprArg{
-              Expr: &ast.BinaryExpr{
-                Op:   "AND",
-                Left: &ast.BinaryExpr{
-                  Op:   "!=",
-                  Left: &ast.Ident{
-                    NamePos: 56,
-                    NameEnd: 62,
-                    Name:    "status",
-                  },
-                  Right: &ast.StringLiteral{
-                    ValuePos: 66,
-                    ValueEnd: 72,
-                    Value:    "OPEN",
-                  },
-                },
-                Right: &ast.BinaryExpr{
-                  Op:   "!=",
-                  Left: &ast.Ident{
-                    NamePos: 77,
-                    NameEnd: 83,
-                    Name:    "status",
-                  },
-                  Right: &ast.StringLiteral{
-                    ValuePos: 87,
-                    ValueEnd: 99,
-                    Value:    "SCHEDULING",
-                  },
+          TrueResult: &ast.CallExpr{
+            Rparen: 143,
+            Func:   &ast.Ident{
+              NamePos: 101,
+              NameEnd: 114,
+              Name:    "TIMESTAMP_ADD",
+            },
+            Distinct: false,
+            Args:     []ast.Arg{
+              &ast.ExprArg{
+                Expr: &ast.Ident{
+                  NamePos: 115,
+                  NameEnd: 125,
+                  Name:    "updated_at",
                 },
               },
-            },
-            &ast.ExprArg{
-              Expr: &ast.CallExpr{
-                Rparen: 143,
-                Func:   &ast.Ident{
-                  NamePos: 101,
-                  NameEnd: 114,
-                  Name:    "TIMESTAMP_ADD",
+              &ast.IntervalArg{
+                Interval: 127,
+                Expr:     &ast.IntLiteral{
+                  ValuePos: 136,
+                  ValueEnd: 139,
+                  Base:     10,
+                  Value:    "120",
                 },
-                Distinct: false,
-                Args:     []ast.Arg{
-                  &ast.ExprArg{
-                    Expr: &ast.Ident{
-                      NamePos: 115,
-                      NameEnd: 125,
-                      Name:    "updated_at",
-                    },
-                  },
-                  &ast.IntervalArg{
-                    Interval: 127,
-                    Expr:     &ast.IntLiteral{
-                      ValuePos: 136,
-                      ValueEnd: 139,
-                      Base:     10,
-                      Value:    "120",
-                    },
-                    Unit: &ast.Ident{
-                      NamePos: 140,
-                      NameEnd: 143,
-                      Name:    "DAY",
-                    },
-                  },
+                Unit: &ast.Ident{
+                  NamePos: 140,
+                  NameEnd: 143,
+                  Name:    "DAY",
                 },
-                NamedArgs:    []*ast.NamedArg(nil),
-                NullHandling: nil,
-                Having:       nil,
-              },
-            },
-            &ast.ExprArg{
-              Expr: &ast.NullLiteral{
-                Null: 146,
               },
             },
+            NamedArgs:    []*ast.NamedArg(nil),
+            NullHandling: nil,
+            Having:       nil,
+          },
+          ElseResult: &ast.NullLiteral{
+            Null: 146,
           },
-          NamedArgs:    []*ast.NamedArg(nil),
-          NullHandling: nil,
-          Having:       nil,
         },
       },
       Hidden:  -1,
diff --git a/testdata/result/ddl/create_placement.sql.txt b/testdata/result/ddl/create_placement.sql.txt
new file mode 100644
index 0000000..efbb1bc
--- /dev/null
+++ b/testdata/result/ddl/create_placement.sql.txt
@@ -0,0 +1,32 @@
+--- create_placement.sql
+CREATE PLACEMENT `europeplacement` OPTIONS (instance_partition="europe-partition")
+--- AST
+&ast.CreatePlacement{
+  Create: 0,
+  Name:   &ast.Ident{
+    NamePos: 17,
+    NameEnd: 34,
+    Name:    "europeplacement",
+  },
+  Options: &ast.Options{
+    Options: 35,
+    Rparen:  81,
+    Records: []*ast.OptionsDef{
+      &ast.OptionsDef{
+        Name: &ast.Ident{
+          NamePos: 44,
+          NameEnd: 62,
+          Name:    "instance_partition",
+        },
+        Value: &ast.StringLiteral{
+          ValuePos: 63,
+          ValueEnd: 81,
+          Value:    "europe-partition",
+        },
+      },
+    },
+  },
+}
+
+--- SQL
+CREATE PLACEMENT europeplacement OPTIONS (instance_partition = "europe-partition") 
diff --git a/testdata/result/expr/edge_case_backquote_keyword.sql.txt b/testdata/result/expr/edge_case_backquote_keyword.sql.txt
new file mode 100644
index 0000000..36e1fe1
--- /dev/null
+++ b/testdata/result/expr/edge_case_backquote_keyword.sql.txt
@@ -0,0 +1,19 @@
+--- edge_case_backquote_keyword.sql
+`IF` + `GRAPH_TABLE`
+--- AST
+&ast.BinaryExpr{
+  Op:   "+",
+  Left: &ast.Ident{
+    NamePos: 0,
+    NameEnd: 4,
+    Name:    "IF",
+  },
+  Right: &ast.Ident{
+    NamePos: 7,
+    NameEnd: 20,
+    Name:    "GRAPH_TABLE",
+  },
+}
+
+--- SQL
+`IF` + `GRAPH_TABLE`
diff --git a/testdata/result/expr/edge_case_backquote_non_keyword.sql.txt b/testdata/result/expr/edge_case_backquote_non_keyword.sql.txt
new file mode 100644
index 0000000..beda1a6
--- /dev/null
+++ b/testdata/result/expr/edge_case_backquote_non_keyword.sql.txt
@@ -0,0 +1,19 @@
+--- edge_case_backquote_non_keyword.sql
+`DATABASE` + `SCHEMA`
+--- AST
+&ast.BinaryExpr{
+  Op:   "+",
+  Left: &ast.Ident{
+    NamePos: 0,
+    NameEnd: 10,
+    Name:    "DATABASE",
+  },
+  Right: &ast.Ident{
+    NamePos: 13,
+    NameEnd: 21,
+    Name:    "SCHEMA",
+  },
+}
+
+--- SQL
+DATABASE + SCHEMA
diff --git a/testdata/result/query/select_expr.sql.txt b/testdata/result/query/select_expr.sql.txt
index 9f1a2bc..00da719 100644
--- a/testdata/result/query/select_expr.sql.txt
+++ b/testdata/result/query/select_expr.sql.txt
@@ -860,52 +860,36 @@ select 1 + 2, 1 - 2,
       &ast.Alias{
         Expr: &ast.BinaryExpr{
           Op:   "+",
-          Left: &ast.CallExpr{
+          Left: &ast.IfExpr{
+            If:     773,
             Rparen: 788,
-            Func:   &ast.Ident{
-              NamePos: 773,
-              NameEnd: 775,
-              Name:    "IF",
-            },
-            Distinct: false,
-            Args:     []ast.Arg{
-              &ast.ExprArg{
-                Expr: &ast.BinaryExpr{
-                  Op:   ">",
-                  Left: &ast.IntLiteral{
-                    ValuePos: 777,
-                    ValueEnd: 778,
-                    Base:     10,
-                    Value:    "1",
-                  },
-                  Right: &ast.IntLiteral{
-                    ValuePos: 781,
-                    ValueEnd: 782,
-                    Base:     10,
-                    Value:    "1",
-                  },
-                },
-              },
-              &ast.ExprArg{
-                Expr: &ast.IntLiteral{
-                  ValuePos: 784,
-                  ValueEnd: 785,
-                  Base:     10,
-                  Value:    "1",
-                },
+            Expr:   &ast.BinaryExpr{
+              Op:   ">",
+              Left: &ast.IntLiteral{
+                ValuePos: 777,
+                ValueEnd: 778,
+                Base:     10,
+                Value:    "1",
               },
-              &ast.ExprArg{
-                Expr: &ast.IntLiteral{
-                  ValuePos: 787,
-                  ValueEnd: 788,
-                  Base:     10,
-                  Value:    "2",
-                },
+              Right: &ast.IntLiteral{
+                ValuePos: 781,
+                ValueEnd: 782,
+                Base:     10,
+                Value:    "1",
               },
             },
-            NamedArgs:    []*ast.NamedArg(nil),
-            NullHandling: nil,
-            Having:       nil,
+            TrueResult: &ast.IntLiteral{
+              ValuePos: 784,
+              ValueEnd: 785,
+              Base:     10,
+              Value:    "1",
+            },
+            ElseResult: &ast.IntLiteral{
+              ValuePos: 787,
+              ValueEnd: 788,
+              Base:     10,
+              Value:    "2",
+            },
           },
           Right: &ast.IntLiteral{
             ValuePos: 790,
diff --git a/testdata/result/statement/alter_table_add_column_with_if_expression.sql.txt b/testdata/result/statement/alter_table_add_column_with_if_expression.sql.txt
index 9188ae2..580c5d1 100644
--- a/testdata/result/statement/alter_table_add_column_with_if_expression.sql.txt
+++ b/testdata/result/statement/alter_table_add_column_with_if_expression.sql.txt
@@ -32,92 +32,76 @@ ALTER TABLE foo ADD COLUMN expired_at TIMESTAMP AS (IF (status != "OPEN" AND sta
         As:     48,
         Stored: 153,
         Rparen: 151,
-        Expr:   &ast.CallExpr{
+        Expr:   &ast.IfExpr{
+          If:     52,
           Rparen: 150,
-          Func:   &ast.Ident{
-            NamePos: 52,
-            NameEnd: 54,
-            Name:    "IF",
+          Expr:   &ast.BinaryExpr{
+            Op:   "AND",
+            Left: &ast.BinaryExpr{
+              Op:   "!=",
+              Left: &ast.Ident{
+                NamePos: 56,
+                NameEnd: 62,
+                Name:    "status",
+              },
+              Right: &ast.StringLiteral{
+                ValuePos: 66,
+                ValueEnd: 72,
+                Value:    "OPEN",
+              },
+            },
+            Right: &ast.BinaryExpr{
+              Op:   "!=",
+              Left: &ast.Ident{
+                NamePos: 77,
+                NameEnd: 83,
+                Name:    "status",
+              },
+              Right: &ast.StringLiteral{
+                ValuePos: 87,
+                ValueEnd: 99,
+                Value:    "SCHEDULING",
+              },
+            },
           },
-          Distinct: false,
-          Args:     []ast.Arg{
-            &ast.ExprArg{
-              Expr: &ast.BinaryExpr{
-                Op:   "AND",
-                Left: &ast.BinaryExpr{
-                  Op:   "!=",
-                  Left: &ast.Ident{
-                    NamePos: 56,
-                    NameEnd: 62,
-                    Name:    "status",
-                  },
-                  Right: &ast.StringLiteral{
-                    ValuePos: 66,
-                    ValueEnd: 72,
-                    Value:    "OPEN",
-                  },
-                },
-                Right: &ast.BinaryExpr{
-                  Op:   "!=",
-                  Left: &ast.Ident{
-                    NamePos: 77,
-                    NameEnd: 83,
-                    Name:    "status",
-                  },
-                  Right: &ast.StringLiteral{
-                    ValuePos: 87,
-                    ValueEnd: 99,
-                    Value:    "SCHEDULING",
-                  },
+          TrueResult: &ast.CallExpr{
+            Rparen: 143,
+            Func:   &ast.Ident{
+              NamePos: 101,
+              NameEnd: 114,
+              Name:    "TIMESTAMP_ADD",
+            },
+            Distinct: false,
+            Args:     []ast.Arg{
+              &ast.ExprArg{
+                Expr: &ast.Ident{
+                  NamePos: 115,
+                  NameEnd: 125,
+                  Name:    "updated_at",
                 },
               },
-            },
-            &ast.ExprArg{
-              Expr: &ast.CallExpr{
-                Rparen: 143,
-                Func:   &ast.Ident{
-                  NamePos: 101,
-                  NameEnd: 114,
-                  Name:    "TIMESTAMP_ADD",
+              &ast.IntervalArg{
+                Interval: 127,
+                Expr:     &ast.IntLiteral{
+                  ValuePos: 136,
+                  ValueEnd: 139,
+                  Base:     10,
+                  Value:    "120",
                 },
-                Distinct: false,
-                Args:     []ast.Arg{
-                  &ast.ExprArg{
-                    Expr: &ast.Ident{
-                      NamePos: 115,
-                      NameEnd: 125,
-                      Name:    "updated_at",
-                    },
-                  },
-                  &ast.IntervalArg{
-                    Interval: 127,
-                    Expr:     &ast.IntLiteral{
-                      ValuePos: 136,
-                      ValueEnd: 139,
-                      Base:     10,
-                      Value:    "120",
-                    },
-                    Unit: &ast.Ident{
-                      NamePos: 140,
-                      NameEnd: 143,
-                      Name:    "DAY",
-                    },
-                  },
+                Unit: &ast.Ident{
+                  NamePos: 140,
+                  NameEnd: 143,
+                  Name:    "DAY",
                 },
-                NamedArgs:    []*ast.NamedArg(nil),
-                NullHandling: nil,
-                Having:       nil,
-              },
-            },
-            &ast.ExprArg{
-              Expr: &ast.NullLiteral{
-                Null: 146,
               },
             },
+            NamedArgs:    []*ast.NamedArg(nil),
+            NullHandling: nil,
+            Having:       nil,
+          },
+          ElseResult: &ast.NullLiteral{
+            Null: 146,
           },
-          NamedArgs:    []*ast.NamedArg(nil),
-          NullHandling: nil,
-          Having:       nil,
         },
       },
       Hidden:  -1,
diff --git a/testdata/result/statement/create_placement.sql.txt b/testdata/result/statement/create_placement.sql.txt
new file mode 100644
index 0000000..efbb1bc
--- /dev/null
+++ b/testdata/result/statement/create_placement.sql.txt
@@ -0,0 +1,32 @@
+--- create_placement.sql
+CREATE PLACEMENT `europeplacement` OPTIONS (instance_partition="europe-partition")
+--- AST
+&ast.CreatePlacement{
+  Create: 0,
+  Name:   &ast.Ident{
+    NamePos: 17,
+    NameEnd: 34,
+    Name:    "europeplacement",
+  },
+  Options: &ast.Options{
+    Options: 35,
+    Rparen:  81,
+    Records: []*ast.OptionsDef{
+      &ast.OptionsDef{
+        Name: &ast.Ident{
+          NamePos: 44,
+          NameEnd: 62,
+          Name:    "instance_partition",
+        },
+        Value: &ast.StringLiteral{
+          ValuePos: 63,
+          ValueEnd: 81,
+          Value:    "europe-partition",
+        },
+      },
+    },
+  },
+}
+
+--- SQL
+CREATE PLACEMENT europeplacement OPTIONS (instance_partition = "europe-partition") 
diff --git a/testdata/result/statement/select_expr.sql.txt b/testdata/result/statement/select_expr.sql.txt
index 9f1a2bc..00da719 100644
--- a/testdata/result/statement/select_expr.sql.txt
+++ b/testdata/result/statement/select_expr.sql.txt
@@ -860,52 +860,36 @@ select 1 + 2, 1 - 2,
       &ast.Alias{
         Expr: &ast.BinaryExpr{
           Op:   "+",
-          Left: &ast.CallExpr{
+          Left: &ast.IfExpr{
+            If:     773,
             Rparen: 788,
-            Func:   &ast.Ident{
-              NamePos: 773,
-              NameEnd: 775,
-              Name:    "IF",
-            },
-            Distinct: false,
-            Args:     []ast.Arg{
-              &ast.ExprArg{
-                Expr: &ast.BinaryExpr{
-                  Op:   ">",
-                  Left: &ast.IntLiteral{
-                    ValuePos: 777,
-                    ValueEnd: 778,
-                    Base:     10,
-                    Value:    "1",
-                  },
-                  Right: &ast.IntLiteral{
-                    ValuePos: 781,
-                    ValueEnd: 782,
-                    Base:     10,
-                    Value:    "1",
-                  },
-                },
-              },
-              &ast.ExprArg{
-                Expr: &ast.IntLiteral{
-                  ValuePos: 784,
-                  ValueEnd: 785,
-                  Base:     10,
-                  Value:    "1",
-                },
+            Expr:   &ast.BinaryExpr{
+              Op:   ">",
+              Left: &ast.IntLiteral{
+                ValuePos: 777,
+                ValueEnd: 778,
+                Base:     10,
+                Value:    "1",
               },
-              &ast.ExprArg{
-                Expr: &ast.IntLiteral{
-                  ValuePos: 787,
-                  ValueEnd: 788,
-                  Base:     10,
-                  Value:    "2",
-                },
+              Right: &ast.IntLiteral{
+                ValuePos: 781,
+                ValueEnd: 782,
+                Base:     10,
+                Value:    "1",
               },
             },
-            NamedArgs:    []*ast.NamedArg(nil),
-            NullHandling: nil,
-            Having:       nil,
+            TrueResult: &ast.IntLiteral{
+              ValuePos: 784,
+              ValueEnd: 785,
+              Base:     10,
+              Value:    "1",
+            },
+            ElseResult: &ast.IntLiteral{
+              ValuePos: 787,
+              ValueEnd: 788,
+              Base:     10,
+              Value:    "2",
+            },
           },
           Right: &ast.IntLiteral{
             ValuePos: 790,
diff --git a/token/quote_test.go b/token/quote_test.go
index 2dfdab0..051f497 100644
--- a/token/quote_test.go
+++ b/token/quote_test.go
@@ -9,6 +9,7 @@ var quoteTestCases = []struct {
 	str, bytes, id string
 }{
 	{"foo", `"foo"`, `B"foo"`, "foo"},
+	{"if", `"if"`, `B"if"`, "`if`"},
 	{"\u0000", `"\u0000"`, `B"\x00"`, "`\\u0000`"},
 	{"\U0010FFFF", `"\U0010FFFF"`, `B"\xF4\x8F\xBF\xBF"`, "`\\U0010FFFF`"},
 	{"a\u2060b", `"a\u2060b"`, `B"a\xE2\x81\xA0b"`, "`a\\u2060b`"},