Repository: future-architect/uroborosql-fmt
Pull request #133: 式(a_expr) 対応の拡充
Author: login: lemonadern
Created: 2025-02-27T11:34:31Z | Merged: 2025-03-11T08:22:29Z

## Summary
式ノード a_expr の対応範囲を広げました

### 二項演算
- [x] 算術演算
- [x] 論理演算

### 述語
- [x] IS
- [x] IN
    - サブクエリは未対応 

### 実装に付随してサポートしたもの
- [x] 括弧

## `handle_*` 系のメソッドについて
今回の実装では、木の走査に従来の `visit_*` だけでなく `handle_*` という命名のメソッドを追加しています。以下でその意図について説明します。

postgres の文法では、 二項演算やIS, IN 式はすべて a_expr として表現され、子ノードをいくつか見てから初めてどんな式であるかが判明するケースが多いです。これは tree-sitter が式の種類ごとに明示的なノード （`binary_expression`, `boolean_expression`, `is_expression`, `in_expression`, etc.） を持つのと対照的です。

```
postgres:
- a_expr
  - a_expr '+'    a_expr （算術演算）
  - a_expr AND    a_expr （論理演算）
  - a_expr IS     NULL （IS式）
  - a_expr IN_P   in_expr （IN式）
           ^^^^ ここまで見ないとどんな式かわからない
```
```
tree-sitter-sql:
- binary_expression
- boolean_expression
- is_expression
- in_expression
  ^^^^^^^^^^^^^ この時点で式の種類がわかる
```
このような構造のため、 a_expr のようなノードを visit すると内部の分岐が増え、子を持つノード単位でのメソッド切り出す従来の方法だとインデントが深くなりすぎる問題がありました。

そのため、通常のパターンマッチによる走査をしながら、要素の種類が決定したタイミングで後続の処理をメソッドとして切り出す実装を取り入れてみました。

例えば論理演算を例に挙げると、

```
a_expr AND a_expr
```

1. a_expr を visit する (cursor: a_expr)
2. 次のノードへカーソルを移動 (cursor: AND)
3. ノードの種類についてパターンマッチをする
4. 式の種類が決定する （ANDなので論理演算）
5. すでに visit した 左辺の式を引数として 論理式を処理するメソッドを呼び出す

という流れになります。
この 5. にあたるメソッドに `handle_*` という命名をしています。
呼び分けの意図として、これらのメソッドは 従来の visit_ なメソッドのように、**呼出し後にカーソルが元のノードを指している**といった性質を満たしていないため、呼出し側で区別できるようにしたいという理由があります。

`handle_` という prefix の良しあしも含め、意見を伺いたいです。




