/*
 * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 1998, 2022 IBM Corporation. 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
//     01/31/2017-2.6 Will Dazey
//       - 511426: Adding cloning support
//     04/11/2018 - Will Dazey
//       - 533148 : Add the eclipselink.jpa.sql-call-deferral property
package org.eclipse.persistence.internal.queries;

import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DatabaseMapping.WriteType;
import org.eclipse.persistence.queries.ConstructorReportItem;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DeleteAllQuery;
import org.eclipse.persistence.queries.ReportQuery;
import org.eclipse.persistence.queries.UpdateAllQuery;
import org.eclipse.persistence.queries.WriteObjectQuery;

/**
 * <p><b>Purpose</b>:
 * Mechanism used for call queries.
 * <p>
 * <p><b>Responsibilities</b>:
 * Executes the appropriate call.
 *
 * @author James Sutherland
 * @since OracleAS TopLink 10<i>g</i> (10.0.3)
 */
public class DatasourceCallQueryMechanism extends DatabaseQueryMechanism {
    protected DatasourceCall call;

    /** Normally only a single call is used, however multiple table may require multiple calls on write. */
    protected Vector calls;

    public DatasourceCallQueryMechanism() {
    }

    /**
     * Initialize the state of the query
     * @param query - owner of mechanism
     */
    public DatasourceCallQueryMechanism(DatabaseQuery query) {
        super(query);
    }

    /**
     * Initialize the state of the query
     * @param query - owner of mechanism
     */
    public DatasourceCallQueryMechanism(DatabaseQuery query, DatasourceCall call) {
        super(query);
        this.call = call;
        call.setQuery(query);
    }

    /**
     * Add the call.
     */
    public void addCall(DatasourceCall call) {
        getCalls().addElement(call);
        call.setQuery(getQuery());
    }

    /**
     * Clone the DatasourceCall and Vector<DatasourceCall>.
     */
    public DatabaseQueryMechanism clone(DatabaseQuery queryClone) {
        DatasourceCallQueryMechanism clone = (DatasourceCallQueryMechanism)super.clone(queryClone);
        if(this.call != null) {
            DatasourceCall callclone = (DatasourceCall)this.call.clone();
            clone.setCall(callclone);
        }
        if(this.calls != null) {
            Vector callsclone = (Vector)this.calls.clone();
            clone.setCalls(callsclone);
        }
        return clone;
    }

    /**
     * Read all rows from the database using a cursored stream.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public DatabaseCall cursorSelectAllRows() throws DatabaseException {
        try {
            return (DatabaseCall)executeCall();
        } catch (java.lang.ClassCastException e) {
            throw QueryException.mustUseCursorStreamPolicy();
        }
    }

    /**
     * Read all rows from the database, return ResultSet
     * @exception  DatabaseException - an error has occurred on the database
     */
    public DatabaseCall selectResultSet() throws DatabaseException {
        try {
            // For CR 2923 must move to session we will execute call on now
            // so correct DatasourcePlatform used by translate.
            AbstractSession sessionToUse = this.query.getExecutionSession();
            DatabaseCall clonedCall = (DatabaseCall)this.call.clone();
            clonedCall.setQuery(this.query);
            clonedCall.translate(this.query.getTranslationRow(), getModifyRow(), sessionToUse);
            clonedCall.returnCursor();
            return (DatabaseCall)sessionToUse.executeCall(clonedCall, this.query.getTranslationRow(), this.query);
        } catch (java.lang.ClassCastException e) {
            throw QueryException.invalidDatabaseCall(this.call);
        }
    }

