/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, 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.ejb.plugins.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.ejb.plugins.cmp.ejbql.ASTAbs;
import org.jboss.ejb.plugins.cmp.ejbql.ASTAbstractSchema;
import org.jboss.ejb.plugins.cmp.ejbql.ASTBooleanLiteral;
import org.jboss.ejb.plugins.cmp.ejbql.ASTCollectionMemberDeclaration;
import org.jboss.ejb.plugins.cmp.ejbql.ASTConcat;
import org.jboss.ejb.plugins.cmp.ejbql.ASTEJBQL;
import org.jboss.ejb.plugins.cmp.ejbql.ASTEntityComparison;
import org.jboss.ejb.plugins.cmp.ejbql.ASTFrom;
import org.jboss.ejb.plugins.cmp.ejbql.ASTIdentifier;
import org.jboss.ejb.plugins.cmp.ejbql.ASTIsEmpty;
import org.jboss.ejb.plugins.cmp.ejbql.ASTLCase;
import org.jboss.ejb.plugins.cmp.ejbql.ASTLength;
import org.jboss.ejb.plugins.cmp.ejbql.ASTLocate;
import org.jboss.ejb.plugins.cmp.ejbql.ASTMemberOf;
import org.jboss.ejb.plugins.cmp.ejbql.ASTNullComparison;
import org.jboss.ejb.plugins.cmp.ejbql.ASTOrderBy;
import org.jboss.ejb.plugins.cmp.ejbql.ASTParameter;
import org.jboss.ejb.plugins.cmp.ejbql.ASTPath;
import org.jboss.ejb.plugins.cmp.ejbql.ASTRangeVariableDeclaration;
import org.jboss.ejb.plugins.cmp.ejbql.ASTSelect;
import org.jboss.ejb.plugins.cmp.ejbql.ASTSqrt;
import org.jboss.ejb.plugins.cmp.ejbql.ASTSubstring;
import org.jboss.ejb.plugins.cmp.ejbql.ASTUCase;
import org.jboss.ejb.plugins.cmp.ejbql.ASTValueClassComparison;
import org.jboss.ejb.plugins.cmp.ejbql.ASTWhere;
import org.jboss.ejb.plugins.cmp.ejbql.BasicVisitor;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.ejb.plugins.cmp.ejbql.EJBQLParser;
import org.jboss.ejb.plugins.cmp.ejbql.EJBQLTypes;
import org.jboss.ejb.plugins.cmp.ejbql.JBossQLParser;
import org.jboss.ejb.plugins.cmp.ejbql.Node;
import org.jboss.ejb.plugins.cmp.ejbql.SimpleNode;
import org.jboss.ejb.plugins.cmp.ejbql.ASTLimitOffset;
import org.jboss.ejb.plugins.cmp.ejbql.ASTCount;
import org.jboss.ejb.plugins.cmp.ejbql.SelectFunction;
import org.jboss.ejb.plugins.cmp.ejbql.ASTExactNumericLiteral;
import org.jboss.ejb.plugins.cmp.ejbql.ASTMax;
import org.jboss.ejb.plugins.cmp.ejbql.ASTMin;
import org.jboss.ejb.plugins.cmp.ejbql.ASTAvg;
import org.jboss.ejb.plugins.cmp.ejbql.ASTSum;
import org.jboss.ejb.plugins.cmp.ejbql.ASTWhereConditionalTerm;
import org.jboss.ejb.plugins.cmp.ejbql.ASTMod;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCFunctionMappingMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCTypeMappingMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCQueryMetaData;
import org.jboss.ejb.plugins.cmp.bridge.CMPFieldBridge;
import org.jboss.ejb.EntityPersistenceStore;
import org.jboss.deployment.DeploymentException;

/**
 * Compiles EJB-QL and JBossQL into SQL.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @version $Revision: 102957 $
 */
public final class JDBCEJBQLCompiler extends BasicVisitor implements QLCompiler
{
   // input objects
   private final Catalog catalog;
   private Class returnType;
   private Class[] parameterTypes;
   private JDBCReadAheadMetaData readAhead;
   private boolean lazyResultSetLoading;

   // alias info
   private AliasManager aliasManager;

   // join info
   private Set declaredPaths = new HashSet();
   private Set ctermJoinPaths = new HashSet();
   private Set allJoinPaths = new HashSet();
   private Map ctermCollectionMemberJoinPaths = new HashMap();
   private Map allCollectionMemberJoinPaths = new HashMap();
   private Map ctermLeftJoinPaths = new HashMap();
   private Map allLeftJoinPaths = new HashMap();

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

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

   /**
    * deep read ahead for cmrs
    */
   private List leftJoinCMRList = new ArrayList();
   private StringBuffer onFindCMRJoin;

   private boolean countCompositePk;
   private String selectAlias;
   private boolean selectDistinct;

