/*
 * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates.
 * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 *
 * Licensed 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 com.amazon.carbonado.repo.jdbc;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.LogFactory;

import com.amazon.carbonado.Cursor;
import com.amazon.carbonado.FetchException;
import com.amazon.carbonado.IsolationLevel;
import com.amazon.carbonado.PersistException;
import com.amazon.carbonado.Query;
import com.amazon.carbonado.Repository;
import com.amazon.carbonado.RepositoryException;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.Storage;
import com.amazon.carbonado.SupportException;
import com.amazon.carbonado.Transaction;
import com.amazon.carbonado.Trigger;
import com.amazon.carbonado.capability.IndexInfo;
import com.amazon.carbonado.cursor.ControllerCursor;
import com.amazon.carbonado.cursor.EmptyCursor;
import com.amazon.carbonado.cursor.LimitCursor;
import com.amazon.carbonado.filter.AndFilter;
import com.amazon.carbonado.filter.Filter;
import com.amazon.carbonado.filter.FilterValues;
import com.amazon.carbonado.filter.OrFilter;
import com.amazon.carbonado.filter.PropertyFilter;
import com.amazon.carbonado.filter.Visitor;
import com.amazon.carbonado.info.ChainedProperty;
import com.amazon.carbonado.info.Direction;
import com.amazon.carbonado.info.OrderedProperty;
import com.amazon.carbonado.info.StorableProperty;
import com.amazon.carbonado.info.StorablePropertyAdapter;
import com.amazon.carbonado.qe.AbstractQueryExecutor;
import com.amazon.carbonado.qe.FilteredQueryExecutor;
import com.amazon.carbonado.qe.OrderingList;
import com.amazon.carbonado.qe.QueryExecutor;
import com.amazon.carbonado.qe.QueryExecutorCache;
import com.amazon.carbonado.qe.QueryExecutorFactory;
import com.amazon.carbonado.qe.QueryFactory;
import com.amazon.carbonado.qe.QueryHints;
import com.amazon.carbonado.qe.SortedQueryExecutor;
import com.amazon.carbonado.qe.StandardQuery;
import com.amazon.carbonado.qe.StandardQueryFactory;
import com.amazon.carbonado.sequence.SequenceValueProducer;
import com.amazon.carbonado.spi.TriggerManager;
import com.amazon.carbonado.txn.TransactionScope;
import com.amazon.carbonado.util.QuickConstructorGenerator;

/**
 *
 *
 * @author Brian S O'Neill
 */
