/*

   Derby - Class org.apache.derby.impl.sql.execute.TableScanResultSet

   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.derby.impl.sql.execute;

import java.util.Properties;

import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.i18n.MessageService;
import org.apache.derby.iapi.services.loader.GeneratedMethod;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.sql.Activation;
import org.apache.derby.iapi.sql.execute.CursorResultSet;
import org.apache.derby.iapi.sql.execute.ExecIndexRow;
import org.apache.derby.iapi.sql.execute.ExecRow;
import org.apache.derby.iapi.sql.execute.NoPutResultSet;
import org.apache.derby.iapi.store.access.BackingStoreHashtable;
import org.apache.derby.iapi.store.access.ConglomerateController;
import org.apache.derby.iapi.store.access.DynamicCompiledOpenConglomInfo;
import org.apache.derby.iapi.store.access.Qualifier;
import org.apache.derby.iapi.store.access.ScanController;
import org.apache.derby.iapi.store.access.StaticCompiledOpenConglomInfo;
import org.apache.derby.iapi.store.access.TransactionController;
import org.apache.derby.iapi.types.DataValueDescriptor;
import org.apache.derby.iapi.types.RowLocation;

/**
 * Takes a table and a table filter and returns
 * the table's rows satisfying the filter as a result set.
 *
 * There are several things we could do during object
 * construction that are done in the open and next calls, to
 * improve performance.
 *
 */
