/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     07/13/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
//     08/24/2012-2.5 Guy Pelletier
//       - 350487: JPA 2.1 Specification defined support for Stored Procedure Calls
package org.eclipse.persistence.internal.queries;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventManager;
import org.eclipse.persistence.descriptors.DescriptorQueryManager;
import org.eclipse.persistence.descriptors.VersionLockingPolicy;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.CommitManager;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DoesExistQuery;
import org.eclipse.persistence.queries.ModifyQuery;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.queries.WriteObjectQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.tools.profiler.QueryMonitor;

/**
 * <p><b>Purpose</b>:
 * Abstract class for all database query mechanism objects.
 * DatabaseQueryMechanism is actually a helper class and currently is required
 * for all types of queries.  Most of the work performed by the query framework is
 * performed in the query mechanism.  The query mechanism contains the internal
 * knowledge necessary to perform the specific database operation.
 * <p>
 * <p><b>Responsibilities</b>:
 * Provide a common protocol for query mechanism objects.
 * Provides all of the database specific work for the assigned query.
 *
 * @author Yvon Lavoie
 * @since TOPLink/Java 1.0
 */
public abstract class DatabaseQueryMechanism implements Cloneable, Serializable {

    /** The database query that uses this mechanism. */
    protected DatabaseQuery query;

    /**
     * Initialize the state of the query.
     */
    public DatabaseQueryMechanism() {
    }

    /**
     * Initialize the state of the query
     * @param query - owner of mechanism
     */
    public DatabaseQueryMechanism(DatabaseQuery query) {
        this.query = query;
    }

    /**
     * Add the initial write lock value to the row for insert.
     */
    protected void addWriteLockFieldForInsert() {
        if (getDescriptor().usesOptimisticLocking()) {
            getDescriptor().getOptimisticLockingPolicy().setupWriteFieldsForInsert(getWriteObjectQuery());
        }
    }

    /**
     * Internal:
     * In the case of EJBQL, an expression needs to be generated. Build the required expression.
     */
    public void buildSelectionCriteria(AbstractSession session) {
        // Default is do nothing
    }

    /**
     * Perform a cache lookup for the query. If the translation row contains
     * all the parameters (which are part of the primary key) from the prepared
     * call, then a cache check will be performed.
     *
     * If the object is found in the cache, return it; otherwise return null.
     */
    public Object checkCacheForObject(AbstractRecord translationRow, AbstractSession session) {
        // Null check added for CR#4295 - TW
        if ((translationRow == null) || (translationRow.isEmpty())) {
            return null;
        }

        // Bug 5529564
        // The query wasn't prepared most likely because arguments were
        // provided. Use them for the cache lookup.
        List queryFields;
        if (query.getCall() == null) {
            // TODO: This is a bug, arguments is a list of Strings, not fields.
            // Also if the call was null, it would be an expression, if it was not
            // prepared the parameters would be empty.
            queryFields = query.getArguments();
        } else {
            // The query was prepared. Use the parameters of the call to look
            // up the object in cache.
            queryFields = query.getCall().getParameters();
        }

        ClassDescriptor descriptor = getDescriptor();
        List<DatabaseField> primaryKeyFields = descriptor.getPrimaryKeyFields();

        // Check that the query is by primary key.
        for (DatabaseField primaryKeyField : primaryKeyFields) {
            if (!queryFields.contains(primaryKeyField)) {
                return null;
            }
        }
        Object primaryKey = descriptor.getObjectBuilder().extractPrimaryKeyFromRow(translationRow, session);
        if (primaryKey == null) {
            return null;
        }

        if (query.isObjectBuildingQuery() && ((ObjectBuildingQuery)query).requiresDeferredLocks() || descriptor.shouldAcquireCascadedLocks()) {
            return session.getIdentityMapAccessorInstance().getFromIdentityMapWithDeferredLock(primaryKey, getReadObjectQuery().getReferenceClass(), false, descriptor);
        } else {
            return session.getIdentityMapAccessorInstance().getFromLocalIdentityMap(primaryKey, getReadObjectQuery().getReferenceClass(), false, descriptor);
        }
    }

