/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.as.cmp.bridge.CMPFieldBridge;
import org.jboss.as.cmp.ejbql.ASTAbs;
import org.jboss.as.cmp.ejbql.ASTAbstractSchema;
import org.jboss.as.cmp.ejbql.ASTAnd;
import org.jboss.as.cmp.ejbql.ASTApproximateNumericLiteral;
import org.jboss.as.cmp.ejbql.ASTArithmeticComparison;
import org.jboss.as.cmp.ejbql.ASTArithmeticParenthetical;
import org.jboss.as.cmp.ejbql.ASTAvg;
import org.jboss.as.cmp.ejbql.ASTBetween;
import org.jboss.as.cmp.ejbql.ASTBooleanComparison;
import org.jboss.as.cmp.ejbql.ASTBooleanLiteral;
import org.jboss.as.cmp.ejbql.ASTCollectionMemberDeclaration;
import org.jboss.as.cmp.ejbql.ASTConcat;
import org.jboss.as.cmp.ejbql.ASTConditionalParenthetical;
import org.jboss.as.cmp.ejbql.ASTCount;
import org.jboss.as.cmp.ejbql.ASTDatetimeComparison;
import org.jboss.as.cmp.ejbql.ASTEJBQL;
import org.jboss.as.cmp.ejbql.ASTEntityComparison;
import org.jboss.as.cmp.ejbql.ASTExactNumericLiteral;
import org.jboss.as.cmp.ejbql.ASTFrom;
import org.jboss.as.cmp.ejbql.ASTIdentifier;
import org.jboss.as.cmp.ejbql.ASTIn;
import org.jboss.as.cmp.ejbql.ASTIsEmpty;
import org.jboss.as.cmp.ejbql.ASTLCase;
import org.jboss.as.cmp.ejbql.ASTLength;
import org.jboss.as.cmp.ejbql.ASTLike;
import org.jboss.as.cmp.ejbql.ASTLimitOffset;
import org.jboss.as.cmp.ejbql.ASTLocate;
import org.jboss.as.cmp.ejbql.ASTMax;
import org.jboss.as.cmp.ejbql.ASTMemberOf;
import org.jboss.as.cmp.ejbql.ASTMin;
import org.jboss.as.cmp.ejbql.ASTMod;
import org.jboss.as.cmp.ejbql.ASTMultDiv;
import org.jboss.as.cmp.ejbql.ASTNegation;
import org.jboss.as.cmp.ejbql.ASTNot;
import org.jboss.as.cmp.ejbql.ASTNullComparison;
import org.jboss.as.cmp.ejbql.ASTOr;
import org.jboss.as.cmp.ejbql.ASTOrderBy;
import org.jboss.as.cmp.ejbql.ASTOrderByPath;
import org.jboss.as.cmp.ejbql.ASTParameter;
import org.jboss.as.cmp.ejbql.ASTPath;
import org.jboss.as.cmp.ejbql.ASTPlusMinus;
import org.jboss.as.cmp.ejbql.ASTRangeVariableDeclaration;
import org.jboss.as.cmp.ejbql.ASTSelect;
import org.jboss.as.cmp.ejbql.ASTSqrt;
import org.jboss.as.cmp.ejbql.ASTStringComparison;
import org.jboss.as.cmp.ejbql.ASTStringLiteral;
import org.jboss.as.cmp.ejbql.ASTStringParenthetical;
import org.jboss.as.cmp.ejbql.ASTSubstring;
import org.jboss.as.cmp.ejbql.ASTSum;
import org.jboss.as.cmp.ejbql.ASTUCase;
import org.jboss.as.cmp.ejbql.ASTValueClassComparison;
import org.jboss.as.cmp.ejbql.ASTWhere;
import org.jboss.as.cmp.ejbql.ASTWhereConditionalTerm;
import org.jboss.as.cmp.ejbql.Catalog;
import org.jboss.as.cmp.ejbql.EJBQLTypes;
import org.jboss.as.cmp.ejbql.SelectFunction;
import org.jboss.as.cmp.ejbql.SimpleNode;
import org.jboss.as.cmp.ejbql.Node;
import org.jboss.as.cmp.ejbql.JBossQLParserVisitor;
import org.jboss.as.cmp.ejbql.JBossQLParser;
import org.jboss.as.cmp.ejbql.EJBQLParser;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractCMRFieldBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.as.cmp.jdbc.metadata.JDBCFunctionMappingMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCQueryMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCReadAheadMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCRelationMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCTypeMappingMetaData;
import org.jboss.logging.Logger;

/**
 * Compiles EJB-QL and JBossQL into SQL using OUTER and INNER joins.
 *
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @version $Revision: 102957 $
 */