   public JDBCEJBQLCompiler(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();
      this.lazyResultSetLoading = metadata.isLazyResultSetLoading();

      // get the parser
      EJBQLParser parser = new EJBQLParser(new StringReader(SQLUtil.EMPTY_STRING));

      try
      {
         // parse the ejbql into an abstract sytax tree
         ASTEJBQL ejbqlNode;
         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();
      this.lazyResultSetLoading = metadata.isLazyResultSetLoading();

      // get the parser
      JBossQLParser parser = new JBossQLParser(new StringReader(SQLUtil.EMPTY_STRING));

      try
      {
         // parse the ejbql into an abstract sytax tree
         ASTEJBQL ejbqlNode;
         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;
      }
   }

   private void reset()
   {
      returnType = null;
      parameterTypes = null;
      readAhead = null;
      inputParameters.clear();
      declaredPaths.clear();
      clearPerTermJoinPaths();
      allJoinPaths.clear();
      allCollectionMemberJoinPaths.clear();
      allLeftJoinPaths.clear();
      selectObject = null;
      selectManager = null;
      typeFactory = null;
      typeMapping = null;
      aliasManager = null;
      subquerySupported = true;
      forceDistinct = false;
      limitParam = 0;
      limitValue = 0;
      offsetParam = 0;
      offsetValue = 0;
      leftJoinCMRList.clear();
      onFindCMRJoin = null;
      countCompositePk = false;
      selectAlias = null;
      selectDistinct = false;
      functionJDBCType = null;
   }

   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 JDBCEntityBridge;
   }

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

   public boolean isSelectField()
   {
      return selectObject instanceof JDBCCMPFieldBridge;
   }

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

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