    /**
     * Clone the mechanism
     */
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }

    /**
     * Clone the mechanism for the specified query clone.
     */
    public DatabaseQueryMechanism clone(DatabaseQuery queryClone) {
        DatabaseQueryMechanism clone = (DatabaseQueryMechanism)clone();
        clone.setQuery(queryClone);
        return clone;
    }

    /**
     * Read all rows from the database using a cursored stream.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public abstract DatabaseCall cursorSelectAllRows() throws DatabaseException;

    /**
     * Delete a collection of objects
     * This should be overridden by subclasses.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public boolean isJPQLCallQueryMechanism() {
        return false;
    }

    public abstract Integer deleteAll() throws DatabaseException;

    /**
     * Delete an object
     * This should be overridden by subclasses.
     * @exception DatabaseException
     * @return the row count.
     */
    public abstract Integer deleteObject() throws DatabaseException;

    /**
     * Execute a execute SQL call.
     * This should be overridden by subclasses.
     * @exception DatabaseException
     * @return true if the first result is a result set and false if it is an
     *   update count or there are no results other than through INOUT and OUT
     *   parameterts, if any.
     */
    public abstract Object execute() throws DatabaseException;

    /**
     * Execute a non selecting SQL call
     * This should be overridden by subclasses.
     * @exception DatabaseException
     * @return the row count.
     */
    public abstract Integer executeNoSelect() throws DatabaseException;

    /**
     * Execute a select SQL call and return the rows.
     * This should be overriden by subclasses.
     * @exception DatabaseException
     */
    public abstract Vector executeSelect() throws DatabaseException;

    /**
     * Check whether the object already exists on the database; then
     * perform an insert or update, as appropriate.
     * This write is used for non-unit of work (old-commit) operations.
     * Return the object being written.
     */
    public Object executeWrite() throws DatabaseException, OptimisticLockException {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        Object object = writeQuery.getObject();
        CommitManager commitManager = getSession().getCommitManager();

        // if the object has already been committed, no work is required
        if (commitManager.isCommitCompletedInPostOrIgnore(object)) {
            return object;
        }

        // check whether the object is already being committed -
        // if it is and it is new, then a shallow insert must be done
        if (commitManager.isCommitInPreModify(object)) {
            shallowInsertObjectForWrite(object, writeQuery, commitManager);
            return object;
        }

        try {
            getSession().beginTransaction();

            if (writeQuery.getObjectChangeSet() == null) {
                // PERF: Avoid events if no listeners.
                if (getDescriptor().getEventManager().hasAnyEventListeners()) {
                    // only throw the events if there is no changeset otherwise the event will be thrown twice
                    // once by the calculate changes code and here
                    getDescriptor().getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PreWriteEvent, writeQuery));
                }
            }
            writeQuery.executeCommit();

            // PERF: Avoid events if no listeners.
            if (getDescriptor().getEventManager().hasAnyEventListeners()) {
                getDescriptor().getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PostWriteEvent, writeQuery));
            }

            getSession().commitTransaction();

            // notify the commit manager of the completion to the commit
            commitManager.markCommitCompleted(object);

            return object;

        } catch (RuntimeException exception) {
            getSession().rollbackTransaction();
            commitManager.markCommitCompleted(object);
            throw exception;
        }
    }

    /**
     * Execute the call that was deferred to the commit manager.
     * This is used to allow multiple table batching and deadlock avoidance.
     */
    public void executeDeferredCall(DatasourceCall call) {
        // Do nothing by default.
    }

    /**
     * Check whether the object already exists on the cadatabase; then
     * perform an insert or update, as appropriate.
     * This method was moved here, from WriteObjectQuery.execute(),
     * so we can hide the source.
     * Return the object being written.
     */
    public Object executeWriteWithChangeSet() throws DatabaseException, OptimisticLockException {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        ObjectChangeSet objectChangeSet = writeQuery.getObjectChangeSet();
        ClassDescriptor descriptor = getDescriptor();
        AbstractSession session = getSession();
        CommitManager commitManager = session.getCommitManager();
        Object object = writeQuery.getObject();
        // If there are no changes then there is no work required
        // Check for forcedUpdate Version and Optimistic read lock (hasForcedChanges() set in ObjectChangePolicy)
        if (!objectChangeSet.hasChanges() && !objectChangeSet.hasForcedChanges()) {
            commitManager.markCommitCompleted(object);
            return object;
        }
        // If the object has already been committed, no work is required
        // need to check for the object to ensure insert wasn't completed already.
        if (commitManager.isCommitCompletedInPostOrIgnore(object)) {
            return object;
        }
        try {
            writeQuery.executeCommitWithChangeSet();

            // PERF: Avoid events if no listeners.
            if (descriptor.getEventManager().hasAnyEventListeners()) {
                descriptor.getEventManager().executeEvent(new DescriptorEvent(DescriptorEventManager.PostWriteEvent, writeQuery));
            }

            // Notify the commit manager of the completion to the commit.
            commitManager.markCommitCompleted(object);

            return object;

        } catch (RuntimeException exception) {
            commitManager.markCommitCompleted(object);
            throw exception;
        }
    }

    /**
     * Convenience method
     */
    protected ClassDescriptor getDescriptor() {
        return this.query.getDescriptor();
    }

    /**
     * Convenience method
     */
    public AbstractRecord getModifyRow() {
        if (this.query.isModifyQuery()) {
            return ((ModifyQuery)this.query).getModifyRow();
        } else {
            return null;
        }
    }

    /**
     * Return the query that uses the mechanism.
     */
    public DatabaseQuery getQuery() {
        return query;
    }

    /**
     * Convenience method
     */
    protected ReadObjectQuery getReadObjectQuery() {
        return (ReadObjectQuery)this.query;
    }

    /**
     * Return the selection criteria for the mechanism.
     * By default this is null. This method exists because both statement and expression
     * mechanisms use an expression and some code in the mappings depends on returning this.
     */
    public Expression getSelectionCriteria() {
        return null;
    }

    /**
     * Convenience method
     */
    protected AbstractSession getSession() {
        return this.query.getSession();
    }

    /**
     * Convenience method
     */
    protected AbstractSession getExecutionSession() {
        return this.query.getExecutionSession();
    }

    /**
     * Convenience method
     */
    protected AbstractRecord getTranslationRow() {
        return this.query.getTranslationRow();
    }

    /**
     * Convenience method
     */
    protected WriteObjectQuery getWriteObjectQuery() {
        return (WriteObjectQuery)this.query;
    }

    /**
     * Insert an object.
     */
    public abstract void insertObject() throws DatabaseException;

    /**
     *  Insert an object and provide the opportunity to reprepare prior to the insert.
     *  This will be overridden
     *  CR#3237
     */
    public void insertObject(boolean reprepare) {
        insertObject();
    }

    /**
     * Insert an object in the database.
     * This is used for both uow and non-uow (old-commit and change-set) operations.
     */
    public void insertObjectForWrite() {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        ClassDescriptor descriptor = getDescriptor();
        DescriptorQueryManager queryManager = descriptor.getQueryManager();
        boolean isFKUpdate = false; // Bug 319276

        // check for user-defined query
        if ((!writeQuery.isUserDefined())// this is not a user-defined query
                 && queryManager.hasInsertQuery()// there is a user-defined query
                 && isExpressionQueryMechanism()) {// this is not a hand-coded call (custom SQL etc.)
            performUserDefinedInsert();
            return;
        }
        Object object = writeQuery.getObject();
        AbstractSession session = writeQuery.getSession();
        ObjectChangeSet changeSet = writeQuery.getObjectChangeSet();
        CommitManager commitManager = session.getCommitManager();
        DescriptorEventManager eventManager = descriptor.getEventManager();

        // This must be done after the custom query check, otherwise it will be done twice.
        commitManager.markPreModifyCommitInProgress(object);

        if (changeSet == null) {
            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                // only throw the events if there is no changeset otherwise the event will be thrown twice
                // once by the calculate changes code and here
                eventManager.executeEvent(new DescriptorEvent(DescriptorEventManager.PreInsertEvent, writeQuery));
            }
        }

        // check whether deep shallow modify is turned on
        if (writeQuery.shouldCascadeParts()) {
            queryManager.preInsert(writeQuery);
        }

        // In a unit of work/writeObjects the preInsert may have caused a shallow insert of this object,
        // in this case this second write must do an update.
        if (commitManager.isShallowCommitted(object)) {
            isFKUpdate = true; // Bug 319276
            updateForeignKeyFieldAfterInsert();
        } else {
            AbstractRecord modifyRow = writeQuery.getModifyRow();
            if (modifyRow == null) {// Maybe have been passed in as in aggregate collection.
                if (writeQuery.shouldCascadeParts()) {
                    writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRow(object, session, WriteType.INSERT));
                } else {
                    writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRowForShallowInsert(object, session));
                }
            } else {
                if (writeQuery.shouldCascadeParts()) {
                    writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRow(modifyRow, object, session, WriteType.INSERT));
                } else {
                    writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRowForShallowInsert(modifyRow, object, session));
                }
            }
            modifyRow = getModifyRow();
            // the modify row and the translation row are the same for insert
            writeQuery.setTranslationRow(modifyRow);
            if (!descriptor.isAggregateCollectionDescriptor()) {// Should/cannot be recomputed in aggregate collection.
                writeQuery.setPrimaryKey(descriptor.getObjectBuilder().extractPrimaryKeyFromObject(object, session));
            }
            addWriteLockFieldForInsert();
            if (descriptor.hasSerializedObjectPolicy()) {
                descriptor.getSerializedObjectPolicy().putObjectIntoRow(modifyRow, object, session);
            }

            // CR#3237
            // Store the size of the modify row so we can determine if the user has added to the row in the insert.
            int modifyRowSize = modifyRow.size();

            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                DescriptorEvent event = new DescriptorEvent(DescriptorEventManager.AboutToInsertEvent, writeQuery);
                event.setRecord(modifyRow);
                eventManager.executeEvent(event);
            }

            if (QueryMonitor.shouldMonitor()) {
                QueryMonitor.incrementInsert(writeQuery);
            }
            // CR#3237
            // Call insert with a boolean that tells it to reprepare if the user has altered the modify row.
            insertObject(modifyRowSize != modifyRow.size());

            // register the object before post insert to resolve possible cycles
            registerObjectInIdentityMap(object, descriptor, session);
        }

        commitManager.markPostModifyCommitInProgress(object);
        // Verify if deep shallow modify is turned on.
        if (writeQuery.shouldCascadeParts()) {
            queryManager.postInsert(writeQuery);
        }

        if ((descriptor.getHistoryPolicy() != null) && descriptor.getHistoryPolicy().shouldHandleWrites()) {
            if (isFKUpdate) { // Bug 319276
                descriptor.getHistoryPolicy().postUpdate(writeQuery, true);
            }
            else {
                descriptor.getHistoryPolicy().postInsert(writeQuery);
            }
        }

        // PERF: Avoid events if no listeners.
        if (eventManager.hasAnyEventListeners()) {
            eventManager.executeEvent(new DescriptorEvent(DescriptorEventManager.PostInsertEvent, writeQuery));
        }
    }

    /**
     * Return true if this is a call query mechanism
     */
    public boolean isCallQueryMechanism() {
        return false;
    }

    /**
     * Return true if this is an expression query mechanism
     */
    public boolean isExpressionQueryMechanism() {
        return false;
    }

    /**
     * Return true if this is a query by example mechanism
     */
    public boolean isQueryByExampleMechanism() {
        return false;
    }

    /**
     * Return true if this is a statement query mechanism
     */
    public boolean isStatementQueryMechanism() {
        return false;
    }

    /**
     * Insert the object using the user defined query.
     * This ensures that the query is cloned and prepared correctly.
     */
    protected void performUserDefinedInsert() {
        performUserDefinedWrite(getDescriptor().getQueryManager().getInsertQuery());
    }

    /**
     * Update the object using the user defined query.
     * This ensures that the query is cloned and prepared correctly.
     */
    protected void performUserDefinedUpdate() {
        performUserDefinedWrite(getDescriptor().getQueryManager().getUpdateQuery());
    }

    /**
     * Write the object using the specified user-defined query.
     * This ensures that the query is cloned and prepared correctly.
     */
    protected void performUserDefinedWrite(WriteObjectQuery userDefinedWriteQuery) {
        WriteObjectQuery query = getWriteObjectQuery();
        userDefinedWriteQuery.checkPrepare(query.getSession(), query.getTranslationRow());

        WriteObjectQuery writeQuery = (WriteObjectQuery)userDefinedWriteQuery.clone();
        writeQuery.setIsExecutionClone(true);
        writeQuery.setObject(query.getObject());
        writeQuery.setObjectChangeSet(query.getObjectChangeSet());
        writeQuery.setCascadePolicy(query.getCascadePolicy());
        writeQuery.setShouldMaintainCache(query.shouldMaintainCache());
        writeQuery.setTranslationRow(query.getTranslationRow());
        writeQuery.setModifyRow(query.getModifyRow());
        writeQuery.setPrimaryKey(query.getPrimaryKey());
        writeQuery.setSession(query.getSession());

        // If there is a changeset, the change set method must be used.
        if (writeQuery.getObjectChangeSet() != null) {
            writeQuery.executeCommitWithChangeSet();
        } else {
            writeQuery.executeCommit();
        }
    }

    /**
     * This is different from 'prepareForExecution()'
     * in that this is called on the original query,
     * and the other is called on the clone of the query.
     * This query is copied for concurrency so this prepare can only setup things that
     * will apply to any future execution of this query.
     */
    public void prepare() throws QueryException {
        // the default is to do nothing
    }

    /**
     * Pre-pare for a cursored execute.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareCursorSelectAllRows() throws QueryException;

    /**
     * Prepare for a delete all.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareDeleteAll() throws QueryException;

    /**
     * Prepare for a delete.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareDeleteObject() throws QueryException;

    /**
     * Pre-pare for a select execute.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareDoesExist(DatabaseField field) throws QueryException;

    /**
     * Prepare for a raw (non-object), non-selecting call.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareExecuteNoSelect() throws QueryException;

    /**
     * Prepare for a raw execute call.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareExecute() throws QueryException;

    /**
     * Prepare for a raw (non-object) select call.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareExecuteSelect() throws QueryException;

    /**
     * Prepare for an insert.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareInsertObject() throws QueryException;

    /**
     * Pre-pare for a select execute.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareReportQuerySelectAllRows() throws QueryException;

    /**
     * Pre-pare a report query for a sub-select.
     */
    public abstract void prepareReportQuerySubSelect() throws QueryException;

    /**
     * Prepare for a select returning (possibly) multiple rows.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareSelectAllRows() throws QueryException;

    /**
     * Prepare for a select returning a single row.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareSelectOneRow() throws QueryException;

    /**
     * Prepare for an update.
     * This is sent to the original query before cloning.
     */
    public abstract void prepareUpdateObject() throws QueryException;

    /**
       * Prepare for an update all.
       * This is sent to the original query before cloning.
       */
    public abstract void prepareUpdateAll() throws QueryException;

    /**
     * Store the query object in the identity map.
     */
    protected void registerObjectInIdentityMap(Object object, ClassDescriptor descriptor, AbstractSession session) {
        WriteObjectQuery query = getWriteObjectQuery();
        if (query.shouldMaintainCache()) {
            if (descriptor.usesOptimisticLocking()) {
                Object optimisticLockValue = descriptor.getOptimisticLockingPolicy().getValueToPutInCache(query.getModifyRow(), session);
                session.getIdentityMapAccessorInstance().putInIdentityMap(object, query.getPrimaryKey(), optimisticLockValue, System.currentTimeMillis(), descriptor);
            } else {
                session.getIdentityMapAccessorInstance().putInIdentityMap(object, query.getPrimaryKey(), null, System.currentTimeMillis(), descriptor);
            }
        }
    }

    /**
     * INTERNAL:
     * Read all rows from the database.
     */
    public abstract Vector selectAllReportQueryRows() throws DatabaseException;

    /**
     * Read and return rows from the database.
     */
    public abstract Vector selectAllRows() throws DatabaseException;

    /**
     * Read and return a row from the database.
     */
    public abstract AbstractRecord selectOneRow() throws DatabaseException;

    /**
     * Read and return a row from the database for an existence check.
     */
    public abstract AbstractRecord selectRowForDoesExist(DatabaseField field) throws DatabaseException;

    /**
     * Set the query that uses this mechanism.
     */
    public void setQuery(DatabaseQuery query) {
        this.query = query;
    }

    /**
     * Shallow insert the specified object, if necessary.
     */
    protected void shallowInsertObjectForWrite(Object object, WriteObjectQuery writeQuery, CommitManager commitManager) throws DatabaseException, OptimisticLockException {
        boolean doesExist;

        if (getSession().isUnitOfWork()) {
            UnitOfWorkImpl uow = (UnitOfWorkImpl)getSession();
            doesExist = !uow.isCloneNewObject(object);
            if (doesExist) {
                doesExist = uow.isObjectRegistered(object);
            }
        } else {
            // clone and initialize the does exist query
            DoesExistQuery existQuery = (DoesExistQuery)getDescriptor().getQueryManager().getDoesExistQuery().clone();
            existQuery.setObject(object);
            existQuery.setPrimaryKey(writeQuery.getPrimaryKey());
            existQuery.setDescriptor(getDescriptor());
            existQuery.setTranslationRow(getTranslationRow());

            doesExist = ((Boolean)getSession().executeQuery(existQuery)).booleanValue();
        }

        if (!doesExist) {
            // a shallow insert must be performed
            writeQuery.dontCascadeParts();
            insertObjectForWrite();
            // mark this object as shallow committed so that the insert will do an update
            commitManager.markShallowCommit(object);
        }
    }

    /**
     * Update the foreign key fields when resolving a bi-directonal reference in a UOW.
     * This must always be dynamic as it is called within an insert query and is really part of the insert
     * and does not fire update events or worry about locking.
     */
    protected void updateForeignKeyFieldAfterInsert() {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        Object object = writeQuery.getObject();

        writeQuery.setPrimaryKey(getDescriptor().getObjectBuilder().extractPrimaryKeyFromObject(object, getSession()));
        // reset the translation row because the insert has occurred and the id has
        // been assigned to the object, but not the row
        writeQuery.setTranslationRow(getDescriptor().getObjectBuilder().buildRowForTranslation(object, getSession()));

        updateForeignKeyFieldAfterInsert(writeQuery);
    }

    /**
       * Issue update SQL statement
       */
    public abstract Integer updateAll() throws DatabaseException;

    /**
     * Update an object.
     * Return the row count.
     */
    public abstract Integer updateObject() throws DatabaseException;

    /**
     * Update the foreign key fields when resolving a bi-directonal reference in a UOW.
     * This must always be dynamic as it is called within an insert query and is really part of the insert
     * and does not fire update events or worry about locking.
     */
    protected abstract void updateForeignKeyFieldAfterInsert(WriteObjectQuery writeQuery);

    /**
     * Update the foreign key fields to null when resolving a deletion cycle.
     * This must always be dynamic as it is called within an delete query and is really part of the delete
     * and does not fire update events or worry about locking.
     */
    public void updateForeignKeyFieldBeforeDelete() {
        // Nothing by default.
    }

    protected void updateObjectAndRowWithReturnRow(Collection returnFields, boolean isFirstCallForInsert) {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        AbstractRecord outputRow = (AbstractRecord)writeQuery.getProperties().get("output");
        if ((outputRow == null) || outputRow.isEmpty()) {
            return;
        }
        AbstractRecord row = new DatabaseRecord();
        for (Iterator iterator = returnFields.iterator(); iterator.hasNext();) {
            DatabaseField field = (DatabaseField)iterator.next();
            if (outputRow.containsKey(field)) {
                row.put(field, outputRow.get(field));
            }
        }
        if (row.isEmpty()) {
            return;
        }

        Object object = writeQuery.getObject();

        ObjectChangeSet objectChangeSet = null;
        if (getSession().isUnitOfWork()) {
            objectChangeSet = writeQuery.getObjectChangeSet();
            if ((objectChangeSet == null) && (((UnitOfWorkImpl)getSession()).getUnitOfWorkChangeSet() != null)) {
                objectChangeSet = (ObjectChangeSet)((UnitOfWorkImpl)getSession()).getUnitOfWorkChangeSet().getObjectChangeSetForClone(object);
            }
        }
        getDescriptor().getObjectBuilder().assignReturnRow(object, writeQuery.getSession(), row, objectChangeSet);

        Object primaryKey = null;
        if (isFirstCallForInsert) {
            AbstractRecord pkToModify = new DatabaseRecord();
            List primaryKeyFields = getDescriptor().getPrimaryKeyFields();
            for (int i = 0; i < primaryKeyFields.size(); i++) {
                DatabaseField field = (DatabaseField)primaryKeyFields.get(i);
                if (row.containsKey(field)) {
                    pkToModify.put(field, row.get(field));
                }
            }
            if (!pkToModify.isEmpty()) {
                primaryKey = getDescriptor().getObjectBuilder().extractPrimaryKeyFromObject(object, getSession());
                writeQuery.setPrimaryKey(primaryKey);
                // Now I need to update the row
                getModifyRow().putAll(pkToModify);
                getDescriptor().getObjectBuilder().addPrimaryKeyForNonDefaultTable(getModifyRow(), object, getSession());
            }
        }

        if (objectChangeSet != null) {
            if (primaryKey != null) {
                objectChangeSet.setId(primaryKey);
            }
        }
    }

    /**
     * Update the object's primary key by fetching a new sequence number from the accessor.
     */
    protected void updateObjectAndRowWithSequenceNumber() throws DatabaseException {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        writeQuery.getDescriptor().getObjectBuilder().assignSequenceNumber(writeQuery);
    }

    /**
     * Update the object.
     * This is only used for non-unit-of-work updates.
     */
    public void updateObjectForWrite() {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        ClassDescriptor descriptor = getDescriptor();
        DescriptorQueryManager queryManager = descriptor.getQueryManager();
        // check for user-defined query
        if ((!writeQuery.isUserDefined())// this is not a user-defined query
                 && queryManager.hasUpdateQuery()// there is a user-defined query
                 && isExpressionQueryMechanism()) {// this is not a hand-coded call (custom SQL etc.)
            performUserDefinedUpdate();
            return;
        }
        Object object = writeQuery.getObject();
        AbstractSession session = getSession();
        CommitManager commitManager = session.getCommitManager();
        // This must be done after the custom query check, otherwise it will be done twice.
        commitManager.markPreModifyCommitInProgress(object);
        DescriptorEventManager eventManager = descriptor.getEventManager();
        if (writeQuery.getObjectChangeSet() == null) {
            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                // only throw the events if there is no changeset otherwise the event will be thrown twice
                // once by the calculate changes code and here
                eventManager.executeEvent(new DescriptorEvent(DescriptorEventManager.PreUpdateEvent, writeQuery));
            }
        }

        // Verify if deep shallow modify is turned on
        if (writeQuery.shouldCascadeParts()) {
            queryManager.preUpdate(writeQuery);
        }

        // The row must not be built until after preUpdate in case the object reference has changed.
        // For a user defined update in the uow to row must be built twice to check if any update is required.
        if ((writeQuery.isUserDefined() || writeQuery.isCallQuery()) && (!getSession().isUnitOfWork())) {
            writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRow(object, getSession(), WriteType.UNDEFINED));
        } else {
            writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRowForUpdate(writeQuery));
        }

        // Optimistic read lock implementation
        Boolean shouldModifyVersionField = null;
        if (session.isUnitOfWork() && ((UnitOfWorkImpl)session).hasOptimisticReadLockObjects()) {
            shouldModifyVersionField = (Boolean)((UnitOfWorkImpl)session).getOptimisticReadLockObjects().get(writeQuery.getObject());
        }

        if (!getModifyRow().isEmpty() || (shouldModifyVersionField != null) || ((descriptor.getCMPPolicy() != null) && (descriptor.getCMPPolicy().getForceUpdate()))) {
            // If user defined the entire row is required. Must not be built until change is known.
            if ((writeQuery.isUserDefined() || writeQuery.isCallQuery()) && getSession().isUnitOfWork()) {
                writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRow(object, getSession(), WriteType.UNDEFINED));
            }

            // Update the write lock field if required.
            if (descriptor.usesOptimisticLocking()) {
                OptimisticLockingPolicy policy = descriptor.getOptimisticLockingPolicy();
                policy.addLockValuesToTranslationRow(writeQuery);

                if (!getModifyRow().isEmpty() || shouldModifyVersionField.booleanValue()) {
                    // Update the row with newer lock value.
                    policy.updateRowAndObjectForUpdate(writeQuery, object);
                } else if (!shouldModifyVersionField.booleanValue() && (policy instanceof VersionLockingPolicy)) {
                    // Add the existing write lock value to the for a "read" lock (requires something to update).
                    ((VersionLockingPolicy)policy).writeLockValueIntoRow(writeQuery, object);
                }
            }
            if (descriptor.hasSerializedObjectPolicy()) {
                descriptor.getSerializedObjectPolicy().putObjectIntoRow(getModifyRow(), object, session);
            }

            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                DescriptorEvent event = new DescriptorEvent(DescriptorEventManager.AboutToUpdateEvent, writeQuery);
                event.setRecord(getModifyRow());
                eventManager.executeEvent(event);
            }

            if (QueryMonitor.shouldMonitor()) {
                QueryMonitor.incrementUpdate(getWriteObjectQuery());
            }
            int rowCount = updateObject().intValue();

            if (rowCount < 1) {
                if (session.hasEventManager()) {
                    session.getEventManager().noRowsModified(writeQuery, object);
                }
            }
            if (descriptor.usesOptimisticLocking()) {
                descriptor.getOptimisticLockingPolicy().validateUpdate(rowCount, object, writeQuery);
            }
        }

        commitManager.markPostModifyCommitInProgress(object);

        // Verify if deep shallow modify is turned on
        if (writeQuery.shouldCascadeParts()) {
            queryManager.postUpdate(writeQuery);
        }
        if ((descriptor.getHistoryPolicy() != null) && descriptor.getHistoryPolicy().shouldHandleWrites()) {
            descriptor.getHistoryPolicy().postUpdate(writeQuery);
        }

        // PERF: Avoid events if no listeners.
        if (eventManager.hasAnyEventListeners()) {
            eventManager.executeEvent(new DescriptorEvent(DescriptorEventManager.PostUpdateEvent, writeQuery));
        }
    }

    /**
     * Update the object.
     * This is used by the unit-of-work update.
     */
    public void updateObjectForWriteWithChangeSet() {
        WriteObjectQuery writeQuery = getWriteObjectQuery();
        ObjectChangeSet changeSet = writeQuery.getObjectChangeSet();
        Object object = writeQuery.getObject();
        ClassDescriptor descriptor = getDescriptor();
        DescriptorQueryManager queryManager = descriptor.getQueryManager();
        AbstractSession session = getSession();
        CommitManager commitManager = session.getCommitManager();
        // check for user-defined query
        if ((!writeQuery.isUserDefined())// this is not a user-defined query
                 && queryManager.hasUpdateQuery()// there is a user-defined query
                 && isExpressionQueryMechanism()) {// this is not a hand-coded call (custom SQL etc.)
            // This must be done here because the user defined update does not use a changeset so it will not be set otherwise
            commitManager.markPreModifyCommitInProgress(object);
            performUserDefinedUpdate();
            return;
        }
        // This must be done after the custom query check, otherwise it will be done twice.
        commitManager.markPreModifyCommitInProgress(object);
        DescriptorEventManager eventManager = descriptor.getEventManager();

        if (changeSet.hasChanges()) {
            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                DescriptorEvent event = new DescriptorEvent(DescriptorEventManager.PreUpdateWithChangesEvent, writeQuery);
                eventManager.executeEvent(event);

                // PreUpdateWithChangesEvent listeners may have altered the object - should recalculate the change set.
                UnitOfWorkChangeSet uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)session).getUnitOfWorkChangeSet();
                if (!uowChangeSet.isChangeSetFromOutsideUOW() && writeQuery.getObjectChangeSet().shouldRecalculateAfterUpdateEvent()){
                    // writeQuery.getObjectChangeSet() is mapped to object in uowChangeSet.
                    // It is first cleared then re-populated by calculateChanges method.
                    if (!descriptor.getObjectChangePolicy().isAttributeChangeTrackingPolicy() ){
                        // clear the change set without clearing the maps keys since they are not alterable by the event
                        // if the map is changed, it will be changed in the owning object and the
                        // change set will be changed there as well.
                        writeQuery.getObjectChangeSet().clear(false);
                    }
                    if (descriptor.getObjectChangePolicy().calculateChangesForExistingObject(object, uowChangeSet, ((UnitOfWorkImpl)session), descriptor, false) == null) {
                        // calculateChanges returns null in case the changeSet doesn't have changes.
                        // It should be removed from the list of ObjectChangeSets that have changes in uowChangeSet.
                        uowChangeSet.getAllChangeSets().remove(writeQuery.getObjectChangeSet());
                    }
                }
            }
        }

        // Verify if deep shallow modify is turned on
        if (writeQuery.shouldCascadeParts()) {
            queryManager.preUpdate(writeQuery);
        }

        // The row must not be built until after preUpdate in case the object reference has changed.
        // For a user defined update in the uow to row must be built twice to check if any update is required.
        writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRowForUpdateWithChangeSet(writeQuery));

        Boolean shouldModifyVersionField = changeSet.shouldModifyVersionField();
        if (!getModifyRow().isEmpty() || shouldModifyVersionField != null || changeSet.hasCmpPolicyForcedUpdate()) {
            // If user defined the entire row is required. Must not be built until change is known.
            if (writeQuery.isUserDefined() || writeQuery.isCallQuery()) {
                writeQuery.setModifyRow(descriptor.getObjectBuilder().buildRow(object, session, WriteType.UNDEFINED));
            }
            OptimisticLockingPolicy lockingPolicy = descriptor.getOptimisticLockingPolicy();

            // Update the write lock field if required.
            if (lockingPolicy != null) {
                lockingPolicy.addLockValuesToTranslationRow(writeQuery);
                // Do not lock an object that has previously been optimistically locked within the RWUoW
                boolean existingOptimisticLock = false;
                if (session instanceof RepeatableWriteUnitOfWork) {
                    RepeatableWriteUnitOfWork uow = (RepeatableWriteUnitOfWork)session;
                    if (uow.getOptimisticReadLockObjects().get(object) != null && uow.getCumulativeUOWChangeSet() != null
                            && uow.getCumulativeUOWChangeSet().getObjectChangeSetForClone(object) != null) {
                        existingOptimisticLock = true;
                    }
                }
                if (!existingOptimisticLock) {
                    // update the row and object if shouldModifyVersionField is non null and has a value of true (a forced update),
                    // or if there is no forced update and modifyRow has modifications
                    if ((shouldModifyVersionField != null && shouldModifyVersionField) || !getModifyRow().isEmpty()) {
                        // Update the row with newer lock value.
                        lockingPolicy.updateRowAndObjectForUpdate(writeQuery, object);
                    } else if (!shouldModifyVersionField && (lockingPolicy instanceof VersionLockingPolicy)) {
                        // Add the existing write lock value to the for a "read" lock (requires something to update).
                        ((VersionLockingPolicy)lockingPolicy).writeLockValueIntoRow(writeQuery, object);
                    }
                }
            }
            if (descriptor.hasSerializedObjectPolicy()) {
                descriptor.getSerializedObjectPolicy().putObjectIntoRow(getModifyRow(), object, session);
            }

            // PERF: Avoid events if no listeners.
            if (eventManager.hasAnyEventListeners()) {
                DescriptorEvent event = new DescriptorEvent(DescriptorEventManager.AboutToUpdateEvent, writeQuery);
                event.setRecord(getModifyRow());
                eventManager.executeEvent(event);
            }

            if (QueryMonitor.shouldMonitor()) {
                QueryMonitor.incrementUpdate(getWriteObjectQuery());
            }
            int rowCount = updateObject().intValue();

            if (rowCount < 1) {
                if (session.hasEventManager()) {
                    session.getEventManager().noRowsModified(writeQuery, object);
                }
            }
            if (lockingPolicy != null) {
                lockingPolicy.validateUpdate(rowCount, object, writeQuery);
            }
        }

        commitManager.markPostModifyCommitInProgress(object);

        // Verify if deep shallow modify is turned on
        if (writeQuery.shouldCascadeParts()) {
            queryManager.postUpdate(writeQuery);
        }
        if ((descriptor.getHistoryPolicy() != null) && descriptor.getHistoryPolicy().shouldHandleWrites()) {
            descriptor.getHistoryPolicy().postUpdate(writeQuery);
        }

        // PERF: Avoid events if no listeners.
        if (eventManager.hasAnyEventListeners()) {
            eventManager.executeEvent(new DescriptorEvent(DescriptorEventManager.PostUpdateEvent, writeQuery));
        }
    }

    /**
     * Unprepare the call if required.
     */
    public void unprepare() {

    }
}