class TableScanResultSet extends ScanResultSet
	implements CursorResultSet, Cloneable
{
    protected ScanController scanController;
	protected boolean		scanControllerOpened;
	protected boolean		isKeyed;
	protected boolean		firstScan = true;
	protected ExecIndexRow	startPosition;
	protected ExecIndexRow	stopPosition;

    // set in constructor and not altered during
    // life of object.
	protected long conglomId;
    protected DynamicCompiledOpenConglomInfo dcoci;
    protected StaticCompiledOpenConglomInfo scoci;
	protected GeneratedMethod startKeyGetter;
	protected int startSearchOperator;
	protected GeneratedMethod stopKeyGetter;
	protected int stopSearchOperator;
	public    Qualifier[][] qualifiers;
	public String userSuppliedOptimizerOverrides;
	protected boolean runTimeStatisticsOn;
    protected int[] indexCols;		//index keys base column position array
	public int rowsPerRead;
	public boolean forUpdate;
	final boolean sameStartStopPosition;
    protected boolean nextDone;
	private RowLocation rlTemplate;

	// Run time statistics
	private Properties scanProperties;
	public String startPositionString;
	public String stopPositionString;
	public boolean isConstraint;
	public boolean coarserLock;
	public boolean oneRowScan;

	protected long	rowsThisScan;

	private long estimatedRowCount;

    /**
     * This field is used by beetle 3865, updateable cursor using index. It
     * is a hash table containing updated rows that are thrown into future
     * direction of the index scan, and as a result we'll hit it again but
     * should skip it. The hash table will spill to disk if it grows too big
     * to be kept in memory.
     */
    protected BackingStoreHashtable past2FutureTbl;

	// For Scrollable insensitive updatable result sets, only qualify a row the 
	// first time it's been read, since an update can change a row so that it 
	// no longer qualifies
    protected boolean qualify;

	// currentRowIsValid is set to the result of positioning at a rowLocation.
	// It will be true if the positioning was successful and false if the row 
	// was deleted under our feet. Whenenver currentRowIsValid is false it means 
	// that the row has been deleted.
    protected boolean currentRowIsValid;
	
	// Indicates whether the scan has been positioned back to a previously read
	// row, or it is accessing a row for the first time.
    protected boolean scanRepositioned;

    //
    // class interface
    //
    TableScanResultSet(long conglomId,
		StaticCompiledOpenConglomInfo scoci, 
		Activation activation, 
		int resultRowTemplate,
		int resultSetNumber,
		GeneratedMethod startKeyGetter, int startSearchOperator,
		GeneratedMethod stopKeyGetter, int stopSearchOperator,
		boolean sameStartStopPosition,
		Qualifier[][] qualifiers,
		String tableName,
		String userSuppliedOptimizerOverrides,
		String indexName,
		boolean isConstraint,
		boolean forUpdate,
		int colRefItem,
		int indexColItem,
		int lockMode,
		boolean tableLocked,
		int isolationLevel,
		int rowsPerRead,
		boolean oneRowScan,
		double optimizerEstimatedRowCount,
		double optimizerEstimatedCost)
			throws StandardException
    {
		super(activation,
				resultSetNumber,
				resultRowTemplate,
				lockMode, tableLocked, isolationLevel,
                colRefItem,
				optimizerEstimatedRowCount,
				optimizerEstimatedCost);

		this.conglomId = conglomId;

		/* Static info created at compile time and can be shared across
		 * instances of the plan.
		 * Dynamic info created on 1st opening of this ResultSet as
		 * it cannot be shared.
		 */
        this.scoci = scoci;

		if (SanityManager.DEBUG) {
			SanityManager.ASSERT( activation!=null, "table scan must get activation context");
			if (sameStartStopPosition)
			{
				SanityManager.ASSERT(stopKeyGetter == null,
					"stopKeyGetter expected to be null when sameStartStopPosition is true");
			}
		}

		this.startKeyGetter = startKeyGetter;
		this.startSearchOperator = startSearchOperator;
		this.stopKeyGetter = stopKeyGetter;
		this.stopSearchOperator = stopSearchOperator;
		this.sameStartStopPosition = sameStartStopPosition;
		this.qualifiers = qualifiers;
		this.tableName = tableName;
		this.userSuppliedOptimizerOverrides = userSuppliedOptimizerOverrides;
		this.indexName = indexName;
		this.isConstraint = isConstraint;
		this.forUpdate = forUpdate;
		this.rowsPerRead = rowsPerRead;
		this.oneRowScan = oneRowScan;

		if (indexColItem != -1)
		{
			this.indexCols = (int[])(activation.getPreparedStatement().
						getSavedObject(indexColItem));
		}
		if (indexCols != null)
			activation.setForUpdateIndexScan(this);

		runTimeStatisticsOn = (activation != null &&
							   activation.getLanguageConnectionContext().getRunTimeStatisticsMode());
		
		/* Always qualify the first time a row is being read */
		qualify = true;
		currentRowIsValid = false;
		scanRepositioned = false;
		
		recordConstructorTime();
    }

	//
	// ResultSet interface (leftover from NoPutResultSet)
	//

	/**
     * open a scan on the table. scan parameters are evaluated
     * at each open, so there is probably some way of altering
     * their values...
	 *
	 * @exception StandardException thrown on failure to open
     */
	public void	openCore() throws StandardException
	{
		if (SanityManager.DEBUG)
		    SanityManager.ASSERT( ! isOpen, "TableScanResultSet already open");

        // Get the current transaction controller
        TransactionController tc = activation.getTransactionController();

		initIsolationLevel();

		if (dcoci == null)
			dcoci = tc.getDynamicCompiledConglomInfo(conglomId);

        initStartAndStopKey();

		/* NOTE: We always open the ScanController on the 1st open
		 * to do the keyed conglomerate check.
		 */

		// Determine whether the conglomerate is keyed.  This determines
		// how we find the RowLocation for the base heap.  For non-keyed
		// conglomerates, we ask the scan.  For keyed conglomerates, it
		// is the last column in the row.
		//
		// Do this here, rather than in the constructor, so we can avoid
		// throwing exceptions from the constructor
		if (firstScan)
		{
			openScanController(tc);

			isKeyed = scanController.isKeyed();

			/*
			** If scan tracing is turned on, print information about this
			** TableScanResultSet when it is first opened.  We would like
			** to do this when it is constructed, but it is not always
			** possible to get the start and stop positioners at the time
			** this object is constructed (because they may depend on outer
			** rows).
			*/
			if (SanityManager.DEBUG)
			{
				if (SanityManager.DEBUG_ON("ScanTrace"))
				{
					//traceScanParameters();
				}
			}
		}

		// Check whether there are any comparisons with unordered nulls
		// on either the start or stop position.  If there are, we can
		// (and must) skip the scan, because no rows can qualify
		if (skipScan(startPosition, stopPosition))
		{
			scanControllerOpened = false;
		}
		/* NOTE: We always open the ScanController on the 1st open
		 * to do the keyed conglomerate check, so we only need to
		 * do it here if not the 1st scan.
		 */
		else if (! firstScan)
		{
			openScanController(tc);
		}

		/* If the scan is on an index and opened for update,
		 * then we cache the scan controller and conglomerate
		 * number in the activation so that the scan controller
		 * can be re-used by the update/delete if the index
		 * that we are scanning also needs to be updated.
		 */
		if (forUpdate && isKeyed)
		{
			activation.setIndexScanController(scanController);
			activation.setIndexConglomerateNumber(conglomId);
		}

		firstScan = false;
	    isOpen = true;
		numOpens++;
		nextDone = false;
		openTime += getElapsedMillis(beginTime);
	}

    /**
     * Initialize the {@code startPosition} and {@code stopPosition} fields
     * which are used to limit the rows returned by the scan.
     */
    void initStartAndStopKey() throws StandardException {
        if (startKeyGetter != null) {
            startPosition = (ExecIndexRow) startKeyGetter.invoke(activation);
            if (sameStartStopPosition) {
                stopPosition = startPosition;
            }
        }
        if (stopKeyGetter != null) {
            stopPosition = (ExecIndexRow) stopKeyGetter.invoke(activation);
        }
    }

	/*
	** Open the scan controller
	**
	** @param transaction controller will open one if null
	*/
	protected void openScanController(TransactionController tc)
		throws StandardException
	{
		DataValueDescriptor[] startPositionRow = 
            startPosition == null ? null : startPosition.getRowArray();
		DataValueDescriptor[] stopPositionRow = 
            stopPosition == null ? null : stopPosition.getRowArray();

		// Clear the Qualifiers's Orderable cache 
		if (qualifiers != null)
		{
			clearOrderableCache(qualifiers);
		}

		// Get the current transaction controller
		if (tc == null)
			tc = activation.getTransactionController();

        int openMode = 0;
        if (forUpdate)
        {
            openMode = TransactionController.OPENMODE_FORUPDATE;

            if (activation.isCursorActivation())
                openMode |= TransactionController.OPENMODE_USE_UPDATE_LOCKS;
        }

		scanController = tc.openCompiledScan(
				activation.getResultSetHoldability(),
				openMode,
				lockMode,
				isolationLevel,
				accessedCols,
				startPositionRow,
					// not used when giving null start position
				startSearchOperator,
				qualifiers,
				stopPositionRow,
					// not used when giving null stop position
				stopSearchOperator,
				scoci,
				dcoci);

		/* Remember that we opened the scan */
		scanControllerOpened = true;

		rowsThisScan = 0;

		/*
		** Inform the activation of the estimated number of rows.  Only
		** do it here, not in reopen, so that we don't do this costly
		** check too often.
		*/
		estimatedRowCount = scanController.getEstimatedRowCount();
		activation.informOfRowCount(
									this,
									scanController.getEstimatedRowCount()
									);
	}

	/*
	** reopen the scan controller
	*/
	protected void reopenScanController() throws StandardException
	{
		DataValueDescriptor[] startPositionRow = 
            startPosition == null ? null : startPosition.getRowArray();
		DataValueDescriptor[] stopPositionRow = 
            stopPosition == null ? null : stopPosition.getRowArray();

        rowsThisScan = 0;

		// Clear the Qualifiers's Orderable cache 
		if (qualifiers != null)
		{
			clearOrderableCache(qualifiers);
		}

		scanController.reopenScan(
						startPositionRow,
						startSearchOperator,
						qualifiers,
						stopPositionRow,
						stopSearchOperator);

		/* Remember that we opened the scan */
		scanControllerOpened = true;
	}

	/**
     * Reopen a table scan.  Here we take advantage
	 * of the reopenScan() interface on scanController
	 * for optimimal performance on joins where we are
	 * an inner table.
	 *
	 * @exception StandardException thrown on failure to open
     */
	public void	reopenCore() throws StandardException
	{
		beginTime = getCurrentTimeMillis();
		if (SanityManager.DEBUG)
		    SanityManager.ASSERT(isOpen, "TableScanResultSet not open, cannot reopen");

        initStartAndStopKey();

		// Check whether there are any comparisons with unordered nulls
		// on either the start or stop position.  If there are, we can
		// (and must) skip the scan, because no rows can qualify
		if (skipScan(startPosition, stopPosition))
		{
			scanControllerOpened = false;
		}
		else
		{
			if (scanController == null)
				openScanController((TransactionController)null);
			else
				reopenScanController();
		
		}

		numOpens++;
		nextDone = false;
		openTime += getElapsedMillis(beginTime);
	}


    boolean loopControl(boolean moreRows) throws StandardException {
        return scanController.fetchNext(candidate.getRowArray());
    }

	/**
     * Return the next row (if any) from the scan (if open).
	 *
	 * @exception StandardException thrown on failure to get next row
	 */
	public ExecRow getNextRowCore() throws StandardException
	{
		if( isXplainOnlyMode() )
			return null;

        checkCancellationFlag();
            
		if (currentRow == null || scanRepositioned)
		{
			currentRow =
				getCompactRow(candidate, accessedCols, isKeyed);
		}

		beginTime = getCurrentTimeMillis();

		ExecRow result = null;

	    if ( isOpen  && !nextDone)
	    {
			/* Only need to do 1 next per scan
			 * for 1 row scans.
			 */
			nextDone = oneRowScan;

			if (scanControllerOpened)
			{
                boolean moreRows = true;

                while (true)
				{
                    // loop control overriden by subclass
                    // ValidateCheckConstraintResultSet..
                    if (! (moreRows = loopControl(moreRows))) {
                        break;
                    }
					rowsSeen++;
					rowsThisScan++;

					/*
					** Skip rows where there are start or stop positioners
					** that do not implement ordered null semantics and
					** there are columns in those positions that contain
					** null.
					** No need to check if start and stop positions are the
					** same, since all predicates in both will be ='s,
					** and hence evaluated in the store.
					*/
					if ((! sameStartStopPosition) && skipRow(candidate))
					{
						rowsFiltered++;
						continue;
					}

					/* beetle 3865, updateable cursor use index. If we have a hash table that
					 * holds updated records, and we hit it again, skip it, and remove it from
					 * hash since we can't hit it again, and we have a space in hash, so can
					 * stop scanning forward.
					 */
					if (past2FutureTbl != null)
					{
						RowLocation rowLoc = (RowLocation) currentRow.getColumn(currentRow.nColumns());
						if (past2FutureTbl.remove(rowLoc) != null){
						    continue;
						}
					}

					result = currentRow;

					break;
				}

				/*
				** If we just finished a full scan of the heap, update
				** the number of rows in the scan controller.
				**
				** NOTE: It would be more efficient to only update the
				** scan controller if the optimizer's estimated number of
				** rows were wrong by more than some threshold (like 10%).
				** This would require a little more work than I have the
				** time for now, however, as the row estimate that is given
				** to this result set is the total number of rows for all
				** scans, not the number of rows per scan.
				*/
				if (! moreRows)
				{
					setRowCountIfPossible(rowsThisScan);
					currentRow = null;
				}
			}
	    }

		setCurrentRow(result);
		currentRowIsValid = true;
		scanRepositioned = false;
		qualify = true;

		nextTime += getElapsedMillis(beginTime);
	    return result;
	}

	/**
	 * If the result set has been opened,
	 * close the open scan.
	 * @exception StandardException on error
	 */
	public void	close() throws StandardException
	{
		beginTime = getCurrentTimeMillis();
		if ( isOpen )
	    {
			/*
			** If scan tracing is turned on, print information about this
			** TableScanResultSet when it is closed.
			*/
			if (SanityManager.DEBUG)
			{
				if (SanityManager.DEBUG_ON("ScanTrace"))
				{
					//traceClose();
				}
			}

			// we don't want to keep around a pointer to the
			// row ... so it can be thrown away.
			// REVISIT: does this need to be in a finally
			// block, to ensure that it is executed?
		    clearCurrentRow();
;
			if (scanController != null)
			{
				// This is where we get the positioner info for inner tables
				if (runTimeStatisticsOn)
				{
					// This is where we get the scan properties for a subquery
					scanProperties = getScanProperties();
					startPositionString = printStartPosition();
					stopPositionString = printStopPosition();
				}
	        	scanController.close();
				scanController = null; // should not access after close

                // Updatable index scans are cached in the activation for
                // easy access from IndexChanger. Remember to clear the cached
                // info here, but only if this is the result set that cached
                // it in the first place (DERBY-4585).
                if (forUpdate && isKeyed) {
                    activation.clearIndexScanInfo();
                }

			}
			scanControllerOpened = false;
			startPosition = null;
			stopPosition = null;

			super.close();

			if (indexCols != null)
			{
				ConglomerateController borrowedBaseCC = activation.getHeapConglomerateController();
				if (borrowedBaseCC != null)
				{
					borrowedBaseCC.close();
					activation.clearHeapConglomerateController();
				}
			}

            if (past2FutureTbl != null)
            {
                past2FutureTbl.close();
            }
	    }
		else
			if (SanityManager.DEBUG)
				SanityManager.DEBUG("CloseRepeatInfo","Close of TableScanResultSet repeated");

		closeTime += getElapsedMillis(beginTime);
	}

	/**
	 * Return the total amount of time spent in this ResultSet
	 *
	 * @param type	CURRENT_RESULTSET_ONLY - time spent only in this ResultSet
	 *				ENTIRE_RESULTSET_TREE  - time spent in this ResultSet and below.
	 *
	 * @return long		The total amount of time spent (in milliseconds).
	 */
	public long getTimeSpent(int type)
	{
		long totTime = constructorTime + openTime + nextTime + closeTime;

		/* RESOLVE - subtract out store time later, when available */
		if (type == NoPutResultSet.CURRENT_RESULTSET_ONLY)
		{
			return	totTime;
		}
		else
		{
			return totTime;
		}
	}


	//
	// CursorResultSet interface
	//

	/**
	 * This result set has its row location from
	 * the last fetch done. If the cursor is closed, 
	 * or the row has been deleted a null is returned.
	 *
	 * @see CursorResultSet
	 *
	 * @return the row location of the current cursor row.
	 * @exception StandardException thrown on failure to get row location
	 */
	public RowLocation getRowLocation() throws StandardException
	{
		RowLocation rl;

		if (! isOpen) return null;

		if ( ! scanControllerOpened)
			return null;

		/*
		** If the conglomerate is keyed, the row location of the base row
		** is in the last column of the current row.  If it's not keyed,
		** we get the row location from the scan of the heap.
		*/
		if (isKeyed)
		{
			if (SanityManager.DEBUG)
			{
				SanityManager.ASSERT(currentRow != null,
				  "There must be a current row when fetching the row location");
			}

			rl = (RowLocation) currentRow.getColumn(
													currentRow.nColumns());
		}
		else
		{
			if (currentRowIsValid) {
				// we reuse the same rowlocation object across several calls.
				if (rlTemplate == null)
					rlTemplate = scanController.newRowLocationTemplate();
				rl = rlTemplate;
				try {
					scanController.fetchLocation(rl);
				} catch (StandardException se) {
					if (se.getMessageId().
						equals(SQLState.HEAP_SCAN_NOT_POSITIONED)) {
						//Have a easier to understand error message than what 
						//we get from store 
						throw StandardException.
							newException(SQLState.NO_CURRENT_ROW);
					}
                    throw se;
				}
			} else {
				rl = null;
			}
		}

		return rl;
	}

	/**
	 * This result set has its row from the last fetch done. 
	 * If the cursor is closed, the row has been deleted, or
	 * no longer qualifies (for forward only result sets) a 
	 * null is returned.
	 *
	 * @see CursorResultSet
	 *
	 * @return the last row returned;
	 * @exception StandardException thrown on failure.
	 */
	/* RESOLVE - this should return activation.getCurrentRow(resultSetNumber),
	 * once there is such a method.  (currentRow is redundant)
	 */
	public ExecRow getCurrentRow() throws StandardException 
	{
		if (SanityManager.DEBUG)
			SanityManager.ASSERT(isOpen, "TSRS expected to be open");

		/* Nothing to do if we're not currently on a row or
		 * if the current row get deleted out from under us
		 * or if there is no current scan (can happen if the
		 * scan is being skipped) or if the current position
		 * no longer qualifies.
		 */
		try
		{
			if ((currentRow == null)                        ||
			(!currentRowIsValid)                            ||
			(!scanControllerOpened)                         ||
			(qualify && scanController.isCurrentPositionDeleted())     ||
			(qualify && (!scanController.doesCurrentPositionQualify())))
			{
				return null;
			}
		}
		catch (StandardException se)
		{
			if (se.getMessageId().equals(SQLState.AM_SCAN_NOT_POSITIONED))
			{
				//bug 4515 - Have a easier to understand error message than what we get from store 
				se=StandardException.newException(SQLState.NO_CURRENT_ROW);
				throw se;
			}
		}

        resultRowBuilder.reset(candidate);
        currentRow = getCompactRow(candidate, accessedCols, isKeyed);

        try
        {
            scanController.fetchWithoutQualify(candidate.getRowArray());
        }
        catch (StandardException se)
        {
            if (se.getMessageId().equals(SQLState.AM_RECORD_NOT_FOUND))
            {
                // Somehow the row got deleted between the above 
                // doesCurrentPositionQualify() call and here (one way is if
                // this scan is read uncommitted isolation level).
                return null;
            }
            else
            {
                throw se;
            }
        }

        setCurrentRow(candidate);
	    return currentRow;
	}

	/**
	 * @see NoPutResultSet#positionScanAtRowLocation
	 * 
	 * Also sets qualify to false so that later calls to getCurrentRow
	 * will not attempt to re-qualify the current row. 
	 */
	public void positionScanAtRowLocation(RowLocation rl) 
		throws StandardException 
	{
		// Check if the scanController is a B-tree scan controller. Do not
		// attempt to re-position a b-tree controller.
		if (!isKeyed) {
			currentRowIsValid = scanController.positionAtRowLocation(rl);
		}
		qualify = false;
		scanRepositioned = true;
	}

	/**
	 * Print the parameters that constructed this result set to the
	 * trace stream.
	 */
/*
	private final void traceScanParameters()
	{
		if (SanityManager.DEBUG)
		{
			HeaderPrintWriter traceStream = SanityManager.GET_DEBUG_STREAM();

			traceStream.println("");
			traceStream.println("TableScanResultSet number " +
								resultSetNumber +
								" parameters:");

			traceStream.println("");
			traceStream.println("\tTable name: " + tableName);
			if (indexName != null)
			{
				traceStream.println("\tIndex name: " + indexName);
			}
			traceStream.println("");
			traceStream.println("\tStart position is: ");
			tracePrintPosition(traceStream,
								startSearchOperator,
								startKeyGetter);
			traceStream.println("");
			traceStream.println("\tStop position is: " );
			tracePrintPosition(traceStream,
								stopSearchOperator,
								stopKeyGetter);
			traceStream.println("");
			traceStream.println("\tQualifiers are: ");
			tracePrintQualifiers(traceStream, qualifiers, 2);
			traceStream.println("");
		}
	}
*/

	/**
	 * Print I/O statistics about a scan when it closes.
	 */
/*
	private final void traceClose()
	{
		if (SanityManager.DEBUG)
		{
			InfoStreams			infoStreams;
			HeaderPrintWriter	traceStream;

			traceStream = SanityManager.GET_DEBUG_STREAM();

			traceStream.println("TableScanResultSet number " +
								resultSetNumber +
								" closed.");
			if (isKeyed)
			{
				traceStream.println("\t" +
									rowCount() +
									" row(s) qualified from " +
									"keyed" +
									" table " +
									tableName +
									" using index " +
									indexName);
			}
			else
			{
				traceStream.println("\t" +
									rowCount() +
									" row(s) qualified from " +
									"non-keyed" +
									" table " +
									tableName);
			}
			traceStream.println("");
		}
	}
*/

	/**
	 * Print a start or stop positioner to the trace stream.
	 */
/*
	private final void tracePrintPosition(HeaderPrintWriter traceStream,
										  int searchOperator,
										  GeneratedMethod positionGetter)
	{
		if (SanityManager.DEBUG)
		{
			if (positionGetter == null)
			{
				traceStream.println("\t\tNone");
				return;
			}

			ExecIndexRow	positioner = null;

			try
			{
				positioner = (ExecIndexRow) positionGetter.invoke(activation);
			}
			catch (StandardException e)
			{
				traceStream.println("\t\tUnexpected exception " +
									e +
									" getting positioner.");
				e.printStackTrace(traceStream.getPrintWriter());
				return;
			}

			if (positioner == null)
			{
				traceStream.println("\t\tNone");
				return;
			}

			String searchOp = null;

			switch (searchOperator)
			{
			  case ScanController.GE:
				searchOp = "GE";
				break;

			  case ScanController.GT:
				searchOp = "GT";
				break;

			  default:
				searchOp = "unknown value (" + searchOperator + ")";
				break;
			}

			traceStream.println("\t\t" +
								searchOp +
								" on first " +
								positioner.nColumns() +
								" column(s).");

			traceStream.print(
					"\t\tOrdered null semantics on the following columns: ");
			for (int position = 0; position < positioner.nColumns(); position++)
			{
				if (positioner.areNullsOrdered(position))
				{
					traceStream.print(position + " ");
				}
			}
			traceStream.println("");
		}
	}
*/


	/**
	 * Print an array of Qualifiers to the trace stream.
	 */
/*
	private final void tracePrintQualifiers(HeaderPrintWriter traceStream,
											Qualifier[][] qualifiers,
											int depth)
	{
		if (SanityManager.DEBUG)
		{
			char[] indentchars = new char[depth];

			/*
			** Form an array of tab characters for indentation.
			*
			while (depth > 0)
			{
				indentchars[depth - 1] = '\t';
				depth--;
			}
			String indent = new String(indentchars);

			if (qualifiers == null)
			{
				traceStream.println(indent +
									MessageService.getTextMessage(
										SQLState.LANG_NONE)
									);
				return;
			}

            // RESOLVE (mikem) We don't support 2-d qualifiers yet.
            if (SanityManager.DEBUG)
            {
                SanityManager.ASSERT(qualifiers.length == 1);
            }

			for (int i = 0; i < qualifiers[0].length; i++)
			{
				Qualifier qual = qualifiers[0][i];

				traceStream.println("");
				traceStream.println(indent + "Column Id: " + qual.getColumnId());
				
				int operator = qual.getOperator();
				String opString = null;
				switch (operator)
				{
				  case Orderable.ORDER_OP_EQUALS:
					opString = "=";
					break;

				  case Orderable.ORDER_OP_LESSOREQUALS:
					opString = "<=";
					break;

				  case Orderable.ORDER_OP_LESSTHAN:
					opString = "<";
					break;

				  default:
					opString = "unknown value (" + operator + ")";
					break;
				}
				traceStream.println(indent + "Operator: " + opString);
				traceStream.println(indent + "Ordered nulls: " +
											qual.getOrderedNulls());
				traceStream.println(indent + "Unknown return value: " +
											qual.getUnknownRV());
				traceStream.println(indent + "Negate comparison result: " +
											qual.negateCompareResult());
				traceStream.println("");
			}
		}
	}
*/

	public String printStartPosition()
	{
		return printPosition(startSearchOperator, startKeyGetter, startPosition);
	}

	public String printStopPosition()
	{
		if (sameStartStopPosition)
		{
			return printPosition(stopSearchOperator, startKeyGetter, startPosition);
		}
		else
		{
			return printPosition(stopSearchOperator, stopKeyGetter, stopPosition);
		}
	}

	/**
	 * Return a start or stop positioner as a String.
	 *
	 * If we already generated the information, then use
	 * that.  Otherwise, invoke the activation to get it.
	 */
	private String printPosition(int searchOperator,
								 GeneratedMethod positionGetter,
								 ExecIndexRow positioner)
	{
		String output = "";
		if (positionGetter == null)
		{
			return "\t" +
					MessageService.getTextMessage(SQLState.LANG_NONE) +
					"\n";
		}
		
		if (positioner == null)
		{
			if (numOpens == 0)
				return "\t" + MessageService.getTextMessage(
					SQLState.LANG_POSITION_NOT_AVAIL) +
                                    "\n";
			try
			{
				positioner = (ExecIndexRow)positionGetter.invoke(activation);
			}
			catch (StandardException e)
			{
				return "\t" + MessageService.getTextMessage(
						SQLState.LANG_UNEXPECTED_EXC_GETTING_POSITIONER,
						e.toString());
			}
		}
		if (positioner == null)
		{
			return "\t" +
					MessageService.getTextMessage(SQLState.LANG_NONE) +
					"\n";
		}
		String searchOp = null;

		switch (searchOperator)
		{
			case ScanController.GE:
				searchOp = ">=";
				break;

			case ScanController.GT:
				searchOp = ">";
				break;

			default:
				if (SanityManager.DEBUG)
				{
					SanityManager.THROWASSERT("Unknown search operator " +
												searchOperator);
				}

				// NOTE: This does not have to be internationalized because
				// this code should never be reached.
				searchOp = "unknown value (" + searchOperator + ")";
				break;
		}

		output = output + "\t" +
						MessageService.getTextMessage(
							SQLState.LANG_POSITIONER,
							searchOp,
							String.valueOf(positioner.nColumns())) +
						"\n";

		output = output + "\t" +
					MessageService.getTextMessage(
						SQLState.LANG_ORDERED_NULL_SEMANTICS) +
					"\n";
		boolean colSeen = false;
		for (int position = 0; position < positioner.nColumns(); position++)
		{
			if (positioner.areNullsOrdered(position))
			{
				output = output + position + " ";
				colSeen = true;
			}

			if (colSeen && position == positioner.nColumns() - 1) {
				output = output +  "\n";
			}
		}

		return output;
	}

	public Properties getScanProperties()
	{
		if (scanProperties == null)
		{
			scanProperties = new Properties();
		}
		try
		{
			if (scanController != null)
			{
				scanController.getScanInfo().getAllScanInfo(scanProperties);
				/* Did we get a coarser lock due to
				 * a covering lock, lock escalation
				 * or configuration?
				 */
				coarserLock = scanController.isTableLocked() &&
					(lockMode == TransactionController.MODE_RECORD);
			}
		}
		catch(StandardException se)
		{
			// ignore
		}

		return scanProperties;
	}

	/**
	 * @see NoPutResultSet#requiresRelocking
	 */
	public boolean requiresRelocking()
	{
		return(
            isolationLevel == 
                TransactionController.ISOLATION_READ_COMMITTED_NOHOLDLOCK);
	}

	/**
	 * Update the number of rows in the scan controller.
	 *
	 * NOTE: It would be more efficient to only update the
	 * scan controller if the optimizer's estimated number of
	 * rows were wrong by more than some threshold (like 10%).
	 * This would require a little more work than I have the
	 * time for now, however, as the row estimate that is given
	 * to this result set is the total number of rows for all
	 * scans, not the number of rows per scan.
	 *
	 *
	 * @param rowsThisScan	The number of rows to update the scanController to
	 *
	 * @exception StandardException		Thrown on error
	 */
	protected final void setRowCountIfPossible(long rowsThisScan)
					throws StandardException
	{
		/*
		** Is it a heap scan with no qualifiers (full table scan?)
		** and is it not for update (we don't want to count rows we're
		** about to delete.
		*/
		if ( ( ! scanController.isKeyed() ) &&
			(qualifiers == null || qualifiers.length == 0) &&
			( ! forUpdate ) )
		{

			// Only update rows if different by more than 10%
			long diff = rowsThisScan - estimatedRowCount;

			long tenPerCent = estimatedRowCount  / 10;

			if (diff < 0)
				diff = -diff;

			if (diff > tenPerCent)
				scanController.setEstimatedRowCount(rowsThisScan);
		}
	}

	/**
	 * Can we get instantaneous locks when getting share row
	 * locks at READ COMMITTED.
	 */
	protected boolean canGetInstantaneousLocks()
	{
		return false;
	}


	/**
	 * Is this ResultSet or it's source result set for update
	 * 
	 * @return Whether or not the result set is for update.
	 */
	public boolean isForUpdate()
	{
		return forUpdate;
	}

	/**
	 * Shallow clone this result set.  Used in trigger reference.
	 * beetle 4373.
	 */
	public Object clone()
	{
		Object clo = null;
		try {
			clo = super.clone();
		}
		catch (CloneNotSupportedException e) {}
		return clo;
	}
}
