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

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.openjpa.datacache.QueryCache;
import org.apache.openjpa.datacache.QueryCacheStoreQuery;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.event.OrphanedKeyAction;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Discriminator;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import org.apache.openjpa.jdbc.meta.strats.SuperclassDiscriminatorStrategy;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.JoinSyntaxes;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.jdbc.sql.SQLFactory;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.jdbc.sql.SelectExecutor;
import org.apache.openjpa.jdbc.sql.Union;
import org.apache.openjpa.kernel.BrokerImpl;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.FinderCache;
import org.apache.openjpa.kernel.LockManager;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCState;
import org.apache.openjpa.kernel.QueryLanguages;
import org.apache.openjpa.kernel.Seq;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.StoreManager;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.kernel.exps.ExpressionParser;
import org.apache.openjpa.lib.jdbc.DelegatingConnection;
import org.apache.openjpa.lib.jdbc.DelegatingPreparedStatement;
import org.apache.openjpa.lib.jdbc.DelegatingStatement;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.rop.MergedResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.OpenJPAId;
import org.apache.openjpa.util.StoreException;
import org.apache.openjpa.util.UserException;
import org.apache.openjpa.util.UuidId;

/**
 * StoreManager plugin that uses JDBC to store persistent data in a
 * relational data store.
 *
 * @author Abe White
 */
public class JDBCStoreManager implements StoreManager, JDBCStore {

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

    private StoreContext _ctx = null;
    private JDBCConfiguration _conf = null;
    private DBDictionary _dict = null;
    private SQLFactory _sql = null;
    private JDBCLockManager _lm = null;
    private DataSource _ds = null;
    private RefCountConnection _conn = null;
    private boolean _active = false;
    private Log _log = null;

    // track the pending statements so we can cancel them
    private List<Statement> _stmnts = Collections.synchronizedList(new ArrayList<>());

    // pool statements so that we can try to reuse rather than recreate
    private List<CancelPreparedStatement> _cancelPreparedStatementsPool = new ArrayList<>();
    private List<CancelStatement> _cancelStatementPool = new ArrayList<>();

    @Override
    public StoreContext getContext() {
        return _ctx;
    }

    @Override
    public void setContext(StoreContext ctx) {
        setContext(ctx, (JDBCConfiguration) ctx.getConfiguration());
    }

    public void setContext(StoreContext ctx, JDBCConfiguration conf) {
        _ctx = ctx;
        _conf = conf;
        _dict = _conf.getDBDictionaryInstance();
        _sql = _conf.getSQLFactoryInstance();
        _log = _conf.getLog(JDBCConfiguration.LOG_DIAG);

        LockManager lm = ctx.getLockManager();
        if (lm instanceof JDBCLockManager)
            _lm = (JDBCLockManager) lm;

        _ds = getDataSource(ctx);

        if (_conf.getUpdateManagerInstance().orderDirty())
            ctx.setOrderDirtyObjects(true);
    }

    private boolean useConnectionFactory2(StoreContext ctx) {
        return (!ctx.isManaged() && _conf.isConnectionFactoryModeManaged());
    }

    private DataSource getDataSource(StoreContext ctx) {
        DataSource ds;

        if(useConnectionFactory2(ctx)) {
            ds = _conf.getDataSource2(ctx);
        }
        else {
            ds = _conf.getDataSource(ctx);
        }
        return ds;
    }

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

    @Override
    public DBDictionary getDBDictionary() {
        return _dict;
    }

    @Override
    public SQLFactory getSQLFactory() {
        return _sql;
    }

    @Override
    public JDBCLockManager getLockManager() {
        return _lm;
    }

    @Override
    public JDBCFetchConfiguration getFetchConfiguration() {
        return (JDBCFetchConfiguration) _ctx.getFetchConfiguration();
    }

    @Override
    public void beginOptimistic() {
    }

    @Override
    public void rollbackOptimistic() {
    }

    @Override
    public void begin() {
        _active = true;
        try {
            if ((!_ctx.isManaged() || !_conf.isConnectionFactoryModeManaged())
                && _conn.getAutoCommit())
                _conn.setAutoCommit(false);
        } catch (SQLException se) {
            _active = false;
            throw SQLExceptions.getStore(se, _dict);
        }
    }