class JDBCStorage<S extends Storable> extends StandardQueryFactory<S>
    implements Storage<S>, JDBCSupport<S>
{
    private static final int FIRST_RESULT_INDEX = 1;

    final JDBCRepository mRepository;
    final JDBCSupportStrategy mSupportStrategy;
    final JDBCStorableInfo<S> mInfo;
    final InstanceFactory mInstanceFactory;
    final QueryExecutorFactory<S> mExecutorFactory;

    final TriggerManager<S> mTriggerManager;

    JDBCStorage(JDBCRepository repository, JDBCStorableInfo<S> info,
                boolean isMaster, boolean autoVersioning, boolean suppressReload)
        throws SupportException, RepositoryException
    {
        super(info.getStorableType());
        mRepository = repository;
        mSupportStrategy = repository.getSupportStrategy();
        mInfo = info;

        Class<? extends S> generatedStorableClass = JDBCStorableGenerator
            .getGeneratedClass(info, isMaster, autoVersioning, suppressReload);

        mInstanceFactory = QuickConstructorGenerator
            .getInstance(generatedStorableClass, InstanceFactory.class);

        mExecutorFactory = new QueryExecutorCache<S>(new ExecutorFactory());

        mTriggerManager = new TriggerManager<S>
            (info.getStorableType(), repository.mTriggerFactories);
    }

    @Override
    public Class<S> getStorableType() {
        return mInfo.getStorableType();
    }

    public S prepare() {
        return (S) mInstanceFactory.instantiate(this);
    }

    public JDBCRepository getJDBCRepository() {
        return mRepository;
    }

    public Repository getRootRepository() {
        return mRepository.getRootRepository();
    }

    public boolean isPropertySupported(String propertyName) {
        JDBCStorableProperty<S> property = mInfo.getAllProperties().get(propertyName);
        return property != null && property.isSupported();
    }

    /**
     * @since 1.2
     */
    public void truncate() throws PersistException {
        String truncateFormat = mSupportStrategy.getTruncateTableStatement();

        try {
            if (truncateFormat == null || mTriggerManager.getDeleteTrigger() != null) {
                query().deleteAll();
                return;
            }

            Connection con = getConnection();
            try {
                java.sql.Statement st = con.createStatement();
                try {
                    st.execute(String.format(truncateFormat, mInfo.getQualifiedTableName()));
                } finally {
                    st.close();
                }
            } catch (SQLException e) {
                throw toPersistException(e);
            } finally {
                yieldConnection(con);
            }
        } catch (FetchException e) {
            throw e.toPersistException();
        }
    }

    public boolean addTrigger(Trigger<? super S> trigger) {
        return mTriggerManager.addTrigger(trigger);
    }

    public boolean removeTrigger(Trigger<? super S> trigger) {
        return mTriggerManager.removeTrigger(trigger);
    }

    public IndexInfo[] getIndexInfo() {
        return mInfo.getIndexInfo();
    }

    public SequenceValueProducer getSequenceValueProducer(String name) throws PersistException {
        try {
            return mRepository.getSequenceValueProducer(name);
        } catch (RepositoryException e) {
            throw e.toPersistException();
        }
    }

    public Trigger<? super S> getInsertTrigger() {
        return mTriggerManager.getInsertTrigger();
    }

    public Trigger<? super S> getUpdateTrigger() {
        return mTriggerManager.getUpdateTrigger();
    }

    public Trigger<? super S> getDeleteTrigger() {
        return mTriggerManager.getDeleteTrigger();
    }

    public Trigger<? super S> getLoadTrigger() {
        return mTriggerManager.getLoadTrigger();
    }

    public void locallyDisableLoadTrigger() {
        mTriggerManager.locallyDisableLoad();
    }

    public void locallyEnableLoadTrigger() {
        mTriggerManager.locallyEnableLoad();
    }

    public boolean isTransactionForUpdate() {
        return mRepository.isTransactionForUpdate();
    }

    public FetchException toFetchException(Throwable e) {
        return mRepository.toFetchException(e);
    }

    public PersistException toPersistException(Throwable e) {
        return mRepository.toPersistException(e);
    }

    public boolean isUniqueConstraintError(SQLException e) {
        return mRepository.isUniqueConstraintError(e);
    }

    public Connection getConnection() throws FetchException {
        return mRepository.getConnection();
    }

    public void yieldConnection(Connection con) throws FetchException {
        mRepository.yieldConnection(con);
    }

    public String getDatabaseProductName() {
        return mRepository.getDatabaseProductName();
    }

    /**
     * @param loader used to reload Blob outside original transaction
     */
    public com.amazon.carbonado.lob.Blob convertBlob(java.sql.Blob blob, JDBCBlobLoader loader)
        throws FetchException
    {
        JDBCBlob jblob = mSupportStrategy.convertBlob(blob, loader);

        if (jblob != null) {
            try {
                JDBCTransaction txn = mRepository.localTransactionScope().getTxn();
                if (txn != null) {
                    txn.register(jblob);
                }
            } catch (Exception e) {
                throw toFetchException(e);
            }
        }

        return jblob;
    }

    /**
     * @param loader used to reload Clob outside original transaction
     */
    public com.amazon.carbonado.lob.Clob convertClob(java.sql.Clob clob, JDBCClobLoader loader)
        throws FetchException
    {
        JDBCClob jclob = mSupportStrategy.convertClob(clob, loader);

        if (jclob != null) {
            try {
                JDBCTransaction txn = mRepository.localTransactionScope().getTxn();
                if (txn != null) {
                    txn.register(jclob);
                }
            } catch (Exception e) {
                throw toFetchException(e);
            }
        }

        return jclob;
    }

    /**
     * @return original blob if too large and post-insert update is required, null otherwise
     * @throws PersistException instead of FetchException since this code is
     * called during an insert operation
     */
    public com.amazon.carbonado.lob.Blob setBlobValue(PreparedStatement ps, int column,
                                                      com.amazon.carbonado.lob.Blob blob)
        throws PersistException
    {
        return mSupportStrategy.setBlobValue(ps, column, blob);
    }

    /**
     * @return original clob if too large and post-insert update is required, null otherwise
     * @throws PersistException instead of FetchException since this code is
     * called during an insert operation
     */
    public com.amazon.carbonado.lob.Clob setClobValue(PreparedStatement ps, int column,
                                                      com.amazon.carbonado.lob.Clob clob)
        throws PersistException
    {
        return mSupportStrategy.setClobValue(ps, column, clob);
    }

    public void updateBlob(com.amazon.carbonado.lob.Blob oldBlob,
                           com.amazon.carbonado.lob.Blob newBlob)
        throws PersistException
    {
        mSupportStrategy.updateBlob(oldBlob, newBlob);
    }

    public void updateClob(com.amazon.carbonado.lob.Clob oldClob,
                           com.amazon.carbonado.lob.Clob newClob)
        throws PersistException
    {
        mSupportStrategy.updateClob(oldClob, newClob);
    }

    protected JDBCStorableInfo<S> getStorableInfo() {
        return mInfo;
    }

    @Override
    protected StandardQuery<S> createQuery(Filter<S> filter,
                                           FilterValues<S> values,
                                           OrderingList<S> ordering,
                                           QueryHints hints)
    {
        return new JDBCQuery(filter, values, ordering, hints);
    }

    static PreparedStatement prepareStatement(Connection con, String sql,
                                              Query.Controller controller)
        throws SQLException
    {
        PreparedStatement ps = con.prepareStatement(sql);

        if (controller != null) {
            long timeout = controller.getTimeout();
            if (timeout >= 0) {
                TimeUnit unit = controller.getTimeoutUnit();
                if (unit != null) {
                    long seconds = unit.toSeconds(timeout);
                    int intSeconds = seconds <= 0 ? 1 :
                        (seconds <= Integer.MAX_VALUE ? ((int) seconds) : 0);
                    ps.setQueryTimeout(intSeconds);
                }
            }
        }

        return ps;
    }

    public S instantiate(ResultSet rs) throws SQLException {
        return (S) mInstanceFactory.instantiate(this, rs, FIRST_RESULT_INDEX);
    }

    public static interface InstanceFactory {
        Storable instantiate(JDBCSupport storage);

        Storable instantiate(JDBCSupport storage, ResultSet rs, int offset) throws SQLException;
    }

    private class ExecutorFactory implements QueryExecutorFactory<S> {
        ExecutorFactory() {
        }

        public Class<S> getStorableType() {
            return JDBCStorage.this.getStorableType();
        }

        public QueryExecutor<S> executor(Filter<S> filter, OrderingList<S> ordering,
                                         QueryHints hints)
            throws RepositoryException
        {
            TableAliasGenerator aliasGenerator = new TableAliasGenerator();

            JoinNode jn;
            try {
                JoinNodeBuilder<S> jnb =
                    new JoinNodeBuilder<S>(mRepository, getStorableInfo(), aliasGenerator);
                if (filter != null) {
                    filter.accept(jnb, null);
                }
                jn = jnb.getRootJoinNode();
                jnb.captureOrderings(ordering);
            } catch (UndeclaredThrowableException e) {
                throw toFetchException(e);
            }

            SQLStatementBuilder<S> selectBuilder = new SQLStatementBuilder<S>(mRepository);
            selectBuilder.append("SELECT ");

            // Don't bother using a table alias for one table. With just one table,
            // there's no need to disambiguate.
            String alias = jn.isAliasRequired() ? jn.getAlias() : null;

            Map<String, JDBCStorableProperty<S>> properties = getStorableInfo().getAllProperties();
            int ordinal = 0;
            for (JDBCStorableProperty<S> property : properties.values()) {
                if (!property.isSelectable()) {
                    continue;
                }
                if (ordinal > 0) {
                    selectBuilder.append(',');
                }
                if (alias != null) {
                    selectBuilder.append(alias);
                    selectBuilder.append('.');
                }
                selectBuilder.append(property.getColumnName());
                ordinal++;
            }

            selectBuilder.append(" FROM");

            SQLStatementBuilder<S> fromWhereBuilder = new SQLStatementBuilder<S>(mRepository);
            fromWhereBuilder.append(" FROM");

            SQLStatementBuilder<S> deleteFromWhereBuilder;

            if (alias == null) {
                // Don't bother defining a table alias for one table.
                jn.appendTableNameTo(selectBuilder);
                jn.appendTableNameTo(fromWhereBuilder);
                deleteFromWhereBuilder = null;
            } else {
                jn.appendFullJoinTo(selectBuilder);
                jn.appendFullJoinTo(fromWhereBuilder);
                // Since the delete is operating with joins, need to generate
                // a special form that uses WHERE EXISTS.
                deleteFromWhereBuilder = new SQLStatementBuilder<S>(mRepository);
                deleteFromWhereBuilder.append(" FROM");
                jn.appendTableNameAndAliasTo(deleteFromWhereBuilder);
            }

            // Appending where clause. Remainder filter is required if a
            // derived property is used. Derived properties in exists filters
            // is not supported.

            Filter<S> remainderFilter = null;

            PropertyFilter<S>[] propertyFilters = null;
            boolean[] propertyFilterNullable = null;

            if (filter != null && !filter.isOpen()) {
                Filter<S> sqlFilter = null;
                
                List<Filter<S>> splitList = filter.conjunctiveNormalFormSplit();
                for (Filter<S> split : splitList) {
                    if (usesDerivedProperty(split)) {
                        remainderFilter = and(remainderFilter, split);
                    } else {
                        sqlFilter = and(sqlFilter, split);
                    }
                }

                if (remainderFilter == null) {
                    // Just use original filter.
                    sqlFilter = filter;
                }

                if (sqlFilter == null) {
                    // Just use original filter for remainder.
                    remainderFilter = filter;
                } else {
                    // Build the WHERE clause only if anything to filter on.
                    WhereBuilder<S> wb = new WhereBuilder<S>
                        (selectBuilder, alias == null ? null : jn, aliasGenerator);
                    wb.append(sqlFilter);

                    propertyFilters = wb.getPropertyFilters();
                    propertyFilterNullable = wb.getPropertyFilterNullable();

                    wb = new WhereBuilder<S>
                        (fromWhereBuilder, alias == null ? null : jn, aliasGenerator);
                    wb.append(sqlFilter);

                    if (deleteFromWhereBuilder != null) {
                        wb = new WhereBuilder<S>(deleteFromWhereBuilder, jn, aliasGenerator);
                        wb.appendExists(sqlFilter);
                    }
                }
            }

            // Append order-by clause. Remainder ordering is required if a derived
            // property is used.

            OrderingList<S> sqlOrdering = ordering;
            OrderingList<S> remainderOrdering = null;

            if (ordering != null && ordering.size() > 0) {
                ordinal = 0;
                for (OrderedProperty<S> orderedProperty : ordering) {
                    if (orderedProperty.getChainedProperty().isDerived()) {
                        sqlOrdering = ordering.subList(0, ordinal);
                        remainderOrdering = ordering.subList(ordinal, ordering.size());
                        break;
                    }
                    ordinal++;
                }

                if (sqlOrdering != null && sqlOrdering.size() > 0) {
                    selectBuilder.append(" ORDER BY ");
                    ordinal = 0;
                    for (OrderedProperty<S> orderedProperty : sqlOrdering) {
                        if (ordinal > 0) {
                            selectBuilder.append(',');
                        }
                        selectBuilder.appendColumn(alias == null ? null : jn,
                                                   orderedProperty.getChainedProperty());
                        if (orderedProperty.getDirection() == Direction.DESCENDING) {
                            selectBuilder.append(" DESC");
                        }
                        ordinal++;
                    }
                }
            }

            SQLStatement<S> selectStatement, fromWhere, deleteFromWhere;

            selectStatement = selectBuilder.build();
            fromWhere = fromWhereBuilder.build();
            deleteFromWhere = deleteFromWhereBuilder == null ? null
                : deleteFromWhereBuilder.build();

            QueryExecutor<S> executor = new Executor(filter,
                                                     sqlOrdering,
                                                     selectStatement,
                                                     fromWhere,
                                                     deleteFromWhere,
                                                     propertyFilters,
                                                     propertyFilterNullable);

            if (remainderFilter != null && !remainderFilter.isOpen()) {
                executor = new FilteredQueryExecutor<S>(executor, remainderFilter);
            }

            if (remainderOrdering != null && remainderOrdering.size() > 0) {
                executor = new SortedQueryExecutor<S>
                    (new SortedQueryExecutor.MergeSortSupport(),
                     executor, sqlOrdering, remainderOrdering);
            }

            return executor;
        }

        private Filter<S> and(Filter<S> left, Filter<S> right) {
            if (left == null) {
                return right;
            }
            if (right == null) {
                return left;
            }
            return left.and(right);
        }

        private boolean usesDerivedProperty(Filter<S> filter) {
            Boolean result = filter.accept(new Visitor<S, Boolean, Object>() {
                @Override
                public Boolean visit(OrFilter<S> orFilter, Object param) {
                    Boolean result = orFilter.getLeftFilter().accept(this, param);
                    if (result != null && result) {
                        // Short-circuit.
                        return result;
                    }
                    return orFilter.getRightFilter().accept(this, param);
                }

                @Override
                public Boolean visit(AndFilter<S> andFilter, Object param) {
                    Boolean result = andFilter.getLeftFilter().accept(this, param);
                    if (result != null && result) {
                        // Short-circuit.
                        return result;
                    }
                    return andFilter.getRightFilter().accept(this, param);
                }

                @Override
                public Boolean visit(PropertyFilter<S> propFilter, Object param) {
                    return propFilter.getChainedProperty().isDerived();
                }
            }, null);

            return result != null && result;
        }
    }

    private class Executor extends AbstractQueryExecutor<S> {
        private final Filter<S> mFilter;
        private final OrderingList<S> mOrdering;

        private final SQLStatement<S> mSelectStatement;
        private final int mMaxSelectStatementLength;
        private final SQLStatement<S> mFromWhere;
        private final int mMaxFromWhereLength;
        private final SQLStatement<S> mDeleteFromWhere;
        private final int mMaxDeleteFromWhereLength;

        // The following arrays all have the same length, or they may all be null.

        private final PropertyFilter<S>[] mPropertyFilters;
        private final boolean[] mPropertyFilterNullable;

        private final Method[] mPreparedStatementSetMethods;

        // Some entries may be null if no adapter required.
        private final Method[] mAdapterMethods;

        // Some entries may be null if no adapter required.
        private final Object[] mAdapterInstances;

        Executor(Filter<S> filter,
                 OrderingList<S> ordering,
                 SQLStatement<S> selectStatement,
                 SQLStatement<S> fromWhere,
                 SQLStatement<S> deleteFromWhere,
                 PropertyFilter<S>[] propertyFilters,
                 boolean[] propertyFilterNullable)
            throws RepositoryException
        {
            mFilter = filter;
            mOrdering = ordering;

            mSelectStatement = selectStatement;
            mMaxSelectStatementLength = selectStatement.maxLength();

            mFromWhere = fromWhere;
            mMaxFromWhereLength = fromWhere.maxLength();

            if (deleteFromWhere == null) {
                mDeleteFromWhere = mFromWhere;
                mMaxDeleteFromWhereLength = mMaxFromWhereLength;
            } else {
                mDeleteFromWhere = deleteFromWhere;
                mMaxDeleteFromWhereLength = deleteFromWhere.maxLength();
            }

            if (propertyFilters == null) {
                mPropertyFilters = null;
                mPropertyFilterNullable = null;
                mPreparedStatementSetMethods = null;
                mAdapterMethods = null;
                mAdapterInstances = null;
            } else {
                mPropertyFilters = propertyFilters;
                mPropertyFilterNullable = propertyFilterNullable;

                int length = propertyFilters.length;

                mPreparedStatementSetMethods = new Method[length];
                mAdapterMethods = new Method[length];
                mAdapterInstances = new Object[length];

                gatherAdapterMethods(propertyFilters);
            }
        }

        private void gatherAdapterMethods(PropertyFilter<S>[] filters)
            throws RepositoryException
        {
            for (int i=0; i<filters.length; i++) {
                PropertyFilter<S> filter = filters[i];
                ChainedProperty<S> chained = filter.getChainedProperty();
                StorableProperty<?> property = chained.getLastProperty();
                JDBCStorableProperty<?> jProperty =
                    mRepository.getJDBCStorableProperty(property);

                Method psSetMethod = jProperty.getPreparedStatementSetMethod();
                mPreparedStatementSetMethods[i] = psSetMethod;

                StorablePropertyAdapter adapter = jProperty.getAppliedAdapter();
                if (adapter != null) {
                    Class toType = psSetMethod.getParameterTypes()[1];
                    mAdapterMethods[i] = adapter.findAdaptMethod(jProperty.getType(), toType);
                    // Special case for converting character to String.
                    if (mAdapterMethods[i] == null) {
                        if (toType == String.class) {
                            mAdapterMethods[i] = adapter
                                .findAdaptMethod(jProperty.getType(), Character.class);
                            if (mAdapterMethods[i] == null) {
                                mAdapterMethods[i] = adapter
                                    .findAdaptMethod(jProperty.getType(), char.class);
                            }
                        }
                    }
                    mAdapterInstances[i] = adapter.getAdapterInstance();
                }
            }
        }

        @Override
        public Cursor<S> fetch(FilterValues<S> values) throws FetchException {
            return fetch(values, null);
        }

        @Override
        public Cursor<S> fetch(FilterValues<S> values, Query.Controller controller)
            throws FetchException
        {
            TransactionScope<JDBCTransaction> scope = mRepository.localTransactionScope();
            boolean forUpdate = scope.isForUpdate();
            Connection con = getConnection();
            try {
                PreparedStatement ps =
                    prepareStatement(con, prepareSelect(values, forUpdate), controller);
                Integer fetchSize = mRepository.getFetchSize();
                if (fetchSize != null) {
                    ps.setFetchSize(fetchSize);
                }

                try {
                    setParameters(ps, values);
                    return ControllerCursor.apply
                        (new JDBCCursor<S>(JDBCStorage.this, scope, con, ps), controller);
                } catch (Exception e) {
                    // in case of exception, close statement
                    try {
                        ps.close();
                    } catch (SQLException e2) {
                        // ignore and allow triggering exception to propagate
                    }
                    throw e;
                }
            } catch (Exception e) {
                // in case of exception, yield connection
                try {
                    yieldConnection(con);
                } catch (FetchException e2) {
                   // ignore and allow triggering exception to propagate
                }
                throw toFetchException(e);
            }
        }

        @Override
        public Cursor<S> fetchSlice(FilterValues<S> values, long from, Long to)
            throws FetchException
        {
            return fetchSlice(values, from, to, null);
        }

        @Override
        public Cursor<S> fetchSlice(FilterValues<S> values, long from, Long to,
                                    Query.Controller controller)
            throws FetchException
        {
            if (to != null && (to - from) <= 0) {
                return EmptyCursor.the();
            }

            JDBCSupportStrategy.SliceOption option = mSupportStrategy.getSliceOption();

            String select;

            switch (option) {
            case NOT_SUPPORTED: default:
                return super.fetchSlice(values, from, to, controller);
            case LIMIT_ONLY:
                if (from > 0 || to == null) {
                    return super.fetchSlice(values, from, to, controller);
                }
                select = prepareSelect(values, false);
                select = mSupportStrategy.buildSelectWithSlice(select, false, true);
                break;
            case OFFSET_ONLY:
                if (from <= 0) {
                    return super.fetchSlice(values, from, to, controller);
                }
                select = prepareSelect(values, false);
                select = mSupportStrategy.buildSelectWithSlice(select, true, false);
                break;
            case LIMIT_AND_OFFSET:
            case OFFSET_AND_LIMIT:
            case FROM_AND_TO:
                select = prepareSelect(values, false);
                select = mSupportStrategy.buildSelectWithSlice(select, from > 0, to != null);
                break;
            }

            TransactionScope<JDBCTransaction> scope = mRepository.localTransactionScope();
            if (scope.isForUpdate()) {
                select = select.concat(" FOR UPDATE");
            }

            Connection con = getConnection();
            try {
                PreparedStatement ps = prepareStatement(con, select, controller);
                Integer fetchSize = mRepository.getFetchSize();
                if (fetchSize != null) {
                    ps.setFetchSize(fetchSize);
                }

                try {
                    int psOrdinal = setParameters(ps, values);

                    if (from > 0) {
                        if (to != null) {
                            switch (option) {
                            case OFFSET_ONLY:
                                ps.setLong(psOrdinal, from);
                                Cursor<S> c =
                                    ControllerCursor.apply
                                    (new JDBCCursor<S>(JDBCStorage.this, scope, con, ps),
                                     controller);
                                return new LimitCursor<S>(c, to - from);
                            case LIMIT_AND_OFFSET:
                                ps.setLong(psOrdinal, to - from);
                                ps.setLong(psOrdinal + 1, from);
                                break;
                            case OFFSET_AND_LIMIT:
                                ps.setLong(psOrdinal, from);
                                ps.setLong(psOrdinal + 1, to - from);
                                break;
                            case FROM_AND_TO:
                                ps.setLong(psOrdinal, from);
                                ps.setLong(psOrdinal + 1, to);
                                break;
                            }
                        } else {
                            ps.setLong(psOrdinal, from);
                        }
                    } else if (to != null) {
                        ps.setLong(psOrdinal, to);
                    }

                    return ControllerCursor.apply
                        (new JDBCCursor<S>(JDBCStorage.this, scope, con, ps), controller);
                } catch (Exception e) {
                    // in case of exception, close statement
                    try {
                        ps.close();
                    } catch (SQLException e2) {
                        // ignore and allow triggering exception to propagate
                    }
                    throw e;
                }
            } catch (Exception e) {
                // in case of exception, yield connection
                try {
                    yieldConnection(con);
                } catch (FetchException e2) {
                   // ignore and allow triggering exception to propagate
                }
                throw toFetchException(e);
            }
        }

        @Override
        public long count(FilterValues<S> values) throws FetchException {
            return count(values, null);
        }

        @Override
        public long count(FilterValues<S> values, Query.Controller controller)
            throws FetchException
        {
            Connection con = getConnection();
            try {
                PreparedStatement ps = prepareStatement(con, prepareCount(values), controller);

                try {
                    setParameters(ps, values);
                    ResultSet rs = ps.executeQuery();
                    try {
                        rs.next();
                        return rs.getLong(1);
                    } finally {
                        rs.close();
                    }
                } finally {
                    ps.close();
                }
            } catch (Exception e) {
                throw toFetchException(e);
            } finally {
                yieldConnection(con);
            }
        }

        @Override
        public Filter<S> getFilter() {
            return mFilter;
        }

        @Override
        public OrderingList<S> getOrdering() {
            return mOrdering;
        }

        @Override
        public boolean printNative(Appendable app, int indentLevel, FilterValues<S> values)
            throws IOException
        {
            indent(app, indentLevel);
            boolean forUpdate = mRepository.localTransactionScope().isForUpdate();
            app.append(prepareSelect(values, forUpdate));
            app.append('\n');
            return true;
        }

        @Override
        public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> values)
            throws IOException
        {
            try {
                boolean forUpdate = mRepository.localTransactionScope().isForUpdate();
                String statement = prepareSelect(values, forUpdate);
                return mRepository.getSupportStrategy().printPlan(app, indentLevel, statement);
            } catch (FetchException e) {
                LogFactory.getLog(JDBCStorage.class).error(null, e);
                return false;
            }
        }

        /**
         * Delete operation is included in cursor factory for ease of implementation.
         */
        int executeDelete(FilterValues<S> filterValues, Query.Controller controller)
            throws PersistException
        {
            Connection con;
            try {
                con = getConnection();
            } catch (FetchException e) {
                throw e.toPersistException();
            }
            try {
                PreparedStatement ps =
                    prepareStatement(con, prepareDelete(filterValues), controller);
                try {
                    setParameters(ps, filterValues);
                    return ps.executeUpdate();
                } finally {
                    ps.close();
                }
            } catch (Exception e) {
                throw toPersistException(e);
            } finally {
                try {
                    yieldConnection(con);
                } catch (FetchException e) {
                    throw e.toPersistException();
                }
            }
        }

        private String prepareSelect(FilterValues<S> filterValues, boolean forUpdate) {
            if (!forUpdate) {
                return mSelectStatement.buildStatement(mMaxSelectStatementLength, filterValues);
            }

            // Allocate with extra room for " FOR UPDATE"
            StringBuilder b = new StringBuilder(mMaxSelectStatementLength + 11);
            mSelectStatement.appendTo(b, filterValues);
            b.append(" FOR UPDATE");
            return b.toString();
        }

        private String prepareCount(FilterValues<S> filterValues) {
            // Allocate with extra room for "SELECT COUNT(*)"
            StringBuilder b = new StringBuilder(15 + mMaxFromWhereLength);
            b.append("SELECT COUNT(*)");
            mFromWhere.appendTo(b, filterValues);
            return b.toString();
        }

        private String prepareDelete(FilterValues<S> filterValues) {
            // Allocate with extra room for "DELETE"
            StringBuilder b = new StringBuilder(6 + mMaxDeleteFromWhereLength);
            b.append("DELETE");
            mDeleteFromWhere.appendTo(b, filterValues);
            return b.toString();
        }

        /**
         * @return next value ordinal
         */
        private int setParameters(PreparedStatement ps, FilterValues<S> filterValues)
            throws Exception
        {
            PropertyFilter<S>[] propertyFilters = mPropertyFilters;

            if (propertyFilters == null) {
                return 1;
            }

            boolean[] propertyFilterNullable = mPropertyFilterNullable;
            Method[] psSetMethods = mPreparedStatementSetMethods;
            Method[] adapterMethods = mAdapterMethods;
            Object[] adapterInstances = mAdapterInstances;

            int ordinal = 0;
            int psOrdinal = 1; // Start at one since JDBC ordinals are one-based.
            for (PropertyFilter<S> filter : propertyFilters) {
                setValue: {
                    Object value = filterValues.getAssignedValue(filter);

                    if (value == null && propertyFilterNullable[ordinal]) {
                        // No '?' parameter to fill since value "IS NULL" or "IS NOT NULL"
                        break setValue;
                    }

                    Method adapter = adapterMethods[ordinal];
                    if (adapter != null) {
                        value = adapter.invoke(adapterInstances[ordinal], value);
                    }

                    // Special case for converting character to String.
                    if (value != null && value instanceof Character) {
                        value = String.valueOf((Character) value);
                    }

                    psSetMethods[ordinal].invoke(ps, psOrdinal, value);
                    psOrdinal++;
                }

                ordinal++;
            }

            return psOrdinal;
        }
    }

    private class JDBCQuery extends StandardQuery<S> {
        JDBCQuery(Filter<S> filter,
                  FilterValues<S> values,
                  OrderingList<S> ordering,
                  QueryHints hints)
        {
            super(filter, values, ordering, hints);
        }

        @Override
        public void deleteAll() throws PersistException {
            deleteAll(null);
        }

        @Override
        public void deleteAll(Controller controller) throws PersistException {
            if (mTriggerManager.getDeleteTrigger() != null) {
                // Super implementation loads one at time and calls
                // delete. This allows delete trigger to be invoked on each.
                super.deleteAll(controller);
            } else {
                try {
                    ((Executor) executor()).executeDelete(getFilterValues(), controller);
                } catch (RepositoryException e) {
                    throw e.toPersistException();
                }
            }
        }

        @Override
        protected Transaction enterTransaction(IsolationLevel level) {
            return getRootRepository().enterTransaction(level);
        }

        @Override
        protected QueryFactory<S> queryFactory() {
            return JDBCStorage.this;
        }

        @Override
        protected QueryExecutorFactory<S> executorFactory() {
            return JDBCStorage.this.mExecutorFactory;
        }

        @Override
        protected StandardQuery<S> newInstance(FilterValues<S> values, OrderingList<S> ordering,
                                               QueryHints hints)
        {
            return new JDBCQuery(values.getFilter(), values, ordering, hints);
        }
    }
}