/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.    
 */
package org.apache.openjpa.jdbc.sql;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 java.util.SortedMap;
import java.util.Stack;
import java.util.TreeMap;

import org.apache.commons.collections.iterators.EmptyIterator;
import org.apache.commons.lang.StringUtils;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.kernel.EagerFetchModes;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCLockManager;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.kernel.JDBCStoreManager;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.Joinable;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import org.apache.openjpa.jdbc.meta.strats.RelationStrategies;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.InternalException;

/**
 * Standard {@link Select} implementation. Usage note: though this class
 * implements {@link Joins}, it should not be used for joining directly.
 * Instead, use the return value of {@link #newJoins}.
 *
 * @author Abe White
 * @nojavadoc
 */
public class SelectImpl
    implements Select, PathJoins {

    private static final int NONAUTO_DISTINCT = 2 << 0;
    private static final int DISTINCT = 2 << 1;
    private static final int NOT_DISTINCT = 2 << 2;
    private static final int IMPLICIT_DISTINCT = 2 << 3;
    private static final int TO_MANY = 2 << 4;
    private static final int AGGREGATE = 2 << 5;
    private static final int LOB = 2 << 6;
    private static final int OUTER = 2 << 7;
    private static final int LRS = 2 << 8;
    private static final int EAGER_TO_ONE = 2 << 9;
    private static final int EAGER_TO_MANY = 2 << 10;
    private static final int RECORD_ORDERED = 2 << 11;
    private static final int GROUPING = 2 << 12;
    private static final int FORCE_COUNT = 2 << 13;

    private static final String[] TABLE_ALIASES = new String[16];
    private static final String[] ORDER_ALIASES = new String[16];
    private static final Object[] NULL_IDS = new Object[16];
    private static final Object[] PLACEHOLDERS = new Object[50];

    private static final Localizer _loc = Localizer.forPackage(Select.class);

    static {
        for (int i = 0; i < TABLE_ALIASES.length; i++)
            TABLE_ALIASES[i] = "t" + i;
        for (int i = 0; i < ORDER_ALIASES.length; i++)
            ORDER_ALIASES[i] = "o" + i;
        for (int i = 0; i < NULL_IDS.length; i++)
            NULL_IDS[i] = new NullId();
        for (int i = 0; i < PLACEHOLDERS.length; i++)
            PLACEHOLDERS[i] = new Placeholder();
    }

    private final JDBCConfiguration _conf;
    private final DBDictionary _dict;

    // map of variable + relation path + table keys to the correct alias index:
    // each relation path/table combination should have a unique alias because
    // it represents a separate object; for example, if a Person class has a
    // 'parent' field representing another Person and also has an 'address'
    // field of type Address:
    // 'address.street' should map to a different table alias than
    // 'parent.address.street' for the purposes of comparisons
    private Map _aliases = null;

    // map of indexes to table aliases like 'TABLENAME t0'
    private SortedMap _tables = null;

    // combined list of selected ids and map of each id to its alias
    protected final Selects _selects = newSelects();
    private List _ordered = null;
    private List _grouped = null;

    // flags
    private int _flags = 0;
    private int _joinSyntax = 0;
    private long _startIdx = 0;
    private long _endIdx = Long.MAX_VALUE;
    private int _nullIds = 0;
    private int _orders = 0;
    private int _placeholders = 0;
    private int _expectedResultCount = 0;

    // query clauses
    private SQLBuffer _ordering = null;
    private SQLBuffer _where = null;
    private SQLBuffer _grouping = null;
    private SQLBuffer _having = null;
    private SQLBuffer _full = null;

    // joins to add to the end of our where clause, and joins to prepend to
    // all selects (see select(classmapping) method)
    private SelectJoins _joins = null;
    private Stack _preJoins = null;

    // map of joins+keys to eager selects and global set of eager keys; the
    // same key can't be used more than once
    private Map _eager = null;
    private Set _eagerKeys = null;

    // subselect support
    private List<SelectImpl> _subsels = null;
    private SelectImpl _parent = null;
    private String _subPath = null;
    private boolean _hasSub = false;

    // from select if this select selects from a tmp table created by another
    private SelectImpl _from = null;
    protected SelectImpl _outer = null;

    // JPQL Query context this select is associated with
    private Context _ctx = null;

    // A path navigation is begin with this schema alias
    private String _schemaAlias = null;
    private ClassMapping _tpcMeta = null;
    private List _joinedTables = null;
    private List _exJoinedTables = null;
    
    public ClassMapping getTablePerClassMeta() {
        return _tpcMeta;
    }
    public void setTablePerClassMeta(ClassMapping meta) {
        _tpcMeta = meta;
    }
    
    public void setJoinedTableClassMeta(List meta) {
        _joinedTables = meta;
    }

    public List getJoinedTableClassMeta() {
        return _joinedTables;
    }
    
    public void setExcludedJoinedTableClassMeta(List meta) {
        _exJoinedTables = meta;
    }

    public List getExcludedJoinedTableClassMeta() {
        return _exJoinedTables;
    }
    
     
    /**
     * Helper method to return the proper table alias for the given alias index.
     */
    static String toAlias(int index) {
        if (index == -1)
            return null;
        if (index < TABLE_ALIASES.length)
            return TABLE_ALIASES[index];
        return "t" + index;
    }

    /**
     * Helper method to return the proper order alias for the given order
     * column index.
     */
    public static String toOrderAlias(int index) {
        if (index == -1)
            return null;
        if (index < ORDER_ALIASES.length)
            return ORDER_ALIASES[index];
        return "o" + index;
    }

    /**
     * Constructor. Supply configuration.
     */
    public SelectImpl(JDBCConfiguration conf) {
        _conf = conf;
        _dict = _conf.getDBDictionaryInstance();
        _joinSyntax = _dict.joinSyntax;
        _selects._dict = _dict;
    }

    public void setContext(Context context) {
        if (_ctx == null) {
            _ctx = context;
            _ctx.setSelect(this);
        }
    }

    public Context ctx() {
        return _ctx;
    }

    public void setSchemaAlias(String schemaAlias) {
        _schemaAlias = schemaAlias;
    }

    /////////////////////////////////
    // SelectExecutor implementation
    /////////////////////////////////

    public JDBCConfiguration getConfiguration() {
        return _conf;
    }

    public SQLBuffer toSelect(boolean forUpdate, JDBCFetchConfiguration fetch) {
        _full = _dict.toSelect(this, forUpdate, fetch);
        return _full;
    }
    
    public SQLBuffer getSQL() {
        return _full;
    }

    public SQLBuffer toSelectCount() {
        return _dict.toSelectCount(this);
    }

    public boolean getAutoDistinct() {
        return (_flags & NONAUTO_DISTINCT) == 0;
    }

    public void setAutoDistinct(boolean val) {
        if (val)
            _flags &= ~NONAUTO_DISTINCT;
        else
            _flags |= NONAUTO_DISTINCT;
    }

    public boolean isDistinct() {
        return (_flags & NOT_DISTINCT) == 0 && ((_flags & DISTINCT) != 0
            || ((_flags & NONAUTO_DISTINCT) == 0
            && (_flags & IMPLICIT_DISTINCT) != 0));
    }

    public void setDistinct(boolean distinct) {
        // need two flags in case set not_distinct, then a to-many join happens
        // and distinct flag gets set automatically
        if (distinct) {
            _flags |= DISTINCT;
            _flags &= ~NOT_DISTINCT;
        } else {
            _flags |= NOT_DISTINCT;
            _flags &= ~DISTINCT;
        }
    }

    public boolean isLRS() {
        return (_flags & LRS) != 0;
    }

    public void setLRS(boolean lrs) {
        if (lrs)
            _flags |= LRS;
        else
            _flags &= ~LRS;
    }

    public int getExpectedResultCount() {
        // if the count isn't forced and we have to-many eager joins that could
        // throw the count off, don't pay attention to it
        if ((_flags & FORCE_COUNT) == 0 && hasEagerJoin(true))
            return 0;
        return _expectedResultCount;
    }

    public void setExpectedResultCount(int expectedResultCount, boolean force) {
        _expectedResultCount = expectedResultCount;
        if (force)
            _flags |= FORCE_COUNT;
        else 
            _flags &= ~FORCE_COUNT;
    }

    public int getJoinSyntax() {
        return _joinSyntax;
    }

    public void setJoinSyntax(int joinSyntax) {
        _joinSyntax = joinSyntax;
    }

    public boolean supportsRandomAccess(boolean forUpdate) {
        return _dict.supportsRandomAccessResultSet(this, forUpdate);
    }

    public boolean supportsLocking() {
        return _dict.supportsLocking(this);
    }

    public boolean hasMultipleSelects() {
        if (_eager == null)
            return false;
        Map.Entry entry;
        for (Iterator itr = _eager.entrySet().iterator(); itr.hasNext();) {
            entry = (Map.Entry) itr.next();
            if (entry.getValue() != this)
                return true;
        }
        return false;
    }

    public int getCount(JDBCStore store)
        throws SQLException {
        Connection conn = null;
        PreparedStatement stmnt = null;
        ResultSet rs = null;
        try {
            SQLBuffer sql = toSelectCount();
            conn = store.getNewConnection();
            stmnt = prepareStatement(conn, sql, null, 
                ResultSet.TYPE_FORWARD_ONLY, 
                ResultSet.CONCUR_READ_ONLY, false);
            _dict.setQueryTimeout(stmnt,
                    store.getFetchConfiguration().getQueryTimeout());
            rs = executeQuery(conn, stmnt, sql, false, store);
            int count =  getCount(rs);
             
            return _dict.applyRange(this, count);
        } finally {
            if (rs != null)
                try { rs.close(); } catch (SQLException se) {}
            if (stmnt != null)
                try { stmnt.close(); } catch (SQLException se) {}
            if (conn != null)
                try { conn.close(); } catch (SQLException se) {}
        }
    }

    public Result execute(JDBCStore store, JDBCFetchConfiguration fetch)
        throws SQLException {
        if (fetch == null)
            fetch = store.getFetchConfiguration();
        return execute(store.getContext(), store, fetch,
            fetch.getReadLockLevel());
    }

    public Result execute(JDBCStore store, JDBCFetchConfiguration fetch,
        int lockLevel)
        throws SQLException {
        if (fetch == null)
            fetch = store.getFetchConfiguration();
        return execute(store.getContext(), store, fetch, lockLevel);
    }

    /**
     * Execute this select in the context of the given store manager. The
     * context is passed in separately for profiling purposes.
     */
    protected Result execute(StoreContext ctx, JDBCStore store, 
        JDBCFetchConfiguration fetch, int lockLevel)
        throws SQLException {
        boolean forUpdate = false;
        if (!isAggregate() && _grouping == null) {
            JDBCLockManager lm = store.getLockManager();
            if (lm != null)
                forUpdate = lm.selectForUpdate(this, lockLevel);
        }

        logEagerRelations();
        SQLBuffer sql = toSelect(forUpdate, fetch);
        boolean isLRS = isLRS();
        int rsType = (isLRS && supportsRandomAccess(forUpdate))
            ? -1 : ResultSet.TYPE_FORWARD_ONLY;
        Connection conn = store.getConnection();
        PreparedStatement stmnt = null;
        ResultSet rs = null;
        try {
            if (isLRS) 
                stmnt = prepareStatement(conn, sql, fetch, rsType, -1, true); 
            else
                stmnt = prepareStatement(conn, sql, null, rsType, -1, false);
            
            _dict.setTimeouts(stmnt, fetch, forUpdate);
            
            rs = executeQuery(conn, stmnt, sql, isLRS, store);
        } catch (SQLException se) {
            // clean up statement
            if (stmnt != null)
                try { stmnt.close(); } catch (SQLException se2) {}
            try { conn.close(); } catch (SQLException se2) {}
            throw se;
        }
        return getEagerResult(conn, stmnt, rs, store, fetch, forUpdate, sql);
    }

    /**
     * Execute our eager selects, adding the results under the same keys
     * to the given result.
     */
    private static void addEagerResults(SelectResult res, SelectImpl sel,
        JDBCStore store, JDBCFetchConfiguration fetch)
        throws SQLException {
        if (sel._eager == null)
            return;

        // execute eager selects
        Map.Entry entry;
        Result eres;
        Map eager;
        for (Iterator itr = sel._eager.entrySet().iterator(); itr.hasNext();) {
            entry = (Map.Entry) itr.next();

            // simulated batched selects for inner/outer joins; for separate
            // selects, don't pass on lock level, because they're probably
            // for relations and therefore should use default level
            if (entry.getValue() == sel)
                eres = res;
            else
                eres = ((SelectExecutor) entry.getValue()).execute(store,
                    fetch);

            eager = res.getEagerMap(false);
            if (eager == null) {
                eager = new HashMap();
                res.setEagerMap(eager);
            }
            eager.put(entry.getKey(), eres);
        }
    }


    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of preparing statement.
     */
    protected PreparedStatement prepareStatement(Connection conn, 
        SQLBuffer sql, JDBCFetchConfiguration fetch, int rsType, 
        int rsConcur, boolean isLRS) throws SQLException {
        if (fetch == null)
            return sql.prepareStatement(conn, rsType, rsConcur);
        else
            return sql.prepareStatement(conn, fetch, rsType, -1);
    }
    
    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of preparing statement.
     */
    public PreparedStatement prepareStatement(Connection conn, 
        String sql) throws SQLException {
        return conn.prepareStatement(sql);
    }    
    
    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of executing query.
     */
    protected ResultSet executeQuery(Connection conn, PreparedStatement stmnt, 
        SQLBuffer sql, boolean isLRS, JDBCStore store) throws SQLException {
        return stmnt.executeQuery();
    }
    
    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of executing query.
     */
    public ResultSet executeQuery(Connection conn, PreparedStatement stmnt, 
        String sql, JDBCStore store, Object[] params, Column[] cols) 
        throws SQLException {
        return stmnt.executeQuery();
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of getting count from the result set.
     */
    protected int getCount(ResultSet rs) throws SQLException {
        rs.next();
        return rs.getInt(1);
    }
    
    /**
     * This method is to provide override for non-JDBC or JDBC-like 
     * implementation of executing eager selects.
     */
    public Result getEagerResult(Connection conn, 
        PreparedStatement stmnt, ResultSet rs, JDBCStore store, 
        JDBCFetchConfiguration fetch, boolean forUpdate, SQLBuffer sql) 
        throws SQLException {
        SelectResult res = new SelectResult(conn, stmnt, rs, _dict);
        res.setSelect(this);
        res.setStore(store);
        res.setLocking(forUpdate);
        try {
            addEagerResults(res, this, store, fetch);
        } catch (SQLException se) {
            res.close();
            throw se;
        }
        return res;
    }

    /////////////////////////
    // Select implementation
    /////////////////////////

    public int indexOf() {
        return 0;
    }

    public List getSubselects() {
        return (_subsels == null) ? Collections.EMPTY_LIST : _subsels;
    }

    public Select getParent() {
        return _parent;
    }

    public String getSubselectPath() {
        return _subPath;
    }

    public void setParent(Select parent, String path) {
        if (path != null)
            _subPath = path;
        else
            _subPath = null;

        if (parent == _parent)
            return;
        if (_parent != null)
            _parent._subsels.remove(this);

        //### right now we can't use sql92 joins with subselects, cause
        //### I can't figure out what to do when the subselect has a join
        //### with an alias also present in the outer select... you don't want
        //### the join to appear in the FROM clause of the subselect cause
        //### then it re-aliases both tables in the scope of the subselect
        //### and the correlation with the outer select is lost
        _parent = (SelectImpl) parent;
        if (_parent != null) {
            if (_parent._subsels == null)
                _parent._subsels = new ArrayList(2);
            _parent._subsels.add(this);
            if (_parent._joinSyntax == JoinSyntaxes.SYNTAX_SQL92)
                _joinSyntax = JoinSyntaxes.SYNTAX_TRADITIONAL;
            else
                _joinSyntax = _parent._joinSyntax;
        }
    }
    
    public void setHasSubselect(boolean hasSub) {
        _hasSub = hasSub;
    }
    
    public boolean getHasSubselect() {
        return _hasSub;    
    }
    
    public Map getAliases() {
        return _aliases;
    }
    
    public void removeAlias(Object key) {
        _aliases.remove(key);
    }
    
    public Map getTables() {
        return _tables;
    }
    
    public void removeTable(Object key) {
        _tables.remove(key);
    }

    public Select getFromSelect() {
        return _from;
    }

    public void setFromSelect(Select sel) {
        _from = (SelectImpl) sel;
        if (_from != null)
            _from._outer = this;
    }

    public boolean hasEagerJoin(boolean toMany) {
        if (toMany)
            return (_flags & EAGER_TO_MANY) != 0;
        return (_flags & EAGER_TO_ONE) != 0;
    }

    public boolean hasJoin(boolean toMany) {
        if (toMany)
            return (_flags & TO_MANY) != 0;
        return _tables != null && _tables.size() > 1;
    }

    public boolean isSelected(Table table) {
        PathJoins pj = getJoins(null, false);
        if (_from != null)
            return _from.getTableIndex(table, pj, false) != -1;
        return getTableIndex(table, pj, false) != -1;
    }

    public Collection getTableAliases() {
        return (_tables == null) ? Collections.EMPTY_SET : _tables.values();
    }

    public List getSelects() {
        return Collections.unmodifiableList(_selects);
    }

    public List getSelectAliases() {
        return _selects.getAliases(false, _outer != null);
    }

    public List getIdentifierAliases() {
        return _selects.getAliases(true, _outer != null);
    }

    public SQLBuffer getOrdering() {
        return _ordering;
    }

    public SQLBuffer getGrouping() {
        return _grouping;
    }

    public SQLBuffer getWhere() {
        return _where;
    }

    public SQLBuffer getHaving() {
        return _having;
    }

    public void addJoinClassConditions() {
        if (_joins == null || _joins.joins() == null)
            return;

        // join set iterator allows concurrent modification
        Join j;
        for (Iterator itr = _joins.joins().iterator(); itr.hasNext();) {
            j = (Join) itr.next();
            if (j.getRelationTarget() != null) {
                j.getRelationTarget().getDiscriminator().addClassConditions
                    (this, j.getSubclasses() == SUBS_JOINABLE, 
                    j.getRelationJoins());
                j.setRelation(null, 0, null);
            }
        }
    }

    public Joins getJoins() {
        return _joins;
    }

    public Iterator getJoinIterator() {
        if (_joins == null || _joins.isEmpty())
            return EmptyIterator.INSTANCE;
        return _joins.joins().joinIterator();
    }

    public long getStartIndex() {
        return _startIdx;
    }

    public long getEndIndex() {
        return _endIdx;
    }

    public void setRange(long start, long end) {
        _startIdx = start;
        _endIdx = end;
    }

    public String getColumnAlias(Column col) {
        return getColumnAlias(col, (Joins) null);
    }

    public String getColumnAlias(Column col, Joins joins) {
        return getColumnAlias(col, getJoins(joins, false));
    }

    /**
     * Return the alias for the given column.
     */
    private String getColumnAlias(Column col, PathJoins pj) {
        return getColumnAlias(col.getIdentifier().getName(), col.getTable(), pj);
    }

    public String getColumnAlias(String col, Table table) {
        return getColumnAlias(col, table, (Joins) null);
    }

    public String getColumnAlias(String col, Table table, Joins joins) {
        return getColumnAlias(col, table, getJoins(joins, false));
    }

    /**
     * Return the alias for the give column
     */
    public String getColumnAlias(Column col, Object path) {
        Table table = col.getTable();
        String tableAlias = null;
        Iterator itr = getJoinIterator();
        while (itr.hasNext()) {
            Join join = (Join) itr.next();
            if (join != null) {
                if (join.getTable1() == table)
                    tableAlias = join.getAlias1();
                else if (join.getTable2() == table)
                    tableAlias = join.getAlias2();
                if (tableAlias != null)
                    return new StringBuilder(tableAlias).append(".").
                        append(_dict.getNamingUtil().toDBName(col.getIdentifier())).toString();
            }
        }
        throw new InternalException("Can not resolve alias for field: " +
            path.toString() + " mapped to column: " + col.getIdentifier().getName() +
            " table: "+table.getIdentifier().getName());
    }

    /**
     * Return the alias for the given column.
     */
    private String getColumnAlias(String col, Table table, PathJoins pj) {
        return getTableAlias(table, pj).append(_dict.getNamingUtil().toDBName(col)).toString();
    }
    
    private StringBuilder getTableAlias(Table table, PathJoins pj) {
        StringBuilder buf = new StringBuilder();
        if (_from != null) {
            String alias = toAlias(_from.getTableIndex(table, pj, true));
            if (_dict.requiresAliasForSubselect)
                return buf.append(FROM_SELECT_ALIAS).append(".").append(alias).
                    append("_");
            return buf.append(alias).append("_");
        }
        return buf.append(toAlias(getTableIndex(table, pj, true))).append(".");
    }

    public boolean isAggregate() {
        return (_flags & AGGREGATE) != 0;
    }

    public void setAggregate(boolean agg) {
        if (agg)
            _flags |= AGGREGATE;
        else
            _flags &= ~AGGREGATE;
    }

    public boolean isLob() {
        return (_flags & LOB) != 0;
    }

    public void setLob(boolean lob) {
        if (lob)
            _flags |= LOB;
        else
            _flags &= ~LOB;
    }

    public void clearSelects() {
        _selects.clear();
    }

    public boolean select(SQLBuffer sql, Object id) {
        return select(sql, id, null);
    }

    public boolean select(SQLBuffer sql, Object id, Joins joins) {
        if (!isGrouping())
            return select((Object) sql, id, joins);
        groupBy(sql, joins);
        return false;
    }

    /**
     * Record the select of the given SQL buffer or string.
     */
    private boolean select(Object sql, Object id, Joins joins) {
        getJoins(joins, true);
        boolean contains;
        if (id == null) {
            int idx = _selects.indexOfAlias(sql);
            contains = idx != -1;
            if (contains)
                id = _selects.get(idx);
            else
                id = nullId();
        } else
            contains = _selects.contains(id);

        if (contains)
            return false;
        _selects.setAlias(id, sql, false);
        return true;
    }

    /**
     * Returns a unique id for a SQL string whose given id is null.
     */
    private Object nullId() {
        if (_nullIds >= NULL_IDS.length)
            return new NullId();
        return NULL_IDS[_nullIds++];
    }

    public boolean select(String sql, Object id) {
        return select(sql, id, null);
    }

    public boolean select(String sql, Object id, Joins joins) {
        if (!isGrouping())
            return select((Object) sql, id, joins);
        groupBy(sql, joins);
        return true;
    }

    public void selectPlaceholder(String sql) {
        Object holder = (_placeholders >= PLACEHOLDERS.length)
            ? new Placeholder() : PLACEHOLDERS[_placeholders++];
        select(sql, holder);
    }

    /**
     * Insert a placeholder at the given index; use a negative index
     * to count from the back of the select list.
     */
    public void insertPlaceholder(String sql, int pos) {
        Object holder = (_placeholders >= PLACEHOLDERS.length)
            ? new Placeholder() : PLACEHOLDERS[_placeholders++];
        _selects.insertAlias(pos, holder, sql);
    }

    /**
     * Clear selected placeholders, and return removed select indexes.
     */
    public void clearPlaceholderSelects() {
        _selects.clearPlaceholders();
    }

    public boolean select(Column col) {
        return select(col, (Joins) null);
    }

    public boolean select(Column col, Joins joins) {
        if (!isGrouping())
            return select(col, getJoins(joins, true), false);
        groupBy(col, joins);
        return false;
    }

    public int select(Column[] cols) {
        return select(cols, null);
    }

    public int select(Column[] cols, Joins joins) {
        if (cols == null || cols.length == 0)
            return 0;
        if (isGrouping()) {
            groupBy(cols, joins);
            return 0;
        }
        PathJoins pj = getJoins(joins, true);
        int seld = 0;
        for (int i = 0; i < cols.length; i++)
            if (select(cols[i], pj, false))
                seld |= 2 << i;
        return seld;
    }

    /**
     * Select the given column after making the given joins.
     */
    private boolean select(Column col, PathJoins pj, boolean ident) {
        // we cache on column object if there are no joins so that when
        // looking up columns in the result we don't have to create a string
        // buffer for the table + column alias; if there are joins, then
        // we key on the alias
        String alias = getColumnAlias(col, pj);
        Object id;
        if (pj == null || pj.path() == null)
            id = col;
        else
            id = alias;
        if (_selects.contains(id))
            return false;

        if (col.getType() == Types.BLOB || col.getType() == Types.CLOB)
            setLob(true);
        _selects.setAlias(id, alias, ident);
        return true;
    }

    public void select(ClassMapping mapping, int subclasses,
        JDBCStore store, JDBCFetchConfiguration fetch, int eager) {
        select(mapping, subclasses, store, fetch, eager, null);
    }

    public void select(ClassMapping mapping, int subclasses,
        JDBCStore store, JDBCFetchConfiguration fetch, int eager,
        Joins joins) {
        select(this, mapping, subclasses, store, fetch, eager, joins, false);
    }

    /**
     * Select the given mapping.
     */
    void select(Select wrapper, ClassMapping mapping, int subclasses,
        JDBCStore store, JDBCFetchConfiguration fetch, int eager,
        Joins joins, boolean ident) {
        // note that this is one case where we don't want to use the result
        // of getJoins(); just use the given joins, which will either be clean
        // or the result of previous pre-joins. this way we don't push extra
        // stack stuff when no actual new joins have been made, and we don't
        // think the user wants outer joins when actually only the previous
        // joins were outer.  we do invoke getJoins(), though, to add these
        // joins (if any) to our top-level joins; otherwise it'd be possible
        // for the user to immediately do another join and select something,
        // and if we're in outer mode all these joins will get switched to outer
        // joins.  caching them as their original join type prevents that
        getJoins(joins, true);

        PathJoins pj = (PathJoins) joins;
        boolean hasJoins = pj != null && pj.isDirty();
        if (hasJoins) {
            if (_preJoins == null)
                _preJoins = new Stack();
            _preJoins.push(pj);
        }

        // if they are selecting this mapping with outer joins, then all joins
        // from this mapping should also be outer
        boolean wasOuter = (_flags & OUTER) != 0;
        if (hasJoins && !wasOuter && pj.isOuter())
            _flags |= OUTER;

        // delegate to store manager to select in same order it loads result
        ((JDBCStoreManager) store).select(wrapper, mapping, subclasses, null,
            null, fetch, eager, ident, (_flags & OUTER) != 0);

        // reset
        if (hasJoins)
            _preJoins.pop();
        if (!wasOuter && (_flags & OUTER) != 0)
            _flags &= ~OUTER;
    }

    public boolean selectIdentifier(Column col) {
        return selectIdentifier(col, (Joins) null);
    }

    public boolean selectIdentifier(Column col, Joins joins) {
        if (!isGrouping())
            return select(col, getJoins(joins, true), true);
        groupBy(col, joins);
        return false;
    }

    public int selectIdentifier(Column[] cols) {
        return selectIdentifier(cols, null);
    }

    public int selectIdentifier(Column[] cols, Joins joins) {
        if (cols == null || cols.length == 0)
            return 0;
        if (isGrouping()) {
            groupBy(cols, joins);
            return 0;
        }
        PathJoins pj = getJoins(joins, true);
        int seld = 0;
        for (int i = 0; i < cols.length; i++)
            if (select(cols[i], pj, true))
                seld |= 2 << i;
        return seld;
    }

    public void selectIdentifier(ClassMapping mapping, int subclasses,
        JDBCStore store, JDBCFetchConfiguration fetch, int eager) {
        selectIdentifier(mapping, subclasses, store, fetch, eager, null);
    }

    public void selectIdentifier(ClassMapping mapping, int subclasses,
        JDBCStore store, JDBCFetchConfiguration fetch, int eager,
        Joins joins) {
        select(this, mapping, subclasses, store, fetch, eager, joins, true);
    }

    public int selectPrimaryKey(ClassMapping mapping) {
        return selectPrimaryKey(mapping, null);
    }

    public int selectPrimaryKey(ClassMapping mapping, Joins joins) {
        return primaryKeyOperation(mapping, true, null, joins, false);
    }

    /**
     * Operate on primary key data. Return a bit mask of selected columns.
     */
    private int primaryKeyOperation(ClassMapping mapping, boolean sel,
        Boolean asc, Joins joins, boolean aliasOrder) {
        if (!sel && asc == null)
            return 0;

        // if this mapping can't select the full pk values, then join to
        // super and recurse
        ClassMapping sup;
        if (!mapping.isPrimaryKeyObjectId(true)) {
            sup = mapping.getJoinablePCSuperclassMapping();
            if (joins == null)
                joins = newJoins();
            joins = mapping.joinSuperclass(joins, false);
            return primaryKeyOperation(sup, sel, asc, joins, aliasOrder);
        }

        Column[] cols = mapping.getPrimaryKeyColumns();
        if (isGrouping()) {
            groupBy(cols, joins);
            return 0;
        }

        PathJoins pj = getJoins(joins, false);
        int seld = 0;
        for (int i = 0; i < cols.length; i++)
            if (columnOperation(cols[i], sel, asc, pj, aliasOrder))
                seld |= 2 << i;

        // if this mapping has not been used in the select yet (and therefore
        // is not joined to anything), but has an other-table superclass that
        // has been used, make sure to join to it
        boolean joined = false;
        for (sup = mapping.getJoinablePCSuperclassMapping(); sup != null;
            mapping = sup, sup = mapping.getJoinablePCSuperclassMapping()) {
            if (sup.getTable() == mapping.getTable())
                continue;

            if (mapping.getTable() != sup.getTable()
                && getTableIndex(mapping.getTable(), pj, false) == -1
                && getTableIndex(sup.getTable(), pj, false) != -1) {
                if (pj == null)
                    pj = (PathJoins) newJoins();
                pj = (PathJoins) mapping.joinSuperclass(pj, false);
                joined = true;
            } else
                break;
        }
        if (joined)
            where(pj);

        return seld;
    }

    /**
     * Perform an operation on a column.
     */
    private boolean columnOperation(Column col, boolean sel, Boolean asc,
        PathJoins pj, boolean aliasOrder) {
        String as = null;
        if (asc != null && (aliasOrder || (_flags & RECORD_ORDERED) != 0)) {
            Object id;
            if (pj == null || pj.path() == null)
                id = col;
            else
                id = getColumnAlias(col, pj);
            if ((_flags & RECORD_ORDERED) != 0) {
                if (_ordered == null)
                    _ordered = new ArrayList(5);
                _ordered.add(id);
            }
            if (aliasOrder) {
                as = toOrderAlias(_orders++);
                _selects.setSelectAs(id, as);
            }
        }

        boolean seld = sel && select(col, pj, false);
        if (asc != null) {
            String alias = (as != null) ? as : getColumnAlias(col, pj);
            appendOrdering(alias, asc.booleanValue());
        }
        return seld;
    }

    /**
     * Append ordering information to our internal buffer.
     */
    private void appendOrdering(Object orderBy, boolean asc) {
        if (_ordering == null)
            _ordering = new SQLBuffer(_dict);
        else
            _ordering.append(", ");

        if (orderBy instanceof SQLBuffer)
            _ordering.append((SQLBuffer) orderBy);
        else
            _ordering.append((String) orderBy);
        if (asc)
            _ordering.append(" ASC");
        else
            _ordering.append(" DESC");
    }

    public int orderByPrimaryKey(ClassMapping mapping, boolean asc,
        boolean sel) {
        return orderByPrimaryKey(mapping, asc, null, sel);
    }

    public int orderByPrimaryKey(ClassMapping mapping, boolean asc,
        Joins joins, boolean sel) {
        return orderByPrimaryKey(mapping, asc, joins, sel, false);
    }

    /**
     * Allow unions to set aliases on order columns.
     */
    public int orderByPrimaryKey(ClassMapping mapping, boolean asc,
        Joins joins, boolean sel, boolean aliasOrder) {
        return primaryKeyOperation(mapping, sel,
            (asc) ? Boolean.TRUE : Boolean.FALSE, joins, aliasOrder);
    }

    public boolean orderBy(Column col, boolean asc, boolean sel) {
        return orderBy(col, asc, null, sel);
    }

    public boolean orderBy(Column col, boolean asc, Joins joins, boolean sel) {
        return orderBy(col, asc, joins, sel, false);
    }

    /**
     * Allow unions to set aliases on order columns.
     */
    boolean orderBy(Column col, boolean asc, Joins joins, boolean sel,
        boolean aliasOrder) {
        return columnOperation(col, sel, (asc) ? Boolean.TRUE : Boolean.FALSE,
            getJoins(joins, true), aliasOrder);
    }

    public int orderBy(Column[] cols, boolean asc, boolean sel) {
        return orderBy(cols, asc, null, sel);
    }

    public int orderBy(Column[] cols, boolean asc, Joins joins, boolean sel) {
        return orderBy(cols, asc, joins, sel, false);
    }

    /**
     * Allow unions to set aliases on order columns.
     */
    int orderBy(Column[] cols, boolean asc, Joins joins, boolean sel,
        boolean aliasOrder) {
        PathJoins pj = getJoins(joins, true);
        int seld = 0;
        for (int i = 0; i < cols.length; i++)
            if (columnOperation(cols[i], sel,
                (asc) ? Boolean.TRUE : Boolean.FALSE, pj, aliasOrder))
                seld |= 2 << i;
        return seld;
    }

    public boolean orderBy(SQLBuffer sql, boolean asc, boolean sel, Value selAs)
    {
        return orderBy(sql, asc, (Joins) null, sel, selAs);
    }

    public boolean orderBy(SQLBuffer sql, boolean asc, Joins joins,
        boolean sel, Value selAs) {
        return orderBy(sql, asc, joins, sel, false, selAs);
    }

    /**
     * Allow unions to set aliases on order columns.
     */
    boolean orderBy(SQLBuffer sql, boolean asc, Joins joins, boolean sel,
        boolean aliasOrder, Value selAs) {
        return orderBy((Object) sql, asc, joins, sel, aliasOrder, selAs);
    }

    /**
     * Order on a SQL buffer or string.
     */
    private boolean orderBy(Object sql, boolean asc, Joins joins, boolean sel,
        boolean aliasOrder, Value selAs) {
        Object order = sql;
        if (aliasOrder) {
            order = toOrderAlias(_orders++);
            _selects.setSelectAs(sql, (String) order);
        }
        if ((_flags & RECORD_ORDERED) != 0) {
            if (_ordered == null)
                _ordered = new ArrayList(5);
            _ordered.add(selAs == null ? sql : selAs);
        }

        getJoins(joins, true);
        appendOrdering(selAs != null ? selAs.getAlias() : order, asc);
        if (sel) {
            int idx = _selects.indexOfAlias(sql);
            if (idx == -1) {
                _selects.setAlias(nullId(), sql, false);
                return true;
            }
        }
        return false;
    }

    public boolean orderBy(String sql, boolean asc, boolean sel) {
        return orderBy(sql, asc, null, sel);
    }

    public boolean orderBy(String sql, boolean asc, Joins joins, boolean sel) {
        return orderBy(sql, asc, joins, sel, false);
    }

    /**
     * Allow unions to set aliases on order columns.
     */
    boolean orderBy(String sql, boolean asc, Joins joins, boolean sel,
        boolean aliasOrder) {
        return orderBy((Object) sql, asc, joins, sel, aliasOrder, null);
    }

    public void clearOrdering() {
        _ordering = null;
        _orders = 0;
    }

    /**
     * Allow unions to record the select list indexes of items we order by.
     */
    void setRecordOrderedIndexes(boolean record) {
        if (record)
            _flags |= RECORD_ORDERED;
        else {
            _ordered = null;
            _flags &= ~RECORD_ORDERED;
        }
    }

    /**
     * Return the indexes in the select list of all items we're ordering
     * by, or null if none. For use with unions.
     */
    List getOrderedIndexes() {
        if (_ordered == null)
            return null;
        List idxs = new ArrayList(_ordered.size());
        for (int i = 0; i < _ordered.size(); i++)
            idxs.add(_selects.indexOf(_ordered.get(i)));
        return idxs;
    }

    public void wherePrimaryKey(Object oid, ClassMapping mapping,
        JDBCStore store) {
        wherePrimaryKey(oid, mapping, null, store);
    }

    /**
     * Add where conditions setting the mapping's primary key to the given
     * oid values. If the given mapping does not use oid values for its
     * primary key, we will recursively join to its superclass until we find
     * an ancestor that does.
     */
    private void wherePrimaryKey(Object oid, ClassMapping mapping, Joins joins,
        JDBCStore store) {
        // if this mapping's identifiers include something other than
        // the pk values, join to super and recurse
        if (!mapping.isPrimaryKeyObjectId(false)) {
            ClassMapping sup = mapping.getJoinablePCSuperclassMapping();
            if (joins == null)
                joins = newJoins();
            joins = mapping.joinSuperclass(joins, false);
            wherePrimaryKey(oid, sup, joins, store);
            return;
        }

        Column[] cols = mapping.getPrimaryKeyColumns();
        where(oid, mapping, cols, cols, null, null, getJoins(joins, true),
            store);
    }

    public void whereForeignKey(ForeignKey fk, Object oid,
        ClassMapping mapping, JDBCStore store) {
        whereForeignKey(fk, oid, mapping, null, store);
    }

    /**
     * Add where conditions setting the given foreign key to the given
     * oid values.
     *
     * @see #wherePrimaryKey
     */
    private void whereForeignKey(ForeignKey fk, Object oid,
        ClassMapping mapping, Joins joins, JDBCStore store) {
        // if this mapping's identifiers include something other than
        // the pk values, or if this foreign key doesn't link to only
        // identifiers, join to table and do a getPrimaryKey
        if (!mapping.isPrimaryKeyObjectId(false) || !containsAll
            (mapping.getPrimaryKeyColumns(), fk.getPrimaryKeyColumns())) {
            if (joins == null)
                joins = newJoins();
            // traverse to foreign key target mapping
            while (mapping.getTable() != fk.getPrimaryKeyTable()) {
                if (joins == null)
                    joins = newJoins();
                joins = mapping.joinSuperclass(joins, false);
                mapping = mapping.getJoinablePCSuperclassMapping();
                if (mapping == null)
                    throw new InternalException();
            }
            joins = joins.join(fk, false, false);
            wherePrimaryKey(oid, mapping, joins, store);
            return;
        }

        Column[] fromCols = fk.getColumns();
        Column[] toCols = fk.getPrimaryKeyColumns();
        Column[] constCols = fk.getConstantColumns();
        Object[] consts = fk.getConstants();
        where(oid, mapping, toCols, fromCols, consts, constCols,
            getJoins(joins, true), store);
    }

    /**
     * Internal method to flush the oid values as where conditions to the
     * given columns.
     */
    private void where(Object oid, ClassMapping mapping, Column[] toCols,
        Column[] fromCols, Object[] vals, Column[] constCols, PathJoins pj,
        JDBCStore store) {
        ValueMapping embed = mapping.getEmbeddingMapping();
        if (embed != null) {
            where(oid, embed.getFieldMapping().getDefiningMapping(),
                toCols, fromCols, vals, constCols, pj, store);
            return;
        }

        // only bother to pack pk values into array if app id
        Object[] pks = null;
        boolean relationId = RelationStrategies.isRelationId(fromCols); 
        if (!relationId && mapping.getIdentityType() == ClassMapping.ID_APPLICATION)
            pks = ApplicationIds.toPKValues(oid, mapping);

        SQLBuffer buf = new SQLBuffer(_dict);
        Joinable join;
        Object val;
        int count = 0;
        for (int i = 0; i < toCols.length; i++, count++) {
            if (pks == null) {
                val = (oid == null) ? null : relationId ? oid : ((Id) oid).getId();
            } else {
                // must be app identity; use pk index to get correct pk value
                join = mapping.assertJoinable(toCols[i]);
                val = pks[mapping.getField(join.getFieldIndex()).
                    getPrimaryKeyIndex()];
                val = join.getJoinValue(val, toCols[i], store);
            }

            if (count > 0)
                buf.append(" AND ");
            buf.append(getColumnAlias(fromCols[i], pj));
            if (val == null)
                buf.append(" IS ");
            else
                buf.append(" = ");
            buf.appendValue(val, fromCols[i]);
        }

        if (constCols != null && constCols.length > 0) {
            for (int i = 0; i < constCols.length; i++, count++) {
                if (count > 0)
                    buf.append(" AND ");
                buf.append(getColumnAlias(constCols[i], pj));

                if (vals[i] == null)
                    buf.append(" IS ");
                else
                    buf.append(" = ");
                buf.appendValue(vals[i], constCols[i]);
            }
        }

        where(buf, pj);
    }

    /**
     * Test to see if the given set of columns contains all the
     * columns in the given potential subset.
     */
    private static boolean containsAll(Column[] set, Column[] sub) {
        if (sub.length > set.length)
            return false;

        // this is obviously n^2, but the number of columns should be in
        // the 1-2 range, so no biggie
        boolean found = true;
        for (int i = 0; i < sub.length && found; i++) {
            found = false;
            for (int j = 0; j < set.length && !found; j++)
                found = sub[i] == set[j];
        }
        return found;
    }

    public void where(Joins joins) {
        if (joins != null)
            where((String) null, joins);
    }

    public void where(SQLBuffer sql) {
        where(sql, (Joins) null);
    }

    public void where(SQLBuffer sql, Joins joins) {
        where(sql, getJoins(joins, true));
    }

    /**
     * Add the given condition to the WHERE clause.
     */
    private void where(SQLBuffer sql, PathJoins pj) {
        // no need to use joins...
        if (sql == null || sql.isEmpty())
            return;

        if (_where == null)
            _where = new SQLBuffer(_dict);
        else if (!_where.isEmpty())
            _where.append(" AND ");
        _where.append(sql);
    }

    public void where(String sql) {
        where(sql, (Joins) null);
    }

    public void where(String sql, Joins joins) {
        where(sql, getJoins(joins, true));
    }

    /**
     * Add the given condition to the WHERE clause.
     */
    private void where(String sql, PathJoins pj) {
        // no need to use joins...
        if (StringUtils.isEmpty(sql))
            return;

        if (_where == null)
            _where = new SQLBuffer(_dict);
        else if (!_where.isEmpty())
            _where.append(" AND ");
        _where.append(sql);
    }

    public void having(SQLBuffer sql) {
        having(sql, (Joins) null);
    }

    public void having(SQLBuffer sql, Joins joins) {
        having(sql, getJoins(joins, true));
    }

    /**
     * Add the given condition to the HAVING clause.
     */
    private void having(SQLBuffer sql, PathJoins pj) {
        // no need to use joins...
        if (sql == null || sql.isEmpty())
            return;

        if (_having == null)
            _having = new SQLBuffer(_dict);
        else if (!_having.isEmpty())
            _having.append(" AND ");
        _having.append(sql);
    }

    public void having(String sql) {
        having(sql, (Joins) null);
    }

    public void having(String sql, Joins joins) {
        having(sql, getJoins(joins, true));
    }

    /**
     * Add the given condition to the HAVING clause.
     */
    private void having(String sql, PathJoins pj) {
        // no need to use joins...
        if (StringUtils.isEmpty(sql))
            return;

        if (_having == null)
            _having = new SQLBuffer(_dict);
        else if (!_having.isEmpty())
            _having.append(" AND ");
        _having.append(sql);
    }

    public void groupBy(SQLBuffer sql) {
        groupBy(sql, (Joins) null);
    }

    public void groupBy(SQLBuffer sql, Joins joins) {
        getJoins(joins, true);
        groupByAppend(sql.getSQL());
    }

    public void groupBy(String sql) {
        groupBy(sql, (Joins) null);
    }

    public void groupBy(String sql, Joins joins) {
        getJoins(joins, true);
        groupByAppend(sql);
    }

    public void groupBy(Column col) {
        groupBy(col, null);
    }

    public void groupBy(Column col, Joins joins) {
        PathJoins pj = getJoins(joins, true);
        groupByAppend(getColumnAlias(col, pj));
    }

    public void groupBy(Column[] cols) {
        groupBy(cols, null);
    }

    public void groupBy(Column[] cols, Joins joins) {
        PathJoins pj = getJoins(joins, true);
        for (int i = 0; i < cols.length; i++) {
            groupByAppend(getColumnAlias(cols[i], pj));
        }
    }
    
    private void groupByAppend(String sql) {
        if (_grouped == null || !_grouped.contains(sql)) {
            if (_grouping == null) {
                _grouping = new SQLBuffer(_dict);
                _grouped = new ArrayList();
            } else
                _grouping.append(", ");

            _grouping.append(sql);
            _grouped.add(sql);
        }
    }

    public void groupBy(ClassMapping mapping, int subclasses, JDBCStore store,
        JDBCFetchConfiguration fetch) {
        groupBy(mapping, subclasses, store, fetch, null);
    }

    public void groupBy(ClassMapping mapping, int subclasses, JDBCStore store,
        JDBCFetchConfiguration fetch, Joins joins) {
        // we implement this by putting ourselves into grouping mode, where
        // all select invocations are re-routed to group-by invocations instead.
        // this allows us to utilize the same select APIs of the store manager
        // and all the mapping strategies, rather than having to create 
        // equivalent APIs and duplicate logic for grouping
        boolean wasGrouping = isGrouping();
        _flags |= GROUPING;
        try {
            select(mapping, subclasses, store, fetch, 
                EagerFetchModes.EAGER_NONE, joins);
        } finally {
            if (!wasGrouping)
                _flags &= ~GROUPING;
        }
    }

    /**
     * Whether we're in group mode, where any select is changed to a group-by
     * call.
     */
    private boolean isGrouping() {
        return (_flags & GROUPING) != 0;
    }

    /**
     * Return the joins to use for column aliases, etc.
     *
     * @param joins joins given by the user
     * @return the joins to use for aliases, etc
     */
    private PathJoins getJoins(Joins joins, boolean record) {
        PathJoins pj = (PathJoins) joins;
        boolean pre = (pj == null || !pj.isDirty())
            && _preJoins != null && !_preJoins.isEmpty();
        if (pre)
            pj = (PathJoins) _preJoins.peek();

        if (pj == null || !pj.isDirty())
            pj = _joins;
        else if (!pre) {
            if ((_flags & OUTER) != 0)
                pj = (PathJoins) outer(pj);
            if (record) {
                if (!pj.isEmpty()) {
                    if (_joins == null)
                        _joins = new SelectJoins(this);
                    if (_joins.joins() == null)
                        _joins.setJoins(new JoinSet(pj.joins()));
                    else
                        _joins.joins().addAll(pj.joins());
                }
            }
        }
        return pj;
    }

    public SelectExecutor whereClone(int sels) {
        if (sels < 1)
            sels = 1;

        Select[] clones = null;
        SelectImpl sel;
        for (int i = 0; i < sels; i++) {
            sel = (SelectImpl) _conf.getSQLFactoryInstance().newSelect();
            sel._flags = _flags;
            sel._flags &= ~AGGREGATE;
            sel._flags &= ~OUTER;
            sel._flags &= ~LRS;
            sel._flags &= ~EAGER_TO_ONE;
            sel._flags &= ~EAGER_TO_MANY;
            sel._flags &= ~FORCE_COUNT;
            sel._joinSyntax = _joinSyntax;
            sel._schemaAlias = _schemaAlias;
            if (_aliases != null)
                sel._aliases = new HashMap(_aliases);
            if (_tables != null)
                sel._tables = new TreeMap(_tables);
            if (_joins != null)
                sel._joins = _joins.clone(sel);
            if (_where != null)
                sel._where = new SQLBuffer(_where);
            if (_from != null) {
                sel._from = (SelectImpl) _from.whereClone(1);
                sel._from._outer = sel;
            }
            if (_subsels != null) {
                sel._subsels = new ArrayList(_subsels.size());
                SelectImpl sub, selSub;
                for (int j = 0; j < _subsels.size(); j++) {
                    sub = (SelectImpl) _subsels.get(j);
                    selSub = (SelectImpl) sub.fullClone(1);
                    selSub._parent = sel;
                    selSub._subPath = sub._subPath;
                    sel._subsels.add(selSub);
                    if (sel._where != null)
                        sel._where.replace(sub, selSub);
                }
            }

            if (sels == 1)
                return sel;
            if (clones == null)
                clones = new Select[sels];
            clones[i] = sel;
        }
        return _conf.getSQLFactoryInstance().newUnion(clones);
    }

    public SelectExecutor fullClone(int sels) {
        if (sels < 1)
            sels = 1;

        Select[] clones = null;
        SelectImpl sel;
        for (int i = 0; i < sels; i++) {
            sel = (SelectImpl) whereClone(1);
            sel._flags = _flags;
            sel._expectedResultCount = _expectedResultCount;
            sel._selects.addAll(_selects);
            if (_ordering != null)
                sel._ordering = new SQLBuffer(_ordering);
            sel._orders = _orders;
            if (_grouping != null)
                sel._grouping = new SQLBuffer(_grouping);
            if (_having != null)
                sel._having = new SQLBuffer(_having);
            if (_from != null) {
                sel._from = (SelectImpl) _from.fullClone(1);
                sel._from._outer = sel;
            }

            if (sels == 1)
                return sel;
            if (clones == null)
                clones = new Select[sels];
            clones[i] = sel;
        }
        return _conf.getSQLFactoryInstance().newUnion(clones);
    }

    public SelectExecutor eagerClone(FieldMapping key, int eagerType,
        boolean toMany, int sels) {
        if (eagerType == EAGER_OUTER
            && _joinSyntax == JoinSyntaxes.SYNTAX_TRADITIONAL)
            return null;
        if (_eagerKeys != null && _eagerKeys.contains(key))
            return null;

        // global set of eager keys
        if (_eagerKeys == null)
            _eagerKeys = new HashSet();
        _eagerKeys.add(key);

        SelectExecutor sel;
        if (eagerType != EAGER_PARALLEL) {
            if (toMany)
                _flags |= EAGER_TO_MANY;
            else
                _flags |= EAGER_TO_ONE;
            sel = this;
        } else if (sels < 2)
            sel = parallelClone();
        else {
            Select[] clones = new Select[sels];
            for (int i = 0; i < clones.length; i++)
                clones[i] = parallelClone();
            sel = _conf.getSQLFactoryInstance().newUnion(clones);
        }

        if (_eager == null)
            _eager = new HashMap();
        _eager.put(toEagerKey(key, getJoins(null, false)), sel);
        return sel;
    }

    /**
     * Return a clone of this select for use in eager parallel selects.
     */
    private SelectImpl parallelClone() {
        SelectImpl sel = (SelectImpl) whereClone(1);
        sel._flags &= ~NONAUTO_DISTINCT;
        sel._eagerKeys = _eagerKeys;
        if (_preJoins != null && !_preJoins.isEmpty()) {
            sel._preJoins = new Stack();
            sel._preJoins.push(((SelectJoins) _preJoins.peek()).
                clone(sel));
        }
        return sel;
    }

    /**
     * Return view of eager selects. May be null.
     */
    public Map getEagerMap() {
        return _eager;
    }

    public void logEagerRelations() {
        if (_eagerKeys != null) {
            _conf.getLog(JDBCConfiguration.LOG_DIAG).trace(
                "Eager relations: "+_eagerKeys);
        }
    }

    public SelectExecutor getEager(FieldMapping key) {
        if (_eager == null || !_eagerKeys.contains(key))
            return null;
        return (SelectExecutor) _eager.get(toEagerKey(key, getJoins(null,
            false)));
    }

    /**
     * Return the eager key to use for the user-given key.
     */
    private static Object toEagerKey(FieldMapping key, PathJoins pj) {
        if (pj == null || pj.path() == null)
            return key;
        return new Key(pj.path().toString(), key);
    }

    public Joins newJoins() {
        if (_preJoins != null && !_preJoins.isEmpty()) {
            SelectJoins sj = (SelectJoins) _preJoins.peek();
            return sj.clone(this);
        }
        // return this for efficiency in case no joins end up being made
        return this;
    }

    public Joins newOuterJoins() {
        return ((PathJoins) newJoins()).setOuter(true);
    }

    public void append(SQLBuffer buf, Joins joins) {
        if (joins == null || joins.isEmpty())
            return;
        if (_joinSyntax == JoinSyntaxes.SYNTAX_SQL92)
            return;

        if (!buf.isEmpty())
            buf.append(" AND ");
        Join join = null;
        for (Iterator itr = ((PathJoins) joins).joins().joinIterator();
            itr.hasNext();) {
            join = (Join) itr.next();
            switch (_joinSyntax) {
                case JoinSyntaxes.SYNTAX_TRADITIONAL:
                    buf.append(_dict.toTraditionalJoin(join));
                    break;
                case JoinSyntaxes.SYNTAX_DATABASE:
                    buf.append(_dict.toNativeJoin(join));
                    break;
                default:
                    throw new InternalException();
            }

            if (itr.hasNext())
                buf.append(" AND ");
        }
    }

    public Joins and(Joins joins1, Joins joins2) {
        return and((PathJoins) joins1, (PathJoins) joins2, true);
    }

    public Select getSelect() {
        return null;
    }

    /**
     * Combine the given joins.
     */
    private SelectJoins and(PathJoins j1, PathJoins j2, boolean nullJoins) {
        if ((j1 == null || j1.isEmpty())
            && (j2 == null || j2.isEmpty()))
            return null;

        SelectJoins sj = new SelectJoins(this);
        if (j1 == null || j1.isEmpty()) {
            if (j2.getSelect() == this) {
                if (nullJoins)
                    sj.setJoins(j2.joins());
                else
                    sj.setJoins(new JoinSet(j2.joins()));
            }
        } else {
            JoinSet set = null;
            if (j1.getSelect() == this) {
                if (nullJoins)
                    set = j1.joins();
                else
                    set = new JoinSet(j1.joins());

                if (j2 != null && !j2.isEmpty()
                    && j2.getSelect() == this)
                    set.addAll(j2.joins());
                sj.setJoins(set);
            }
        }

        // null previous joins; all are combined into this one
        if (nullJoins && j1 != null)
            j1.nullJoins();
        if (nullJoins && j2 != null)
            j2.nullJoins();

        return sj;
    }

    public Joins or(Joins joins1, Joins joins2) {
        PathJoins j1 = (PathJoins) joins1;
        PathJoins j2 = (PathJoins) joins2;

        // if no common joins, return null; if one side of the or clause has
        // different joins than the other, then we need to use distinct
        boolean j1Empty = j1 == null || j1.isEmpty();
        boolean j2Empty = j2 == null || j2.isEmpty();
        if (j1Empty || j2Empty) {
            if (j1Empty && !j2Empty) {
                collectOuterJoins(j2);
                if (!j2.isEmpty())
                    _flags |= IMPLICIT_DISTINCT;
            } else if (j2Empty && !j1Empty) {
                collectOuterJoins(j1);
                if (!j1.isEmpty())
                    _flags |= IMPLICIT_DISTINCT;
            }
            return null;
        }

        // if all common joins, move all joins to returned instance
        SelectJoins sj = new SelectJoins(this);
        if (j1.joins().equals(j2.joins())) {
            sj.setJoins(j1.joins());
            j1.nullJoins();
            j2.nullJoins();
        } else {
            JoinSet commonJoins = new JoinSet(j1.joins());
            commonJoins.retainAll(j2.joins());
            if (!commonJoins.isEmpty()) {
                // put common joins in returned instance; remove them from
                // each given instance
                sj.setJoins(commonJoins);
                j1.joins().removeAll(commonJoins);
                j2.joins().removeAll(commonJoins);
            }
            collectOuterJoins(j1);
            collectOuterJoins(j2);

            // if one side of the or clause has different joins than the other,
            // then we need to use distinct
            if (!j1.isEmpty() || !j2.isEmpty())
                _flags |= IMPLICIT_DISTINCT;
        }
        return sj;
    }

    public Joins outer(Joins joins) {
        if (_joinSyntax == JoinSyntaxes.SYNTAX_TRADITIONAL || joins == null)
            return joins;

        // record that this is an outer join set, even if it's empty
        PathJoins pj = ((PathJoins) joins).setOuter(true);
        if (pj.isEmpty())
            return pj;

        Join join;
        Join rec;
        boolean hasJoins = _joins != null && _joins.joins() != null;
        for (Iterator itr = pj.joins().iterator(); itr.hasNext();) {
            join = (Join) itr.next();
            if (join.getType() == Join.TYPE_INNER) {
                if (!hasJoins)
                    join.setType(Join.TYPE_OUTER);
                else {
                    rec = _joins.joins().getRecordedJoin(join);
                    if (rec == null || rec.getType() == Join.TYPE_OUTER)
                        join.setType(Join.TYPE_OUTER);
                }
            }
        }
        return joins;
    }

    /**
     * Moves the joins from the given instance into our outer joins set.
     */
    private void collectOuterJoins(PathJoins pj) {
        if (_joinSyntax == JoinSyntaxes.SYNTAX_TRADITIONAL || pj == null
            || pj.isEmpty())
            return;

        if (_joins == null)
            _joins = new SelectJoins(this);

        boolean add = true;
        if (_joins.joins() == null) {
            _joins.setJoins(pj.joins());
            add = false;
        }

        Join join;
        for (Iterator itr = pj.joins().iterator(); itr.hasNext();) {
            join = (Join) itr.next();
            if (join.getType() == Join.TYPE_INNER) {
                if (join.getForeignKey() != null
                    && !_dict.canOuterJoin(_joinSyntax, join.getForeignKey())) {
                    Log log = _conf.getLog(JDBCConfiguration.LOG_JDBC);
                    if (log.isWarnEnabled())
                        log.warn(_loc.get("cant-outer-fk",
                            join.getForeignKey()));
                } else
                    join.setType(Join.TYPE_OUTER);
            }
            if (add)
                _joins.joins().add(join);
        }
        pj.nullJoins();
    }

    /**
     * Return the alias for the given table under the given joins.
     * NOTE: WE RELY ON THESE INDEXES BEING MONOTONICALLY INCREASING FROM 0
     */
    private int getTableIndex(Table table, PathJoins pj, boolean create) {
        // if we have a from select, then there are no table aliases
        if (_from != null)
            return -1;

        Integer i = null;
        Object key = table.getFullIdentifier().getName();
        if (pj != null && pj.path() != null)
            key = new Key(pj.getPathStr(), key);

        if (_ctx != null && (_parent != null || _subsels != null || _hasSub)) {
            i = findAliasForQuery(table, pj, key, create);
        }

        if (i != null)
            return i.intValue();

        // check out existing aliases
        i = findAlias(table, key);

        if (i != null)
            return i.intValue();
        if (!create)
            return -1;

        // not found; create alias
        i = aliasSize(false, null);
//        System.out.println("GetTableIndex\t"+
//                ((_parent != null) ? "Sub" :"") +
//                " created alias: "+
//                i.intValue()+ " "+ key);
        recordTableAlias(table, key, i);
        return i.intValue();
    }

    private Integer findAliasForQuery(Table table, PathJoins pj, Object key,
        boolean create) {
        Integer i = null;
        SelectImpl sel = this;
        String alias = _schemaAlias;
        if (isPathInThisContext(pj) || table.isAssociation())          
            alias = null;

        // find the context where this alias is defined
        Context ctx = (alias != null) ?
            _ctx.findContext(alias) : null;
        if (ctx != null)
            sel = (SelectImpl) ctx.getSelect();

        if (!create) 
            i = sel.findAlias(table, key);  // find in parent and in myself
        else
            i = sel.getAlias(table, key); // find in myself
        if (i != null)
            return i;
        
        if (create) { // create here
            i = sel.createAlias(table, key);
        } else if (ctx != null && ctx != ctx()) { // create in other select
            i = ((SelectImpl)ctx.getSelect()).createAlias(table, key);
        }

        return i;
    }

    private boolean isPathInThisContext(PathJoins pj) {
        // currCtx is set from Action, it is reset to null after the PCPath initialization
        Context currCtx = pj == null ? null : ((PathJoinsImpl)pj).context;
        
        // lastCtx is set to currCtx after the SelectJoins.join. pj.lastCtx and pj.path string are 
        // the last snapshot of pj. They will be used together for later table alias resolution in
        // the getColumnAlias(). 
        Context lastCtx = pj == null ? null : ((PathJoinsImpl)pj).lastContext;
        Context thisCtx = currCtx == null ? lastCtx : currCtx;
        String corrVar = pj == null ? null : pj.getCorrelatedVariable();
        
        return (pj != null && pj.path() != null && 
            (corrVar == null || (thisCtx != null && ctx() == thisCtx)));
    }
 
    private Integer getAlias(Table table, Object key) {
        Integer alias = null;
        if (_aliases != null)
            alias = (Integer) _aliases.get(key);
        return alias;
    }

    private int createAlias(Table table, Object key) {
        Integer i = ctx().nextAlias();
//        System.out.println("\t"+
//                ((_parent != null) ? "Sub" :"") +
//                "Query created alias: "+ 
//                i.intValue()+ " "+ key);
        recordTableAlias(table, key, i);
        return i.intValue();
    }

    private Integer findAlias(Table table, Object key) {
        Integer alias = null;
        if (_aliases != null) {
            alias = (Integer) _aliases.get(key);
            if (alias != null) {
                return alias;
            }
        }
        if (_parent != null) {
            alias = _parent.findAlias(table, key);
            if (alias != null) {
                return alias;
            }
        }
        return alias;
    }

    /**
     * Record the mapping of the given key to the given alias.
     */
    private void recordTableAlias(Table table, Object key, Integer alias) {
        if (_aliases == null)
            _aliases = new HashMap();
        _aliases.put(key, alias);

        String tableString = _dict.getFullName(table, false) + " "
            + toAlias(alias.intValue());
        if (_tables == null)
            _tables = new TreeMap();
        _tables.put(alias, tableString);
    }

    
    /**
     * Calculate total number of aliases.
     * 
     * From 1.2.x
     */
    private int aliasSize(boolean fromParent, SelectImpl fromSub) {
        int aliases = (fromParent || _parent == null) ? 0 : _parent.aliasSize(false, this);
        aliases += (_aliases == null) ? 0 : _aliases.size();
        if (_subsels != null) {
            for (SelectImpl sub : _subsels) {
                if (sub != fromSub)
                    aliases += sub.aliasSize(true, null);
            }
        }
        return aliases;
    }
    
    public String toString() {
        return toSelect(false, null).getSQL();
    }

    ////////////////////////////
    // PathJoins implementation
    ////////////////////////////

    public boolean isOuter() {
        return false;
    }

    public PathJoins setOuter(boolean outer) {
        return new SelectJoins(this).setOuter(true);
    }

    public boolean isDirty() {
        return false;
    }

    public StringBuilder path() {
        return null;
    }
    
    public String getPathStr() {
        return null;
    }

    public JoinSet joins() {
        return null;
    }

    public int joinCount() {
        return 0;
    }

    public void nullJoins() {
    }

    public boolean isEmpty() {
        return true;
    }

    public Joins crossJoin(Table localTable, Table foreignTable) {
        return new SelectJoins(this).crossJoin(localTable, foreignTable);
    }

    public Joins join(ForeignKey fk, boolean inverse, boolean toMany) {
        return new SelectJoins(this).join(fk, inverse, toMany);
    }

    public Joins outerJoin(ForeignKey fk, boolean inverse, boolean toMany) {
        return new SelectJoins(this).outerJoin(fk, inverse, toMany);
    }

    public Joins joinRelation(String name, ForeignKey fk, ClassMapping target,
        int subs, boolean inverse, boolean toMany) {
        return new SelectJoins(this).joinRelation(name, fk, target, subs, 
            inverse, toMany);
    }

    public Joins outerJoinRelation(String name, ForeignKey fk, 
        ClassMapping target, int subs, boolean inverse, boolean toMany) {
        return new SelectJoins(this).outerJoinRelation(name, fk, target, subs, 
            inverse, toMany);
    }

    public Joins setVariable(String var) {
        if (var == null)
            return this;
        return new SelectJoins(this).setVariable(var);
    }

    public Joins setSubselect(String alias) {
        if (alias == null)
            return this;
        return new SelectJoins(this).setSubselect(alias);
    }

    /**
     * Represents a SQL string selected with null id.
     */
    private static class NullId {
    }

    /**
     * Represents a placeholder SQL string.
     */
    private static class Placeholder {
    }
    
    public SelectImpl clone(Context ctx) {
        SelectImpl sel = (SelectImpl) _conf.getSQLFactoryInstance().newSelect();
        sel._ctx = ctx;
        if (_parent != null && _parent.ctx() != null)
            sel._parent = (SelectImpl)_parent.ctx().getSelect();
        sel._schemaAlias = _schemaAlias;
        sel._flags = _flags;
        return sel;
    }

    /**
     * Key type used for aliases.
     */
    private static class Key {

        private final String _path;
        private final Object _key;

        public Key(String path, Object key) {
            _path = path;
            _key = key;
        }

        public int hashCode() {
            return ((_path == null) ? 0  : _path.hashCode()) ^ ((_key == null) ? 0  : _key.hashCode());
        }

        public boolean equals(Object other) {
            if (other == null)
                return false;
            if (other == this)
                return true;
            if (other.getClass() != getClass())
                return false;
            Key k = (Key) other;
            if (k._key == null || k._path == null || _key == null || _path == null)
            	return false;
            return k._path.equals(_path) && k._key.equals(_key);
        }

        public String toString() {
            return _path + "|" + _key;
        }
        
        Object getKey() {
            return _key;
        }
    }

    /**
     * A {@link Result} implementation wrapped around this select.
     */
    public static class SelectResult
        extends ResultSetResult
        implements PathJoins {

        private SelectImpl _sel = null;
        private Map<CachedColumnAliasKey, Object> cachedColumnAlias_ = null;

        // position in selected columns list where we expect the next load
        private int _pos = 0;
        private Stack _preJoins = null;

        /**
         * Constructor.
         */
        public SelectResult(Connection conn, Statement stmnt, ResultSet rs,
            DBDictionary dict) {
            super(conn, stmnt, rs, dict);
        }

        /**
         * Select for this result.
         */
        public SelectImpl getSelect() {
            return _sel;
        }

        /**
         * Select for this result.
         */
        public void setSelect(SelectImpl sel) {
            _sel = sel;
        }

        public Object getEager(FieldMapping key) {
            // don't bother creating key if we know we don't have any
            // eager results
            if (_sel._eager == null || !_sel._eagerKeys.contains(key))
                return null;
            Map map = SelectResult.this.getEagerMap(true);
            if (map == null)
                return null;
            return map.get(_sel.toEagerKey(key, getJoins(null)));
        }

        public void putEager(FieldMapping key, Object res) {
            Map map = SelectResult.this.getEagerMap(true);
            if (map == null) {
                map = new HashMap();
                setEagerMap(map);
            }
            map.put(_sel.toEagerKey(key, getJoins(null)), res);
        }

        public Object load(ClassMapping mapping, JDBCStore store,
            JDBCFetchConfiguration fetch, Joins joins)
            throws SQLException {
            boolean hasJoins = joins != null
                && ((PathJoins) joins).path() != null;
            if (hasJoins) {
                if (_preJoins == null)
                    _preJoins = new Stack();
                _preJoins.push(joins);
            }

            Object obj = super.load(mapping, store, fetch, joins);

            // reset
            if (hasJoins)
                _preJoins.pop();
            return obj;
        }

        public Joins newJoins() {
            PathJoins pre = getPreJoins();
            if (pre == null || pre.path() == null)
                return this;

            PathJoinsImpl pj = new PathJoinsImpl();
            pj.path = new StringBuilder(pre.path().toString());
            return pj;
        }

        protected boolean containsInternal(Object obj, Joins joins) {
            // we key directly on objs and join-less cols, or on the alias
            // for cols with joins
            PathJoins pj = getJoins(joins);
            if (pj != null && pj.path() != null) {
                Object columnAlias = getColumnAlias((Column) obj, pj);
                if (joins == null) {
                    if (cachedColumnAlias_ == null) {
                        cachedColumnAlias_ = new HashMap<CachedColumnAliasKey, Object>();
                    }
                    cachedColumnAlias_.put(new CachedColumnAliasKey((Column) obj, pj), columnAlias);
                }
                return columnAlias != null && _sel._selects.contains(columnAlias);
            }
            return obj != null && _sel._selects.contains(obj);
        }

        protected boolean containsAllInternal(Object[] objs, Joins joins)
            throws SQLException {
            PathJoins pj = getJoins(joins);
            Object obj;
            for (int i = 0; i < objs.length; i++) {
                if (pj != null && pj.path() != null)
                    obj = getColumnAlias((Column) objs[i], pj);
                else
                    obj = objs[i];
                if (obj == null || !_sel._selects.contains(obj))
                    return false;
            }
            return true;
        }

        public void pushBack()
            throws SQLException {
            _pos = 0;
            super.pushBack();
        }

        protected boolean absoluteInternal(int row)
            throws SQLException {
            _pos = 0;
            return super.absoluteInternal(row);
        }

        protected boolean nextInternal()
            throws SQLException {
            _pos = 0;
            return super.nextInternal();
        }

        protected int findObject(Object obj, Joins joins)
            throws SQLException {
            Object orig = obj;
            if (_pos == _sel._selects.size())
                _pos = 0;

            // we key directly on objs and join-less cols, or on the alias
            // for cols with joins
            PathJoins pj = getJoins(joins);
            Boolean pk = null;
            if (pj != null && pj.path() != null) {
                Column col = (Column) obj;
                pk = (col.isPrimaryKey()) ? Boolean.TRUE : Boolean.FALSE;
                if (joins == null && cachedColumnAlias_ != null) {
                    obj = cachedColumnAlias_.get(new CachedColumnAliasKey((Column) obj, pj));
                    if (obj == null) {
                        obj = getColumnAlias(col, pj);
                    }
                } else {
                    obj = getColumnAlias(col, pj);
                }
                if (obj == null)
                    throw new SQLException(col.getTable() + ": "
                        + pj.path() + " (" + _sel._aliases + ")");
            }

            // we load in the same order we select, more or less...
            if (_sel._selects.get(_pos).equals(obj))
                return ++_pos;

            // if we're looking for a primary key, try back a couple places,
            // since pks might be selected in a slightly different order than
            // they are loaded back; don't change the marker position
            if (pk == null)
                pk = (obj instanceof Column && ((Column) obj).isPrimaryKey())
                    ? Boolean.TRUE : Boolean.FALSE;
            if (pk.booleanValue()) {
                for (int i = _pos - 1; i >= 0 && i >= _pos - 3; i--)
                    if (_sel._selects.get(i).equals(obj))
                        return i + 1;
            }

            // search forward on the assumption that we might be skipping
            // selects for sibling classes; advance the position if we find
            // something forward
            for (int i = _pos + 1; i < _sel._selects.size(); i++) {
                if (_sel._selects.get(i).equals(obj)) {
                    _pos = i;
                    return ++_pos;
                }
            }

            // maybe the column was selected by 2 different mappings, so it's
            // somewhere prior to the current position; in this case leave the
            // position marker at its current place cause subsequent loads will
            // still probably start from there
            for (int i = 0; i < _pos; i++)
                if (_sel._selects.get(i).equals(obj))
                    return i + 1;

            // somethings's wrong...
            throw new SQLException(obj.toString());
        }

        /**
         * Return the joins to use to find column data.
         */
        private PathJoins getJoins(Joins joins) {
            PathJoins pj = (PathJoins) joins;
            if (pj != null && pj.path() != null)
                return pj;
            return getPreJoins();
        }

        /**
         * Return the pre joins for the result, or null if none. Note that
         * we have to take the Select's pre joins into account too, since
         * batched selects can have additional pre joins on the stack even
         * on execution.
         */
        private PathJoins getPreJoins() {
            if (_preJoins != null && !_preJoins.isEmpty())
                return (PathJoins) _preJoins.peek();
            if (_sel._preJoins != null && !_sel._preJoins.isEmpty())
                return (PathJoins) _sel._preJoins.peek();
            return null;
        }

        /**
         * Return the alias used to key on the column data, considering the
         * given joins.
         */
        private String getColumnAlias(Column col, PathJoins pj) {
            String alias;
            if (_sel._from != null) {
                alias = _sel.toAlias(_sel._from.getTableIndex
                    (col.getTable(), pj, false));
                if (alias == null)
                    return null;
                if (_sel._dict.requiresAliasForSubselect)
                    return FROM_SELECT_ALIAS + "." + alias + "_" + col;
                return alias + "_" + col;
            }
            alias = _sel.toAlias(_sel.getTableIndex(col.getTable(), pj, false));
            return (alias == null) ? null : alias + "." + col;
        }

        ////////////////////////////
        // PathJoins implementation
        ////////////////////////////

        public boolean isOuter() {
            return false;
        }

        public PathJoins setOuter(boolean outer) {
            return this;
        }

        public boolean isDirty() {
            return false;
        }

        public StringBuilder path() {
            return null;
        }
        
        public String getPathStr() {
            return null;
        }

        public JoinSet joins() {
            return null;
        }

        public int joinCount() {
            return 0;
        }

        public void nullJoins() {
        }

        public boolean isEmpty() {
            return true;
        }

        public Joins crossJoin(Table localTable, Table foreignTable) {
            return this;
        }

        public Joins join(ForeignKey fk, boolean inverse, boolean toMany) {
            return this;
        }

        public Joins outerJoin(ForeignKey fk, boolean inverse, boolean toMany) {
            return this;
        }

        public Joins joinRelation(String name, ForeignKey fk, 
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            return new PathJoinsImpl().joinRelation(name, fk, target, subs, 
                inverse, toMany);
        }

        public Joins outerJoinRelation(String name, ForeignKey fk,
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            return new PathJoinsImpl().outerJoinRelation(name, fk, target, subs,
                inverse, toMany);
        }

        public Joins setVariable(String var) {
            if (var == null)
                return this;
            return new PathJoinsImpl().setVariable(var);
        }

        public Joins setSubselect(String alias) {
            if (alias == null)
                return this;
            return new PathJoinsImpl().setSubselect(alias);
        }

        public Joins setCorrelatedVariable(String var) {
            return this;
        }

        public Joins setJoinContext(Context ctx) {
            return this;
        }

        public String getCorrelatedVariable() {
            return null;
        }

        public void moveJoinsToParent() {
        }
        
        private static final class CachedColumnAliasKey {
            private final Column col;
            private final PathJoins pjs;

            public CachedColumnAliasKey(Column c, PathJoins p) {
                col = c;
                pjs = p;
            }

            @Override
            public int hashCode() {
                final int prime = 31;
                int result = 1;
                result = prime * result + ((col == null) ? 0 : col.hashCode());
                result = prime * result + ((pjs == null) ? 0 : pjs.hashCode());
                return result;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                CachedColumnAliasKey other = (CachedColumnAliasKey) obj;
                if (col == null) {
                    if (other.col != null)
                        return false;
                } else if (!col.equals(other.col))
                    return false;
                if (pjs == null) {
                    if (other.pjs != null)
                        return false;
                } else if (!pjs.equals(other.pjs))
                    return false;
                return true;
            }

        }
    }

    /**
     * Base joins implementation.
     */
    private static class PathJoinsImpl
        implements PathJoins {

        protected StringBuilder path = null;
        protected String var = null;
        protected String correlatedVar = null;
        protected Context context = null;
        protected Context lastContext = null;
        protected String pathStr = null;

        public Select getSelect() {
            return null;
        }

        public boolean isOuter() {
            return false;
        }

        public PathJoins setOuter(boolean outer) {
            return this;
        }

        public boolean isDirty() {
            return var != null || path != null;
        }

        public StringBuilder path() {
            return path;
        }

        public JoinSet joins() {
            return null;
        }

        public int joinCount() {
            return 0;
        }

        public void nullJoins() {
        }

        public Joins setVariable(String var) {
            this.var = var;
            return this;
        }

        public String getVariable() {
            return var;
        }
        
        public Joins setCorrelatedVariable(String var) {
            this.correlatedVar = var;
            return this;
        }
        
        public String getCorrelatedVariable() {
            return correlatedVar;
        }

        public Joins setJoinContext(Context context) {
            this.context = context;
            return this;
         }

        public Joins setSubselect(String alias) {
            append(alias);
            return this;
        }

        public boolean isEmpty() {
            return true;
        }

        public Joins crossJoin(Table localTable, Table foreignTable) {
            append(var);
            var = null;
            return this;
        }

        public Joins join(ForeignKey fk, boolean inverse, boolean toMany) {
            append(var);
            var = null;
            return this;
        }

        public Joins outerJoin(ForeignKey fk, boolean inverse, boolean toMany) {
            append(var);
            var = null;
            return this;
        }

        public Joins joinRelation(String name, ForeignKey fk, 
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            append(name);
            append(var);
            var = null;
            return this;
        }

        public Joins outerJoinRelation(String name, ForeignKey fk,
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            append(name);
            append(var);
            var = null;
            return this;
        }

        protected void append(String str) {
            if (str != null) {
                if (path == null)
                    path = new StringBuilder(str);
                else
                    path.append('.').append(str);
                pathStr = null;
            }
        }
        
        public String getPathStr() {
            if (pathStr == null) {
                pathStr = path.toString();
            }
            return pathStr;
        }

        public String toString() {
            return "PathJoinsImpl<" + hashCode() + ">: "
                + String.valueOf(path);
        }

    public void moveJoinsToParent() {
    }
}

/**
     * Joins implementation.
     */
    private static class SelectJoins
        extends PathJoinsImpl 
        implements Cloneable {

        private final SelectImpl _sel;
        private JoinSet _joins = null;
        private boolean _outer = false;
        private int _count = 0;

        public SelectJoins(SelectImpl sel) {
            _sel = sel;
        }

        public Select getSelect() {
            return _sel;
        }

        public boolean isOuter() {
            return _outer;
        }

        public PathJoins setOuter(boolean outer) {
            _outer = outer;
            return this;
        }

        public boolean isDirty() {
            return super.isDirty() || !isEmpty();
        }

        public JoinSet joins() {
            return _joins;
        }

        public void setJoins(JoinSet joins) {
            _joins = joins;
            _outer = joins != null && joins.last() != null
                && joins.last().getType() == Join.TYPE_OUTER;
        }

        public int joinCount() {
            if (_joins == null)
                return _count;
            return Math.max(_count, _joins.size());
        }

        public void nullJoins() {
            if (_joins != null)
                _count = Math.max(_count, _joins.size());
            _joins = null;
        }

        public boolean isEmpty() {
            return _joins == null || _joins.isEmpty();
        }

        public Joins crossJoin(Table localTable, Table foreignTable) {
            // cross joins are for unbound variables; unfortunately we have
            // to always go DISTINCT for unbound vars because there are certain
            // cases that require it, and we can't differentiate them from the
            // cases that don't
            _sel._flags |= IMPLICIT_DISTINCT;

            if (_sel.getJoinSyntax() != JoinSyntaxes.SYNTAX_SQL92
                || _sel._from != null) {
                // don't make any joins, but update the path if a variable
                // has been set
                if (this.var != null) {
                    this.append(this.var);
                } else if (this.path == null && this.correlatedVar != null && _sel._dict.isImplicitJoin()) {
                    this.append(this.correlatedVar);
                }
                this.var = null;
                _outer = false;
                return this;
            }

            // don't let the get alias methods see that a var has been set
            // until we get past the local table
            String var = this.var;
            this.var = null;
            Context ctx = context; 
            context = null; 

            int alias1 = _sel.getTableIndex(localTable, this, true);
            this.append(var);
            this.append(correlatedVar);
            context = ctx; 
            
            int alias2 = _sel.getTableIndex(foreignTable, this, true);
            Join j = new Join(localTable, alias1, foreignTable, alias2,
                null, false);
            j.setType(Join.TYPE_CROSS);

            if (_joins == null)
                _joins = new JoinSet();
            _joins.add(j);
            setCorrelated(j);
            _outer = false;
            lastContext =  context;
            context = null;
            return this;
        }

        public Joins join(ForeignKey fk, boolean inverse, boolean toMany) {
            return join(null, fk, null, -1, inverse, toMany, false);
        }

        public Joins outerJoin(ForeignKey fk, boolean inverse, boolean toMany) {
            return join(null, fk, null, -1, inverse, toMany, true);
        }

        public Joins joinRelation(String name, ForeignKey fk, 
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            return join(name, fk, target, subs, inverse, toMany, false);
        }

        public Joins outerJoinRelation(String name, ForeignKey fk,
            ClassMapping target, int subs, boolean inverse, boolean toMany) {
            return join(name, fk, target, subs, inverse, toMany, true);
        }

        private Joins join(String name, ForeignKey fk, ClassMapping target,
            int subs, boolean inverse, boolean toMany, boolean outer) {
            // don't let the get alias methods see that a var has been set
            // until we get past the local table
            String var = this.var;
            this.var = null;
            Context ctx = context; 
            context = null; 

            // get first table alias before updating path; if there is a from
            // select then we shouldn't actually create a join object, since
            // the joins will all be done in the from select
            boolean createJoin = _sel._from == null;
            Table table1 = null;
            int alias1 = -1;
            if (createJoin) {
                boolean createIndex = true;
                table1 = (inverse) ? fk.getPrimaryKeyTable() : fk.getTable();
                if (correlatedVar != null)
                    createIndex = false;  // not to create here
                alias1 = _sel.getTableIndex(table1, this, createIndex);
            }

            // update the path with the relation name before getting pk alias
            this.append(name);
            this.append(var);
            if (var == null)
                this.append(correlatedVar);
            context = ctx; 
            
            if (toMany) {
                _sel._flags |= IMPLICIT_DISTINCT;
                _sel._flags |= TO_MANY;
            }
            _outer = outer;

            if (createJoin) {
                boolean createIndex = true;
                Table table2 = (inverse) ? fk.getTable() 
                    : fk.getPrimaryKeyTable();
                boolean created = false;
                int alias2 = -1;
                if (table2.isAssociation()) {
                    alias2 = _sel.getTableIndex(table2, this, false);
                    if (alias2 == -1)
                        createIndex = true;
                    else 
                        created = true;
                }
                else if (context == _sel.ctx()) 
                   createIndex = true;
                else if (correlatedVar != null)
                    createIndex = false;
                if (!created)
                    alias2 = _sel.getTableIndex(table2, this, createIndex);
                Join j = new Join(table1, alias1, table2, alias2, fk, inverse);
                j.setType((outer) ? Join.TYPE_OUTER : Join.TYPE_INNER);

                if (_joins == null)
                    _joins = new JoinSet();
                if (_joins.add(j) && (subs == Select.SUBS_JOINABLE 
                    || subs == Select.SUBS_NONE))
                    j.setRelation(target, subs, clone(_sel));

                setCorrelated(j);
            }
            lastContext = context;
            context = null;
            return this;
        }

        private void setCorrelated(Join j) {
            if (_sel._parent == null)
                return;

            if (_sel._aliases == null) {
                j.setIsNotMyJoin();
               return;
            }

            Object aliases[] = _sel._aliases.values().toArray();
            boolean found1 = false;
            boolean found2 = false;

            for (int i = 0; i < aliases.length; i++) {
                int alias = ((Integer)aliases[i]).intValue();
                if (alias == j.getIndex1())
                    found1 = true;
                if (alias == j.getIndex2())
                    found2 = true;
            }
                
            if (found1 && found2)
                return;
            else if (!found1 && !found2) {
                j.setIsNotMyJoin();
                return;
            }
            else {
                j.setCorrelated();
            }
        }

        public void moveJoinsToParent() {
            if (_joins == null)
                return;
           Join j = null;
           List<Join> removed = new ArrayList<Join>(5);
           for (Iterator itr = _joins.iterator(); itr.hasNext();) {
               j = (Join) itr.next();
               if (j.isNotMyJoin()) {
                   addJoinsToParent(_sel._parent, j);
                   removed.add(j);
               }
           }
           for (Join join : removed) {
               _joins.remove(join);
           }
        }

        private void addJoinsToParent(SelectImpl parent, Join join) {
            if (parent._aliases == null)
                return;
            Object aliases[] = parent._aliases.values().toArray();
            boolean found1 = false;
            boolean found2 = false;

            for (int i = 0; i < aliases.length; i++) {
                int alias = ((Integer)aliases[i]).intValue();
                if (alias == join.getIndex1())
                    found1 = true;
                if (alias == join.getIndex2())
                    found2 = true;
            }
                
            if (found1 && found2) {
                // this is my join, add join
                if (parent._joins == null) 
                    parent._joins = new SelectJoins(parent);
                SelectJoins p = parent._joins;
                if (p.joins() == null)
                    p.setJoins(new JoinSet());                
                p.joins().add(join);
            }
            else if (parent._parent != null)
                addJoinsToParent(parent._parent, join);
        }

        public SelectJoins clone(SelectImpl sel) {
            SelectJoins sj = new SelectJoins(sel);
            sj.var = var;
            if (path != null)
                sj.path = new StringBuilder(path.toString());
            if (_joins != null && !_joins.isEmpty())
                sj._joins = new JoinSet(_joins);
            sj._outer = _outer;
            return sj;
        }

        public String toString() {
            return super.toString() + " (" + _outer + "): " + _joins;
        }
    }
    
    protected Selects newSelects() {
        return new Selects();
    }
    
    public DBDictionary getDictionary() { 
        return _dict;
    }

    /**
     * Helper class to track selected columns, with fast contains method.
     * Acts as a list of select ids, with additional methods to manipulate
     * the alias of each selected id.
     */
    protected static class Selects
        extends AbstractList {

        protected List _ids = null;
        protected List _idents = null;
        protected Map _aliases = null;
        protected Map _selectAs = null;
        protected DBDictionary _dict = null;

        /**
         * Add all aliases from another instance.
         */
        public void addAll(Selects sels) {
            if (_ids == null && sels._ids != null)
                _ids = new ArrayList(sels._ids);
            else if (sels._ids != null)
                _ids.addAll(sels._ids);

            if (_idents == null && sels._idents != null)
                _idents = new ArrayList(sels._idents);
            else if (sels._idents != null)
                _idents.addAll(sels._idents);

            if (_aliases == null && sels._aliases != null)
                _aliases = new HashMap(sels._aliases);
            else if (sels._aliases != null)
                _aliases.putAll(sels._aliases);

            if (_selectAs == null && sels._selectAs != null)
                _selectAs = new HashMap(sels._selectAs);
            else if (sels._selectAs != null)
                _selectAs.putAll(sels._selectAs);
        }

        /**
         * Returns the alias of a given id.
         */
        public Object getAlias(Object id) {
            return (_aliases == null) ? null : _aliases.get(id);
        }

        /**
         * Set an alias for a given id.
         */
        public int setAlias(Object id, Object alias, boolean ident) {
            if (_ids == null) {
                _ids = new ArrayList();
                _aliases = new HashMap();
            }

            int idx;
            if (_aliases.put(id, alias) != null)
                idx = _ids.indexOf(id);
            else {
                _ids.add(id);
                idx = _ids.size() - 1;

                if (ident) {
                    if (_idents == null)
                        _idents = new ArrayList(3);
                    _idents.add(id);
                }
            }
            return idx;
        }

        /**
         * Set an alias for a given index.
         */
        public void setAlias(int idx, Object alias) {
            Object id = _ids.get(idx);
            _aliases.put(id, alias);
        }

        /**
         * Insert an alias before the given index, using negative indexes
         * to count backwards.
         */
        public void insertAlias(int idx, Object id, Object alias) {
            _aliases.put(id, alias);
            if (idx >= 0)
                _ids.add(idx, id);
            else
                _ids.add(_ids.size() + idx, id);
        }

        /**
         * Return the index of the given alias.
         */
        public int indexOfAlias(Object alias) {
            if (_aliases == null)
                return -1;
            for (int i = 0; i < _ids.size(); i++)
                if (alias.equals(_aliases.get(_ids.get(i))))
                    return i;
            return -1;
        }

        /**
         * A list representation of the aliases, in select order, with
         * AS aliases present.
         */
        public List getAliases(final boolean ident, final boolean inner) {
            if (_ids == null)
                return Collections.EMPTY_LIST;

            return new AbstractList() {
                public int size() {
                    return (ident && _idents != null) ? _idents.size()
                        : _ids.size();
                }

                public Object get(int i) {
                    Object id = (ident && _idents != null) ? _idents.get(i)
                        : _ids.get(i);
                    Object alias = _aliases.get(id);
                    if (id instanceof Column && ((Column) id).isXML())
                        alias = alias + _dict.getStringVal;
                        
                    String as = null;
                    if (inner) {
                        if (alias instanceof String)
                            as = ((String) alias).replace('.', '_');
                    } else if (_selectAs != null)
                        as = (String) _selectAs.get(id);
                    else if (id instanceof Value)
                        as = ((Value) id).getAlias();

                    if (as != null) {
                        if (ident && _idents != null)
                            return as;
                        if (alias instanceof SQLBuffer)
                            alias = new SQLBuffer((SQLBuffer) alias).
                                append(" AS ").append(as);
                        else
                            alias = alias + " AS " + as;
                    }
                    return alias;
                }
            };
        }

        /**
         * Set that a given id's alias has an AS value.
         */
        public void setSelectAs(Object id, String as) {
            if (_selectAs == null)
                _selectAs = new HashMap((int) (5 * 1.33 + 1));
            _selectAs.put(id, as);
        }

        /**
         * Clear all placeholders and select AS clauses.
         */
        public void clearPlaceholders() {
            if (_ids == null)
                return;

            Object id;
            for (Iterator itr = _ids.iterator(); itr.hasNext();) {
                id = itr.next();
                if (id instanceof Placeholder) {
                    itr.remove();
                    _aliases.remove(id);
                }
            }
        }

        public boolean contains(Object id) {
            return _aliases != null && _aliases.containsKey(id);
        }

        public Object get(int i) {
            if (_ids == null)
                throw new ArrayIndexOutOfBoundsException();
            return _ids.get(i);
        }

        public int size() {
            return (_ids == null) ? 0 : _ids.size();
        }

        public void clear() {
            _ids = null;
            _aliases = null;
            _selectAs = null;
            _idents = null;
        }
    }

    public Joins setCorrelatedVariable(String var) {
        if (var == null)
            return this;
        return new SelectJoins(this).setCorrelatedVariable(var);
    }
    
    public Joins setJoinContext(Context ctx) {
        if (ctx == null)
            return this;
        return new SelectJoins(this).setJoinContext(ctx);
    }

    public String getCorrelatedVariable() {
        return null;
    }

    public void moveJoinsToParent() {
    }
}

/**
 * Common joins interface used internally. Cannot be made an inner class
 * because the outer class (Select) has to implement it.
 */
interface PathJoins
    extends Joins {

    /**
     * Mark this as an outer joins set.
     */
    public PathJoins setOuter(boolean outer);

    /**
     * Return true if this instance has a path, any joins, or a variable.
     */
    public boolean isDirty();

    /**
     * Return the relation path traversed by these joins, or null if none.
     */
    public StringBuilder path();

    /**
     * Return the set of {@link Join} elements, or null if none.
     */
    public JoinSet joins();

    /**
     * Return the maximum number of joins contained in this instance at any
     * time.
     */
    public int joinCount();

    /**
     * Null the set of {@link Join} elements.
     */
    public void nullJoins();

    /**
     * The select owner of this join
     * @return
     */
    public Select getSelect();
    
    public String getPathStr();
}

