/*
 * 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.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.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.InternalException;

import static java.util.Collections.emptyIterator;

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

    @Override
    public ClassMapping getTablePerClassMeta() {
        return _tpcMeta;
    }
    @Override
    public void setTablePerClassMeta(ClassMapping meta) {
        _tpcMeta = meta;
    }

    @Override
    public void setJoinedTableClassMeta(List meta) {
        _joinedTables = meta;
    }

    @Override
    public List getJoinedTableClassMeta() {
        return _joinedTables;
    }

    @Override
    public void setExcludedJoinedTableClassMeta(List meta) {
        _exJoinedTables = meta;
    }

    @Override
    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;
    }

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

    @Override
    public Context ctx() {
        return _ctx;
    }

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

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

    @Override
    public JDBCConfiguration getConfiguration() {
        return _conf;
    }

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

    @Override
    public SQLBuffer getSQL() {
        return _full;
    }

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

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

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

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

    @Override
    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;
        }
    }

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

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

    @Override
    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;
    }

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

    @Override
    public int getJoinSyntax() {
        return _joinSyntax;
    }

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

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

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

    @Override
    public boolean hasMultipleSelects() {
        if (_eager == null)
            return false;
        Map.Entry entry;
        for (Object o : _eager.entrySet()) {
            entry = (Map.Entry) o;
            if (entry.getValue() != this)
                return true;
        }
        return false;
    }

    @Override
    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) {}
        }
    }

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

    @Override
    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 (Object o : sel._eager.entrySet()) {
            entry = (Map.Entry) o;

            // 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
    /////////////////////////

    @Override
    public int indexOf() {
        return 0;
    }

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

    @Override
    public Select getParent() {
        return _parent;
    }

    @Override
    public String getSubselectPath() {
        return _subPath;
    }

    @Override
    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;
        }
    }

    @Override
    public void setHasSubselect(boolean hasSub) {
        _hasSub = hasSub;
    }

    @Override
    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);
    }

    @Override
    public Select getFromSelect() {
        return _from;
    }

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

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

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

    @Override
    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;
    }

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

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

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

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

    @Override
    public SQLBuffer getOrdering() {
        return _ordering;
    }

    @Override
    public SQLBuffer getGrouping() {
        return _grouping;
    }

    @Override
    public SQLBuffer getWhere() {
        return _where;
    }

    @Override
    public SQLBuffer getHaving() {
        return _having;
    }

    @Override
    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);
            }
        }
    }

    @Override
    public Joins getJoins() {
        return _joins;
    }

    @Override
    public Iterator getJoinIterator() {
        if (_joins == null || _joins.isEmpty())
            return emptyIterator();
        return _joins.joins().joinIterator();
    }

    @Override
    public long getStartIndex() {
        return _startIdx;
    }

    @Override
    public long getEndIndex() {
        return _endIdx;
    }

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

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

    @Override
    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);
    }

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

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

    /**
     * Return the alias for the give column
     */
    @Override
    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(".");
    }

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

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

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

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

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

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

    @Override
    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++];
    }

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

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

    @Override
    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();
    }

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

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

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

    @Override
    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;
    }

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

    @Override
    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;
    }

    @Override