    @Override
    public void commit() {
        try {
            if (!_ctx.isManaged() || !_conf.isConnectionFactoryModeManaged())
                _conn.commit();
        } catch (SQLException se) {
            try {
                _conn.rollback();
            } catch (SQLException se2) {
            }
            throw SQLExceptions.getStore(se, _dict);
        } finally {
            _active = false;
        }
    }

    @Override
    public void rollback() {
        // already rolled back ourselves?
        if (!_active)
            return;

        try {
            if (_conn != null
                && (!_ctx.isManaged() || !_conf
                    .isConnectionFactoryModeManaged()))
                _conn.rollback();
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, _dict);
        } finally {
            _active = false;
        }
    }

    @Override
    public void retainConnection() {
        connect(false);
        _conn.setRetain(true);
    }

    @Override
    public void releaseConnection() {
        if (_conn != null)
            _conn.setRetain(false);
    }

    @Override
    public Object getClientConnection() {
        return new ClientConnection(getConnection());
    }

    @Override
    public Connection getConnection() {
        connect(true);
        return _conn;
    }

    protected DataSource getDataSource() {
        return _ds;
    }

    @Override
    public boolean exists(OpenJPAStateManager sm, Object context) {
        // add where conditions on base class to avoid joins if subclass
        // doesn't use oid as identifier
        ClassMapping mapping = (ClassMapping) sm.getMetaData();
        return exists(mapping, sm.getObjectId(), context);
    }

    @Override
    public boolean isCached(List<Object> oids, BitSet edata) {
        // JDBCStoreManager doesn't store oids in memory.
        return false;
    }

    private boolean exists(ClassMapping mapping, Object oid, Object context) {
        // add where conditions on base class to avoid joins if subclass
        // doesn't use oid as identifier
        Select sel = _sql.newSelect();
        while (mapping.getJoinablePCSuperclassMapping() != null)
            mapping = mapping.getJoinablePCSuperclassMapping();

        sel.wherePrimaryKey(oid, mapping, this);
        if (_log.isTraceEnabled()) {
            _log.trace("exists: oid="+oid+" "+mapping.getDescribedType());
        }
        try {
            return sel.getCount(this) != 0;
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, _dict);
        }
    }

    @Override
    public boolean syncVersion(OpenJPAStateManager sm, Object context) {
        ClassMapping mapping = (ClassMapping) sm.getMetaData();
        try {
            return mapping.getVersion().checkVersion(sm, this, true);
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, _dict, getReadLockLevel());
        }
    }

    private int getReadLockLevel() {
        JDBCFetchConfiguration fetch = getFetchConfiguration();
        if (fetch != null) {
            return fetch.getReadLockLevel();
        }
        return -1;
    }

    @Override
    public int compareVersion(OpenJPAStateManager state, Object v1, Object v2) {
        ClassMapping mapping = (ClassMapping) state.getMetaData();
        return mapping.getVersion().compareVersion(v1, v2);
    }

    @Override
    public boolean initialize(OpenJPAStateManager sm, PCState state,
        FetchConfiguration fetch, Object context) {
        ConnectionInfo info = (ConnectionInfo) context;
        try {
            return initializeState(sm, state, (JDBCFetchConfiguration) fetch,
                info);
        } catch (ClassNotFoundException cnfe) {
            throw new UserException(cnfe);
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, Exceptions.toString(sm.getPersistenceCapable()),
                    _dict, fetch.getReadLockLevel());
        }
    }

    /**
     * Initialize a newly-loaded instance.
     */
    protected boolean initializeState(OpenJPAStateManager sm, PCState state,
        JDBCFetchConfiguration fetch, ConnectionInfo info)
        throws ClassNotFoundException, SQLException {
        Object oid = sm.getObjectId();
        ClassMapping mapping = (ClassMapping) sm.getMetaData();
        Result res = null;
        try {
            if (info != null && info.result != null) {
                res = info.result;
                info.sm = sm;
                if (info.mapping == null)
                    info.mapping = mapping;
                mapping = info.mapping;
            } else if (oid instanceof OpenJPAId
                && !((OpenJPAId) oid).hasSubclasses()) {
                Boolean custom = customLoad(sm, mapping, state, fetch);
                if (custom != null)
                    return custom;
                res = getInitializeStateResult(sm, mapping, fetch,
                    Select.SUBS_EXACT);
                if (res == null && !selectPrimaryKey(sm, mapping, fetch))
                    return false;
                if (isEmptyResult(res))
                    return false;
            } else {
                ClassMapping[] mappings = mapping.
                    getIndependentAssignableMappings();
                if (mappings.length == 1) {
                    mapping = mappings[0];
                    Boolean custom = customLoad(sm, mapping, state, fetch);
                    if (custom != null)
                        return custom;
                    res = getInitializeStateResult(sm, mapping, fetch,
                        Select.SUBS_ANY_JOINABLE);
                    if (res == null && !selectPrimaryKey(sm, mapping, fetch))
                        return false;
                } else
                    res = getInitializeStateUnionResult(sm, mapping, mappings,
                        fetch);
                if (isEmptyResult(res))
                    return false;
            }

            // figure out what type of object this is; the state manager
            // only guarantees to provide a base class
            Class<?> type;
            if ((type = getType(res, mapping)) == null) {
                if (res.getBaseMapping() != null)
                    mapping = res.getBaseMapping();
                res.startDataRequest(mapping.getDiscriminator());
                try {
                    type = mapping.getDiscriminator().getClass(this, mapping,
                        res);
                } finally {
                    res.endDataRequest();
                }
            }

            // initialize the state manager; this may change the mapping
            // and the object id instance if the type as determined
            // from the indicator is a subclass of expected type
            sm.initialize(type, state);

            if (info != null && info.result != null) {
                FieldMapping mappedByFieldMapping = info.result.
                    getMappedByFieldMapping();
                Object mappedByObject = info.result.getMappedByValue();
                if (mappedByFieldMapping != null && mappedByObject != null)
                    if (mappedByObject instanceof OpenJPAId &&
                        mapping.getExtraFieldDataIndex(mappedByFieldMapping.getIndex()) != -1) {
                        // The inverse relation can not be set since
                        // we are eagerly loading this sm for
                        // a sm owner that is still in the process of
                        // initializing itself.
                        // Remember owner oid by setIntermediate().
                        // The inverse relation is set later by
                        // setInverseRelation() when the sm owner is fully
                        // initialized.
                        int index = mappedByFieldMapping.getIndex();
                        if (sm.getLoaded().get(index)) {
                            sm.setImplData(index, mappedByObject);
                        } else {
                            sm.setIntermediate(index, mappedByObject);
                        }
                    } else {
                        setMappedBy(sm, mappedByFieldMapping, mappedByObject);
                    }
            }
            // load the selected mappings into the given state manager
            if (res != null) {
                // re-get the mapping in case the instance was a subclass
                mapping = (ClassMapping) sm.getMetaData();
                load(mapping, sm, fetch, res);
                getVersion(mapping, sm, res);
                setInverseRelation(sm, mapping, res);
            }
            return true;
        } finally {
            if (res != null && (info == null || res != info.result))
                res.close();
        }
    }

    private void setInverseRelation(OpenJPAStateManager owner,
        ClassMapping mapping, Result res) {
        FieldMapping[] fms = mapping.getFieldMappings();

        // At this point, the owner is fully initialized.
        // Check if the owner has eagerly loaded ToMany relations.
        for (FieldMapping fm : fms) {
            if (res.getEager(fm) != null) {
                if (!fm.getElement().isTypePC()) {
                    continue;
                }
                Object coll = owner.fetchObject(fm.getIndex());
                if (coll instanceof Map)
                    coll = ((Map<?, ?>) coll).values();
                if (coll instanceof Collection<?> &&
                        ((Collection<?>) coll).size() > 0) {
                    // Found eagerly loaded collection.
                    // Publisher (1) <==>  (M) Magazine
                    //    publisher has a EAGER OneToMany relation
                    //    magazine has a EAGER or LAZY ManyToOne publisher
                    // For each member (Magazine) in the collection,
                    // set its inverse relation (Publisher).
                    for (Object o : (Collection<?>) coll) {
                        PersistenceCapable pc = (PersistenceCapable) o;
                        if (pc == null) {
                            continue;
                        }
                        OpenJPAStateManager sm = (OpenJPAStateManager) pc.pcGetStateManager();
                        ClassMapping cm =
                                (ClassMapping) _conf.getMetaDataRepositoryInstance().getCachedMetaData(pc.getClass());
                        FieldMapping[] fmd = cm.getFieldMappings();
                        for (FieldMapping fieldMapping : fmd) {
                            // don't check the oids for basic fields.
                            if (fieldMapping.isTypePC()) {
                                Object oid = sm.getIntermediate(fieldMapping.getIndex());
                                // if oid was setIntermediate() previously and it is the same as the owner,generate
                                // then set the inverse relation
                                if (oid != null && oid.equals(owner.getObjectId())) {
                                    sm.storeObject(fieldMapping.getIndex(), owner.getPersistenceCapable());
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    protected void setMappedBy(OpenJPAStateManager sm,
        FieldMapping mappedByFieldMapping, Object mappedByObject) {
        ClassMapping mapping = (ClassMapping) sm.getMetaData();
        FieldMapping[] fms = mapping.getFieldMappings();
        for (FieldMapping fm : fms) {
            if (fm == mappedByFieldMapping) {
                sm.storeObject(fm.getIndex(), mappedByObject);
                return;
            }
        }
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of getting version from the result set.
     */
    protected void getVersion(ClassMapping mapping, OpenJPAStateManager sm,
        Result res) throws SQLException {
        mapping.getVersion().afterLoad(sm, this);
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of checking whether the result set is empty or not.
     */
    protected boolean isEmptyResult(Result res) throws SQLException {
        if (res != null && !res.next())
            return true;
        return false;
    }

    /**
     * This method is to provide override for non-JDBC or JDBC-like
     * implementation of getting type from the result set.
     */
    protected Class<?> getType(Result res, ClassMapping mapping){
        if (res == null)
            return mapping.getDescribedType();
        return null;
    }

    /**
     * Allow the mapping to custom load data. Return null if the mapping
     * does not use custom loading.
     */
    private Boolean customLoad(OpenJPAStateManager sm, ClassMapping mapping,
        PCState state, JDBCFetchConfiguration fetch)
        throws ClassNotFoundException, SQLException {
        // check to see if the mapping takes care of initialization
        if (!mapping.customLoad(sm, this, state, fetch))
            return null;
        if (sm.getManagedInstance() != null) {
            mapping.getVersion().afterLoad(sm, this);
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * Select the data for the given instance and return the result. Return
     * null if there is no data in the current fetch groups to select.
     */
    private Result getInitializeStateResult(OpenJPAStateManager sm,
        ClassMapping mapping, JDBCFetchConfiguration fetch, int subs)
        throws SQLException {
        FinderQueryImpl fq = getFinder(mapping, fetch);
        if (fq != null)
            return fq.execute(sm, this, fetch);
        Select sel = _sql.newSelect();
        if (!select(sel, mapping, subs, sm, null, fetch,
            EagerFetchModes.EAGER_JOIN, true, false))
            return null;
        sel.wherePrimaryKey(sm.getObjectId(), mapping, this);
        sel.setExpectedResultCount(1, false);
        if (_log.isTraceEnabled()) {
            _log.trace("getInitializeStateResult: oid="+sm.getObjectId()+" "+mapping.getDescribedType());
        }
        Result result = sel.execute(this, fetch);
        cacheFinder(mapping, sel, fetch);
        return result;
    }

    /**
     * Select a union of the data for the given instance from possible concrete
     * mappings and return the result.
     */
    private Result getInitializeStateUnionResult(final OpenJPAStateManager sm,
        ClassMapping mapping, final ClassMapping[] mappings,
        final JDBCFetchConfiguration fetch) throws SQLException {
        FinderQueryImpl fq = getFinder(mapping, fetch);
        if (fq != null)
            return fq.execute(sm, this, fetch);
        final JDBCStoreManager store = this;
        final int eager = Math.min(fetch.getEagerFetchMode(),
            EagerFetchModes.EAGER_JOIN);

        Union union = _sql.newUnion(mappings.length);
        union.setExpectedResultCount(1, false);
        if (fetch.getSubclassFetchMode(mapping) != EagerFetchModes.EAGER_JOIN)
            union.abortUnion();
        union.select(new Union.Selector() {
            @Override
            public void select(Select sel, int i) {
                sel.select(mappings[i], Select.SUBS_ANY_JOINABLE, store, fetch,
                    eager);
                sel.wherePrimaryKey(sm.getObjectId(), mappings[i], store);
            }
        });
        Result result = union.execute(this, fetch);
        cacheFinder(mapping, union, fetch);
        return result;
    }

    /**
     * Select primary key data to make sure the given instance exists, locking
     * if needed.
     */
    private boolean selectPrimaryKey(OpenJPAStateManager sm,
        ClassMapping mapping, JDBCFetchConfiguration fetch)
        throws SQLException {
        // select pks from base class record to ensure it exists and lock
        // it if needed
        ClassMapping base = mapping;
        while (base.getJoinablePCSuperclassMapping() != null)
            base = base.getJoinablePCSuperclassMapping();

        Select sel = _sql.newSelect();
        sel.select(base.getPrimaryKeyColumns());
        sel.wherePrimaryKey(sm.getObjectId(), base, this);
        if (_log.isTraceEnabled()) {
            _log.trace("selectPrimaryKey: oid="+sm.getObjectId()+" "+mapping.getDescribedType());
        }
        Result exists = sel.execute(this, fetch);
        try {
            if (isEmptyResult(exists))
                return false;

            // record locked?
            if (_active && _lm != null && exists.isLocking())
                _lm.loadedForUpdate(sm);
            return true;
        } finally {
            exists.close();
        }
    }

    @Override
    public boolean load(OpenJPAStateManager sm, BitSet fields,
        FetchConfiguration fetch, int lockLevel, Object context) {
        JDBCFetchConfiguration jfetch = (JDBCFetchConfiguration) fetch;

        // get a connection, or reuse current one
        ConnectionInfo info = (ConnectionInfo) context;
        Result res = null;
        if (info != null) {
            // if initialize() fails to load required fields, then this method
            // is called; make sure not to try to use the given result if it's
            // the same one we just failed to completely initialize() with
            if (info.sm != sm)
                res = info.result;
            info.sm = null;
        }
        try {
            // if there's an existing result, load all we can from it
            ClassMapping mapping = (ClassMapping) sm.getMetaData();
            if (res != null) {
                load(mapping, sm, jfetch, res);
                removeLoadedFields(sm, fields);
            }

            // if the instance is hollow and there's a customized
            // get by id method, use it
            if (sm.getLoaded().length() == 0
                && mapping.customLoad(sm, this, null, jfetch))
                removeLoadedFields(sm, fields);


            //### select is kind of a big object, and in some cases we don't
            //### use it... would it be worth it to have a small shell select
            //### object that only creates a real select when actually used?
            //### Delayed proxy specific optimization: If the only fields that
            //### need to be loaded are delayed proxies, building the select is
            //### not necessary.

            if (!isDelayedLoadOnly(sm, fields, mapping)) {
	            Select sel = _sql.newSelect();
	            if (select(sel, mapping, Select.SUBS_EXACT, sm, fields, jfetch,
	                EagerFetchModes.EAGER_JOIN, true, false)) {
	                sel.wherePrimaryKey(sm.getObjectId(), mapping, this);
	                if (_log.isTraceEnabled()) {
	                    _log.trace("load: "+mapping.getDescribedType()+" oid: "+sm.getObjectId());
	                }
	                res = sel.execute(this, jfetch, lockLevel);
	                try {
	                    if (isEmptyResult(res))
	                        return false;
	                    load(mapping, sm, jfetch, res);
	                } finally {
	                    res.close();
	                }
	            }
            }

            // now allow the fields to load themselves individually too
            FieldMapping[] fms = mapping.getFieldMappings();
            for (int i = 0; i < fms.length; i++)
                if (fields.get(i) && (!sm.getLoaded().get(i) || sm.isDelayed(i))) {
                    if (_log.isTraceEnabled()) {
                        _log.trace("load field: '"+ fms[i].getName() + "' for oid="+sm.getObjectId()
                            +" "+mapping.getDescribedType());
                    }
                    fms[i].load(sm, this, jfetch.traverseJDBC(fms[i]));
                }
            mapping.getVersion().afterLoad(sm, this);
            return true;
        } catch (ClassNotFoundException cnfe) {
            throw new StoreException(cnfe);
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, _dict, lockLevel);
        }
    }

    private boolean isDelayedLoadOnly(OpenJPAStateManager sm, BitSet fields, ClassMapping mapping) {
        if (!sm.getContext().getConfiguration().getProxyManagerInstance().getDelayCollectionLoading()
            || fields.isEmpty()) {
            return false;
        }
        FieldMapping[] fms = mapping.getFieldMappings();
        for (int i = 0; i < fms.length; i++) {
            if (fields.get(i)) {
                if (!(fms[i].isDelayCapable() && (!sm.getLoaded().get(i) || sm.isDelayed(i)))) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Return a list formed by removing all loaded fields from the given one.
     */
    private void removeLoadedFields(OpenJPAStateManager sm, BitSet fields) {
        for (int i = 0, len = fields.length(); i < len; i++)
            if (fields.get(i) && sm.getLoaded().get(i))
                fields.clear(i);
    }

    @Override
    public Collection loadAll(Collection sms, PCState state, int load,
        FetchConfiguration fetch, Object context) {
        return ImplHelper.loadAll(sms, this, state, load, fetch, context);
    }

    @Override
    public void beforeStateChange(OpenJPAStateManager sm, PCState fromState,
        PCState toState) {
    }

    @Override
    public Collection flush(Collection<OpenJPAStateManager> sms) {
        try {
            if (_conn != null && _conn.getInnermostDelegate().isReadOnly())
                _conn.setReadOnly(false);
        } catch (SQLException e) {
        }
        return _conf.getUpdateManagerInstance().flush(sms, this);
    }

    @Override
    public boolean cancelAll() {
        // note that this method does not lock the context, since
        // we want to allow a different thread to be able to cancel the
        // outstanding statement on a different context

        List<Statement> stmnts;
        synchronized (_stmnts) {
            if (_stmnts.isEmpty())
                return false;
            stmnts = new ArrayList<>(_stmnts);
        }

        try {
            for (Statement stmnt : stmnts) {
                stmnt.cancel();
            }
            return true;
        } catch (SQLException se) {
            throw SQLExceptions.getStore(se, _dict);
        }
    }

    @Override
    public boolean assignObjectId(OpenJPAStateManager sm, boolean preFlush) {
        ClassMetaData meta = sm.getMetaData();
        if (meta.getIdentityType() == ClassMetaData.ID_APPLICATION)
            return ApplicationIds.assign(sm, this, preFlush);

        // datastore identity
        Object val = ImplHelper.generateIdentityValue(_ctx, meta,
            JavaTypes.LONG);
        if (val == null && meta.getIdentityStrategy() != ValueStrategies.NATIVE)
            return false;
        if (val == null)
            val = getDataStoreIdSequence(meta).next(_ctx, meta);
        sm.setObjectId(newDataStoreId(val, meta));
        return true;
    }

    @Override
    public boolean assignField(OpenJPAStateManager sm, int field,
        boolean preFlush) {
        FieldMetaData fmd = sm.getMetaData().getField(field);
        Object val = ImplHelper.generateFieldValue(_ctx, fmd);
        if (val == null)
            return false;
        sm.store(field, val);
        return true;
    }

    @Override
    public Class<?> getManagedType(Object oid) {
        if (oid instanceof Id)
            return ((Id) oid).getType();
        return null;
    }

    @Override
    public Class<?> getDataStoreIdType(ClassMetaData meta) {
        return Id.class;
    }

    @Override
    public Object copyDataStoreId(Object oid, ClassMetaData meta) {
        Id id = (Id) oid;
        return new Id(meta.getDescribedType(), id.getId(), id.hasSubclasses());
    }

    @Override
    public Object newDataStoreId(Object val, ClassMetaData meta) {
        return Id.newInstance(meta.getDescribedType(), val);
    }

    @Override
    public Id newDataStoreId(long id, ClassMapping mapping, boolean subs) {
        return new Id(mapping.getDescribedType(), id, subs);
    }

    @Override
    public ResultObjectProvider executeExtent(ClassMetaData meta,
        final boolean subclasses, FetchConfiguration fetch) {
        ClassMapping mapping = (ClassMapping) meta;
        final ClassMapping[] mappings;
        if (subclasses)
            mappings = mapping.getIndependentAssignableMappings();
        else
            mappings = new ClassMapping[] { mapping };

        ResultObjectProvider[] rops = null;
        final JDBCFetchConfiguration jfetch = (JDBCFetchConfiguration) fetch;
        if (jfetch.getSubclassFetchMode(mapping) != EagerFetchModes.EAGER_JOIN)
            rops = new ResultObjectProvider[mappings.length];

        try {
            // check for custom loads
            ResultObjectProvider rop;
            for (int i = 0; i < mappings.length; i++) {
                rop = mappings[i].customLoad(this, subclasses, jfetch, 0,
                    Long.MAX_VALUE);
                if (rop != null) {
                    if (rops == null)
                        rops = new ResultObjectProvider[mappings.length];
                    rops[i] = rop;
                }
            }

            // if we're selecting independent mappings separately or have
            // custom loads, do individual selects for each class
            rop = null;
            if (rops != null) {
                for (int i = 0; i < mappings.length; i++) {
                    if (rops[i] != null)
                        continue;

                    Select sel = _sql.newSelect();