Diff:
diff --git a/Cargo.lock b/Cargo.lock
index fb3cbe53..efe159c2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -332,7 +332,7 @@ dependencies = [
 [[package]]
 name = "postgresql-cst-parser"
 version = "0.1.0"
-source = "git+https://github.com/future-architect/postgresql-cst-parser#ee10565f628d8865817889e9760507528c402d49"
+source = "git+https://github.com/future-architect/postgresql-cst-parser#ca0afedcd845bf01c74e725eea189f396c234ef3"
 dependencies = [
  "cstree",
  "miniz_oxide",
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr.rs
index 918e1178..e814e67f 100644
--- a/crates/uroborosql-fmt/src/new_visitor/pg_expr.rs
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr.rs
@@ -1,38 +1,16 @@
 mod a_expr;
+mod c_expr;
 
-use postgresql_cst_parser::{syntax_kind::SyntaxKind, tree_sitter::TreeCursor};
+use postgresql_cst_parser::syntax_kind::SyntaxKind;
+use postgresql_cst_parser::tree_sitter::TreeCursor;
 
 use crate::{
-    cst::{AsteriskExpr, Expr, PrimaryExpr, PrimaryExprKind},
+    cst::{AlignedExpr, Expr},
     error::UroboroSQLFmtError,
-    util::convert_identifier_case,
 };
 
 use super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
 
-/*
-    postgres の文法における Expression
-    - a_expr
-        - c_expr
-        - ...
-    - b_expr
-        - ...
-    - c_expr
-        - columnref
-        - AexprConst
-        - PARAM opt_indirection
-        - '(' a_expr ')' opt_indirection
-        - case_expr
-        - func_expr
-        - select_with_parens
-        - select_with_parens indirection
-        - EXISTS select_with_parens
-        - ARRAY select_with_parens
-        - ARRAY array_expr
-        - explicit_row
-        - implicit_row
-        - GROUPING '(' expr_list ')'
-*/
 impl Visitor {
     fn visit_b_expr(
         &mut self,
@@ -42,202 +20,46 @@ impl Visitor {
         unimplemented!()
     }
 
-    fn visit_c_expr(
+    /// 呼出し後、 cursor は expr_list を指している
+    pub(crate) fn visit_expr_list(
         &mut self,
         cursor: &mut TreeCursor,
         src: &str,
-    ) -> Result<Expr, UroboroSQLFmtError> {
-        cursor.goto_first_child();
-
-        let expr = match cursor.node().kind() {
-            SyntaxKind::columnref => self.visit_columnref(cursor, src)?,
-            SyntaxKind::AexprConst => self.visit_aexpr_const(cursor, src)?,
-            SyntaxKind::PARAM => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): PARAM is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::select_with_parens => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): select_with_parens is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::EXISTS => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): EXISTS is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::ARRAY => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): ARRAY is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::explicit_row => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): explicit_row is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::implicit_row => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): implicit_row is not implemented".to_string(),
-                ))
-            }
-            SyntaxKind::GROUPING => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_c_expr(): GROUPING is not implemented".to_string(),
-                ))
-            }
-            _ => {
-                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
-                    "visit_c_expr(): unexpected syntaxkind\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )))
-            }
-        };
-
-        cursor.goto_parent();
-        pg_ensure_kind(cursor, SyntaxKind::c_expr, src)?;
-
-        Ok(expr)
-    }
-
-    // AexprConst
-    fn visit_aexpr_const(
-        &mut self,
-        cursor: &mut TreeCursor,
-        src: &str,
-    ) -> Result<Expr, UroboroSQLFmtError> {
-        // AexprConst
-        // - Iconst
-        //   - ICONST
-        // - FCONST
-        // - Sconst
-        //   - SCONST
-        // - BCONST
-        // - XCONST
-        // - func_name Sconst
-        // - func_name '(' func_arg_list opt_sort_clause ')' Sconst
-        // - ConstTypename Sconst
-        // - ConstInterval Sconst opt_interval
-        // - ConstInterval '(' Iconst ')' Sconst
-        // - TRUE_P
-        // - FALSE_P
-        // - NULL_P
-
-        cursor.goto_first_child();
-        let expr = match cursor.node().kind() {
-            SyntaxKind::Iconst
-            | SyntaxKind::FCONST
-            | SyntaxKind::Sconst
-            | SyntaxKind::BCONST
-            | SyntaxKind::XCONST => {
-                PrimaryExpr::with_pg_node(cursor.node(), PrimaryExprKind::Expr)?.into()
-            }
-            SyntaxKind::func_name => {
-                // func_name Sconst
-                // func_name '(' func_arg_list opt_sort_clause ')' Sconst
-                return Err(UroboroSQLFmtError::Unimplemented(format!(
-                    "visit_aexpr_const(): func_name is not implemented\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )));
-            }
-            SyntaxKind::ConstTypename => {
-                // ConstTypename Sconst
-                return Err(UroboroSQLFmtError::Unimplemented(format!(
-                    "visit_aexpr_const(): ConstTypename is not implemented\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )));
-            }
-            SyntaxKind::ConstInterval => {
-                // ConstInterval Sconst opt_interval
-                // ConstInterval '(' Iconst ')' Sconst
-                return Err(UroboroSQLFmtError::Unimplemented(format!(
-                    "visit_aexpr_const(): ConstInterval is not implemented\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )));
-            }
-            SyntaxKind::TRUE_P | SyntaxKind::FALSE_P | SyntaxKind::NULL_P => {
-                PrimaryExpr::with_pg_node(cursor.node(), PrimaryExprKind::Keyword)?.into()
-            }
-            _ => {
-                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
-                    "visit_aexpr_const(): unexpected node kind\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )));
-            }
-        };
-
-        cursor.goto_parent();
-        pg_ensure_kind(cursor, SyntaxKind::AexprConst, src)?;
-
-        Ok(expr)
-    }
+    ) -> Result<Vec<AlignedExpr>, UroboroSQLFmtError> {
+        // cursor -> expr_list
+        pg_ensure_kind(cursor, SyntaxKind::expr_list, src)?;
 
-    fn visit_columnref(
-        &mut self,
-        cursor: &mut TreeCursor,
-        src: &str,
-    ) -> Result<Expr, UroboroSQLFmtError> {
-        // columnref
-        // - ColId
-        // - ColId indirection
-        //   - e.g.: `a.field`, `a.field[1]`
-
-        // cursor -> ColId (必ず存在する)
         cursor.goto_first_child();
+        // cursor -> a_expr
 
-        pg_ensure_kind(cursor, SyntaxKind::ColId, src)?;
-        let mut columnref_text = cursor.node().text().to_string();
+        let mut exprs = Vec::new();
 
-        if cursor.goto_next_sibling() {
-            // cursor -> indirection
-            pg_ensure_kind(cursor, SyntaxKind::indirection, src)?;
-
-            // indirection
-            // - indirection_el
-            //    - `.` attr_name
-            //    - `.` `*`
-            //    - `[` a_expr `]`
-            //    - `[` opt_slice_bound `:` opt_slice_bound `]`
-            //
-            // indirection はフラット化されている: https://github.com/future-architect/postgresql-cst-parser/pull/7
-
-            let indirection_text = cursor.node().text();
+        // 最初の要素
+        if cursor.node().kind() == SyntaxKind::a_expr {
+            exprs.push(self.visit_a_expr(cursor, src)?.to_aligned());
+        }
 
-            // 配列アクセスは unimplemented
-            if indirection_text.contains('[') {
-                return Err(UroboroSQLFmtError::Unimplemented(format!(
-                    "visit_columnref(): array access is not implemented\n{}",
-                    pg_error_annotation_from_cursor(cursor, src)
-                )));
+        // 残りの要素
+        // cursor -> a_expr | Comma
+        while cursor.goto_next_sibling() {
+            match cursor.node().kind() {
+                SyntaxKind::Comma => {}
+                SyntaxKind::a_expr => {
+                    exprs.push(self.visit_a_expr(cursor, src)?.to_aligned());
+                }
+                _ => {
+                    return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                        "visit_expr_list(): Unexpected syntax. node: {}\n{}",
+                        cursor.node().kind(),
+                        pg_error_annotation_from_cursor(cursor, src)
+                    )));
+                }
             }
-
-            // indirection にあたるテキストから空白文字を除去し、そのまま追加している
-            let whitespace_removed = indirection_text
-                .chars()
-                .filter(|c| !c.is_whitespace())
-                .collect::<String>();
-            columnref_text.push_str(&whitespace_removed);
         }
 
-        // アスタリスクが含まれる場合はAsteriskExprに変換する
-        let expr = if columnref_text.contains('*') {
-            AsteriskExpr::new(
-                convert_identifier_case(&columnref_text),
-                cursor.node().range().into(),
-            )
-            .into()
-        } else {
-            PrimaryExpr::new(
-                convert_identifier_case(&columnref_text),
-                cursor.node().range().into(),
-            )
-            .into()
-        };
-
         cursor.goto_parent();
-        pg_ensure_kind(cursor, SyntaxKind::columnref, src)?;
+        // cursor -> expr_list
 
-        Ok(expr)
+        Ok(exprs)
     }
 }
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs
index 567c9179..8ae7fe2c 100644
--- a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs
@@ -1,9 +1,13 @@
+mod arithmetic;
+mod comparison;
+mod in_expr;
+mod is_expr;
+mod logical;
+mod unary;
+
 use postgresql_cst_parser::{syntax_kind::SyntaxKind, tree_sitter::TreeCursor};
 
-use crate::{
-    cst::{unary::UnaryExpr, Expr, Location},
-    error::UroboroSQLFmtError,
-};
+use crate::{cst::Expr, error::UroboroSQLFmtError};
 
 use super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
 
@@ -18,7 +22,6 @@ use super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
  * - '+' a_expr
  * - '-' a_expr
  * - NOT a_expr
- * - NOT_LA a_expr
  * - qual_Op a_expr
  *
  * 3. 二項算術演算子
@@ -104,7 +107,7 @@ use super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
  */
 
 impl Visitor {
-    // 呼び出した後、cursorは a_expr を指している
+    /// 呼び出した後、cursorは a_expr を指している
     pub(crate) fn visit_a_expr(
         &mut self,
         cursor: &mut TreeCursor,
@@ -113,8 +116,29 @@ impl Visitor {
         cursor.goto_first_child();
 
         // cursor -> c_expr | DEFAULT | Plus | Minus | NOT | qual_Op | a_expr | UNIQUE
-        let expr = match cursor.node().kind() {
-            SyntaxKind::c_expr => self.visit_c_expr(cursor, src)?,
+        let expr = self.handle_a_expr_inner(cursor, src)?;
+
+        // cursor -> (last_node)
+        assert!(!cursor.goto_next_sibling());
+
+        cursor.goto_parent();
+        // cursor -> a_expr (parent)
+        pg_ensure_kind(cursor, SyntaxKind::a_expr, src)?;
+
+        Ok(expr)
+    }
+
+    /// a_expr の 子ノードを走査する
+    /// 呼出し時、cursor は a_expr の最初の子ノードを指している
+    /// 呼出し後、cursor は a_expr の最後の子ノードを指している
+    fn handle_a_expr_inner(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // cursor -> c_expr | DEFAULT | Plus | Minus | NOT | qual_Op | a_expr | UNIQUE
+        match cursor.node().kind() {
+            SyntaxKind::c_expr => self.visit_c_expr(cursor, src),
             SyntaxKind::DEFAULT => {
                 return Err(UroboroSQLFmtError::Unimplemented(
                     "visit_a_expr(): DEFAULT is not implemented".to_string(),
@@ -122,35 +146,32 @@ impl Visitor {
             }
             // Unary Expression
             SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::NOT | SyntaxKind::qual_Op => {
-                // op a_expr
-
-                // cursor -> op
-                let operator = cursor.node().text();
-                let mut loc = Location::from(cursor.node().range());
+                self.handle_unary_expr_nodes(cursor, src)
+            }
+            SyntaxKind::a_expr => {
+                // cursor -> a_expr
+                let lhs = self.visit_a_expr(cursor, src)?;
 
                 cursor.goto_next_sibling();
-                // cursor -> a_expr
+                // cursor -> 算術演算子 | 比較演算子 | 論理演算子 | TYPECAST | COLLATE | AT | LIKE | ILIKE | SIMILAR | IS | ISNULL | NOTNULL | IN | サブクエリ
 
-                let operand = self.visit_a_expr(cursor, src)?;
-                loc.append(operand.loc());
+                let expr = self.handle_nodes_after_a_expr(cursor, src, lhs)?;
 
-                Expr::Unary(Box::new(UnaryExpr::new(operator, operand, loc)))
+                Ok(expr)
             }
-            SyntaxKind::NOT_LA => {
-                // NOT キーワードのうち、 NOT LIKE, NOT ILIKE, NOT SIMILAR TO 等のケース
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_a_expr(): Not_LA is not implemented".to_string(),
-                ));
-            }
-            SyntaxKind::a_expr => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_a_expr(): a_expr is not implemented".to_string(),
-                ))
+            SyntaxKind::row => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
             }
             SyntaxKind::UNIQUE => {
-                return Err(UroboroSQLFmtError::Unimplemented(
-                    "visit_a_expr(): UNIQUE is not implemented".to_string(),
-                ))
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
             }
             _ => {
                 return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
@@ -159,11 +180,139 @@ impl Visitor {
                     pg_error_annotation_from_cursor(cursor, src)
                 )));
             }
-        };
+        }
+    }
 
-        cursor.goto_parent();
-        pg_ensure_kind(cursor, SyntaxKind::a_expr, src)?;
+    /// a_expr の子ノードのうち、最初に a_expr が現れた後のノードを走査する
+    /// 呼出時、cursor は a_expr の次のノードを指している
+    /// 呼出後、cursor は a_expr の最後の子ノードを指している
+    fn handle_nodes_after_a_expr(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // cursor -> 算術演算子 | 比較演算子 | 論理演算子 | TYPECAST | COLLATE | AT | LIKE | ILIKE | SIMILAR | IS | ISNULL | NOTNULL | IN | サブクエリ
+        match cursor.node().kind() {
+            // 算術演算
+            SyntaxKind::Plus
+            | SyntaxKind::Minus
+            | SyntaxKind::Star
+            | SyntaxKind::Slash
+            | SyntaxKind::Percent
+            | SyntaxKind::Caret => self.handle_arithmetic_binary_expr_nodes(cursor, src, lhs),
+            // 比較演算
+            SyntaxKind::Less
+            | SyntaxKind::Greater
+            | SyntaxKind::Equals
+            | SyntaxKind::LESS_EQUALS
+            | SyntaxKind::GREATER_EQUALS
+            | SyntaxKind::NOT_EQUALS
+            | SyntaxKind::qual_Op => self.handle_comparison_expr_nodes(cursor, src, lhs),
+            // 論理
+            SyntaxKind::AND | SyntaxKind::OR => self.handle_logical_expr_nodes(cursor, src, lhs),
+            // 型変換
+            SyntaxKind::TYPECAST => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            // 属性関連
+            SyntaxKind::COLLATE | SyntaxKind::AT => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            // パターンマッチング
+            SyntaxKind::LIKE | SyntaxKind::ILIKE | SyntaxKind::SIMILAR => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            // IS
+            SyntaxKind::IS => self.handle_is_expr_nodes(cursor, src, lhs),
+            SyntaxKind::ISNULL | SyntaxKind::NOTNULL => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            // IN
+            SyntaxKind::IN_P => {
+                // IN_P in_expr
+                let aligned = self.handle_in_expr_nodes(cursor, src, lhs, None)?;
+                Ok(Expr::Aligned(Box::new(aligned)))
+            }
+            // BETWEEN
+            SyntaxKind::BETWEEN => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            // サブクエリ
+            SyntaxKind::subquery_Op => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_a_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::NOT_LA => {
+                // NOT キーワードのうち、 後に BETWEEN, IN, LIKE, ILIKE, SIMILAR のいずれかが続くケース
+                // cursor -> NOT_LA
+                let not_text = cursor.node().text();
 
-        Ok(expr)
+                cursor.goto_next_sibling();
+                // cursor -> BETWEEN | IN | LIKE | ILIKE | SIMILAR
+
+                match cursor.node().kind() {
+                    SyntaxKind::BETWEEN => {
+                        // NOT_LA BETWEEN
+                        return Err(UroboroSQLFmtError::Unimplemented(format!(
+                            "visit_a_expr(): {} is not implemented.\n{}",
+                            cursor.node().kind(),
+                            pg_error_annotation_from_cursor(cursor, src)
+                        )));
+                    }
+                    SyntaxKind::IN_P => {
+                        // NOT_LA IN_P
+                        let aligned =
+                            self.handle_in_expr_nodes(cursor, src, lhs, Some(not_text))?;
+                        Ok(Expr::Aligned(Box::new(aligned)))
+                    }
+                    SyntaxKind::LIKE | SyntaxKind::ILIKE | SyntaxKind::SIMILAR => {
+                        // NOT_LA (LIKE | ILIKE | SIMILAR)
+                        return Err(UroboroSQLFmtError::Unimplemented(format!(
+                            "visit_a_expr(): {} is not implemented.\n{}",
+                            cursor.node().kind(),
+                            pg_error_annotation_from_cursor(cursor, src)
+                        )));
+                    }
+                    _ => {
+                        return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                            "visit_a_expr(): Unexpected syntax. node: {}\n{}",
+                            cursor.node().kind(),
+                            pg_error_annotation_from_cursor(cursor, src)
+                        )));
+                    }
+                }
+            }
+            _ => {
+                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                    "visit_a_expr(): Unexpected syntax. node: {}\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+        }
     }
 }
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/arithmetic.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/arithmetic.rs
new file mode 100644
index 00000000..7e5f05d4
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/arithmetic.rs
@@ -0,0 +1,42 @@
+use postgresql_cst_parser::tree_sitter::TreeCursor;
+
+use crate::{
+    cst::{Expr, ExprSeq, PrimaryExpr, PrimaryExprKind},
+    error::UroboroSQLFmtError,
+};
+
+use super::Visitor;
+
+impl Visitor {
+    /// 左辺の式を受け取り、算術の二項演算にあたるノード群を走査する
+    ///
+    /// 呼出時、 cursor は演算子を指している
+    /// 呼出後、 cursor は 最後の a_expr を指している
+    ///
+    pub fn handle_arithmetic_binary_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // a_expr op a_expr
+        // ^      ^  ^
+        // lhs    │  │
+        //        │  └ 呼出後
+        //        └ 呼出前
+
+        // cursor -> op
+        let op_node = cursor.node();
+
+        // cursor -> a_expr
+        cursor.goto_next_sibling();
+        let rhs = self.visit_a_expr(cursor, src)?;
+
+        // 演算子を PrimaryExpr として扱う
+        let op = PrimaryExpr::with_pg_node(op_node, PrimaryExprKind::Expr)?;
+
+        // ExprSeq として返す
+        let seq = ExprSeq::new(&[lhs, op.into(), rhs]);
+        Ok(Expr::ExprSeq(Box::new(seq)))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/comparison.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/comparison.rs
new file mode 100644
index 00000000..1ff00ebe
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/comparison.rs
@@ -0,0 +1,48 @@
+use postgresql_cst_parser::tree_sitter::TreeCursor;
+
+use crate::{
+    cst::{AlignedExpr, Expr},
+    error::UroboroSQLFmtError,
+    CONFIG,
+};
+
+use super::Visitor;
+
+impl Visitor {
+    /// 左辺の式を受け取り、比較演算にあたるノード群を走査する
+    ///
+    /// 呼出時、 cursor は演算子を指している
+    /// 呼出後、 cursor は 最後の a_expr を指している
+    ///
+    pub fn handle_comparison_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // a_expr op a_expr
+        // ^      ^  ^
+        // lhs    │  │
+        //        │  └ 呼出後
+        //        └ 呼出前
+
+        // cursor -> op
+        let op_node = cursor.node();
+
+        // unify_not_equalがtrueの場合は <> を != に統一する
+        let op_str = if CONFIG.read().unwrap().unify_not_equal && op_node.text() == "<>" {
+            "!=".to_string()
+        } else {
+            op_node.text().to_string()
+        };
+
+        // cursor -> a_expr
+        cursor.goto_next_sibling();
+        let rhs = self.visit_a_expr(cursor, src)?;
+
+        let mut aligned = AlignedExpr::new(lhs);
+        aligned.add_rhs(Some(op_str), rhs);
+
+        Ok(Expr::Aligned(Box::new(aligned)))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/in_expr.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/in_expr.rs
new file mode 100644
index 00000000..389aaaa9
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/in_expr.rs
@@ -0,0 +1,132 @@
+use postgresql_cst_parser::{syntax_kind::SyntaxKind, tree_sitter::TreeCursor};
+
+use crate::{
+    cst::{AlignedExpr, ColumnList, Expr, Location},
+    error::UroboroSQLFmtError,
+    util::convert_keyword_case,
+};
+
+use super::super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
+
+impl Visitor {
+    /// 左辺の式とオプションのNOTキーワードを受け取り、IN述語にあたるノード群を走査する
+    ///
+    /// 呼出時、 cursor は IN_P を指している
+    /// 呼出後、cursor は in_expr （同階層の最後の要素）を指している
+    ///
+    pub fn handle_in_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+        not_keyword: Option<&str>,
+    ) -> Result<AlignedExpr, UroboroSQLFmtError> {
+        // a_expr NOT? IN_P in_expr
+        // ^      ^    ^    ^
+        // lhs    |    │    └ 呼出後
+        //        |    └ 呼出時
+        //        |
+        //        └ not_keyword
+
+        // cursor -> IN_P
+        pg_ensure_kind(cursor, SyntaxKind::IN_P, src)?;
+
+        // op_text: NOT IN or IN
+        let op_text = if let Some(not_keyword) = not_keyword {
+            let mut op_text = String::from(not_keyword);
+            op_text.push(' ');
+
+            op_text.push_str(cursor.node().text());
+            op_text
+        } else {
+            cursor.node().text().to_string()
+        };
+
+        // TODO: バインドパラメータ対応
+
+        cursor.goto_next_sibling();
+        // cursor -> in_expr
+        let rhs = self.visit_pg_in_expr(cursor, src)?;
+
+        let mut aligned = AlignedExpr::new(lhs);
+        aligned.add_rhs(Some(convert_keyword_case(&op_text)), rhs);
+
+        Ok(aligned)
+    }
+
+    /// in_expr を Expr に変換する
+    /// 呼出し後、cursorは in_expr を指している
+    ///
+    fn visit_pg_in_expr(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // in_expr
+        //   - select_with_parens
+        //   - '(' expr_list ')'
+
+        // cursor -> in_expr
+        cursor.goto_first_child();
+        // cursor -> select_with_parens | '('
+
+        match cursor.node().kind() {
+            SyntaxKind::select_with_parens => {
+                // Expr::Sub を返す
+                // Ok(Expr::Sub(Box::new(subquery)))
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_in_expr(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::LParen => {
+                // Expr::ColumnList を返す
+                // '(' expr_list ')' を ColumnList に変換する
+                let column_list = self.visit_parenthesized_expr_list(cursor, src)?;
+                cursor.goto_parent();
+                // cursor -> in_expr
+
+                Ok(Expr::ColumnList(Box::new(column_list)))
+            }
+            _ => {
+                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                    "visit_in_expr(): Unexpected syntax. node: {}\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+        }
+    }
+
+    /// 括弧で囲まれた式リストを処理するメソッド
+    fn visit_parenthesized_expr_list(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<ColumnList, UroboroSQLFmtError> {
+        // TODO: コメント処理
+
+        // cursor -> '('
+        pg_ensure_kind(cursor, SyntaxKind::LParen, src)?;
+
+        cursor.goto_next_sibling();
+        // cursor -> expr_list
+
+        let exprs = self.visit_expr_list(cursor, src)?;
+
+        cursor.goto_next_sibling();
+        // cursor -> ')'
+
+        pg_ensure_kind(cursor, SyntaxKind::RParen, src)?;
+
+        let parent = cursor
+            .node()
+            .parent()
+            .expect("visit_parenthesized_expr_list(): parent not found");
+        let loc = Location::from(parent.range());
+
+        // TODO: コメント処理
+        Ok(ColumnList::new(exprs, loc, vec![]))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/is_expr.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/is_expr.rs
new file mode 100644
index 00000000..1d5482e1
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/is_expr.rs
@@ -0,0 +1,111 @@
+use postgresql_cst_parser::{syntax_kind::SyntaxKind, tree_sitter::TreeCursor};
+
+use crate::{
+    cst::{unary::UnaryExpr, AlignedExpr, Expr, Location, PrimaryExpr, PrimaryExprKind},
+    error::UroboroSQLFmtError,
+    util::convert_keyword_case,
+};
+
+use super::super::pg_error_annotation_from_cursor;
+use super::Visitor;
+
+impl Visitor {
+    /// 左辺の式を受け取り、IS述語にあたるノード群を走査する
+    ///
+    /// 呼出時、 cursor は IS キーワードを指している
+    /// 呼出後、 cursor は 最後のノード（NULL_P, TRUE_P, FALSE_P など）を指している
+    ///
+    pub fn handle_is_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // a_expr IS [NOT] NULL/TRUE/FALSE/...
+        // ^      ^        ^
+        // lhs    │        │
+        //        │        └ 呼出後
+        //        │
+        //        └ 呼出前
+
+        // cursor -> IS
+        let op = convert_keyword_case(cursor.node().text());
+
+        cursor.goto_next_sibling();
+        // cursor -> NOT?
+
+        let not_node = if cursor.node().kind() == SyntaxKind::NOT {
+            let not_node = cursor.node();
+            cursor.goto_next_sibling();
+
+            Some(not_node)
+        } else {
+            None
+        };
+
+        // cursor -> NULL_P | TRUE_P | FALSE_P | DISTINCT | UNKNOWN | DOCUMENT_P | NORMALIZED | unicode_normal_form | json_predicate_type_constraint
+        let last_expr = match cursor.node().kind() {
+            SyntaxKind::NULL_P | SyntaxKind::TRUE_P | SyntaxKind::FALSE_P => {
+                let primary = PrimaryExpr::with_pg_node(cursor.node(), PrimaryExprKind::Keyword)?;
+                Expr::Primary(Box::new(primary))
+            }
+            SyntaxKind::DISTINCT => {
+                // IS NOT? DISTINCT FROM
+
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "handle_is_expr_nodes(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::UNKNOWN => {
+                // IS NOT? UNKNOWN
+
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "handle_is_expr_nodes(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::DOCUMENT_P
+            | SyntaxKind::NORMALIZED
+            | SyntaxKind::unicode_normal_form
+            | SyntaxKind::json_predicate_type_constraint => {
+                // - IS NOT? DOCUMENT_P
+                // - IS NOT? unicode_normal_form? NORMALIZED
+                // - IS NOT? json_predicate_type_constraint json_key_uniqueness_constraint_opt
+
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "handle_is_expr_nodes(): {} is not implemented.\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            _ => {
+                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                    "handle_is_expr_nodes(): Unexpected syntax. node: {}\n{}",
+                    cursor.node().kind(),
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+        };
+
+        // AlignedExpr の右辺
+        // NOT がある場合は NOT を演算子とした UnaryExpr にする
+        let rhs = if let Some(not_node) = not_node {
+            let mut loc = Location::from(not_node.range());
+            loc.append(last_expr.loc());
+
+            let unary = UnaryExpr::new(not_node.text(), last_expr, loc);
+            Expr::Unary(Box::new(unary))
+        } else {
+            last_expr
+        };
+
+        // IS を演算子とした AlignedExpr
+        let mut aligned = AlignedExpr::new(lhs);
+        aligned.add_rhs(Some(op), rhs);
+
+        Ok(Expr::Aligned(Box::new(aligned)))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/logical.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/logical.rs
new file mode 100644
index 00000000..2cc176c7
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/logical.rs
@@ -0,0 +1,66 @@
+use postgresql_cst_parser::tree_sitter::TreeCursor;
+
+use crate::{
+    cst::{Comment, Expr, SeparatedLines},
+    error::UroboroSQLFmtError,
+    util::convert_keyword_case,
+};
+
+use super::Visitor;
+
+impl Visitor {
+    /// 左辺の式を受け取り、論理演算（AND, OR）にあたるノード群を走査する
+    ///
+    /// 呼出時、 cursor は演算子を指している
+    /// 呼出後、 cursor は 最後の a_expr を指している
+    ///
+    pub fn handle_logical_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+        lhs: Expr,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // a_expr AND/OR a_expr
+        // ^      ^      ^
+        // lhs    │      │
+        //        │      └ 呼出後
+        //        └ 呼出前
+
+        let mut boolean_expr = SeparatedLines::new();
+
+        // 左辺がBooleanの場合は初期化したBooleanExprを左辺で上書き
+        match lhs {
+            Expr::Boolean(boolean) => boolean_expr = *boolean,
+            _ => boolean_expr.add_expr(lhs.to_aligned(), None, vec![]),
+        }
+
+        // cursor -> COMMENT | op
+
+        while cursor.node().is_comment() {
+            boolean_expr.add_comment_to_child(Comment::pg_new(cursor.node()))?;
+            cursor.goto_next_sibling();
+        }
+
+        let sep = convert_keyword_case(cursor.node().text());
+
+        cursor.goto_next_sibling();
+        // cursor -> _expression
+
+        let mut comments = vec![];
+        while cursor.node().is_comment() {
+            comments.push(Comment::pg_new(cursor.node()));
+            cursor.goto_next_sibling();
+        }
+
+        let right = self.visit_a_expr(cursor, src)?;
+
+        if let Expr::Boolean(boolean) = right {
+            // 右辺がbooleanの場合はマージ処理を行う
+            boolean_expr.merge_boolean_expr(sep, *boolean);
+        } else {
+            boolean_expr.add_expr(right.to_aligned(), Some(sep), comments);
+        }
+
+        Ok(Expr::Boolean(Box::new(boolean_expr)))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/unary.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/unary.rs
new file mode 100644
index 00000000..e529b0e3
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr/unary.rs
@@ -0,0 +1,41 @@
+use postgresql_cst_parser::tree_sitter::TreeCursor;
+
+use crate::{
+    cst::{unary::UnaryExpr, Expr, Location},
+    error::UroboroSQLFmtError,
+};
+
+use super::Visitor;
+
+impl Visitor {
+    /// 単項演算子の処理
+    ///
+    /// 呼出時、 cursor は演算子ノードを指している
+    /// 呼出後、 cursor は a_expr を指している
+    ///
+    pub fn handle_unary_expr_nodes(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // op a_expr
+        // ^  ^
+        // │  │
+        // │  └ 呼出後
+        // └ 呼出前
+
+        // cursor -> op
+        let operator = cursor.node().text();
+        let mut loc = Location::from(cursor.node().range());
+
+        cursor.goto_next_sibling();
+        // cursor -> a_expr
+
+        let operand = self.visit_a_expr(cursor, src)?;
+        loc.append(operand.loc());
+
+        Ok(Expr::Unary(Box::new(UnaryExpr::new(
+            operator, operand, loc,
+        ))))
+    }
+}
diff --git a/crates/uroborosql-fmt/src/new_visitor/pg_expr/c_expr.rs b/crates/uroborosql-fmt/src/new_visitor/pg_expr/c_expr.rs
new file mode 100644
index 00000000..4c407c55
--- /dev/null
+++ b/crates/uroborosql-fmt/src/new_visitor/pg_expr/c_expr.rs
@@ -0,0 +1,304 @@
+use postgresql_cst_parser::{syntax_kind::SyntaxKind, tree_sitter::TreeCursor};
+
+use crate::{
+    cst::{AsteriskExpr, Comment, Expr, Location, ParenExpr, PrimaryExpr, PrimaryExprKind},
+    error::UroboroSQLFmtError,
+    util::convert_identifier_case,
+};
+
+use super::{pg_ensure_kind, pg_error_annotation_from_cursor, Visitor};
+
+/*
+ * c_expr の構造
+ *
+ * - columnref
+ * - AexprConst
+ * - PARAM opt_indirection
+ * - '(' a_expr ')' opt_indirection
+ * - case_expr
+ * - func_expr
+ * - select_with_parens
+ * - select_with_parens indirection
+ * - EXISTS select_with_parens
+ * - ARRAY select_with_parens
+ * - ARRAY array_expr
+ * - explicit_row
+ * - implicit_row
+ * - GROUPING '(' expr_list ')'
+ */
+
+impl Visitor {
+    pub fn visit_c_expr(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        cursor.goto_first_child();
+
+        let expr = match cursor.node().kind() {
+            SyntaxKind::columnref => self.visit_columnref(cursor, src)?,
+            SyntaxKind::AexprConst => self.visit_aexpr_const(cursor, src)?,
+            SyntaxKind::PARAM => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): PARAM is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::LParen => {
+                // '(' a_expr ')' opt_indirection
+
+                // cursor -> '('
+                pg_ensure_kind(cursor, SyntaxKind::LParen, src)?;
+
+                cursor.goto_next_sibling();
+                // cursor -> comments?
+
+                let mut start_comment_buf = vec![];
+                while cursor.node().is_comment() {
+                    let comment = Comment::pg_new(cursor.node());
+                    start_comment_buf.push(comment);
+                    cursor.goto_next_sibling();
+                }
+
+                // cursor -> expr
+                pg_ensure_kind(cursor, SyntaxKind::a_expr, src)?;
+                let expr = self.visit_a_expr(cursor, src)?;
+                // TODO: remove_redundant_nest
+
+                cursor.goto_next_sibling();
+
+                // cursor -> comments?
+                let mut end_comment_buf = vec![];
+                while cursor.node().is_comment() {
+                    let comment = Comment::pg_new(cursor.node());
+                    end_comment_buf.push(comment);
+                    cursor.goto_next_sibling();
+                }
+
+                // cursor -> ")"
+                pg_ensure_kind(cursor, SyntaxKind::RParen, src)?;
+
+                // 親(c_expr) の location を設定
+                // 親が無いことはありえないので、parent() の返り値が None の場合は panic する
+                let parent = cursor
+                    .node()
+                    .parent()
+                    .expect("visit_c_expr(): parent is None");
+
+                // 親の location を設定
+                let mut paren_expr = ParenExpr::new(expr, parent.range().into());
+
+                // 開きかっこと式の間にあるコメントを追加
+                for comment in start_comment_buf {
+                    paren_expr.add_start_comment(comment);
+                }
+
+                // 式から閉じかっこの間にあるコメントを追加
+                for comment in end_comment_buf {
+                    paren_expr.add_end_comment(comment);
+                }
+
+                Expr::ParenExpr(Box::new(paren_expr))
+            }
+            SyntaxKind::case_expr => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): case_expr is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::func_expr => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): func_expr is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::select_with_parens => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): select_with_parens is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::EXISTS => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): EXISTS is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::ARRAY => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): ARRAY is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::explicit_row => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): explicit_row is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::implicit_row => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): implicit_row is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            SyntaxKind::GROUPING => {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_c_expr(): GROUPING is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+            _ => {
+                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                    "visit_c_expr(): unexpected syntaxkind\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )))
+            }
+        };
+
+        cursor.goto_parent();
+        pg_ensure_kind(cursor, SyntaxKind::c_expr, src)?;
+
+        Ok(expr)
+    }
+
+    // AexprConst
+    fn visit_aexpr_const(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // AexprConst
+        // - Iconst
+        //   - ICONST
+        // - FCONST
+        // - Sconst
+        //   - SCONST
+        // - BCONST
+        // - XCONST
+        // - func_name Sconst
+        // - func_name '(' func_arg_list opt_sort_clause ')' Sconst
+        // - ConstTypename Sconst
+        // - ConstInterval Sconst opt_interval
+        // - ConstInterval '(' Iconst ')' Sconst
+        // - TRUE_P
+        // - FALSE_P
+        // - NULL_P
+
+        cursor.goto_first_child();
+        let expr = match cursor.node().kind() {
+            SyntaxKind::Iconst
+            | SyntaxKind::FCONST
+            | SyntaxKind::Sconst
+            | SyntaxKind::BCONST
+            | SyntaxKind::XCONST => {
+                PrimaryExpr::with_pg_node(cursor.node(), PrimaryExprKind::Expr)?.into()
+            }
+            SyntaxKind::func_name => {
+                // func_name Sconst
+                // func_name '(' func_arg_list opt_sort_clause ')' Sconst
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_aexpr_const(): func_name is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::ConstTypename => {
+                // ConstTypename Sconst
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_aexpr_const(): ConstTypename is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::ConstInterval => {
+                // ConstInterval Sconst opt_interval
+                // ConstInterval '(' Iconst ')' Sconst
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_aexpr_const(): ConstInterval is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+            SyntaxKind::TRUE_P | SyntaxKind::FALSE_P | SyntaxKind::NULL_P => {
+                PrimaryExpr::with_pg_node(cursor.node(), PrimaryExprKind::Keyword)?.into()
+            }
+            _ => {
+                return Err(UroboroSQLFmtError::UnexpectedSyntax(format!(
+                    "visit_aexpr_const(): unexpected node kind\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+        };
+
+        cursor.goto_parent();
+        pg_ensure_kind(cursor, SyntaxKind::AexprConst, src)?;
+
+        Ok(expr)
+    }
+
+    fn visit_columnref(
+        &mut self,
+        cursor: &mut TreeCursor,
+        src: &str,
+    ) -> Result<Expr, UroboroSQLFmtError> {
+        // columnref
+        // - ColId
+        // - ColId indirection
+        //   - e.g.: `a.field`, `a.field[1]`
+
+        // cursor -> ColId (必ず存在する)
+        cursor.goto_first_child();
+
+        pg_ensure_kind(cursor, SyntaxKind::ColId, src)?;
+        let mut columnref_text = cursor.node().text().to_string();
+
+        if cursor.goto_next_sibling() {
+            // cursor -> indirection
+            pg_ensure_kind(cursor, SyntaxKind::indirection, src)?;
+
+            // indirection
+            // - indirection_el
+            //    - `.` attr_name
+            //    - `.` `*`
+            //    - `[` a_expr `]`
+            //    - `[` opt_slice_bound `:` opt_slice_bound `]`
+            //
+            // indirection はフラット化されている: https://github.com/future-architect/postgresql-cst-parser/pull/7
+
+            let indirection_text = cursor.node().text();
+
+            // 配列アクセスは unimplemented
+            if indirection_text.contains('[') {
+                return Err(UroboroSQLFmtError::Unimplemented(format!(
+                    "visit_columnref(): array access is not implemented\n{}",
+                    pg_error_annotation_from_cursor(cursor, src)
+                )));
+            }
+
+            // indirection にあたるテキストから空白文字を除去し、そのまま追加している
+            let whitespace_removed = indirection_text
+                .chars()
+                .filter(|c| !c.is_whitespace())
+                .collect::<String>();
+            columnref_text.push_str(&whitespace_removed);
+        }
+
+        // アスタリスクが含まれる場合はAsteriskExprに変換する
+        let expr = if columnref_text.contains('*') {
+            AsteriskExpr::new(
+                convert_identifier_case(&columnref_text),
+                cursor.node().range().into(),
+            )
+            .into()
+        } else {
+            PrimaryExpr::new(
+                convert_identifier_case(&columnref_text),
+                cursor.node().range().into(),
+            )
+            .into()
+        };
+
+        cursor.goto_parent();
+        pg_ensure_kind(cursor, SyntaxKind::columnref, src)?;
+
+        Ok(expr)
+    }
+}
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/020_binary_arith_expr.sql b/crates/uroborosql-fmt/test_normal_cases/dst/020_binary_arith_expr.sql
new file mode 100644
index 00000000..8efbb346
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/020_binary_arith_expr.sql
@@ -0,0 +1,10 @@
+select
+	1	+	2
+;
+select
+	1	+	2	+	3
+;
+select
+	1	+	2
+,	1	+	2	+	3
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/021_binary_comp_expr.sql b/crates/uroborosql-fmt/test_normal_cases/dst/021_binary_comp_expr.sql
new file mode 100644
index 00000000..297543a7
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/021_binary_comp_expr.sql
@@ -0,0 +1,52 @@
+select
+	1	=	1
+;
+select
+	1	<	2
+;
+select
+	2	>	1
+;
+select
+	1	<=	1
+;
+select
+	1	>=	1
+;
+select
+	1	!=	2
+;
+select
+	1	!=	2
+;
+-- qual_Op
+select
+	'a'	~	'a'
+;
+select
+	'a'	!~	'b'
+;
+select
+	'A'	~*	'a'
+;
+select
+	'A'	!~*	'b'
+;
+select
+	1	=	1
+,	2	>	1
+,	3	!=	4
+;
+select
+	1	=	1		as	eq
+,	2	>	1		as	gt
+,	1	<	2		as	lt
+,	1	<=	1		as	lte
+,	1	>=	1		as	gte
+,	1	!=	2		as	neq
+,	1	!=	2		as	neq2
+,	'abc'	~	'a'	as	regex_match
+,	'abc'	!~	'd'	as	regex_not_match
+,	'ABC'	~*	'a'	as	regex_match_case_insensitive
+,	'ABC'	!~*	'd'	as	regex_not_match_case_insensitive
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/022_paren_c_expr.sql b/crates/uroborosql-fmt/test_normal_cases/dst/022_paren_c_expr.sql
new file mode 100644
index 00000000..bd620a22
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/022_paren_c_expr.sql
@@ -0,0 +1,6 @@
+select
+	(1	+	2)	as	a
+;
+select
+	(1	+	(2	+	3))	as	b
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/023_comp_is.sql b/crates/uroborosql-fmt/test_normal_cases/dst/023_comp_is.sql
new file mode 100644
index 00000000..4bf8f412
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/023_comp_is.sql
@@ -0,0 +1,15 @@
+select
+	t.t	is	true
+;
+select
+	t.f	is	false
+;
+select
+	t.f	is	not	true
+;
+select
+	t.n	is	null
+;
+select
+	t.t	is	not	null
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/024_comp_in.sql b/crates/uroborosql-fmt/test_normal_cases/dst/024_comp_in.sql
new file mode 100644
index 00000000..0cb37c49
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/024_comp_in.sql
@@ -0,0 +1,6 @@
+select
+	t.ab	in	('A', 'B')
+;
+select
+	t.bc	not in	('D', 'E')
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql b/crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql
new file mode 100644
index 00000000..79613d8c
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql
@@ -0,0 +1,24 @@
+select
+		tab1.num			=		1
+and	tab1.nuuuuuuuuuuuum	=		2
+and	tab1.a				=		3
+and	not	(tab1.b	=	5)
+and	tab.t				is		true
+and	tab.f				is		not	false
+and	tab.n				is		null
+and	tbl.ab				in		('A', 'B')
+and	tbl.bc				not in	('D', 'E')
+
+;
+select
+		tab1.num			=		1
+or	tab1.nuuuuuuuuuuuum	=		2
+or	tab1.a				=		3
+or	not	(tab1.b	=	5)
+or	tab.t				is		true
+or	tab.f				is		not	false
+or	tab.n				is		null
+or	tbl.ab				in		('A', 'B')
+or	tbl.bc				not in	('D', 'E')
+
+;
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/020_binary_arith_expr.sql b/crates/uroborosql-fmt/test_normal_cases/src/020_binary_arith_expr.sql
new file mode 100644
index 00000000..a803b420
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/020_binary_arith_expr.sql
@@ -0,0 +1,3 @@
+select 1 + 2;
+select 1 + 2 + 3;
+select 1 + 2, 1 + 2 + 3;
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/021_binary_comp_expr.sql b/crates/uroborosql-fmt/test_normal_cases/src/021_binary_comp_expr.sql
new file mode 100644
index 00000000..93d61875
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/021_binary_comp_expr.sql
@@ -0,0 +1,28 @@
+select 1 = 1;
+select 1 < 2;
+select 2 > 1;
+select 1 <= 1;
+select 1 >= 1;
+select 1 <> 2;
+select 1 != 2;
+
+-- qual_Op
+select 'a' ~ 'a';
+select 'a' !~ 'b';
+select 'A' ~* 'a';
+select 'A' !~* 'b';
+
+select 1 = 1, 2 > 1, 3 <> 4;
+
+select 
+1 = 1 as eq,
+2 > 1 as gt,
+1 < 2 as lt,
+1 <= 1 as lte,
+1 >= 1 as gte,
+1 <> 2 as neq,
+1 != 2 as neq2,
+'abc' ~ 'a' as regex_match,
+'abc' !~ 'd' as regex_not_match,
+'ABC' ~* 'a' as regex_match_case_insensitive,
+'ABC' !~* 'd' as regex_not_match_case_insensitive;
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/022_paren_c_expr.sql b/crates/uroborosql-fmt/test_normal_cases/src/022_paren_c_expr.sql
new file mode 100644
index 00000000..dde2edc6
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/022_paren_c_expr.sql
@@ -0,0 +1,2 @@
+select (1 + 2) as a;
+select (1+ (2+3)) as b;
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/023_comp_is.sql b/crates/uroborosql-fmt/test_normal_cases/src/023_comp_is.sql
new file mode 100644
index 00000000..458208f6
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/023_comp_is.sql
@@ -0,0 +1,14 @@
+select
+    t.t is true;
+
+select
+    t.f is false;
+
+select
+    t.f is not true;
+
+select
+    t.n is null;
+
+select
+    t.t is not null;
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/024_comp_in.sql b/crates/uroborosql-fmt/test_normal_cases/src/024_comp_in.sql
new file mode 100644
index 00000000..06418f2b
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/024_comp_in.sql
@@ -0,0 +1,5 @@
+select
+    t.ab in ('A', 'B');
+
+select
+    t.bc not in ('D', 'E');
diff --git a/crates/uroborosql-fmt/test_normal_cases/src/025_and_or.sql b/crates/uroborosql-fmt/test_normal_cases/src/025_and_or.sql
new file mode 100644
index 00000000..e0cf867e
--- /dev/null
+++ b/crates/uroborosql-fmt/test_normal_cases/src/025_and_or.sql
@@ -0,0 +1,19 @@
+select tab1.num = 1 and tab1.nuuuuuuuuuuuum = 2
+and tab1.a = 3
+and not (tab1.b = 5)
+and tab.t is true
+and tab.f is not false
+and tab.n is null
+and	tbl.ab	in ('A', 'B')
+and	tbl.bc	not in ('D', 'E')
+;
+
+select tab1.num = 1 or tab1.nuuuuuuuuuuuum = 2
+or tab1.a = 3
+or not (tab1.b = 5)
+or tab.t is true
+or tab.f is not false
+or tab.n is null
+or	tbl.ab	in ('A', 'B')
+or	tbl.bc	not in ('D', 'E')
+;
diff --git a/crates/uroborosql-fmt/testfiles/dst/select/unary.sql b/crates/uroborosql-fmt/testfiles/dst/select/unary.sql
index ba2f84a7..cb98e681 100644
--- a/crates/uroborosql-fmt/testfiles/dst/select/unary.sql
+++ b/crates/uroborosql-fmt/testfiles/dst/select/unary.sql
@@ -25,7 +25,7 @@ select
 select
 	|/25	as	square_root
 ;
--- select @-5 as absolute_value; : PostgreSQLでは、`@-5` と書くと `@-` が一つのトークンとして扱われるため無効なSQLになる
+-- select @-5 as absolute_value; : PostgreSQLでは、`@-5` と書くと `@-` が一つのトークンとして扱われるため無効なSQLになる
 select
 	@	-5	as	absolute_value
 ;


Reviews:
user: login: tanzaku
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T05:25:10Z
user: login: lemonadern
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T06:24:04Z
user: login: lemonadern
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T06:27:37Z
user: login: tanzaku
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T07:20:18Z
user: login: tanzaku
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T08:22:17Z

Review comments:
diff_hunk: @@ -122,35 +125,161 @@ impl Visitor {
             }
             // Unary Expression
             SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::NOT | SyntaxKind::qual_Op => {
-                // op a_expr
+                self.handle_unary_expr_nodes(cursor, src)?
+            }
+            SyntaxKind::a_expr => {
+                // cursor -> a_expr
+                let expr = self.visit_a_expr(cursor, src)?;
 
-                // cursor -> op
-                let operator = cursor.node().text();
-                let mut loc = Location::from(cursor.node().range());
+                // a_expr の 子供が 単一の a_expr のケース
+                if !cursor.goto_next_sibling() {
+                    cursor.goto_parent();
+                    return Ok(expr);
path: crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs
user: login: tanzaku
body: > a_expr の 子供が 単一の a_expr のケース

このルートに来るSQLが思いつかないのですが、どういうSQLでこのルートに来るのでしょうか。
created_at: 2025-03-11T04:33:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,24 @@
+select
+		tab1.num			=		1
+and	tab1.nuuuuuuuuuuuum	=		2
+and	tab1.a				=		3
+and	not	(tab1.b	=	5)
+and	tab.t				is		true
+and	tab.f				is		not	false
+and	tab.n				is		null
+and	tbl.ab				in		('A', 'B')
+and	tbl.bc				not in	('D', 'E')
+
+;
path: crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql
user: login: tanzaku
body: フォーマット結果に空行が入ってしまっているようです
created_at: 2025-03-11T05:14:43Z
author_association: COLLABORATOR
diff_hunk: @@ -122,35 +125,161 @@ impl Visitor {
             }
             // Unary Expression
             SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::NOT | SyntaxKind::qual_Op => {
-                // op a_expr
+                self.handle_unary_expr_nodes(cursor, src)?
+            }
+            SyntaxKind::a_expr => {
+                // cursor -> a_expr
+                let expr = self.visit_a_expr(cursor, src)?;
 
-                // cursor -> op
-                let operator = cursor.node().text();
-                let mut loc = Location::from(cursor.node().range());
+                // a_expr の 子供が 単一の a_expr のケース
+                if !cursor.goto_next_sibling() {
+                    cursor.goto_parent();
+                    return Ok(expr);
path: crates/uroborosql-fmt/src/new_visitor/pg_expr/a_expr.rs
user: login: lemonadern
body: 確認したところミスでした :bow: 
単に `goto_next_sibling` が必要なだけの場所だったので修正しました。

修正： https://github.com/future-architect/uroborosql-fmt/pull/133/commits/5d9155d6f93b0103d565a35c0c5dcf28263f2644
created_at: 2025-03-11T06:24:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,24 @@
+select
+		tab1.num			=		1
+and	tab1.nuuuuuuuuuuuum	=		2
+and	tab1.a				=		3
+and	not	(tab1.b	=	5)
+and	tab.t				is		true
+and	tab.f				is		not	false
+and	tab.n				is		null
+and	tbl.ab				in		('A', 'B')
+and	tbl.bc				not in	('D', 'E')
+
+;
path: crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql
user: login: lemonadern
body: tree-sitter 版のフォーマッタでも同じ挙動になるためこのフォーマット結果を利用しています。
原因は把握できていないのですが、 where 句ではなく select 句の body に AND, OR からなる式を書くとこのように空行が発生するようです。

created_at: 2025-03-11T06:27:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,24 @@
+select
+		tab1.num			=		1
+and	tab1.nuuuuuuuuuuuum	=		2
+and	tab1.a				=		3
+and	not	(tab1.b	=	5)
+and	tab.t				is		true
+and	tab.f				is		not	false
+and	tab.n				is		null
+and	tbl.ab				in		('A', 'B')
+and	tbl.bc				not in	('D', 'E')
+
+;
path: crates/uroborosql-fmt/test_normal_cases/dst/025_and_or.sql
user: login: tanzaku
body: なるほど、元々発生していたんですね。
通常はあまり select 句の body に AND, OR は書かないと思うので、一旦スルーしようと思います。
created_at: 2025-03-11T07:11:57Z
author_association: COLLABORATOR

PR comments:
user: login: tanzaku
created_at: 2025-03-11T05:25:27Z
body: > この 5. にあたるメソッドに handle_* という命名をしています。
> 呼び分けの意図として、これらのメソッドは 従来の visit_ なメソッドのように、呼出し後にカーソルが元のノードを指しているといった性質を満たしていないため、呼出し側で区別できるようにしたいという理由があります。
> 
> handle_ という prefix の良しあしも含め、意見を伺いたいです。

良いと思います 👍
関連して一点提案ですが、 visit_a_expr メソッドの以下の match 内部が肥大化してきており visit_a_expr メソッドの見通しが少し悪く感じるので、match 内部だけ `handle_a_expr_inner` のようなメソッドに切り出すのはどうでしょうか。

```rust
        // cursor -> c_expr | DEFAULT | Plus | Minus | NOT | qual_Op | a_expr | UNIQUE
        let expr = match cursor.node().kind() {
            ...
        };
```
author_association: COLLABORATOR
user: login: lemonadern
created_at: 2025-03-11T07:02:53Z
body: > 良いと思います 👍
関連して一点提案ですが、 visit_a_expr メソッドの以下の match 内部が肥大化してきており visit_a_expr メソッドの見通しが少し悪く感じるので、match 内部だけ handle_a_expr_inner のようなメソッドに切り出すのはどうでしょうか。

ありがとうございます！match 文を切り出すリファクタを入れました。

新たに次の二つのメソッドを増やしています。（1. が提案いただいたものです）
1. a_expr 内部をハンドリングする処理を切り出したメソッド（ `handle_a_expr_inner` ）
2. a_expr の最初の子が a_expr のとき、そのあとの分岐をする match 文を切り出したメソッド （`handle_nodes_after_a_expr`）

commit: https://github.com/future-architect/uroborosql-fmt/pull/133/commits/e4684304b8a30c2840e90d1c0ea5882ebea634aa

author_association: COLLABORATOR
user: login: tanzaku
created_at: 2025-03-11T07:27:42Z
body: ご対応ありがとうございます！

追加で postgresql-cst-parser に修正が入ったので、Cargo.lock のコミットもお願いします。

あと今回のMRとは関係ないのですが、 `cargo test` を実行すると改行コードの違いで `testfiles\dst\select\unary.sql` がgitの差分として出てきてしまうので `unary.sql` の更新もこのMRでお願いできますか？
author_association: COLLABORATOR
user: login: lemonadern
created_at: 2025-03-11T07:40:50Z
body: 本当ですね、`unary.sql` に差分出てましたね...

unary.sql の更新 と postgresql-cst-parser の更新に伴う Cargo.lock の更新、それぞれ実施しました。
author_association: COLLABORATOR