public final class EJBQLToSQL92Compiler
        implements QLCompiler, JBossQLParserVisitor {
    private static final Logger log = Logger.getLogger(EJBQLToSQL92Compiler.class);

    // input objects
    private final Catalog catalog;
    private Class returnType;
    private Class[] parameterTypes;
    private JDBCReadAheadMetaData readAhead;

    // alias info
    private AliasManager aliasManager;
    private Map joinPaths = new HashMap();
    private Map identifierToTable = new HashMap();
    private Set joinedAliases = new HashSet();

    // mapping metadata
    private JDBCTypeMappingMetaData typeMapping;
    private JDBCTypeFactory typeFactory;

    // output objects
    private boolean forceDistinct;
    private String sql;
    private int offsetParam;
    private int offsetValue;
    private int limitParam;
    private int limitValue;
    private JDBCEntityPersistenceStore selectManager;
    private Object selectObject;
    private List inputParameters = new ArrayList();
    private JDBCType functionJDBCType;

    private List leftJoinCMRList = new ArrayList();
    private StringBuffer onFindCMRJoin;

    private boolean countCompositePk;
    private boolean selectDistinct;

    public EJBQLToSQL92Compiler(Catalog catalog) {
        this.catalog = catalog;
    }

    public void compileEJBQL(String ejbql, Class returnType, Class[] parameterTypes, JDBCQueryMetaData metadata)
            throws Exception {
        // reset all state variables
        reset();

        // set input arguemts
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.readAhead = metadata.getReadAhead();

        // get the parser
        EJBQLParser parser = new EJBQLParser(new StringReader(""));

        try {
            // parse the ejbql into an abstract sytax tree
            ASTEJBQL ejbqlNode = parser.parse(catalog, parameterTypes, ejbql);

            // translate to sql
            sql = ejbqlNode.jjtAccept(this, new StringBuffer()).toString();
        } catch (Exception e) {
            // if there is a problem reset the state before exiting
            reset();
            throw e;
        } catch (Error e) {
            // lame javacc lexer throws Errors
            reset();
            throw e;
        }
    }

    public void compileJBossQL(String ejbql, Class returnType, Class[] parameterTypes, JDBCQueryMetaData metadata)
            throws Exception {
        // reset all state variables
        reset();

        // set input arguemts
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.readAhead = metadata.getReadAhead();

        // get the parser
        JBossQLParser parser = new JBossQLParser(new StringReader(""));

        try {
            // parse the ejbql into an abstract sytax tree
            ASTEJBQL ejbqlNode = parser.parse(catalog, parameterTypes, ejbql);

            // translate to sql
            sql = ejbqlNode.jjtAccept(this, new StringBuffer()).toString();

            if (log.isTraceEnabled()) {
                log.trace("ejbql: " + ejbql);
                log.trace("sql: " + sql);
            }
        } catch (Exception e) {
            // if there is a problem reset the state before exiting
            reset();
            throw e;
        } catch (Error e) {
            // lame javacc lexer throws Errors
            reset();
            throw e;
        }
    }

    public String getSQL() {
        return sql;
    }

    public int getOffsetValue() {
        return offsetValue;
    }

    public int getOffsetParam() {
        return offsetParam;
    }

    public int getLimitValue() {
        return limitValue;
    }

    public int getLimitParam() {
        return limitParam;
    }

    public boolean isSelectEntity() {
        return selectObject instanceof JDBCAbstractEntityBridge;
    }

    public JDBCAbstractEntityBridge getSelectEntity() {
        return (JDBCAbstractEntityBridge) selectObject;
    }

    public boolean isSelectField() {
        boolean result;
        if (selectObject instanceof JDBCFieldBridge) {
            JDBCFieldBridge field = (JDBCFieldBridge) selectObject;
            result = field.isCMPField();
        } else {
            result = false;
        }
        return result;
    }

    public JDBCFieldBridge getSelectField() {
        return (JDBCFieldBridge) selectObject;
    }

    public SelectFunction getSelectFunction() {
        return (SelectFunction) selectObject;
    }

    public JDBCEntityPersistenceStore getStoreManager() {
        return selectManager;
    }

    public List getInputParameters() {
        return inputParameters;
    }

    public List getLeftJoinCMRList() {
        return leftJoinCMRList;
    }

    public boolean isSelectDistinct() {
        return selectDistinct;
    }

    public Object visit(SimpleNode node, Object data) {
        throw new RuntimeException("Internal error: Found unknown node type in " +
                "EJB-QL abstract syntax tree: node=" + node);
    }

    public Object visit(ASTEJBQL node, Object data) {
        Node selectNode = node.jjtGetChild(0);
        Node fromNode = node.jjtGetChild(1);

        // compile selectNode
        StringBuffer selectClause = new StringBuffer(50);
        selectNode.jjtAccept(this, selectClause);

        StringBuffer whereClause = null;
        StringBuffer orderByClause = null;
        for (int i = 2; i < node.jjtGetNumChildren(); ++i) {
            Node childNode = node.jjtGetChild(i);
            if (childNode instanceof ASTWhere) {
                whereClause = new StringBuffer(20);
                childNode.jjtAccept(this, whereClause);
            } else if (childNode instanceof ASTOrderBy) {
                orderByClause = new StringBuffer();
                childNode.jjtAccept(this, orderByClause);
            } else if (childNode instanceof ASTLimitOffset) {
                childNode.jjtAccept(this, null);
            }
        }

        // compile fromNode
        StringBuffer fromClause = new StringBuffer(30);
        fromNode.jjtAccept(this, fromClause);

        // left-join
        for (Iterator iter = identifierToTable.entrySet().iterator(); iter.hasNext(); ) {
            final Map.Entry entry = (Map.Entry) iter.next();
            final String identifier = (String) entry.getKey();
            final String table = (String) entry.getValue();
            final String alias = aliasManager.getAlias(identifier);

            fromClause.append(table).append(' ').append(alias);
            join(alias, fromClause);

            if (iter.hasNext()) {
                fromClause.append(SQLUtil.COMMA);
            }
        }

        selectDistinct = ((ASTSelect) selectNode).distinct || returnType == Set.class || forceDistinct;

        // assemble sql
        StringBuffer sql = (StringBuffer) data;
        if (selectManager.getMetaData().hasRowLocking() && !(selectObject instanceof SelectFunction)) {
            JDBCFunctionMappingMetaData rowLockingTemplate = typeMapping.getRowLockingTemplate();
            if (rowLockingTemplate == null) {
                throw new IllegalStateException("Row locking template is not defined for given mapping: " + typeMapping.getName());
            }

            boolean distinct = selectDistinct;

            Object[] args = new Object[]{
                    distinct ? SQLUtil.DISTINCT + selectClause : selectClause.toString(),
                    fromClause,
                    whereClause == null || whereClause.length() == 0 ? null : whereClause,
                    orderByClause == null || orderByClause.length() == 0 ? null : orderByClause
            };
            rowLockingTemplate.getFunctionSql(args, sql);
        } else {
            sql.append(SQLUtil.SELECT);
            if (selectDistinct) {
                sql.append(SQLUtil.DISTINCT);
            }
            sql.append(selectClause)
                    .append(SQLUtil.FROM)
                    .append(fromClause);

            if (whereClause != null && whereClause.length() > 0) {
                sql.append(SQLUtil.WHERE).append(whereClause);
            }

            if (orderByClause != null && orderByClause.length() > 0) {
                sql.append(SQLUtil.ORDERBY).append(orderByClause);
            }
        }

        if (countCompositePk) {
            sql.insert(0, "SELECT COUNT(*) FROM (").append(") t_count");
        }

        return data;
    }

    public Object visit(ASTOrderBy node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < node.jjtGetNumChildren(); i++) {
            buf.append(SQLUtil.COMMA);
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTOrderByPath node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        if (node.ascending) {
            buf.append(SQLUtil.ASC);
        } else {
            buf.append(SQLUtil.DESC);
        }
        return data;
    }

    public Object visit(ASTLimitOffset node, Object data) {
        int child = 0;
        if (node.hasOffset) {
            Node offsetNode = node.jjtGetChild(child++);
            if (offsetNode instanceof ASTParameter) {
                ASTParameter param = (ASTParameter) offsetNode;
                Class parameterType = getParameterType(param.number);
                if (int.class != parameterType && Integer.class != parameterType) {
                    throw new IllegalStateException("OFFSET parameter must be an int");
                }
                offsetParam = param.number;
            } else {
                ASTExactNumericLiteral param = (ASTExactNumericLiteral) offsetNode;
                offsetValue = (int) param.value;
            }
        }

        if (node.hasLimit) {
            Node limitNode = node.jjtGetChild(child);
            if (limitNode instanceof ASTParameter) {
                ASTParameter param = (ASTParameter) limitNode;
                Class parameterType = getParameterType(param.number);
                if (int.class != parameterType && Integer.class != parameterType) {
                    throw new IllegalStateException("LIMIT parameter must be an int");
                }
                limitParam = param.number;
            } else {
                ASTExactNumericLiteral param = (ASTExactNumericLiteral) limitNode;
                limitValue = (int) param.value;
            }
        }
        return data;
    }

    public Object visit(ASTSelect select, Object data) {
        StringBuffer sql = (StringBuffer) data;

        final Node child0 = select.jjtGetChild(0);
        final ASTPath path;
        if (child0 instanceof ASTPath) {
            path = (ASTPath) child0;

            if (path.isCMPField()) {
                // set the select object
                JDBCFieldBridge selectField = (JDBCFieldBridge) path.getCMPField();
                selectManager = selectField.getManager();
                selectObject = selectField;
                setTypeFactory(selectManager.getJDBCTypeFactory());

                // todo inner or left?
                //addLeftJoinPath(path);
                addInnerJoinPath(path);

                String alias = aliasManager.getAlias(path.getPath(path.size() - 2));
                SQLUtil.getColumnNamesClause(selectField, alias, sql);
            } else {
                JDBCAbstractEntityBridge selectEntity = (JDBCAbstractEntityBridge) path.getEntity();
                selectManager = selectEntity.getManager();
                selectObject = selectEntity;
                setTypeFactory(selectEntity.getManager().getJDBCTypeFactory());

                final String alias = aliasManager.getAlias(path.getPath());
                if (select.distinct) {
                    SQLUtil.getSearchableColumnNamesClause(selectEntity.getTableFields(), alias, sql);
                } else {
                    SQLUtil.getColumnNamesClause(selectEntity.getTableFields(), alias, sql);
                }

                /*
                if(readAhead.isOnFind())
                {
                   String eagerLoadGroupName = readAhead.getEagerLoadGroup();
                   boolean[] loadGroupMask = selectEntity.getLoadGroupMask(eagerLoadGroupName);
                   SQLUtil.appendColumnNamesClause(
                      selectEntity.getTableFields(),
                      loadGroupMask,
                      alias,
                      sql
                   );
                }
                */

                addLeftJoinPath(path);
            }
        } else {
            // the function should take a path expresion as a parameter
            path = getPathFromChildren(child0);

            if (path == null) {
                throw new IllegalStateException("The function in SELECT clause does not contain a path expression.");
            }

            if (path.isCMPField()) {
                JDBCFieldBridge selectField = (JDBCFieldBridge) path.getCMPField();
                selectManager = selectField.getManager();
                setTypeFactory(selectManager.getJDBCTypeFactory());
                if (selectField.getJDBCType().hasMapper())
                    this.functionJDBCType = selectField.getJDBCType();
            } else if (path.isCMRField()) {
                JDBCFieldBridge cmrField = (JDBCFieldBridge) path.getCMRField();
                selectManager = cmrField.getManager();
                setTypeFactory(selectManager.getJDBCTypeFactory());
                addLeftJoinPath(path);
            } else {
                final JDBCAbstractEntityBridge entity = (JDBCAbstractEntityBridge) path.getEntity();
                selectManager = entity.getManager();
                setTypeFactory(selectManager.getJDBCTypeFactory());
                addLeftJoinPath(path);
            }

            selectObject = child0;
            child0.jjtAccept(this, data);
        }

        return data;
    }

    public Object visit(ASTWhere node, Object data) {
        node.jjtGetChild(0).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTOr node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < node.jjtGetNumChildren(); ++i) {
            buf.append(SQLUtil.OR);
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTWhereConditionalTerm node, Object data) {
        for (int i = 0; i < node.jjtGetNumChildren(); ++i) {
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTAnd node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < node.jjtGetNumChildren(); i++) {
            buf.append(SQLUtil.AND);
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTNot node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append(SQLUtil.NOT);
        node.jjtGetChild(0).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTConditionalParenthetical node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append('(');
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(')');
        return data;
    }

    public Object visit(ASTBetween node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        if (node.not) {
            buf.append(SQLUtil.NOT);
        }
        buf.append(SQLUtil.BETWEEN);
        node.jjtGetChild(1).jjtAccept(this, data);
        buf.append(SQLUtil.AND);
        node.jjtGetChild(2).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTIn node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        if (node.not) {
            buf.append(SQLUtil.NOT);
        }
        buf.append(SQLUtil.IN).append('(');
        node.jjtGetChild(1).jjtAccept(this, data);
        for (int i = 2; i < node.jjtGetNumChildren(); i++) {
            buf.append(SQLUtil.COMMA);
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        buf.append(')');
        return data;
    }

    public Object visit(ASTLike node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        if (node.not) {
            buf.append(SQLUtil.NOT);
        }
        buf.append(SQLUtil.LIKE);
        node.jjtGetChild(1).jjtAccept(this, data);
        if (node.jjtGetNumChildren() == 3) {
            buf.append(" {ESCAPE ");
            node.jjtGetChild(2).jjtAccept(this, data);
            buf.append('}');
        }
        return data;
    }

    public Object visit(ASTNullComparison node, Object data) {
        StringBuffer sql = (StringBuffer) data;

        final Node child0 = node.jjtGetChild(0);
        if (child0 instanceof ASTPath) {
            ASTPath path = (ASTPath) child0;
            addLeftJoinPath(path);

            JDBCFieldBridge field = (JDBCFieldBridge) path.getField();

            if (field instanceof JDBCAbstractCMRFieldBridge) {
                JDBCAbstractCMRFieldBridge cmrField = (JDBCAbstractCMRFieldBridge) field;
                final String alias;
                final JDBCFieldBridge[] keyFields;

                if (cmrField.hasForeignKey()) {
                    alias = aliasManager.getAlias(path.getPath(path.size() - 2));
                    keyFields = cmrField.getForeignKeyFields();
                } else {
                    alias = aliasManager.getAlias(path.getPath());
                    if (cmrField.getMetaData().getRelationMetaData().isTableMappingStyle()) {
                        keyFields = cmrField.getRelatedCMRField().getEntity().getPrimaryKeyFields();
                    } else {
                        keyFields = cmrField.getRelatedCMRField().getForeignKeyFields();
                    }
                }

                SQLUtil.getIsNullClause(node.not, keyFields, alias, sql);
            } else {
                String alias = aliasManager.getAlias(path.getPath(path.size() - 2));
                SQLUtil.getIsNullClause(node.not, field, alias, sql);
            }
        } else if (child0 instanceof ASTParameter) {
            ASTParameter param = (ASTParameter) child0;
            Class type = getParameterType(param.number);

            QueryParameter queryParam = new QueryParameter(param.number - 1, typeFactory.getJDBCType(type));
            inputParameters.add(queryParam);

            sql.append("? IS ");
            if (node.not) {
                sql.append(SQLUtil.NOT);
            }
            sql.append(SQLUtil.NULL);
        } else {
            throw new IllegalStateException("Unexpected node in IS NULL clause: " + node);
        }

        return data;
    }

    public Object visit(ASTIsEmpty node, Object data) {
        ASTPath path = (ASTPath) node.jjtGetChild(0);
        if (!path.isCMRField()) {
            throw new IllegalStateException("IS EMPTY can be applied only to collection valued CMR field.");
        }

        addLeftJoinPath(path);

        StringBuffer sql = (StringBuffer) data;
        JDBCAbstractCMRFieldBridge cmrField = (JDBCAbstractCMRFieldBridge) path.getCMRField();
        JDBCAbstractEntityBridge relatedEntity = (JDBCAbstractEntityBridge) cmrField.getRelatedEntity();
        String alias = aliasManager.getAlias(path.getPath());
        SQLUtil.getIsNullClause(node.not, relatedEntity.getPrimaryKeyFields(), alias, sql);

        return data;
    }

    public Object visit(ASTMemberOf node, Object data) {
        Node member = node.jjtGetChild(0);
        ASTPath colPath = (ASTPath) node.jjtGetChild(1);
        JDBCAbstractEntityBridge colEntity = (JDBCAbstractEntityBridge) colPath.getEntity();

        StringBuffer sql = (StringBuffer) data;

        if (node.not) {
            sql.append(SQLUtil.NOT);
        }

        sql.append(SQLUtil.EXISTS).append('(').append(SQLUtil.SELECT);

        if (member instanceof ASTParameter) {
            ASTParameter toParam = (ASTParameter) member;
            verifyParameterEntityType(toParam.number, colEntity);
            inputParameters.addAll(QueryParameter.createParameters(toParam.number - 1, colEntity));

            String parentAlias = aliasManager.getAlias(colPath.getPath(0));
            String localParentAlias = aliasManager.getAlias(colPath.getPath(0) + "_local");
            JDBCAbstractEntityBridge parentEntity = (JDBCAbstractEntityBridge) colPath.getEntity(0);
            SQLUtil.getColumnNamesClause(parentEntity.getPrimaryKeyFields(), localParentAlias, sql);
            sql.append(SQLUtil.FROM)
                    .append(parentEntity.getQualifiedTableName()).append(' ').append(localParentAlias);
            innerJoinPath(colPath, sql);

            sql.append(SQLUtil.WHERE);

            JDBCAbstractEntityBridge col0 = (JDBCAbstractEntityBridge) colPath.getEntity(0);
            SQLUtil.getSelfCompareWhereClause(col0.getPrimaryKeyFields(), parentAlias, localParentAlias, sql);
            sql.append(SQLUtil.AND);

            String localColAlias = aliasManager.getAlias(colPath.getPath() + "_local");
            SQLUtil.getWhereClause(colEntity.getPrimaryKeyFields(), localColAlias, sql);
        } else {
            ASTPath memberPath = (ASTPath) member;
            JDBCAbstractEntityBridge memberEntity = (JDBCAbstractEntityBridge) memberPath.getEntity();

            if (!memberEntity.equals(colEntity)) {
                throw new IllegalStateException("Member must be if the same type as the collection, got: member="
                        +
                        memberEntity.getEntityName()
                        + ", collection=" + colEntity.getEntityName());
            }

            String memberAlias = aliasManager.getAlias(memberPath.getPath());

            if (memberPath.size() > 1) {
                String parentAlias = aliasManager.getAlias(memberPath.getPath(0) + "_local");
                JDBCAbstractEntityBridge parentEntity = (JDBCAbstractEntityBridge) memberPath.getEntity(0);
                SQLUtil.getColumnNamesClause(parentEntity.getPrimaryKeyFields(), parentAlias, sql);
                sql.append(SQLUtil.FROM)
                        .append(parentEntity.getQualifiedTableName()).append(' ').append(parentAlias);
                innerJoinPath(memberPath, sql);
                innerJoinPath(colPath, sql);
            } else if (colPath.size() > 1) {
                String parentAlias = aliasManager.getAlias(colPath.getPath(0) + "_local");
                JDBCAbstractEntityBridge parentEntity = (JDBCAbstractEntityBridge) colPath.getEntity(0);
                SQLUtil.getColumnNamesClause(parentEntity.getPrimaryKeyFields(), parentAlias, sql);
                sql.append(SQLUtil.FROM)
                        .append(parentEntity.getQualifiedTableName()).append(' ').append(parentAlias);
                innerJoinPath(colPath, sql);
            } else {
                throw new IllegalStateException(
                        "There should be collection valued path expression, not identification variable.");
            }

            sql.append(SQLUtil.WHERE);

            JDBCAbstractEntityBridge member0 = (JDBCAbstractEntityBridge) memberPath.getEntity(0);
            String colAliasLocal = aliasManager.getAlias(colPath.getPath() + "_local");
            if (memberPath.size() > 1) {
                String memberAliasLocal = aliasManager.getAlias(memberPath.getPath() + "_local");
                SQLUtil.getSelfCompareWhereClause(colEntity.getPrimaryKeyFields(),
                        memberAliasLocal,
                        colAliasLocal,
                        sql);

                sql.append(SQLUtil.AND);

                String member0Alias = aliasManager.getAlias(memberPath.getPath(0));
                String member0AliasLocal = aliasManager.getAlias(memberPath.getPath(0) + "_local");
                SQLUtil.getSelfCompareWhereClause(member0.getPrimaryKeyFields(),
                        member0Alias,
                        member0AliasLocal,
                        sql);
            } else {
                SQLUtil.getSelfCompareWhereClause(member0.getPrimaryKeyFields(), memberAlias, colAliasLocal, sql);

                sql.append(SQLUtil.AND);

                String col0Alias = aliasManager.getAlias(colPath.getPath(0));
                String col0AliasLocal = aliasManager.getAlias(colPath.getPath(0) + "_local");
                SQLUtil.getSelfCompareWhereClause(colEntity.getPrimaryKeyFields(),
                        col0Alias,
                        col0AliasLocal,
                        sql);
            }
        }

        sql.append(')');

        return data;
    }

    private void innerJoinPath(ASTPath path, StringBuffer sql) {
        if (path.size() < 2) {
            return;
        }

        String parentAlias = aliasManager.getAlias(path.getPath(0) + "_local");
        String leftAlias = parentAlias;
        for (int i = 1; i < path.size(); ++i) {
            String curPath = path.getPath(i);
            final String joinAlias = aliasManager.getAlias(curPath + "_local");

            final JDBCAbstractCMRFieldBridge cmrField = (JDBCAbstractCMRFieldBridge) path.getCMRField(i);
            final JDBCAbstractEntityBridge joinEntity = (JDBCAbstractEntityBridge) cmrField.getRelatedEntity();

            JDBCRelationMetaData relation = cmrField.getMetaData().getRelationMetaData();

            String join = " INNER JOIN ";

            if (relation.isTableMappingStyle()) {
                String relTableAlias = aliasManager.getRelationTableAlias(curPath + "_local");
                sql.append(join)
                        .append(cmrField.getQualifiedTableName())
                        .append(' ')
                        .append(relTableAlias)
                        .append(" ON ");
                SQLUtil.getRelationTableJoinClause(cmrField, leftAlias, relTableAlias, sql);

                sql.append(join)
                        .append(joinEntity.getQualifiedTableName())
                        .append(' ')
                        .append(joinAlias)
                        .append(" ON ");
                SQLUtil.getRelationTableJoinClause(cmrField.getRelatedCMRField(), joinAlias, relTableAlias, sql);
            } else {
                sql.append(join)
                        .append(joinEntity.getQualifiedTableName())
                        .append(' ')
                        .append(joinAlias)
                        .append(" ON ");

                SQLUtil.getJoinClause(cmrField, leftAlias, joinAlias, sql);
            }

            leftAlias = joinAlias;
        }
    }

    public Object visit(ASTStringComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(' ').append(node.opp).append(' ');
        node.jjtGetChild(1).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTBooleanComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        if (node.jjtGetNumChildren() == 2) {
            buf.append(' ').append(node.opp).append(' ');
            node.jjtGetChild(1).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTDatetimeComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(' ').append(node.opp).append(' ');
        node.jjtGetChild(1).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTValueClassComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;

        boolean not = (node.opp.equals(SQLUtil.NOT_EQUAL));
        String comparison = node.opp;
        buf.append('(');
        if (not) {
            buf.append(SQLUtil.NOT).append('(');
            comparison = "=";
        }

        // setup the from path
        ASTPath fromPath = (ASTPath) node.jjtGetChild(0);
        addInnerJoinPath(fromPath);
        String fromAlias = aliasManager.getAlias(fromPath.getPath(fromPath.size() - 2));
        CMPFieldBridge fromCMPField = (CMPFieldBridge) fromPath.getCMPField();

        Node toNode = node.jjtGetChild(1);
        if (toNode instanceof ASTParameter) {
            ASTParameter toParam = (ASTParameter) toNode;

            // can only compare like kind entities
            Class parameterType = getParameterType(toParam.number);
            if (!(fromCMPField.getFieldType().equals(parameterType))) {
                throw new IllegalStateException("Only like types can be " +
                        "compared: from CMP field=" +
                        fromCMPField.getFieldType() +
                        " to parameter=" + parameterType);
            }

            inputParameters.addAll(QueryParameter.createParameters(toParam.number - 1, fromCMPField));
            SQLUtil.getWhereClause(fromCMPField.getJDBCType(), fromAlias, comparison, buf);
        } else {
            ASTPath toPath = (ASTPath) toNode;
            addInnerJoinPath(toPath);
            String toAlias = aliasManager.getAlias(toPath.getPath(toPath.size() - 2));
            JDBCCMPFieldBridge toCMPField = (JDBCCMPFieldBridge) toPath.getCMPField();

            // can only compare like kind entities
            if (!(fromCMPField.getFieldType().equals(toCMPField.getFieldType()))) {
                throw new IllegalStateException("Only like types can be " +
                        "compared: from CMP field=" +
                        fromCMPField.getFieldType() +
                        " to CMP field=" + toCMPField.getFieldType());
            }

            SQLUtil.getSelfCompareWhereClause(fromCMPField, toCMPField, fromAlias, toAlias, comparison, buf);
        }

        return (not ? buf.append(')') : buf).append(')');
    }

    public Object visit(ASTEntityComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        Node arg0 = node.jjtGetChild(0);
        Node arg1 = node.jjtGetChild(1);
        if (node.opp.equals(SQLUtil.NOT_EQUAL)) {
            compareEntity(true, arg0, arg1, buf);
        } else {
            compareEntity(false, arg0, arg1, buf);
        }
        return data;
    }

    public Object visit(ASTArithmeticComparison node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(' ').append(node.opp).append(' ');
        node.jjtGetChild(1).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTPlusMinus node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < node.jjtGetNumChildren(); i++) {
            buf.append(' ').append(node.opps.get(i - 1)).append(' ');
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTMultDiv node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < node.jjtGetNumChildren(); i++) {
            buf.append(' ').append(node.opps.get(i - 1)).append(' ');
            node.jjtGetChild(i).jjtAccept(this, data);
        }
        return data;
    }

    public Object visit(ASTNegation node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append('-');
        node.jjtGetChild(0).jjtAccept(this, data);
        return data;
    }

    public Object visit(ASTArithmeticParenthetical node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append('(');
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(')');
        return data;
    }

    public Object visit(ASTStringParenthetical node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append('(');
        node.jjtGetChild(0).jjtAccept(this, data);
        buf.append(')');
        return data;
    }

    public Object visit(ASTConcat node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.CONCAT);
        Object[] args = childrenToStringArr(2, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTSubstring node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.SUBSTRING);
        Object[] args = childrenToStringArr(3, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTUCase node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.UCASE);
        Object[] args = childrenToStringArr(1, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTLCase node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LCASE);
        Object[] args = childrenToStringArr(1, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTLength node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LENGTH);
        Object[] args = childrenToStringArr(1, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTLocate node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LOCATE);
        Object[] args = new Object[3];
        args[0] = node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString();
        args[1] = node.jjtGetChild(1).jjtAccept(this, new StringBuffer()).toString();
        if (node.jjtGetNumChildren() == 3) {
            args[2] = node.jjtGetChild(2).jjtAccept(this, new StringBuffer()).toString();
        } else {
            args[2] = "1";
        }
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTAbs node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.ABS);
        Object[] args = childrenToStringArr(1, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTSqrt node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.SQRT);
        Object[] args = childrenToStringArr(1, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTMod node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.MOD);
        Object[] args = childrenToStringArr(2, node);
        function.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTAvg node, Object data) {
        if (functionJDBCType != null) {
            node.setResultType(functionJDBCType.getJavaTypes()[0]);
            node.setJDBCType(functionJDBCType);
        } else
            node.setResultType(returnType);
        StringBuffer buf = (StringBuffer) data;
        Object[] args = new Object[]{
                node.distinct,
                node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString(),
        };
        JDBCTypeMappingMetaData.AVG_FUNC.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTMax node, Object data) {
        if (functionJDBCType != null) {
            node.setResultType(functionJDBCType.getJavaTypes()[0]);
            node.setJDBCType(functionJDBCType);
        } else
            node.setResultType(returnType);
        StringBuffer buf = (StringBuffer) data;
        Object[] args = new Object[]{
                node.distinct,
                node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString(),
        };
        JDBCTypeMappingMetaData.MAX_FUNC.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTMin node, Object data) {
        if (functionJDBCType != null) {
            node.setResultType(functionJDBCType.getJavaTypes()[0]);
            node.setJDBCType(functionJDBCType);
        } else
            node.setResultType(returnType);
        StringBuffer buf = (StringBuffer) data;
        Object[] args = new Object[]{
                node.distinct,
                node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString(),
        };
        JDBCTypeMappingMetaData.MIN_FUNC.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTSum node, Object data) {
        if (functionJDBCType != null) {
            node.setResultType(functionJDBCType.getJavaTypes()[0]);
            node.setJDBCType(functionJDBCType);
        } else
            node.setResultType(returnType);
        StringBuffer buf = (StringBuffer) data;
        Object[] args = new Object[]{
                node.distinct,
                node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString(),
        };
        JDBCTypeMappingMetaData.SUM_FUNC.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTCount node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        node.setResultType(returnType);

        Object[] args;
        final ASTPath cntPath = (ASTPath) node.jjtGetChild(0);
        if (cntPath.isCMPField()) {
            args = new Object[]{node.distinct, node.jjtGetChild(0).jjtAccept(this, new StringBuffer()).toString()};
        } else {
            JDBCAbstractEntityBridge entity = (JDBCAbstractEntityBridge) cntPath.getEntity();
            final JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
            if (pkFields.length > 1) {
                countCompositePk = true;
                forceDistinct = node.distinct.length() > 0;

                addLeftJoinPath(cntPath);

                String alias = aliasManager.getAlias(cntPath.getPath());
                SQLUtil.getColumnNamesClause(entity.getPrimaryKeyFields(),
                        alias,
                        buf);

                return buf;
            } else {
                final String alias = aliasManager.getAlias(cntPath.getPath());
                StringBuffer keyColumn = new StringBuffer(20);
                SQLUtil.getColumnNamesClause(pkFields[0], alias, keyColumn);
                args = new Object[]{node.distinct, keyColumn.toString()};
            }
        }

        JDBCTypeMappingMetaData.COUNT_FUNC.getFunctionSql(args, buf);
        return data;
    }

    public Object visit(ASTPath node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        if (!node.isCMPField()) {
            throw new IllegalStateException("Can only visit cmp valued path node. "
                    + "Should have been handled at a higher level.");
        }

        JDBCFieldBridge cmpField = (JDBCFieldBridge) node.getCMPField();

        // make sure this is mapped to a single column
        switch (node.type) {
            case EJBQLTypes.ENTITY_TYPE:
            case EJBQLTypes.VALUE_CLASS_TYPE:
                if (cmpField.getJDBCType().hasMapper() ||
                        cmpField.getJDBCType().getParameterSetter() != null) {
                    break;
                }
            case EJBQLTypes.UNKNOWN_TYPE:
                throw new IllegalStateException("Can not visit multi-column path " +
                        "node. Should have been handled at a higher level.");
        }

        addLeftJoinPath(node);
        String alias = aliasManager.getAlias(node.getPath(node.size() - 2));
        SQLUtil.getColumnNamesClause(cmpField, alias, buf);
        return data;
    }

    public Object visit(ASTAbstractSchema node, Object data) {
        throw new IllegalStateException("Can not visit abstract schema node. "
                + " Should have been handled at a higher level.");
    }

    public Object visit(ASTIdentifier node, Object data) {
        throw new UnsupportedOperationException("Must not visit ASTIdentifier noe.");
    }

    public Object visit(ASTParameter node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        Class type = getParameterType(node.number);

        // make sure this is mapped to a single column
        int ejbqlType = EJBQLTypes.getEJBQLType(type);
        if (ejbqlType == EJBQLTypes.ENTITY_TYPE
                ||
                ejbqlType == EJBQLTypes.VALUE_CLASS_TYPE ||
                ejbqlType == EJBQLTypes.UNKNOWN_TYPE) {
            throw new IllegalStateException("Can not visit multi-column " +
                    "parameter node. Should have been handled at a higher level.");
        }

        QueryParameter param = new QueryParameter(node.number - 1, typeFactory.getJDBCType(type));
        inputParameters.add(param);
        buf.append('?');

        return data;
    }

    public Object visit(ASTExactNumericLiteral node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append(node.literal);
        return data;
    }

    public Object visit(ASTApproximateNumericLiteral node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append(node.literal);
        return data;
    }

    public Object visit(ASTStringLiteral node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        buf.append(node.value);
        return data;
    }

    public Object visit(ASTBooleanLiteral node, Object data) {
        StringBuffer buf = (StringBuffer) data;
        if (node.value) {
            buf.append(typeMapping.getTrueMapping());
        } else {
            buf.append(typeMapping.getFalseMapping());
        }
        return data;
    }

    public Object visit(ASTFrom from, Object data) {
        StringBuffer sql = (StringBuffer) data;
        from.jjtGetChild(0).jjtAccept(this, data);
        for (int i = 1; i < from.jjtGetNumChildren(); ++i) {
            from.jjtGetChild(i).jjtAccept(this, data);
        }

        return data;
    }

    public Object visit(ASTCollectionMemberDeclaration node, Object data) {
        ASTPath path = (ASTPath) node.jjtGetChild(0);

        // assign the same alias for path and identifier
        ASTIdentifier id = (ASTIdentifier) node.jjtGetChild(1);
        String alias = aliasManager.getAlias(id.identifier);
        aliasManager.addAlias(path.getPath(), alias);

        addInnerJoinPath(path);

        return data;
    }

    public Object visit(ASTRangeVariableDeclaration node, Object data) {
        ASTAbstractSchema schema = (ASTAbstractSchema) node.jjtGetChild(0);
        JDBCAbstractEntityBridge entity = (JDBCAbstractEntityBridge) schema.entity;
        ASTIdentifier id = (ASTIdentifier) node.jjtGetChild(1);
        declareTable(id.identifier, entity.getQualifiedTableName());
        return data;
    }

    // Private

    private void compareEntity(boolean not, Node fromNode, Node toNode, StringBuffer buf) {
        buf.append('(');
        if (not) {
            buf.append(SQLUtil.NOT).append('(');
        }

        ASTPath fromPath = (ASTPath) fromNode;
        addLeftJoinPath(fromPath);
        String fromAlias = aliasManager.getAlias(fromPath.getPath());
        JDBCAbstractEntityBridge fromEntity = (JDBCAbstractEntityBridge) fromPath.getEntity();

        if (toNode instanceof ASTParameter) {
            ASTParameter toParam = (ASTParameter) toNode;

            // can only compare like kind entities
            verifyParameterEntityType(toParam.number, fromEntity);

            inputParameters.addAll(QueryParameter.createParameters(toParam.number - 1, fromEntity));

            SQLUtil.getWhereClause(fromEntity.getPrimaryKeyFields(), fromAlias, buf);
        } else {
            ASTPath toPath = (ASTPath) toNode;
            addLeftJoinPath(toPath);
            String toAlias = aliasManager.getAlias(toPath.getPath());
            JDBCAbstractEntityBridge toEntity = (JDBCAbstractEntityBridge) toPath.getEntity();

            // can only compare like kind entities
            if (!fromEntity.equals(toEntity)) {
                throw new IllegalStateException("Only like types can be "
                        +
                        "compared: from entity="
                        +
                        fromEntity.getEntityName()
                        + " to entity=" + toEntity.getEntityName());
            }

            SQLUtil.getSelfCompareWhereClause(fromEntity.getPrimaryKeyFields(), fromAlias, toAlias, buf);
        }

        if (not) {
            buf.append(')');
        }
        buf.append(')');
    }

    private void join(String alias, StringBuffer sql) {
        Map paths = (Map) joinPaths.get(alias);
        if (paths == null || paths.isEmpty()) {
            return;
        }

        for (Iterator iter = paths.values().iterator(); iter.hasNext(); ) {
            String leftAlias = alias;
            ASTPath path = (ASTPath) iter.next();
            for (int i = 1; i < path.size(); ++i) {
                if (path.isCMRField(i)) {
                    final String curPath = path.getPath(i);
                    final String joinAlias = aliasManager.getAlias(curPath);

                    if (joinedAliases.add(joinAlias)) {
                        final JDBCAbstractCMRFieldBridge cmrField = (JDBCAbstractCMRFieldBridge) path.getCMRField(i);
                        final JDBCAbstractEntityBridge joinEntity = (JDBCAbstractEntityBridge) cmrField.getRelatedEntity();

                        JDBCRelationMetaData relation = cmrField.getMetaData().getRelationMetaData();

                        String join = (path.innerJoin ? " INNER JOIN " : " LEFT OUTER JOIN ");

                        if (relation.isTableMappingStyle()) {
                            String relTableAlias = aliasManager.getRelationTableAlias(curPath);
                            sql.append(join)
                                    .append(cmrField.getQualifiedTableName())
                                    .append(' ')
                                    .append(relTableAlias)
                                    .append(" ON ");
                            SQLUtil.getRelationTableJoinClause(cmrField, leftAlias, relTableAlias, sql);

                            sql.append(join)
                                    .append(joinEntity.getQualifiedTableName())
                                    .append(' ')
                                    .append(joinAlias)
                                    .append(" ON ");
                            SQLUtil.getRelationTableJoinClause(cmrField.getRelatedCMRField(), joinAlias, relTableAlias, sql);
                        } else {
                            sql.append(join)
                                    .append(joinEntity.getQualifiedTableName())
                                    .append(' ')
                                    .append(joinAlias)
                                    .append(" ON ");

                            SQLUtil.getJoinClause(cmrField, leftAlias, joinAlias, sql);
                        }

                        join(joinAlias, sql);
                    }
                    leftAlias = joinAlias;
                }
            }
        }
    }

    private void declareTable(String alias, String table) {
        identifierToTable.put(alias, table);
    }

    private void addLeftJoinPath(ASTPath path) {
        if (path.size() > 1 && path.isCMRField(1)) {
            final String identifier = path.getPath(0);
            final String alias = aliasManager.getAlias(identifier);
            Map paths = (Map) joinPaths.get(alias);
            if (paths == null) {
                paths = new HashMap();
                joinPaths.put(alias, paths);
            }

            ASTPath oldPath = (ASTPath) paths.put(path, path);
            if (oldPath != null && oldPath.innerJoin) {
                path.innerJoin = true;
            }
        }
    }

    private void addInnerJoinPath(ASTPath path) {
        if (path.size() > 1 && path.isCMRField(1)) {
            final String identifier = path.getPath(0);
            final String alias = aliasManager.getAlias(identifier);
            Map paths = (Map) joinPaths.get(alias);
            if (paths == null) {
                paths = new HashMap();
                joinPaths.put(alias, paths);
            }

            path.innerJoin = true;
            paths.put(path, path);
        }
    }

    private Object[] childrenToStringArr(int numChildren, Node node) {
        Object[] args = new Object[numChildren];
        for (int i = 0; i < numChildren; ++i) {
            args[i] = node.jjtGetChild(i).jjtAccept(this, new StringBuffer()).toString();
        }
        return args;
    }

    /**
     * Recursively searches for ASTPath among children.
     *
     * @param selectFunction a node implements SelectFunction
     * @return ASTPath child or null if there was no child of type ASTPath
     */
    private ASTPath getPathFromChildren(Node selectFunction) {
        for (int childInd = 0; childInd < selectFunction.jjtGetNumChildren(); ++childInd) {
            Node child = selectFunction.jjtGetChild(childInd);
            if (child instanceof ASTPath) {
                return (ASTPath) child;
            } else if (child instanceof SelectFunction) {
                Node path = getPathFromChildren(child);
                if (path != null) {
                    return (ASTPath) path;
                }
            }
        }
        return null;
    }

    private void setTypeFactory(JDBCTypeFactory typeFactory) {
        this.typeFactory = typeFactory;
        this.typeMapping = typeFactory.getTypeMapping();
        aliasManager = new AliasManager(typeMapping.getAliasHeaderPrefix(),
                typeMapping.getAliasHeaderSuffix(),
                typeMapping.getAliasMaxLength());
    }

    private Class getParameterType(int index) {
        int zeroBasedIndex = index - 1;
        Class[] params = parameterTypes;
        if (zeroBasedIndex < params.length) {
            return params[zeroBasedIndex];
        }
        return null;
    }

    // verify that parameter is the same type as the entity
    private void verifyParameterEntityType(int number, JDBCAbstractEntityBridge entity) {
        Class parameterType = getParameterType(number);
        Class remoteClass = entity.getRemoteInterface();
        Class localClass = entity.getLocalInterface();
        if ((localClass == null || !localClass.isAssignableFrom(parameterType)) &&
                (remoteClass == null || !remoteClass.isAssignableFrom(parameterType))) {
            throw new IllegalStateException("Only like types can be compared: from entity=" +
                    entity.getEntityName() + " to parameter type=" + parameterType);
        }
    }

    private void reset() {
        returnType = null;
        parameterTypes = null;
        readAhead = null;
        inputParameters.clear();
        selectObject = null;
        selectManager = null;
        typeFactory = null;
        typeMapping = null;
        aliasManager = null;
        forceDistinct = false;
        limitParam = 0;
        limitValue = 0;
        offsetParam = 0;
        offsetValue = 0;
        leftJoinCMRList.clear();
        onFindCMRJoin = null;
        countCompositePk = false;
        joinPaths.clear();
        identifierToTable.clear();
        joinedAliases.clear();
        selectDistinct = false;
        functionJDBCType = null;
    }
}