    /**
     * INTERNAL:
     * Delete a collection of objects. Assume call is correct.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Integer deleteAll() throws DatabaseException {
        if(((DeleteAllQuery)this.query).isPreparedUsingTempStorage()) {
            return deleteAllUsingTempTables();
        } else {
            if (hasMultipleCalls()) {
                Integer returnedRowCount = null;

                // Deletion must occur in reverse order.
                for (int index = getCalls().size() - 1; index >= 0; index--) {
                    DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                    returnedRowCount = (Integer)executeCall(databseCall);
                }
                // returns the number of rows removed from the first table in insert order
                return returnedRowCount;
            } else {
                return (Integer)executeCall();
            }
        }
    }

    /**
     * Execute deleteAll using temp tables
     * @exception  DatabaseException - an error has occurred on the database.
     * @return the row count.
     */
    public Integer deleteAllUsingTempTables() throws DatabaseException {
        DatabaseException ex = null;
        Integer returnedRowCount = null;

        // Deletion must occur in reverse order.

        // first call - crete temp table.
        // may fail in case global temp table already exists.
        try {
            DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(getCalls().size() - 1);
            executeCall(databseCall);
        } catch (DatabaseException databaseEx) {
            // ignore
        }

        // second call - populate temp table.
        // if that fails save the exception and untill cleanup
        try {
            DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(getCalls().size() - 2);
            executeCall(databseCall);
        } catch (DatabaseException databaseEx) {
            ex = databaseEx;
        }

        // third (a call per table) - delete from original tables calls.
        // if that fails save the exception untill cleanup
        for (int index = getCalls().size() - 3; index >= 1 && ex == null; index--) {
            DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
            try {
                // returns the number of rows removed from the first table in insert order
                returnedRowCount = (Integer)executeCall(databseCall);
            } catch (DatabaseException databaseEx) {
                ex = databaseEx;
            }
        }

        // last call - cleanup temp table.
        // ignore exceptions here.
        try {
            DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(0);
            executeCall(databseCall);
        } catch (DatabaseException databaseEx) {
            // ignore
        }

        if(ex != null) {
            throw ex;
        }

        return returnedRowCount;
    }

    /**
     * INTERNAL:
     * Delete an object.  Assume call is correct
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Integer deleteObject() throws DatabaseException {
        if (hasMultipleCalls()) {
            Integer returnedRowCount = null;

            // Deletion must occur in reverse order.
            for (int index = getCalls().size() - 1; index >= 0; index--) {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                Integer rowCount = (Integer)executeCall(databseCall);
                if ((index == (getCalls().size() - 1)) || (rowCount.intValue() <= 0)) {// Row count returned must be from first table or zero if any are zero.
                    returnedRowCount = rowCount;
                }
            }
            return returnedRowCount;
        } else {
            return (Integer)executeCall();
        }
    }

    /**
     * INTERNAL:
     * Execute a call.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Object execute() throws DatabaseException {
        return executeCall();
    }

    /**
     * Execute the call.  It is assumed the call has been fully prepared.
     * @exception  DatabaseException - an error has occurred on the database.
     */
    protected Object executeCall() throws DatabaseException {
        return executeCall(this.call);
    }

    /**
     * Execute the call.  It is assumed the call has been fully prepared.
     * @exception  DatabaseException - an error has occurred on the database.
     */
    protected Object executeCall(DatasourceCall databaseCall) throws DatabaseException {
        // For CR 2923 must move to session we will execute call on now
        // so correct DatasourcePlatform used by translate.
        AbstractSession sessionToUse = this.query.getExecutionSession();
        DatasourceCall clonedCall = (DatasourceCall)databaseCall.clone();
        clonedCall.setQuery(this.query);
        clonedCall.translate(this.query.getTranslationRow(), getModifyRow(), sessionToUse);
        return sessionToUse.executeCall(clonedCall, this.query.getTranslationRow(), this.query);
    }

    /**
     * Execute a non selecting call.
     * @exception  DatabaseException - an error has occurred on the database.
     * @return Returns either a {@link DatabaseCall} or Integer value, 
     * depending on if this INSERT call needs to return generated keys
     */
    public Object executeNoSelect() throws DatabaseException {
        if(((DatabaseCall)this.call).shouldReturnGeneratedKeys()) {
            return generateKeysExecuteNoSelect();
        }
        return executeNoSelectCall();
    }