   public EntityPersistenceStore 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);
   }

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

   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,
                                          JDBCEntityBridge entity)
   {
      Class parameterType = getParameterType(number);
      Class remoteClass = entity.getMetaData().getRemoteClass();
      Class localClass = entity.getMetaData().getLocalClass();
      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 compareEntity(boolean not,
                              Node fromNode,
                              Node toNode,
                              StringBuffer buf)
   {
      buf.append('(');
      if(not)
      {
         buf.append(SQLUtil.NOT).append('(');
      }

      String fromAlias;
      JDBCEntityBridge fromEntity;
      ASTPath fromPath = (ASTPath) fromNode;
      addJoinPath(fromPath);
      fromAlias = aliasManager.getAlias(fromPath.getPath());
      fromEntity = (JDBCEntityBridge) 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
      {
         String toAlias;
         JDBCEntityBridge toEntity;
         ASTPath toPath = (ASTPath) toNode;
         addJoinPath(toPath);
         toAlias = aliasManager.getAlias(toPath.getPath());
         toEntity = (JDBCEntityBridge) 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 existsClause(ASTPath path, StringBuffer buf, boolean not)
   {
      if(!path.isCMRField())
      {
         throw new IllegalArgumentException("path must be a cmr field");
      }

      JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField();
      String pathStr = path.getPath(path.size() - 2);
      String parentAlias = aliasManager.getAlias(pathStr);

      // if exists is not supported we use a left join and is null
      if(!subquerySupported)
      {
         // add the path to the list of paths to left join
         addLeftJoinPath(pathStr, path);
         forceDistinct = true;

         addJoinPath(path);

         if(cmrField.getRelationMetaData().isForeignKeyMappingStyle())
         {
            JDBCEntityBridge childEntity = (JDBCEntityBridge) cmrField.getRelatedEntity();
            String childAlias = aliasManager.getAlias(path.getPath());
            SQLUtil.getIsNullClause(!not, childEntity.getPrimaryKeyFields(), childAlias, buf);
         }
         else
         {
            String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath());
            SQLUtil.getIsNullClause(!not, cmrField.getTableKeyFields(), relationTableAlias, buf);
         }
         return;
      }

      if(not)
      {
         buf.append(SQLUtil.NOT);
      }
      buf.append(SQLUtil.EXISTS).append('(');

      if(cmrField.getRelationMetaData().isForeignKeyMappingStyle())
      {
         JDBCEntityBridge childEntity = (JDBCEntityBridge) cmrField.getRelatedEntity();
         String childAlias = aliasManager.getAlias(path.getPath());

         buf.append(SQLUtil.SELECT);

         SQLUtil.getColumnNamesClause(childEntity.getPrimaryKeyFields(), childAlias, buf)
            .append(SQLUtil.FROM)
            .append(childEntity.getQualifiedTableName()).append(' ').append(childAlias)
            .append(SQLUtil.WHERE);
         SQLUtil.getJoinClause(cmrField, parentAlias, childAlias, buf);
      }
      else
      {
         String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath());
         buf.append(SQLUtil.SELECT);
         SQLUtil.getColumnNamesClause(cmrField.getTableKeyFields(), relationTableAlias, buf)
            .append(SQLUtil.FROM)
            .append(cmrField.getQualifiedTableName())
            .append(' ')
            .append(relationTableAlias)
            .append(SQLUtil.WHERE);
         SQLUtil.getRelationTableJoinClause(cmrField, parentAlias, relationTableAlias, buf);
      }

      buf.append(')');
   }

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

      for(int childNode = 2; childNode < node.jjtGetNumChildren(); childNode++)
      {
         Node temp = node.jjtGetChild(childNode);
         if(temp instanceof ASTWhere)
         {
            whereNode = temp;
         }
         else if(temp instanceof ASTOrderBy)
         {
            orderByNode = temp;
         }
         else if(temp instanceof ASTLimitOffset)
         {
            limitNode = temp;
         }
      }

      // translate select and add it to the buffer
      StringBuffer select = new StringBuffer();
      selectNode.jjtAccept(this, select);

      // conditional term paths from the where clause are treated separately from those
      // in select, from and order by.
      // TODO come up with a nicer treatment implementation.
      Set selectJoinPaths = new HashSet(ctermJoinPaths);
      Map selectCollectionMemberJoinPaths = new HashMap(ctermCollectionMemberJoinPaths);
      Map selectLeftJoinPaths = new HashMap(ctermLeftJoinPaths);

      // translate where and save results to append later
      StringBuffer where = new StringBuffer();
      if(whereNode != null)
      {
         whereNode.jjtAccept(this, where);
      }

      // reassign conditional term paths and add paths from order by and from to select paths
      ctermJoinPaths = selectJoinPaths;
      ctermCollectionMemberJoinPaths = selectCollectionMemberJoinPaths;
      ctermLeftJoinPaths = selectLeftJoinPaths;

      // translate order by and save results to append later
      StringBuffer orderBy = new StringBuffer();
      if(orderByNode != null)
      {
         orderByNode.jjtAccept(this, orderBy);

         // hack alert - this should use the visitor approach
         for(int i = 0; i < orderByNode.jjtGetNumChildren(); i++)
         {
            Node orderByPath = orderByNode.jjtGetChild(i);
            ASTPath path = (ASTPath) orderByPath.jjtGetChild(0);
            if(!isSelected(path))
            {
               select.append(SQLUtil.COMMA);
               path.jjtAccept(this, select);
            }
         }
      }

      if(limitNode != null)
      {
         limitNode.jjtAccept(this, null);
      }

      StringBuffer from = new StringBuffer(50);
      fromNode.jjtAccept(this, from);

      StringBuffer fromThetaJoin = new StringBuffer();
      createThetaJoin(fromThetaJoin);

      if(where.length() != 0 && fromThetaJoin.length() != 0)
      {
         where.insert(0, '(')
            .append(')')
            .append(SQLUtil.AND)
            .append(fromThetaJoin);
      }
      else if(fromThetaJoin.length() != 0)
      {
         where.append(fromThetaJoin.toString());
      }

      selectDistinct = isDistinct(selectNode);

      // select size
      if(lazyResultSetLoading)
      {
         StringBuffer buf = new StringBuffer(200);
         if(isSelectEntity())
         {
            final JDBCFieldBridge[] pkFields = getSelectEntity().getPrimaryKeyFields();
            if(pkFields.length == 1)
            {
               buf.append('(').append(SQLUtil.SELECT).append("count(");
               if(selectDistinct)
               {
                  buf.append(SQLUtil.DISTINCT);
               }
               SQLUtil.getColumnNamesClause(pkFields, selectAlias, buf);
               buf.append(')').append(SQLUtil.FROM);
               buf.append(from);
               if(where.length() > 0)
               {
                  buf.append(SQLUtil.WHERE).append(where);
               }
               buf.append("), ");
               select.insert(0, buf);
            }
            else
            {
               buf.append('(').append(SQLUtil.SELECT).append("count(*)").append(SQLUtil.FROM)
                  .append('(')
                  .append(SQLUtil.SELECT);

               if(selectDistinct)
               {
                  buf.append(SQLUtil.DISTINCT);
               }

               SQLUtil.getColumnNamesClause(pkFields, selectAlias, buf);
               buf.append(SQLUtil.FROM).append(from);

               if(where.length() > 0)
               {
                  buf.append(SQLUtil.WHERE).append(where);
               }
               buf.append(") t_count), ");
               select.insert(0, buf);
            }
         }
         else if(isSelectField())
         {
            buf.append('(').append(SQLUtil.SELECT).append("count(");
            if(selectDistinct)
            {
               buf.append(SQLUtil.DISTINCT);
            }
            buf.append(select).append(')').append(SQLUtil.FROM);
            buf.append(from);
            if(where.length() > 0)
            {
               buf.append(SQLUtil.WHERE).append(where);
            }
            buf.append("), ");
            select.insert(0, buf);
         }
      }

      // distinct
      if(selectDistinct)
      {
         select.insert(0, SQLUtil.DISTINCT);
      }

      StringBuffer buf = (StringBuffer) data;
      if(selectManager.getMetaData().hasRowLocking())
      {
         JDBCFunctionMappingMetaData rowLockingTemplate = typeMapping.getRowLockingTemplate();
         Object args[] = new Object[]{
            select,
            from,
            where.length() == 0 ? null : where,
            orderBy.length() == 0 ? null : orderBy
         };
         rowLockingTemplate.getFunctionSql(args, buf);
      }
      else
      {
         buf.append(SQLUtil.SELECT)
            .append(select)
            .append(SQLUtil.FROM)
            .append(from);

         if(where.length() > 0)
         {
            buf.append(SQLUtil.WHERE)
               .append(where);
         }

         if(orderBy.length() != 0)
         {
            buf.append(SQLUtil.ORDERBY)
               .append(orderBy);
         }
      }

      // todo: ...
      if(countCompositePk)
      {
         buf.insert(0, "SELECT COUNT(*) FROM (").append(") t_count");
      }

      return buf;
   }

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

      node.jjtGetChild(0).jjtAccept(this, buf);
      for(int i = 1; i < node.jjtGetNumChildren(); i++)
      {
         buf.append(SQLUtil.COMMA);
         node.jjtGetChild(i).jjtAccept(this, buf);
      }

      // add all the additional path tables
      if(!allJoinPaths.isEmpty())
      {
         for(Iterator iter = allJoinPaths.iterator(); iter.hasNext();)
         {
            ASTPath path = (ASTPath) iter.next();
            for(int i = 0; i < path.size(); i++)
            {
               declareTables(path, i, buf);
            }
         }
      }

      // add all parent paths for collection member join paths
      if(!allCollectionMemberJoinPaths.isEmpty())
      {
         for(Iterator iter = allCollectionMemberJoinPaths.values().iterator(); iter.hasNext();)
         {
            ASTPath path = (ASTPath) iter.next();
            // don't declare the last one as the first path was left joined
            for(int i = 0; i < path.size() - 1; i++)
            {
               declareTables(path, i, buf);
            }
         }
      }

      // get all the left joined paths
      if(!allLeftJoinPaths.isEmpty())
      {
         Set allLeftJoins = new HashSet();
         for(Iterator iter = allLeftJoinPaths.values().iterator(); iter.hasNext();)
         {
            allLeftJoins.addAll((Set) iter.next());
         }

         // add all parent paths for left joins
         for(Iterator iter = allLeftJoins.iterator(); iter.hasNext();)
         {
            ASTPath path = (ASTPath) iter.next();
            // don't declare the last one as the first path was left joined
            for(int i = 0; i < path.size() - 1; i++)
            {
               declareTables(path, i, buf);
            }
         }
      }

      return buf;
   }

   private void declareTables(ASTPath path, int i, StringBuffer buf)
   {
      if(!path.isCMRField(i) || declaredPaths.contains(path.getPath(i)))
      {
         return;
      }

      JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField(i);
      JDBCEntityBridge entity = (JDBCEntityBridge) path.getEntity(i);

      buf.append(SQLUtil.COMMA)
         .append(entity.getQualifiedTableName())
         .append(' ')
         .append(aliasManager.getAlias(path.getPath(i)));
      leftJoins(path.getPath(i), buf);

      if(cmrField.getRelationMetaData().isTableMappingStyle())
      {
         String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath(i));
         buf.append(SQLUtil.COMMA)
            .append(cmrField.getQualifiedTableName())
            .append(' ')
            .append(relationTableAlias);
      }

      declaredPaths.add(path.getPath(i));
   }

   private void leftJoins(String parentPath, StringBuffer buf)
   {
      Set paths = (Set) ctermLeftJoinPaths.get(parentPath);
      if(subquerySupported || paths == null)
      {
         return;
      }

      for(Iterator iter = paths.iterator(); iter.hasNext();)
      {
         ASTPath path = (ASTPath) iter.next();

         JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField();
         String parentAlias = aliasManager.getAlias(parentPath);

         if(cmrField.getRelationMetaData().isForeignKeyMappingStyle())
         {
            JDBCEntityBridge childEntity = (JDBCEntityBridge) cmrField.getRelatedEntity();
            String childAlias = aliasManager.getAlias(path.getPath());

            buf.append(SQLUtil.LEFT_JOIN)
               .append(childEntity.getQualifiedTableName())
               .append(' ')
               .append(childAlias)
               .append(SQLUtil.ON);
            SQLUtil.getJoinClause(cmrField, parentAlias, childAlias, buf);
         }
         else
         {
            String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath());
            buf.append(SQLUtil.LEFT_JOIN)
               .append(cmrField.getQualifiedTableName())
               .append(' ')
               .append(relationTableAlias)
               .append(SQLUtil.ON);
            SQLUtil.getRelationTableJoinClause(cmrField, parentAlias, relationTableAlias, buf);
         }
      }
   }

   private void createThetaJoin(StringBuffer buf)
   {
      Set joinedAliases = new HashSet();
      // add all the additional path tables
      if(!ctermJoinPaths.isEmpty())
      {
         for(Iterator iter = ctermJoinPaths.iterator(); iter.hasNext();)
         {
            ASTPath path = (ASTPath) iter.next();
            for(int i = 0; i < path.size(); i++)
            {
               createThetaJoin(path, i, joinedAliases, buf);
            }
         }
      }

      // add all the collection member path tables
      if(!ctermCollectionMemberJoinPaths.isEmpty())
      {
         for(Iterator iter = ctermCollectionMemberJoinPaths.entrySet().iterator(); iter.hasNext();)
         {
            Map.Entry entry = (Map.Entry) iter.next();
            String childAlias = (String) entry.getKey();
            ASTPath path = (ASTPath) entry.getValue();

            // join the memeber path
            createThetaJoin(path, path.size() - 1, joinedAliases, childAlias, buf);

            // join the memeber path parents
            for(int i = 0; i < path.size() - 1; i++)
            {
               createThetaJoin(path, i, joinedAliases, buf);
            }
         }
      }

      // get all the left joined paths
      if(!ctermLeftJoinPaths.isEmpty())
      {
         Set allLeftJoins = new HashSet();
         for(Iterator iter = ctermLeftJoinPaths.values().iterator(); iter.hasNext();)
         {
            allLeftJoins.addAll((Set) iter.next());
         }

         // add all parent paths for left joins
         for(Iterator iter = allLeftJoins.iterator(); iter.hasNext();)
         {
            ASTPath path = (ASTPath) iter.next();
            // don't declare the last one as the first path was left joined
            for(int i = 0; i < path.size() - 1; i++)
            {
               createThetaJoin(path, i, joinedAliases, buf);
            }
         }
      }
   }

   private void createThetaJoin(ASTPath path,
                                int i,
                                Set joinedAliases,
                                StringBuffer buf)
   {
      String childAlias = aliasManager.getAlias(path.getPath(i));
      createThetaJoin(path, i, joinedAliases, childAlias, buf);
   }

   private void createThetaJoin(ASTPath path,
                                int i,
                                Set joinedAliases,
                                String childAlias,
                                StringBuffer buf)
   {
      if(!path.isCMRField(i) || joinedAliases.contains(childAlias))
      {
         return;
      }

      JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField(i);
      String parentAlias = aliasManager.getAlias(path.getPath(i - 1));

      if(joinedAliases.size() > 0)
      {
         buf.append(SQLUtil.AND);
      }

      if(cmrField.getRelationMetaData().isForeignKeyMappingStyle())
      {
         SQLUtil.getJoinClause(cmrField, parentAlias, childAlias, buf);
      }
      else
      {
         String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath(i));

         // parent to relation table
         SQLUtil.getRelationTableJoinClause(cmrField, parentAlias, relationTableAlias, buf)
            .append(SQLUtil.AND);
         // child to relation table
         SQLUtil.getRelationTableJoinClause(cmrField.getRelatedCMRField(), childAlias, relationTableAlias, buf);
      }

      joinedAliases.add(childAlias);
   }


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

      // first arg is a collection valued path
      ASTPath path = (ASTPath) node.jjtGetChild(0);

      // add this path to the list of declared paths
      declaredPaths.add(path.getPath());

      // get the entity at the end of this path
      JDBCEntityBridge entity = (JDBCEntityBridge) path.getEntity();

      // second arg is the identifier
      ASTIdentifier id = (ASTIdentifier) node.jjtGetChild(1);

      // get the alias
      String alias = aliasManager.getAlias(id.identifier);

      // add this path to the list of join paths so parent paths will be joined
      addCollectionMemberJoinPath(alias, path);

      // declare the alias mapping
      aliasManager.addAlias(path.getPath(), alias);

      buf.append(entity.getQualifiedTableName());
      buf.append(' ');
      buf.append(alias);
      leftJoins(path.getPath(), buf);

      if(onFindCMRJoin != null && alias.equals(selectAlias))
      {
         buf.append(onFindCMRJoin);
         onFindCMRJoin = null;
      }

      // add the relation-table
      JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField();
      if(cmrField.getRelationMetaData().isTableMappingStyle())
      {
         String relationTableAlias = aliasManager.getRelationTableAlias(path.getPath());
         buf.append(SQLUtil.COMMA)
            .append(cmrField.getQualifiedTableName())
            .append(' ')
            .append(relationTableAlias);
      }

      return buf;
   }

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

      ASTAbstractSchema schema = (ASTAbstractSchema) node.jjtGetChild(0);
      JDBCEntityBridge entity = (JDBCEntityBridge) schema.entity;
      ASTIdentifier id = (ASTIdentifier) node.jjtGetChild(1);

      String alias = aliasManager.getAlias(id.identifier);
      buf.append(entity.getQualifiedTableName())
         .append(' ')
         .append(alias);
      leftJoins(id.identifier, buf);

      if(onFindCMRJoin != null && alias.equals(selectAlias))
      {
         buf.append(onFindCMRJoin);
         onFindCMRJoin = null;
      }

      return buf;
   }

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

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

         if(path.isCMPField())
         {
            // set the select object
            JDBCCMPFieldBridge selectField = (JDBCCMPFieldBridge) path.getCMPField();
            selectManager = (JDBCStoreManager) selectField.getManager();
            selectObject = selectField;
            setTypeFactory(selectManager.getJDBCTypeFactory());
            addJoinPath(path);
            selectAlias = aliasManager.getAlias(path.getPath(path.size() - 2));
            SQLUtil.getColumnNamesClause(selectField, selectAlias, buf);
         }
         else
         {
            // set the select object
            JDBCEntityBridge selectEntity = (JDBCEntityBridge) path.getEntity();
            selectManager = (JDBCStoreManager) selectEntity.getManager();
            selectObject = selectEntity;
            setTypeFactory(selectManager.getJDBCTypeFactory());
            selectEntity(path, node.distinct, buf);
         }
      }
      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())
         {
            JDBCCMPFieldBridge selectField = (JDBCCMPFieldBridge) path.getCMPField();
            selectManager = (JDBCStoreManager) selectField.getManager();
            if(selectField.getJDBCType().hasMapper())
               this.functionJDBCType = selectField.getJDBCType();
         }
         else if(path.isCMRField())
         {
            JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField();
            selectManager = (JDBCStoreManager) cmrField.getEntity().getManager();
            addJoinPath(path);
         }
         else
         {
            final JDBCEntityBridge entity = (JDBCEntityBridge) path.getEntity();
            selectManager = (JDBCStoreManager) entity.getManager();
            addJoinPath(path);
         }

         setTypeFactory(selectManager.getJDBCTypeFactory());
         selectObject = child0;
         child0.jjtAccept(this, buf);
      }

      return buf;
   }

   /**
    * Generates where clause without the "WHERE" keyword.
    */
   public Object visit(ASTWhere node, Object data)
   {
      node.jjtGetChild(0).jjtAccept(this, data);
      return data;
   }

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

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

         if(path.isCMRField())
         {
            JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) path.getCMRField();
            if(cmrField.getRelationMetaData().isTableMappingStyle())
            {
               existsClause(path, buf, !node.not);
               return buf;
            }
         }

         String alias = aliasManager.getAlias(path.getPath(path.size() - 2));
         JDBCFieldBridge field = (JDBCFieldBridge) path.getField();

         // if jdbc type is null then it should be a cmr field in
         // a one-to-one mapping that isn't a foreign key.
         // handle it the way the IS EMPTY on the one side of one-to-many
         // relationship is handled
         if(field.getJDBCType() == null)
         {
            existsClause(path, buf, !node.not);
            return buf;
         }

         // check the path for cmr fields and add them to join paths
         if(path.fieldList.size() > 2)
         {
            for(int i = 0; i < path.fieldList.size(); ++i)
            {
               Object pathEl = path.fieldList.get(i);
               if(pathEl instanceof JDBCCMRFieldBridge)
               {
                  addJoinPath(path);
                  break;
               }
            }
         }

         buf = SQLUtil.getIsNullClause(node.not, field, alias, buf);
      }
      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);

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

      return buf;
   }

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

      existsClause(path, buf, !node.not);
      return buf;
   }

   /**
    * Compare entity
    */
   public Object visit(ASTMemberOf node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;

      // setup compare to vars first, so we can compre types in from vars
      ASTPath toPath = (ASTPath) node.jjtGetChild(1);

      JDBCCMRFieldBridge toCMRField = (JDBCCMRFieldBridge) toPath.getCMRField();

      JDBCEntityBridge toChildEntity = (JDBCEntityBridge) toPath.getEntity();

      String pathStr = toPath.getPath(toPath.size() - 2);
      String toParentAlias = aliasManager.getAlias(pathStr);
      String toChildAlias = aliasManager.getAlias(toPath.getPath());
      String relationTableAlias = null;
      if(toCMRField.getRelationMetaData().isTableMappingStyle())
      {
         relationTableAlias = aliasManager.getRelationTableAlias(toPath.getPath());
      }

      // setup from variables
      String fromAlias = null;
      int fromParamNumber = -1;
      if(node.jjtGetChild(0) instanceof ASTParameter)
      {
         ASTParameter fromParam = (ASTParameter) node.jjtGetChild(0);

         // can only compare like kind entities
         verifyParameterEntityType(fromParam.number, toChildEntity);

         fromParamNumber = fromParam.number;
      }
      else
      {
         ASTPath fromPath = (ASTPath) node.jjtGetChild(0);
         addJoinPath(fromPath);

         JDBCEntityBridge fromEntity = (JDBCEntityBridge) fromPath.getEntity();
         fromAlias = aliasManager.getAlias(fromPath.getPath());

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

      // add the path to the list of paths to left join
      addLeftJoinPath(pathStr, toPath);

      // first part makes toChild not in toParent.child
      if(!subquerySupported)
      {
         addJoinPath(toPath);

         // subquery not supported; use a left join and is not null
         if(node.not)
         {
            buf.append(SQLUtil.NOT);
         }
         buf.append('(');

         if(relationTableAlias == null)
         {
            SQLUtil.getIsNullClause(true, toChildEntity.getPrimaryKeyFields(), toChildAlias, buf);
         }
         else
         {
            SQLUtil.getIsNullClause(true, toCMRField.getTableKeyFields(), relationTableAlias, buf);
         }
      }
      else
      {
         // subquery supported; use exists subquery
         if(node.not)
         {
            buf.append(SQLUtil.NOT);
         }

         buf.append(SQLUtil.EXISTS).append('(');

         if(relationTableAlias == null)
         {
            buf.append(SQLUtil.SELECT);
            SQLUtil.getColumnNamesClause(toChildEntity.getPrimaryKeyFields(), toChildAlias, buf)
               .append(SQLUtil.FROM)
               .append(toChildEntity.getQualifiedTableName())
               .append(' ')
               .append(toChildAlias)
               .append(SQLUtil.WHERE);
            SQLUtil.getJoinClause(toCMRField, toParentAlias, toChildAlias, buf);
         }
         else
         {
            buf.append(SQLUtil.SELECT);
            SQLUtil.getColumnNamesClause(toCMRField.getRelatedCMRField().getTableKeyFields(), relationTableAlias, buf)
               .append(SQLUtil.FROM)
               .append(toCMRField.getQualifiedTableName())
               .append(' ')
               .append(relationTableAlias)
               .append(SQLUtil.WHERE);
            SQLUtil.getRelationTableJoinClause(toCMRField, toParentAlias, relationTableAlias, buf);
         }
      }

      buf.append(SQLUtil.AND);

      // second part makes fromNode equal toChild
      if(fromAlias != null)
      {
         // compre pk to pk
         if(relationTableAlias == null)
         {
            SQLUtil.getSelfCompareWhereClause(toChildEntity.getPrimaryKeyFields(),
               toChildAlias,
               fromAlias,
               buf);
         }
         else
         {
            SQLUtil.getRelationTableJoinClause(toCMRField.getRelatedCMRField(),
               fromAlias,
               relationTableAlias,
               buf);
         }
      }
      else
      {
         // add the parameters
         inputParameters.addAll(QueryParameter.createParameters(fromParamNumber - 1,
            toChildEntity));

         // compare pk to parameter
         if(relationTableAlias == null)
         {
            SQLUtil.getWhereClause(toChildEntity.getPrimaryKeyFields(), toChildAlias, buf);
         }
         else
         {
            SQLUtil.getWhereClause(toCMRField.getRelatedCMRField().getTableKeyFields(),
               relationTableAlias,
               buf);
         }
      }

      buf.append(')');

      return buf;
   }

   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);
      addJoinPath(fromPath);
      String fromAlias = aliasManager.getAlias(fromPath.getPath(fromPath.size() - 2));
      JDBCCMPFieldBridge fromCMPField = (JDBCCMPFieldBridge) 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;
         addJoinPath(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(')');
   }

   /**
    * compreEntity(arg0, arg1)
    */
   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 buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTConcat node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.CONCAT);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
         new NodeStringWrapper(node.jjtGetChild(1)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTSubstring node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.SUBSTRING);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
         new NodeStringWrapper(node.jjtGetChild(1)),
         new NodeStringWrapper(node.jjtGetChild(2)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTLCase node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LCASE);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTUCase node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.UCASE);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTLength node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LENGTH);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTLocate node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;

      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.LOCATE);
      Object[] args = new Object[3];
      args[0] = new NodeStringWrapper(node.jjtGetChild(0));
      args[1] = new NodeStringWrapper(node.jjtGetChild(1));
      if(node.jjtGetNumChildren() == 3)
      {
         args[2] = new NodeStringWrapper(node.jjtGetChild(2));
      }
      else
      {
         args[2] = "1";
      }

      // add the sql to the current buffer
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTAbs node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.ABS);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTMod node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.MOD);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0)),
         new NodeStringWrapper(node.jjtGetChild(1)),
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   /**
    * Type-mapping function translation
    */
   public Object visit(ASTSqrt node, Object data)
   {
      StringBuffer buf = (StringBuffer) data;
      JDBCFunctionMappingMetaData function = typeMapping.getFunctionMapping(JDBCTypeMappingMetaData.SQRT);
      Object[] args = new Object[]{
         new NodeStringWrapper(node.jjtGetChild(0))
      };
      function.getFunctionSql(args, buf);
      return buf;
   }

   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, new NodeStringWrapper(cntPath)};
      }
      else
      {
         JDBCEntityBridge entity = (JDBCEntityBridge) cntPath.getEntity();
         final JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
         if(pkFields.length > 1)
         {
            countCompositePk = true;
            forceDistinct = node.distinct.length() > 0;
            selectEntity(cntPath, forceDistinct, 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()};
         }
      }

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

   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,
         new NodeStringWrapper(node.jjtGetChild(0))
      };
      return JDBCTypeMappingMetaData.MAX_FUNC.getFunctionSql(args, buf);
   }

   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,
         new NodeStringWrapper(node.jjtGetChild(0))
      };
      return JDBCTypeMappingMetaData.MIN_FUNC.getFunctionSql(args, buf);
   }

   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,
         new NodeStringWrapper(node.jjtGetChild(0)),
      };
      return JDBCTypeMappingMetaData.AVG_FUNC.getFunctionSql(args, buf);
   }

   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,
         new NodeStringWrapper(node.jjtGetChild(0))
      };
      return JDBCTypeMappingMetaData.SUM_FUNC.getFunctionSql(args, buf);
   }

   /**
    * tableAlias.columnName
    */
   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.");
      }

      JDBCCMPFieldBridge cmpField = (JDBCCMPFieldBridge) 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.");
      }

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

   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(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 buf;
   }

   /**
    * typeMapping.get<True/False>Mapping()
    */
   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(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 UnsupportedOperationException("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 UnsupportedOperationException("LIMIT parameter must be an int");
            }
            limitParam = param.number;
         }
         else
         {
            ASTExactNumericLiteral param = (ASTExactNumericLiteral) limitNode;
            limitValue = (int) param.value;
         }
      }
      return data;
   }

   public Object visit(ASTWhereConditionalTerm node, Object data)
   {
      // clear per term paths
      clearPerTermJoinPaths();

      StringBuffer buf = (StringBuffer) data;
      buf.append('(');
      for(int i = 0; i < node.jjtGetNumChildren(); ++i)
      {
         node.jjtGetChild(i).jjtAccept(this, data);
      }

      StringBuffer thetaJoin = new StringBuffer();
      createThetaJoin(thetaJoin);

      if(thetaJoin.length() > 0)
      {
         buf.append(SQLUtil.AND).append(thetaJoin.toString());
      }

      buf.append(')');
      return data;
   }

   /**
    * Wrap a node with a class that when ever toString is called visits the
    * node.  This is used by the function implmentations, for parameters.
    * <p/>
    * Be careful with this class because it visits the node for each call of
    * toString, which could have undesireable result if called multiple times.
    */
   private final class NodeStringWrapper
   {
      final Node node;

      public NodeStringWrapper(Node node)
      {
         this.node = node;
      }

      public String toString()
      {
         return node.jjtAccept(JDBCEJBQLCompiler.this, new StringBuffer()).toString();
      }
   }

   /**
    * 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;
   }

   /**
    * Checks whether the path passed in is already in the SELECT clause.
    *
    * @param path the path to check.
    * @return true  if the path is already in the SELECT clause.
    */
   private boolean isSelected(ASTPath path)
   {
      boolean selected = false;

      CMPFieldBridge cmpField = path.getCMPField();
      if(selectObject instanceof JDBCCMPFieldBridge && cmpField == selectObject)
      {
         selected = true;
      }
      else if(selectObject instanceof JDBCEntityBridge)
      {
         JDBCEntityBridge entity = (JDBCEntityBridge) selectObject;
         JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
         for(int pkInd = 0; pkInd < pkFields.length; ++pkInd)
         {
            if(pkFields[pkInd] == cmpField)
            {
               selected = true;
               break;
            }
         }
      }
      else if(selectObject instanceof SelectFunction)
      {
         Node funcNode = (Node) selectObject;
         ASTPath fieldPath = getPathFromChildren(funcNode);
         if(fieldPath.getCMPField() == cmpField)
         {
            selected = true;
         }
      }

      return selected;
   }

   private void selectEntity(ASTPath path, boolean distinct, StringBuffer buf)
   {
      JDBCEntityBridge selectEntity = (JDBCEntityBridge) path.getEntity();

      StringBuffer columnNamesClause = new StringBuffer(200);
      addJoinPath(path);
      selectAlias = aliasManager.getAlias(path.getPath());

      // get a list of all fields to be loaded
      // get the identifier for this field
      SQLUtil.getColumnNamesClause(selectEntity.getPrimaryKeyFields(), selectAlias, columnNamesClause);

      if(readAhead.isOnFind())
      {
         String eagerLoadGroupName = readAhead.getEagerLoadGroup();
         boolean[] loadGroupMask = selectEntity.getLoadGroupMask(eagerLoadGroupName);
         if(distinct)
            SQLUtil.appendSearchableColumnNamesClause(selectEntity.getTableFields(), loadGroupMask, selectAlias, columnNamesClause);
         else
            SQLUtil.appendColumnNamesClause(selectEntity.getTableFields(), loadGroupMask, selectAlias, columnNamesClause);

         try
         {
            leftJoinCMRList = JDBCAbstractQueryCommand.getLeftJoinCMRNodes(
               selectEntity, path.getPath(), readAhead.getLeftJoins(), declaredPaths);
         }
         catch(DeploymentException e)
         {
            throw new IllegalStateException(e.getMessage());
         }

         if(!leftJoinCMRList.isEmpty())
         {
            onFindCMRJoin = new StringBuffer(100);
            JDBCAbstractQueryCommand.leftJoinCMRNodes(selectAlias, leftJoinCMRList, aliasManager, onFindCMRJoin);
            JDBCAbstractQueryCommand.appendLeftJoinCMRColumnNames(leftJoinCMRList, aliasManager, columnNamesClause);
         }
      }
      buf.append(columnNamesClause);
   }

   private void addJoinPath(ASTPath path)
   {
      ctermJoinPaths.add(path);
      allJoinPaths.add(path);
   }

   private void addCollectionMemberJoinPath(String alias, ASTPath path)
   {
      ctermCollectionMemberJoinPaths.put(alias, path);
      allCollectionMemberJoinPaths.put(alias, path);
   }

   private void addLeftJoinPath(String pathStr, ASTPath path)
   {
      Set set = (Set) ctermLeftJoinPaths.get(pathStr);
      if(set == null)
      {
         set = new HashSet();
         ctermLeftJoinPaths.put(pathStr, set);
      }
      set.add(path);

      set = (Set) allLeftJoinPaths.get(pathStr);
      if(set == null)
      {
         set = new HashSet();
         allLeftJoinPaths.put(pathStr, set);
      }
      set.add(path);
   }

   private void clearPerTermJoinPaths()
   {
      ctermJoinPaths.clear();
      ctermCollectionMemberJoinPaths.clear();
      ctermLeftJoinPaths.clear();
   }

   private boolean isDistinct(Node selectNode)
   {
      return ((ASTSelect) selectNode).distinct || returnType.equals(Set.class) || forceDistinct;
   }
}
