FoundationDB SQL Parser による SQL の構文解析

Pocket

【この記事を読むのに必要な時間は約 13 分です】

FoundationDB SQL Parser は、SQL 内で参照しているテーブル・カラムや Where / GOUP BY / ORDER BY 句等の条件、関数を抽出することができるフリー(Apache License 2.0)の Java のパーサです。

FoundationDB/sql-parser

以下に実装例を示します。

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.foundationdb.sql.StandardException;
import com.foundationdb.sql.parser.*;
import com.foundationdb.sql.unparser.NodeToString;

public class SqlParserTest {

    /** Logger. */
    private static Log log = LogFactory.getLog(SqlParserTest.class);

    public String getSchemaName() {
        return schemaName;
    }

    public static void main (String arg[]) throws StandardException  {

        String sql = "SELECT PRODUCT.NAME, PRODUCT.PRICE " +
                        "FROM TB_SALES_INFO SALES, TB_MST_PRODUCT PRODUCT " +
                        "WHERE PRODUCT.DEAL_CD NOT IN ('001','007') " +
                            "AND SUBSTR(SALES.RGST_DATE, 1, 8) = '20141101' " +
                            "AND PRODUCT.DELETE_FLG <> '1' " +
                        "GROUP BY SALES.SECTION_CD";

        SQLParser parser = new SQLParser();
        QueryVisitor qVis = new QueryVisitor();

        StatementNode stmt = null;
        try {
            // パース実行
            stmt = parser.parseStatement(sql);
            stmt.accept(qVis);
            // debug
            //stmt.treePrint();
        } catch (StandardException e) {
            log.error("SQとして不正である場合はここでエラー");
        }

        // 1.ステートメント
        System.out.println("statement : " + stmt.statementToString());

        // 2.テーブル名
        for (FromTable table : qVis.fromList) {
            String tableName = table.getTableName().toString();
            System.out.println("table name : " + tableName);

        }

        // 3.selectフィールド名
        NodeToString nodeToString = new NodeToString();
        System.out.println("field name : " +
                nodeToString.toString(qVis.resultList));

        /*
         *  statement : SELECT
            table name : sales
            table name : product
            field name : product.name AS name, product.price AS price
         */

        // NodeType によって Where 条件を解析
        // 複数の Where 条件がある場合、ネストされている
        switch (qVis.whereClauses.getNodeType()) {
            case NodeTypes.AND_NODE:
                AndNode andNode = (AndNode) qVis.whereClauses;
                // 左辺
                ValueNode leftNode = andNode.getLeftOperand();
                // 右辺
                ValueNode rightNode = andNode.getRightOperand();
                break;
            case NodeTypes.OR_NODE:
                OrNode orNode = (OrNode) qVis.whereClauses;
                // 左辺
                ValueNode orLeftNode = orNode.getLeftOperand();
                // 右辺
                ValueNode orRightNode = orNode.getRightOperand();
                break;
            case NodeTypes.IN_LIST_OPERATOR_NODE:
                InListOperatorNode inOpeNode =
                    (InListOperatorNode)qVis.whereClauses;
                break;
            // BinaryRelationalOperatorNode
            // This class represents the 6 binary operators:
            // LessThan, LessThanEquals, Equals, NotEquals,
            // GreaterThan and GreaterThanEquals.
            case NodeTypes.BINARY_EQUALS_OPERATOR_NODE:
            case NodeTypes.BINARY_GREATER_EQUALS_OPERATOR_NODE:
            case NodeTypes.BINARY_GREATER_THAN_OPERATOR_NODE:
            case NodeTypes.BINARY_LESS_EQUALS_OPERATOR_NODE:
            case NodeTypes.BINARY_LESS_THAN_OPERATOR_NODE:
            case NodeTypes.BINARY_NOT_EQUALS_OPERATOR_NODE:
                // 比較演算
                break;
            default:
                break;
        }
    }
}

32行目のパースによりにパース結果がノード構造で StatementNode に保持されます。さらに33 行目 accept() メソッドの実行により、子ノードを以下の独自 Visitor クラス内で WHERE、FROM、GROUP BY、ORDER BY、などに区別します。

import com.foundationdb.sql.StandardException;
import com.foundationdb.sql.parser.*;

public class QueryVisitor implements Visitor {

    public ResultColumnList resultList;
    public FromList fromList;
    public ValueNode whereClauses;
    public ValueNode havingClauses;
    public GroupByList groupList;
    public OrderByList orderbyList;

    @Override
    public Visitable visit(Visitable visitable)  {
        QueryTreeNode node = (QueryTreeNode) visitable;

        switch (node.getNodeType()) {
            case NodeTypes.SELECT_NODE:
                SelectNode sn = (SelectNode) node;
                resultList = sn.getResultColumns();
                fromList = sn.getFromList();
                whereClauses = sn.getWhereClause();
                havingClauses = sn.getHavingClause();
                groupList = sn.getGroupByList();
                break;
            case NodeTypes.CURSOR_NODE:
                orderbyList = ((CursorNode)node).getOrderByList();
                break;
            default:
                break;
        }

        return visitable;
    }

    @Override
    public boolean visitChildrenFirst(Visitable node) {
        return false;
    }

    @Override
    public boolean stopTraversal() {
        return false;
    }

    @Override
    public boolean skipChildren(Visitable node) throws StandardException {
        return false;
    }
}

40~53行目では、ステートメントと参照するテーブル名、カラム名を抽出しています。結果は56~59行目のようになります。

64行目以降では、WHERE 句の内容を判定しています。ValueNode が条件によってネストされていく構造ですので、実際には条件の数だけループする必要があります。

whereClauses は、実行時には以下のような構造で条件を保持しています(抜粋)。 左辺(leftOperand)と右辺(rightOperand)を演算子(operator)によってネストの深い位置から順に、繰り返し判定していきます。
演算子・被演算子(operand)にはそれぞれ NodeType が定められており、NodeType を判定することで条件を個別に解析していくことができます。

foundationdb_treeView

使用上の注意

FoundationDB SQL Parser の利用において注意すべき点としては、標準SQL (SQL-92 以降) に準拠している構文のみがサポート対象となることです。
基本的に Oracle, MySQL などのクエリは正常に解析することができますが、標準 SQL に含まれない一部の機能(たとえば LEAD )は構文エラーが発生してしまうため、その場合は以下の JAVACC に機能を追加することで対応します。

https://github.com/FoundationDB/sql-parser/blob/8f00e61bff2bf3c91e5bd8057f6d5034ad6eab01/src/main/javacc/SQLGrammar.jj

また、パース時に SQL 内の大小文字の区別は保持されません。カラム名、テーブル名はパース後は小文字に変換されています。SQL は文字の大小を区別しませんので問題はないのですが、実装時には留意が必要となります。

コメントを残す

メールアドレスが公開されることはありません。