    /**
     * Execute a non selecting call.
     * @exception  DatabaseException - an error has occurred on the database.
     * @return the row count.
     */
    public DatabaseCall generateKeysExecuteNoSelect() throws DatabaseException {
        return (DatabaseCall)executeCall();
    }

    /**
     * Execute a non selecting call.
     * @exception  DatabaseException - an error has occurred on the database.
     * @return the row count.
     */
    public Integer executeNoSelectCall() throws DatabaseException {
        if (hasMultipleCalls()) {
            Integer returnedRowCount = null;
            for (int index = 0; index < getCalls().size(); index++) {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                Integer rowCount = (Integer)executeCall(databseCall);
                if ((index == 0) || (rowCount.intValue() <= 0)) {// Row count returned must be from first table or zero if any are zero.
                    returnedRowCount = rowCount;
                }
            }
            return returnedRowCount;
        } else {
            return (Integer)executeCall();
        }
    }

    /**
     * INTERNAL:
     * Execute a selecting call.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Vector executeSelect() throws DatabaseException {
        return executeSelectCall();
    }

    /**
     * INTERNAL:
     * Execute a selecting call.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Vector executeSelectCall() throws DatabaseException {
        if (hasMultipleCalls()) {
            Vector results = new Vector();
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databseCall = (DatasourceCall)callsEnum.nextElement();
                Helper.addAllToVector(results, (Vector)executeCall(databseCall));
            }

            return results;
        } else {
            return (Vector)executeCall();
        }
    }

    /**
     * Return the call.
     */
    public DatasourceCall getCall() {
        return call;
    }

    /**
     * Normally only a single call is used, however multiple table may require multiple calls on write.
     * This is lazy initialised to conserve space.
     */
    public Vector getCalls() {
        if (calls == null) {
            calls = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(3);
        }
        return calls;
    }

    /**
     * Normally only a single call is used, however multiple table may require multiple calls on write.
     * This is lazy initialised to conserve space.
     */
    public boolean hasMultipleCalls() {
        return (this.calls != null) && (!this.calls.isEmpty());
    }

    /**
     * Insert the object.  Assume the call is correct.
     * @exception  DatabaseException - an error has occurred on the database
     */
    public void insertObject() throws DatabaseException {
        ClassDescriptor descriptor = getDescriptor();
        boolean usesSequencing = descriptor.usesSequenceNumbers();
        boolean shouldAcquireValueAfterInsert = false;
        if (usesSequencing) {
            shouldAcquireValueAfterInsert = descriptor.getSequence().shouldAcquireValueAfterInsert();
        }
        Collection returnFields = null;
        if (descriptor.getReturnFieldsToMergeInsert() != null) {
            returnFields = descriptor.getReturnFieldsToMergeInsert();
        }

        // Check to see if sequence number should be retrieved after insert
        if (usesSequencing && !shouldAcquireValueAfterInsert) {
            // PERF: Unit of work always assigns sequence, so only need to check it here for non unit of work/change set query.
            if (getWriteObjectQuery().getObjectChangeSet() == null) {
                // This is the normal case.  Update object with sequence number before insert.
                updateObjectAndRowWithSequenceNumber();
            }
        }

        if (hasMultipleCalls()) {
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatasourceCall databseCall = (DatasourceCall)this.calls.get(index);
                if ((index > 0) && isExpressionQueryMechanism()
                        && this.query.shouldCascadeOnlyDependentParts() && !descriptor.hasMultipleTableConstraintDependecy()
                        && this.query.getSession().getProject().allowSQLDeferral()) {
                    DatabaseTable table = descriptor.getMultipleTableInsertOrder().get(index);
                    this.query.getSession().getCommitManager().addDeferredCall(table, databseCall, this);
                } else {
                    Object result = executeCall(databseCall);
                    // Set the return row if one was returned (Postgres).
                    if (result instanceof AbstractRecord) {
                        this.query.setProperty("output", result);
                    }
                    if (returnFields != null) {
                        updateObjectAndRowWithReturnRow(returnFields, index == 0);
                    }
                    if ((index == 0) && usesSequencing && shouldAcquireValueAfterInsert) {
                        if(result instanceof DatabaseCall) {
                            updateObjectAndRowWithSequenceNumber((DatabaseCall) result);
                        } else {
                            updateObjectAndRowWithSequenceNumber();
                        }
                    }
                }
            }
        } else {
            Object result = executeCall();
            // Set the return row if one was returned (Postgres).
            if (result instanceof AbstractRecord) {
                this.query.setProperty("output", result);
            }
            if (returnFields != null) {
                updateObjectAndRowWithReturnRow(returnFields, true);
            }
            if (usesSequencing && shouldAcquireValueAfterInsert) {
                if(result instanceof DatabaseCall) {
                    updateObjectAndRowWithSequenceNumber((DatabaseCall) result);
                } else {
                    updateObjectAndRowWithSequenceNumber();
                }
            }
        }

