Java : DSL : SQL Query

[id:lethevert:20060202:p4]の続き。
Javaでもある程度のDSLが書けるんじゃない?ということで、ちょっとやってみました。
JavaDSLというと、JParsecを思い出すのだけど、それとはちょっと違う感じで。

サンプルコード

対象とした領域は、SQLの組み立てということにします。で、以下、テストコード。コメントに書いてあるのが実行結果です(見やすくなるように、改行とインデントを追加しています)。
"Expr where()"をオーバーライドして、条件を作成しています。比較演算子や論理演算子が前置になってしまうのは、2項演算子が定義できないJavaの制限です。これで2項演算子が定義できたら完璧なのにと思うけど、Lispも前置記法なことを考えれば、許容範囲かな。*1

import java.util.Date;

public class Main {
  public static void main(String[] args) {
    String sql;

    sql = new Query("tableA").sql();
    System.out.println(sql); // -> SELECT * FROM tableA

    sql = new Query("tableA", "tableB") {
        Expr where() {
          return
            and( isNull( col("colA")),
                 eq( col("colB"), num(1)),
                 ne( col("colC"), str("abc'de")),
                 or( le( date("2000/01/01"), col("colD")),
                     ge( date(new Date()), col("colE"))));
        }
      }.sql();
    System.out.println(sql);
    /* SELECT * FROM tableA, tableB
     *   WHERE (colA IS NULL AND colB = 1 AND colC != 'abc''de'
     *          AND (TO_DATE('2000/01/01', 'YYYY/MM/DD') <= colD
     *               OR TO_DATE('2006/02/03', 'YYYY/MM/DD') >= colE))
     */
  }
}

実装

本体は、Queryクラスです。内部クラスとしてExprとTermが定義されています。その他、条件式構築のために必要な関数をQueryクラスのメソッドとして必要な数だけ定義しています。
[id:lethevert:20060122:p2]で作ったFoldクラスを拡張して使っているので、まず、その定義から。

public abstract class Fold<A, B> {
  abstract A each(A s0, B s1);
  public A $(A ret, B[] sa) {
    for (int i=0; i<sa.length; ++i) {
      ret = each(ret, sa[i]);
    }
    return ret;
  }
  public A snd(A ret, B[] sa) {
    for (int i=1; i<sa.length; ++i) {
      ret = each(ret, sa[i]);
    }
    return ret;
  }
}

そして、本体のQuery.java

import java.util.Date;
import java.text.SimpleDateFormat;
import java.text.ParseException;

public class Query {
  abstract class Expr {
    abstract String sql();
    public String toString() { return sql();}
  }
  abstract class Term {
    abstract String sql();
    public String toString() { return sql();}
  }

  private static final JoinString commajoin = new JoinString(", ");
  private static final JoinString andjoin = new JoinString(" AND ");
  private static final JoinString orjoin = new JoinString(" OR ");

  private final String[] tables;
  public Query(String... tables) {
    assert tables.length > 0 : "error: short of tables";
    this.tables = tables;
  }
  public String sql() {
    return "SELECT * FROM "
      + commajoin.$(tables)
      + (null == where?"":" WHERE "+where.sql());
  }

  final Expr where = where();
  Expr where() { return null;}

  protected Term col(final String colnm) {
    return new Term() {
        String sql() { return colnm;}
      };
  }
  protected Term str(final String str) {
    return new Term() {
        String sql() { return "'" + str.replaceAll("'","''") + "'";}
      };
  }
  protected Term num(final int num) {
    return new Term() {
        String sql() { return Integer.toString(num);}
      };
  }
  protected Term num(final double num) {
    return new Term() {
        String sql() { return Double.toString(num);}
      };
  }
  protected Term date(final Date date) {
    return new Term() {
        String sql() {
          return "TO_DATE('"
            + new SimpleDateFormat("yyyy/MM/dd").format(date)
            + "', 'YYYY/MM/DD')";
        }
      };
  }
  protected Term date(final String date) {
    try {
      return date(new SimpleDateFormat("yyyy/MM/dd").parse(date));
    } catch(ParseException e) {
      assert false : e.toString();
      return null;
    }
  }

  protected Expr isNull(final Term t1) {
    return new Expr() {
        String sql() {
          return t1.sql() + " IS NULL";
        }
      };
  }
  protected Expr eq(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " = " + t2.sql();
        }
      };
  }
  protected Expr ne(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " != " + t2.sql();
        }
      };
  }
  protected Expr lt(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " < " + t2.sql();
        }
      };
  }
  protected Expr le(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " <= " + t2.sql();
        }
      };
  }
  protected Expr gt(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " > " + t2.sql();
        }
      };
  }
  protected Expr ge(final Term t1, final Term t2) {
    return new Expr() {
        String sql() {
          return t1.sql() + " >= " + t2.sql();
        }
      };
  }
  protected Expr and(final Expr... ts) {
    return new Expr() {
        String sql() {
          return "(" + andjoin.$(ts) + ")";
        }
      };
  }
  protected Expr or(final Expr... ts) {
    return new Expr() {
        String sql() {
          return "(" + orjoin.$(ts) + ")";
        }
      };
  }

  static class JoinString {
    final String sep;
    JoinString(String sep) { this.sep = sep;}

    final Fold<StringBuffer, Object>
      joiner = new Fold<StringBuffer, Object>() {
        StringBuffer each(StringBuffer s1, Object s2) {
          return s1.append(sep).append(s2.toString());
        }
      };
    String $(Object[] strs) {
      return joiner.snd(new StringBuffer(strs[0].toString()), strs).toString();
    }
  }
}

*1:こういう構文比較では、LispHaskellを引き合いに出してきてどうだということが言えれば、それで十分なのではないかと思う。