        // Bug 3110860: RETURNINGPOLICY-OBTAINED PK CAUSES LOB TO BE INSERTED INCORRECTLY
        // The deferred locator SELECT calls should be generated and executed after ReturningPolicy
        // merges PK obtained from the db into the object held by the query.
        //
        //Oracle thin driver handles LOB differently. During the insert, empty lob would be
        //insert first, and then the LOb locator is retrieved and LOB data are written through
        //the locator.
        //
        // Bug 2804663 - LOBValueWriter is no longer a singleton, so we execute any deferred
        // select calls through the DatabaseAccessor which holds the writer instance
        AbstractSession executionSession = this.query.getExecutionSession();
        for (Accessor accessor : executionSession.getAccessors()) {
            accessor.flushSelectCalls(executionSession);
        }
    }

    /**
     * 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) {
        Object result = executeCall(call);
        // Set the return row if one was returned (Postgres).
        if (result instanceof AbstractRecord) {
            this.query.setProperty("output", result);
        }
        Collection returnFields = null;
        if (this.query.getDescriptor().hasReturningPolicy()) {
            returnFields = this.query.getDescriptor().getReturningPolicy().getFieldsToMergeInsert();
        }
        if (returnFields != null) {
            updateObjectAndRowWithReturnRow(returnFields, false);
        }
    }

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

    /**
     * INTERNAL:
     * This is different from 'prepareForExecution' in that this is called on the original query,
     * and the other is called on the copy 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() {
        if ((!hasMultipleCalls()) && (getCall() == null)) {
            throw QueryException.sqlStatementNotSetProperly(getQuery());
        }
    }

    /**
     * INTERNAL:
     * This is different from 'prepareForExecution' in that this is called on the original query,
     * and the other is called on the copy 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 prepareCall() throws QueryException {
        DatabaseQuery query = getQuery();
        AbstractSession executionSession = query.getExecutionSession();
        if (hasMultipleCalls()) {
            for (DatasourceCall call : (List<DatasourceCall>)getCalls()) {
                call.prepare(executionSession);
            }
        } else if (getCall() != null) {
            getCall().prepare(executionSession);
        }
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareCursorSelectAllRows() throws QueryException {
        getCall().returnCursor();
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareDeleteAll() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall call = (DatasourceCall)callsEnum.nextElement();
                call.returnNothing();
            }
        } else {
            getCall().returnNothing();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareDeleteObject() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall call = (DatasourceCall)callsEnum.nextElement();
                call.returnNothing();
            }
        } else {
            getCall().returnNothing();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareDoesExist(DatabaseField field) {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                ((DatasourceCall)callsEnum.nextElement()).returnOneRow();
            }
        } else {
            getCall().returnOneRow();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareExecuteNoSelect() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                ((DatasourceCall)callsEnum.nextElement()).returnNothing();
            }
        } else {
            getCall().returnNothing();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call. This method assumes the query was built
     * using a stored procedure query which is a single call.
     *
     * The return type on the call will already be set and
     */
    public void prepareExecute() {
        getCall().setExecuteUpdate();
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareExecuteSelect() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databseCall = (DatasourceCall)callsEnum.nextElement();
                databseCall.returnManyRows();
            }
        } else {
            getCall().returnManyRows();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareInsertObject() {
        if (hasMultipleCalls()) {
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatabaseCall call = (DatabaseCall)this.calls.get(index);
                if (!call.isReturnSet()) {
                    call.returnNothing();
                }
            }
        } else {
            if (!this.call.isReturnSet()) {
                this.call.returnNothing();
            }
        }
        prepareCall();
    }

    /**
     * Prepare the report items.
     * Indexes of results need to be calculated.
     */
    protected void prepareReportQueryItems(){
        //calculate indexes after normalize to insure expressions are set up correctly
        //take into account any field expressions added to the ReportQuery
        ReportQuery query = (ReportQuery)getQuery();
        computeAndSetItemOffset(query, query.getItems(), query.getQueryExpressions().size());
    }

    /**
     * calculate indexes for given items, given the current Offset
     */
    protected int computeAndSetItemOffset(ReportQuery query, List<ReportItem> items, int itemOffset) {
        for(ReportItem item : items) {
            if (item.isConstructorItem()) {
                List<ReportItem> reportItems = ((ConstructorReportItem) item).getReportItems();
                itemOffset = computeAndSetItemOffset(query, reportItems, itemOffset);
            } else {
                //Don't set the offset on the ConstructorItem
                item.setResultIndex(itemOffset);
                if (item.getAttributeExpression() != null) {
                    if (item.hasJoining()){
                        itemOffset = item.getJoinedAttributeManager().computeJoiningMappingIndexes(true, getSession(), itemOffset);
                    } else {
                        if (item.getDescriptor() != null) {
                            itemOffset += item.getDescriptor().getAllSelectionFields(query).size();
                        } else {
                            if (item.getMapping() != null && item.getMapping().isAggregateObjectMapping()) {
                                itemOffset += item.getMapping().getFields().size(); // Aggregate object may consist out of 1..n fields
                            } else {
                                ++itemOffset; //only a single attribute can be selected
                            }
                        }
                    }
                }
            }
        }
        return itemOffset;
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareReportQuerySelectAllRows() {
        prepareReportQueryItems();
        prepareExecuteSelect();
    }

    /**
     * Prepare for a sub select using a call.
     */
    public void prepareReportQuerySubSelect() {
        prepareReportQueryItems();
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareSelectAllRows() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databseCall = (DatasourceCall)callsEnum.nextElement();
                databseCall.returnManyRows();
            }
        } else {
            getCall().returnManyRows();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareSelectOneRow() {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databseCall = (DatasourceCall)callsEnum.nextElement();
                databseCall.returnOneRow();
            }
        } else {
            getCall().returnOneRow();
        }
        prepareCall();
    }

    /**
     * Pre-build configure the call.
     */
    public void prepareUpdateObject() {
        if (hasMultipleCalls()) {
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatabaseCall call = (DatabaseCall)this.calls.get(index);
                if (!call.isReturnSet()) {
                    call.returnNothing();
                }
            }
        } else if (getCall() != null) {
            if (!call.isReturnSet()) {
                this.call.returnNothing();
            }
        }
        prepareCall();
    }

    /**
       * Pre-build configure the call.
       */
    public void prepareUpdateAll() {
        if (getCall() != null) {
            getCall().returnNothing();
        }

        prepareCall();
    }

    /**
     * Read all rows from the database. Assume call is correct returns the required fields.
     * @return Vector containing the database rows
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Vector selectAllReportQueryRows() throws DatabaseException {
        return executeSelect();
    }

    /**
     * Read all rows from the database. Assume call is correct returns the required fields.
     * @return Vector containing the database rows
     * @exception  DatabaseException - an error has occurred on the database
     */
    public Vector selectAllRows() throws DatabaseException {
        return executeSelectCall();
    }

    /**
     * Read a single row from the database. Assume call is correct.
     * @return row containing data
     * @exception  DatabaseException - an error has occurred on the database
     */
    public AbstractRecord selectOneRow() throws DatabaseException {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databaseCall = (DatasourceCall)callsEnum.nextElement();
                AbstractRecord result = (AbstractRecord)executeCall(databaseCall);
                if (result != null) {
                    return result;
                }
            }

            return null;
        } else {
            return (AbstractRecord)executeCall();
        }
    }

    /**
     * Perform a does exist check
     * @param field - the field used for does exist check
     * @return  the associated row from the database
     * @exception  DatabaseException - an error has occurred on the database
     */
    public AbstractRecord selectRowForDoesExist(DatabaseField field) throws DatabaseException {
        if (hasMultipleCalls()) {
            for (Enumeration callsEnum = getCalls().elements(); callsEnum.hasMoreElements();) {
                DatasourceCall databaseCall = (DatasourceCall)callsEnum.nextElement();
                AbstractRecord result = (AbstractRecord)executeCall(databaseCall);
                if (result != null) {
                    return result;
                }
            }

            return null;
        } else {
            return (AbstractRecord)executeCall();
        }
    }

    /**
     * Set the call.
     */
    public void setCall(DatasourceCall call) {
        this.call = call;
        if (call != null) {
            call.setQuery(getQuery());
        }
    }

    /**
     * Normally only a single call is used, however multiple table may require multiple calls on write.
     * This is lazy initialised to conserve space.
     */
    protected void setCalls(Vector calls) {
        this.calls = calls;
    }

    /**
     * Update the object.  Assume the call is correct.
     * @exception  DatabaseException - an error has occurred on the database.
     * @return the row count.
     */
    public Integer updateObject() throws DatabaseException {
        ClassDescriptor descriptor = getDescriptor();
        Collection returnFields = null;
        if (descriptor.getReturnFieldsToMergeUpdate() != null) {
            returnFields = descriptor.getReturnFieldsToMergeUpdate();
        }
        Integer returnedRowCount = null;
        if (hasMultipleCalls()) {
            int size = this.calls.size();
            for (int index = 0; index < size; index++) {
                DatasourceCall databseCall = (DatasourceCall)this.calls.get(index);
                if ((index > 0) && isExpressionQueryMechanism()
                        && this.query.shouldCascadeOnlyDependentParts() && !descriptor.hasMultipleTableConstraintDependecy()
                        && this.query.getSession().getProject().allowSQLDeferral()) {
                    DatabaseTable table = descriptor.getMultipleTableInsertOrder().get(index);
                    this.query.getSession().getCommitManager().addDeferredCall(table, databseCall, this);
                } else {
                    Object result = executeCall(databseCall);
                    // Set the return row if one was returned (Postgres).
                    Integer rowCount;
                    if (result instanceof AbstractRecord) {
                        this.query.setProperty("output", result);
                        rowCount = Integer.valueOf(1);
                    } else {
                        rowCount = (Integer)result;
                    }
                    if ((index == 0) || (rowCount.intValue() <= 0)) {// Row count returned must be from first table or zero if any are zero.
                        returnedRowCount = rowCount;
                    }
                    if (returnFields != null) {
                        updateObjectAndRowWithReturnRow(returnFields, false);
                    }
                }
            }
        } else {
            Object result = executeCall();
            // Set the return row if one was returned (Postgres).
            if (result instanceof AbstractRecord) {
                this.query.setProperty("output", result);
                returnedRowCount = Integer.valueOf(1);
            } else {
                returnedRowCount = (Integer)result;
            }
            if (returnFields != null) {
                updateObjectAndRowWithReturnRow(returnFields, false);
            }
        }

        //Oracle thin driver handles LOB differently. During the insert, empty lob would be
        //insert first, and then the LOb locator is retrieved and LOB data are written through
        //the locator.
        //
        // Bug 2804663 - LOBValueWriter is no longer a singleton, so we execute any deferred
        // select calls through the DatabaseAccessor which holds the writer instance
        //
        // Building of SELECT statements is no longer done in DatabaseAccessor.basicExecuteCall
        // because DatabaseCall.isUpdateCall() can't recognize update in case StoredProcedureCall
        // is used.
        AbstractSession executionSession = this.query.getExecutionSession();
        for (Accessor accessor : executionSession.getAccessors()) {
            accessor.flushSelectCalls(executionSession);
        }
        return returnedRowCount;
    }

    /**
       * Update the rows on the database.  Assume the call is correct.
       * @exception  DatabaseException - an error has occurred on the database.
       */
    public Integer updateAll() throws DatabaseException {
        if(((UpdateAllQuery)this.query).isPreparedUsingTempStorage() && getExecutionSession().getPlatform().supportsTempTables()) {
            return updateAllUsingTempTables();
        } else {
            Integer rowCount = executeNoSelectCall();
            if(((UpdateAllQuery)this.query).isPreparedUsingTempStorage()) {
                // the query was prepared using Oracle anonymous block
                AbstractRecord outputRow = (AbstractRecord)this.query.getProperty("output");
                rowCount = (Integer)outputRow.get("ROW_COUNT");
            }
            return rowCount;
        }
    }

    /**
     * Execute updateAll using temp tables
     * @exception  DatabaseException - an error has occurred on the database.
     * @return the row count.
     */
    public Integer updateAllUsingTempTables() throws DatabaseException {
        int nTables = getCalls().size() / 4;
        DatabaseException ex = null;
        Integer returnedRowCount = null;

        // first quarter - crete temp tables calls.
        // may fail in case global temp table already exists.
        for (int index = 0; index < nTables; index++) {
            try {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                executeCall(databseCall);
            } catch (DatabaseException databaseEx) {
                // ignore
            }
        }

        // second quarter - populate temp tables calls.
        // if that fails save the exception and until cleanup
        for (int index = nTables; index < nTables*2 && ex == null; index++) {
            try {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                executeCall(databseCall);
            } catch (DatabaseException databaseEx) {
                ex = databaseEx;
            }
        }

        // third quarter - update original tables calls.
        // if that fails save the exception and until cleanup
        for (int index = nTables*2; index < nTables*3 && ex == null; index++) {
            try {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                Integer rowCount = (Integer)executeCall(databseCall);
                if ((index == nTables*2) || (rowCount.intValue() <= 0)) {// Row count returned must be from first table or zero if any are zero.
                    returnedRowCount = rowCount;
                }
            } catch (DatabaseException databaseEx) {
                ex = databaseEx;
            }
        }

        // last quarter - cleanup temp tables calls.
        // ignore exceptions here.
        for (int index = nTables*3; index < nTables*4; index++) {
            try {
                DatasourceCall databseCall = (DatasourceCall)getCalls().elementAt(index);
                executeCall(databseCall);
                } catch (DatabaseException databaseEx) {
                    // ignore
                }
        }

        if(ex != null) {
            throw ex;
        }

        return returnedRowCount;
    }

    /**
     * Update the foreign key fields when resolving a bi-directional reference in a UOW.
     * This is rare to occur for non-relational, however if it does each of the calls must be re-executed.
     */
    protected void updateForeignKeyFieldAfterInsert(WriteObjectQuery writeQuery) {
        writeQuery.setModifyRow(this.getDescriptor().getObjectBuilder().buildRow(writeQuery.getObject(), this.getSession(), WriteType.INSERT));

        // For CR 2923 must move to session we will execute call on now
        // so correct DatasourcePlatform used by translate.
        AbstractSession sessionToUse = this.query.getExecutionSession();

        // yes - this is a bit ugly...
        Vector calls = ((DatasourceCallQueryMechanism)this.getDescriptor().getQueryManager().getUpdateQuery().getQueryMechanism()).getCalls();
        for (Enumeration stream = calls.elements(); stream.hasMoreElements();) {
            DatasourceCall call = (DatasourceCall)((DatasourceCall)stream.nextElement()).clone();
            call.setQuery(writeQuery);
            sessionToUse.executeCall(call, this.getTranslationRow(), writeQuery);
        }
    }
}
