/* Copyright (c) 2001-2020, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;

import org.hsqldb.HsqlNameManager.HsqlName;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.jdbc.JDBCConnection;
import org.hsqldb.jdbc.JDBCDriver;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.CountUpDownLatch;
import org.hsqldb.lib.HashMap;
import org.hsqldb.lib.HsqlArrayList;
import org.hsqldb.lib.HsqlDeque;
import org.hsqldb.lib.Iterator;
import org.hsqldb.lib.OrderedHashSet;
import org.hsqldb.lib.SimpleLog;
import org.hsqldb.map.ValuePool;
import org.hsqldb.navigator.RowSetNavigator;
import org.hsqldb.navigator.RowSetNavigatorClient;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.persist.HsqlProperties;
import org.hsqldb.persist.PersistentStore;
import org.hsqldb.result.Result;
import org.hsqldb.result.ResultConstants;
import org.hsqldb.result.ResultLob;
import org.hsqldb.result.ResultProperties;
import org.hsqldb.rights.Grantee;
import org.hsqldb.rights.User;
import org.hsqldb.types.BlobDataID;
import org.hsqldb.types.ClobDataID;
import org.hsqldb.types.TimeData;
import org.hsqldb.types.TimestampData;
import org.hsqldb.types.Type;
import org.hsqldb.types.TypedComparator;

/**
 * Implementation of SQL sessions.
 *
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.5.1
 * @since 1.7.0
 */
public class Session implements SessionInterface {

    //
    private volatile boolean isClosed;

    //
    public Database    database;
    private final User sessionUser;
    private User       user;
    private Grantee    role;

    // transaction support
    public boolean          isReadOnlyDefault;
    int isolationLevelDefault = SessionInterface.TX_READ_COMMITTED;
    int isolationLevel        = SessionInterface.TX_READ_COMMITTED;
    boolean                 isReadOnlyIsolation;
    int                     actionIndex;
    long                    actionStartTimestamp;
    long                    actionTimestamp;
    long                    statementStartTimestamp;
    long                    transactionTimestamp;
    long                    transactionEndTimestamp;
    final boolean           txConflictRollback;
    final boolean           txInterruptRollback;
    boolean                 isPreTransaction;
    boolean                 isTransaction;
    boolean                 isBatch;
    volatile boolean        abortAction;
    volatile boolean        abortTransaction;
    volatile boolean        redoAction;
    HsqlArrayList           rowActionList;
    volatile boolean        tempUnlocked;
    public OrderedHashSet   waitedSessions;
    public OrderedHashSet   waitingSessions;
    OrderedHashSet          tempSet;
    OrderedHashSet          actionSet;
    public CountUpDownLatch latch = new CountUpDownLatch();
    TimeoutManager          timeoutManager;

    // current settings
    final String       zoneString;
    final int          sessionTimeZoneSeconds;
    int                timeZoneSeconds;
    boolean            isNetwork;
    private int        sessionMaxRows;
    int                sessionOptimization = 8;
    private final long sessionId;
    int                sessionTxId = -1;
    private boolean    ignoreCase;
    private long       sessionStartTimestamp;

    // internal connection
    private JDBCConnection intConnection;

    // external connection
    private JDBCConnection extConnection;

    // schema
    public HsqlName currentSchema;
    public HsqlName loggedSchema;

    // query processing
    ParserCommand         parser;
    boolean               isProcessingScript;
    boolean               isProcessingLog;
    public SessionContext sessionContext;
    int                   resultMaxMemoryRows;

    //
    public SessionData sessionData;

    //
    public StatementManager statementManager;

    //
    public Object special;

    /**
     * Constructs a new Session object.
     *
     * @param  db the database to which this represents a connection
     * @param  user the initial user
     * @param  autocommit the initial autocommit value
     * @param  readonly the initial readonly value
     * @param  id the session identifier, as known to the database
     */
    Session(Database db, User user, boolean autocommit, boolean readonly,
            long id, String zoneString, int timeZoneSeconds) {

        sessionId                   = id;
        database                    = db;
        this.user                   = user;
        this.sessionUser            = user;
        this.zoneString             = zoneString;
        this.sessionTimeZoneSeconds = timeZoneSeconds;
        this.timeZoneSeconds        = timeZoneSeconds;
        rowActionList               = new HsqlArrayList(32, true);
        waitedSessions              = new OrderedHashSet();
        waitingSessions             = new OrderedHashSet();
        tempSet                     = new OrderedHashSet();
        actionSet                   = new OrderedHashSet();
        isolationLevelDefault       = database.defaultIsolationLevel;
        ignoreCase                  = database.sqlIgnoreCase;
        isolationLevel              = isolationLevelDefault;
        txConflictRollback          = database.txConflictRollback;
        txInterruptRollback         = database.txInterruptRollback;
        isReadOnlyDefault           = readonly;
        isReadOnlyIsolation = isolationLevel
                              == SessionInterface.TX_READ_UNCOMMITTED;
        sessionContext              = new SessionContext(this);
        sessionContext.isAutoCommit = autocommit ? Boolean.TRUE
                                                 : Boolean.FALSE;
        sessionContext.isReadOnly   = isReadOnlyDefault ? Boolean.TRUE
                                                        : Boolean.FALSE;
        parser                      = new ParserCommand(this, new Scanner());

        setResultMemoryRowCount(database.getResultMaxMemoryRows());
        resetSchema();

        sessionData           = new SessionData(database, this);
        statementManager      = new StatementManager(database);
        timeoutManager        = new TimeoutManager();
        sessionStartTimestamp = System.currentTimeMillis();
    }

    void resetSchema() {
        loggedSchema  = null;
        currentSchema = user.getInitialOrDefaultSchema();
    }

    /**
     *  Retrieves the session identifier for this Session.
     *
     * @return the session identifier for this Session
     */
    public long getId() {
        return sessionId;
    }

    public int getRandomId() {
        return randomId;
    }

    /**
     * Closes this Session.
     */
    public synchronized void close() {

        if (isClosed) {
            return;
        }

        rollback(false);

        try {
            database.logger.writeOtherStatement(this, Tokens.T_DISCONNECT);
        } catch (HsqlException e) {}

        sessionData.closeAllNavigators();
        sessionData.persistentStoreCollection.release();
        statementManager.reset();

        // keep sessionContext and sessionData
        rowActionList.clear();

        isClosed                    = true;
        user                        = null;
        sessionContext.savepoints   = null;
        sessionContext.lastIdentity = null;
        intConnection               = null;

        database.sessionManager.removeSession(this);
        database.closeIfLast();

        database = null;
    }

    /**
     * Retrieves whether this Session is closed.
     *
     * @return true if this Session is closed
     */
    public boolean isClosed() {
        return isClosed;
    }

    public synchronized void setIsolationDefault(int level) {

        if (level == SessionInterface.TX_READ_UNCOMMITTED) {
            level = SessionInterface.TX_READ_COMMITTED;
        }

        if (level == isolationLevelDefault) {
            return;
        }

        isolationLevelDefault = level;

        if (!isInMidTransaction()) {
            isolationLevel = isolationLevelDefault;
            isReadOnlyIsolation = level
                                  == SessionInterface.TX_READ_UNCOMMITTED;
        }
    }

    /**
     * sets ISOLATION for the next transaction only
     */
    public void setIsolation(int level) {

        if (isInMidTransaction()) {
            throw Error.error(ErrorCode.X_25001);
        }

        if (level == SessionInterface.TX_READ_UNCOMMITTED) {
            level = SessionInterface.TX_READ_COMMITTED;
        }

        if (isolationLevel != level) {
            isolationLevel = level;
            isReadOnlyIsolation = level
                                  == SessionInterface.TX_READ_UNCOMMITTED;
        }
    }

    public synchronized int getIsolation() {
        return isolationLevel;
    }

    /**
     * Setter for iLastIdentity attribute.
     *
     * @param  i the new value
     */
    void setLastIdentity(Number i) {
        sessionContext.lastIdentity = i;
    }

    /**
     * Getter for iLastIdentity attribute.
     *
     * @return the current value
     */
    public Number getLastIdentity() {
        return sessionContext.lastIdentity;
    }

    /**
     * Retrieves the Database instance to which this
     * Session represents a connection.
     *
     * @return the Database object to which this Session is connected
     */
    public Database getDatabase() {
        return database;
    }

    /**
     * Retrieves the name, as known to the database, of the
     * user currently controlling this Session.
     *
     * @return the name of the user currently connected within this Session
     */
    public String getUsername() {
        return user.getName().getNameString();
    }

    /**
     * Retrieves the User object representing the user currently controlling
     * this Session.
     *
     * @return this Session's User object
     */
    public User getUser() {
        return user;
    }

    public Grantee getGrantee() {
        return user;
    }

    public Grantee getRole() {
        return role;
    }

    /**
     * Sets this Session's User object to the one specified by the
     * user argument.
     *
     * @param  user the new User object for this session
     */
    public void setUser(User user) {
        this.user = user;
    }

    public void setRole(Grantee role) {
        this.role = role;
    }

    int getMaxRows() {
        return sessionContext.currentMaxRows;
    }

    /**
     * The SQL command SET MAXROWS n will override the Statement.setMaxRows(n)
     * for the next direct statement only
     *
     * NB this is dedicated to the SET MAXROWS sql statement and should not
     * otherwise be called. (fredt@users)
     */
    void setSQLMaxRows(int rows) {
        sessionMaxRows = rows;
    }

    void setFeature(String feature, boolean value) {

        int number = 8;

        if (value) {
            sessionOptimization |= number;
        } else {
            sessionOptimization &= ~number;
        }
    }

    /**
     * Checks whether this Session's current User has the privileges of
     * the ADMIN role.
     */
    void checkAdmin() {
        user.checkAdmin();
    }

    /**
     * This is used for reading - writing to existing tables.
     * @throws  HsqlException
     */
    void checkReadWrite() {

        if (sessionContext.isReadOnly.booleanValue() || isReadOnlyIsolation) {
            throw Error.error(ErrorCode.X_25006);
        }
    }

    /**
     * This is used for creating new database objects such as tables.
     * @throws  HsqlException
     */
    void checkDDLWrite() {

        if (isProcessingScript || isProcessingLog) {
            return;
        }

        checkReadWrite();
    }

    public long getActionTimestamp() {
        return actionTimestamp;
    }

    /**
     *  Adds a delete action to the row and the transaction manager.
     *
     * @param  table the table of the row
     * @param  row the deleted row
     * @throws  HsqlException
     */
    public void addDeleteAction(Table table, PersistentStore store, Row row,
                                int[] changedColumns) {

//        tempActionHistory.add("add delete action " + actionTimestamp);
        if (abortTransaction) {
            throw Error.error(ErrorCode.X_40001);
        }

        if (abortAction) {
            throw Error.error(ErrorCode.X_40502);
        }

        getTransactionUTC();
        database.txManager.addDeleteAction(this, table, store, row,
                                           changedColumns);
    }

    void addInsertAction(Table table, PersistentStore store, Row row,
                         int[] changedColumns) {

        getTransactionUTC();

//        tempActionHistory.add("add insert to transaction " + actionTimestamp);
        database.txManager.addInsertAction(this, table, store, row,
                                           changedColumns);

        // abort only after adding so that the new row gets removed from indexes
        if (abortTransaction) {
            throw Error.error(ErrorCode.X_40001);
        }

        if (abortAction) {
            throw Error.error(ErrorCode.X_40502);
        }
    }

    public HsqlArrayList getRowActionList() {
        return rowActionList;
    }

    /**
     *  Setter for the autocommit attribute.
     *
     * @param  autocommit the new value
     * @throws  HsqlException
     */
    public synchronized void setAutoCommit(boolean autocommit) {

        if (isClosed) {
            return;
        }

        if (sessionContext.depth > 0) {
            return;
        }

        if (sessionContext.isAutoCommit.booleanValue() != autocommit) {
            commit(false);

            sessionContext.isAutoCommit = autocommit ? Boolean.TRUE
                                                     : Boolean.FALSE;
        }
    }

    public void beginAction(Statement cs) {

        actionIndex = rowActionList.size();

        database.txManager.beginAction(this, cs);
        database.txManager.beginActionResume(this);
    }

    public void endAction(Result result) {

//        tempActionHistory.add("endAction " + actionTimestamp);
        abortAction = false;

        timeoutManager.endTimeout();
        sessionData.persistentStoreCollection.clearStatementTables();

        if (result.mode == ResultConstants.ERROR) {
            sessionData.persistentStoreCollection.clearResultTables(
                actionTimestamp);
            database.txManager.rollbackAction(this);
        } else {
            sessionContext
                .diagnosticsVariables[ExpressionColumn.idx_row_count] =
                    result.mode == ResultConstants.UPDATECOUNT
                    ? Integer.valueOf(result.getUpdateCount())
                    : ValuePool.INTEGER_0;

            database.txManager.completeActions(this);
        }

//        tempActionHistory.add("endAction ends " + actionTimestamp);
    }

    /**
     * Explicit start of transaction by user
     */
    public void startTransaction() {
        database.txManager.beginTransaction(this);
    }

    public synchronized void startPhasedTransaction() {}

    /**
     * @todo - fredt - for two phased pre-commit - after this call, further
     * state changing calls should fail
     */
    public synchronized void prepareCommit() {

        if (isClosed) {
            throw Error.error(ErrorCode.X_08003);
        }

        if (!database.txManager.prepareCommitActions(this)) {

//            tempActionHistory.add("commit aborts " + actionTimestamp);
            rollbackNoCheck(false);

            throw Error.error(ErrorCode.X_40001);
        }
    }

    /**
     * Commits any uncommitted transaction this Session may have open
     *
     * @throws  HsqlException
     */
    public synchronized void commit(boolean chain) {

//        tempActionHistory.add("commit " + actionTimestamp);
        if (isClosed) {
            return;
        }

        if (sessionContext.depth > 0) {
            return;
        }

        if (isTransaction) {
            if (!database.txManager.commitTransaction(this)) {

                // tempActionHistory.add("commit aborts " + actionTimestamp);
                rollbackNoCheck(chain);

                throw Error.error(ErrorCode.X_40001);
            }
        } else {
            logSequences();
        }

        endTransaction(true, chain);

        if (database != null && !sessionUser.isSystem()
                && database.logger.needsCheckpointReset()) {
            database.checkpointRunner.start();
        }
    }

    /**
     * Rolls back any uncommitted transaction this Session may have open.
     *
     * @throws  HsqlException
     */
    public synchronized void rollback(boolean chain) {

        //        tempActionHistory.add("rollback " + actionTimestamp);
        if (sessionContext.depth > 0) {
            return;
        }

        rollbackNoCheck(chain);
    }

    synchronized void rollbackNoCheck(boolean chain) {

        if (isClosed) {
            return;
        }

        if (isTransaction) {
            database.txManager.rollback(this);
        }

        endTransaction(false, chain);
    }

    private void endTransaction(boolean commit, boolean chain) {

        abortAction       = false;
        abortTransaction  = false;
        transactionUTCSet = false;

        sessionContext.resetStack();
        sessionContext.savepoints.clear();
        sessionContext.savepointTimestamps.clear();
        rowActionList.clear();
        sessionData.persistentStoreCollection.clearTransactionTables();
        sessionData.closeAllTransactionNavigators();

        if (!chain) {
            sessionContext.isReadOnly = isReadOnlyDefault ? Boolean.TRUE
                                                          : Boolean.FALSE;

            setIsolation(isolationLevelDefault);
        }

        if (database.logger.getSqlEventLogLevel() > 0) {
            Statement endTX = commit ? StatementSession.commitNoChainStatement
                                     : StatementSession
                                         .rollbackNoChainStatement;

            database.logger.logStatementEvent(this, endTX, null,
                                              Result.updateZeroResult,
                                              SimpleLog.LOG_ERROR);
        }
/* debug 190
        tempActionHistory.add("commit ends " + actionTimestamp);
        tempActionHistory.clear();
//*/
    }

    /**
     * Clear structures and reset variables to original. For JDBC use only.
     * Note: sets autocommit true
     */
    public synchronized void resetSession() {

        if (isClosed) {
            return;
        }

        rollbackNoCheck(false);
        sessionData.closeAllNavigators();
        sessionData.persistentStoreCollection.clearAllTables();
        statementManager.reset();

        sessionContext.lastIdentity = ValuePool.INTEGER_0;
        sessionContext.isAutoCommit = Boolean.TRUE;

        setResultMemoryRowCount(database.getResultMaxMemoryRows());

        user = sessionUser;

        resetSchema();
        setZoneSeconds(sessionTimeZoneSeconds);

        sessionMaxRows = 0;
        ignoreCase     = database.sqlIgnoreCase;

        setIsolation(isolationLevelDefault);
    }

    /**
     *  Registers a transaction SAVEPOINT. A new SAVEPOINT with the
     *  name of an existing one replaces the old SAVEPOINT.
     *
     * @param  name name of the savepoint
     * @throws  HsqlException if there is no current transaction
     */
    public synchronized void savepoint(String name) {

        int index = sessionContext.savepoints.getIndex(name);

        if (index != -1) {
            sessionContext.savepoints.remove(name);
            sessionContext.savepointTimestamps.remove(index);
        }

        actionTimestamp = database.txManager.getNextGlobalChangeTimestamp();

        sessionContext.savepoints.add(name,
                                      ValuePool.getInt(rowActionList.size()));
        sessionContext.savepointTimestamps.addLast(actionTimestamp);
    }

    /**
     *  Performs a partial transaction ROLLBACK to savepoint.
     *
     * @param  name name of savepoint
     * @throws  HsqlException
     */
    public synchronized void rollbackToSavepoint(String name) {

        if (isClosed) {
            return;
        }

        int index = sessionContext.savepoints.getIndex(name);

        if (index < 0) {
            throw Error.error(ErrorCode.X_3B001, name);
        }

        database.txManager.rollbackSavepoint(this, index);
    }

    /**
     * Performs a partial transaction ROLLBACK of current savepoint level.
     *
     * @throws  HsqlException
     */
    public synchronized void rollbackToSavepoint() {

        if (isClosed) {
            return;
        }

        database.txManager.rollbackSavepoint(this, 0);
    }

    public synchronized void rollbackAction(int start, long timestamp) {

        if (isClosed) {
            return;
        }

        database.txManager.rollbackPartial(this, start, timestamp);
    }

    /**
     * Releases a savepoint
     *
     * @param  name name of savepoint
     * @throws  HsqlException if name does not correspond to a savepoint
     */
    public synchronized void releaseSavepoint(String name) {

        // remove this and all later savepoints
        int index = sessionContext.savepoints.getIndex(name);

        if (index < 0) {
            throw Error.error(ErrorCode.X_3B001, name);
        }

        while (sessionContext.savepoints.size() > index) {
            sessionContext.savepoints.remove(sessionContext.savepoints.size()
                                             - 1);
            sessionContext.savepointTimestamps.removeLast();
        }
    }

    public boolean isInMidTransaction() {
        return isTransaction;
    }

    public void setNoSQL() {
        sessionContext.noSQL = Boolean.TRUE;
    }

    public void setIgnoreCase(boolean mode) {
        ignoreCase = mode;
    }

    public boolean isIgnorecase() {
        return ignoreCase;
    }

    /**
     * sets READ ONLY for next transaction / subtransaction only
     *
     * @param  readonly the new value
     */
    public void setReadOnly(boolean readonly) {

        if (!readonly && database.databaseReadOnly) {
            throw Error.error(ErrorCode.DATABASE_IS_READONLY);
        }

        if (isInMidTransaction()) {
            throw Error.error(ErrorCode.X_25001);
        }

        sessionContext.isReadOnly = readonly ? Boolean.TRUE
                                             : Boolean.FALSE;
    }

    public synchronized void setReadOnlyDefault(boolean readonly) {

        if (!readonly && database.databaseReadOnly) {
            throw Error.error(ErrorCode.DATABASE_IS_READONLY);
        }

        isReadOnlyDefault = readonly;

        if (!isInMidTransaction()) {
            sessionContext.isReadOnly = isReadOnlyDefault ? Boolean.TRUE
                                                          : Boolean.FALSE;
        }
    }

    /**
     *  Getter for readonly attribute.
     *
     * @return the current value
     */
    public boolean isReadOnly() {
        return sessionContext.isReadOnly.booleanValue() || isReadOnlyIsolation;
    }

    public synchronized boolean isReadOnlyDefault() {
        return isReadOnlyDefault;
    }

    /**
     *  Getter for autoCommit attribute.
     *
     * @return the current value
     */
    public synchronized boolean isAutoCommit() {
        return sessionContext.isAutoCommit.booleanValue();
    }

    public synchronized int getStreamBlockSize() {
        return lobStreamBlockSize;
    }

    /**
     * Retrieves an internal Connection object equivalent to the one
     * that created this Session.
     *
     * @return  internal connection.
     */
    JDBCConnection getInternalConnection() {

        if (intConnection == null) {
            intConnection = new JDBCConnection(this);
        }

        JDBCDriver.driverInstance.threadConnection.set(intConnection);

        return intConnection;
    }

    void releaseInternalConnection() {

        if (sessionContext.depth == 0) {
            JDBCDriver.driverInstance.threadConnection.set(null);
        }
    }

    /**
     * Retrieves the external JDBC connection
     */
    public JDBCConnection getJDBCConnection() {
        return extConnection;
    }

    public void setJDBCConnection(JDBCConnection connection) {
        extConnection = connection;
    }

    public String getDatabaseUniqueName() {
        return database.getNameString();
    }

// campbell-burnet@users 20020810 metadata 1.7.2
//----------------------------------------------------------------
    private final long connectTime = System.currentTimeMillis();

// more efficient for MetaData concerns than checkAdmin

    /**
     * Getter for admin attribute.
     *
     * @return the current value
     */
    public boolean isAdmin() {
        return user.isAdmin();
    }

    /**
     * Getter for connectTime attribute.
     *
     * @return the value
     */
    public long getConnectTime() {
        return connectTime;
    }

    /**
     * Count of actions in current transaction.
     *
     * @return the current value
     */
    public int getTransactionSize() {
        return rowActionList.size();
    }

    public long getTransactionTimestamp() {
        return transactionTimestamp;
    }

    public Statement compileStatement(String sql, int props) {

        parser.reset(this, sql);

        Statement cs = parser.compileStatement(props);

        return cs;
    }

    public Statement compileStatement(String sql) {

        parser.reset(this, sql);

        Statement cs =
            parser.compileStatement(ResultProperties.defaultPropsValue);

        cs.setCompileTimestamp(Long.MAX_VALUE);

        return cs;
    }

    /**
     * Executes the command encapsulated by the cmd argument.
     *
     * @param cmd the command to execute
     * @return the result of executing the command
     */
    public synchronized Result execute(Result cmd) {

        if (isClosed) {
            return Result.newErrorResult(Error.error(ErrorCode.X_08503));
        }

        sessionContext.currentMaxRows = 0;
        isBatch                       = false;

        switch (cmd.mode) {

            case ResultConstants.LARGE_OBJECT_OP : {
                return performLOBOperation((ResultLob) cmd);
            }
            case ResultConstants.EXECUTE : {
                int maxRows = cmd.getUpdateCount();

                if (maxRows == -1) {
                    sessionContext.currentMaxRows = 0;
                } else {
                    sessionContext.currentMaxRows = maxRows;
                }

                Statement cs = cmd.statement;

                if (cs == null
                        || cs.compileTimestamp
                           < database.schemaManager.schemaChangeTimestamp) {
                    long csid = cmd.getStatementID();

                    cs = statementManager.getStatement(this, csid);

                    cmd.setStatement(cs);

                    if (cs == null) {

                        // invalid sql has been removed already
                        return Result.newErrorResult(
                            Error.error(ErrorCode.X_07502));
                    }
                }

                Object[] pvals = (Object[]) cmd.valueData;
                Result result = executeCompiledStatement(cs, pvals,
                    cmd.queryTimeout);

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.BATCHEXECUTE : {
                isBatch = true;

                Result result = executeCompiledBatchStatement(cmd);

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.EXECDIRECT : {
                Result result = executeDirectStatement(cmd);

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.BATCHEXECDIRECT : {
                isBatch = true;

                Result result = executeDirectBatchStatement(cmd);

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.PREPARE : {
                Statement cs;

                try {
                    cs = statementManager.compile(this, cmd);
                } catch (Throwable t) {
                    String errorString = cmd.getMainString();

                    return Result.newErrorResult(t, errorString);
                }

                Result result = Result.newPrepareResponse(cs);

                if (cs.getType() == StatementTypes.SELECT_CURSOR
                        || cs.getType() == StatementTypes.CALL) {
                    sessionData.setResultSetProperties(cmd, result);
                }

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.CLOSE_RESULT : {
                closeNavigator(cmd.getResultId());

                return Result.updateZeroResult;
            }
            case ResultConstants.UPDATE_RESULT : {
                Result result = this.executeResultUpdate(cmd);

                result = performPostExecute(cmd, result);

                return result;
            }
            case ResultConstants.FREESTMT : {
                statementManager.freeStatement(cmd.getStatementID());

                return Result.updateZeroResult;
            }
            case ResultConstants.GETSESSIONATTR : {
                int id = cmd.getStatementType();

                return getAttributesResult(id);
            }
            case ResultConstants.SETSESSIONATTR : {
                return setAttributes(cmd);
            }
            case ResultConstants.ENDTRAN : {
                switch (cmd.getActionType()) {

                    case ResultConstants.TX_COMMIT :
                        try {
                            commit(false);
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }
                        break;

                    case ResultConstants.TX_COMMIT_AND_CHAIN :
                        try {
                            commit(true);
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }
                        break;

                    case ResultConstants.TX_ROLLBACK :
                        rollback(false);
                        break;

                    case ResultConstants.TX_ROLLBACK_AND_CHAIN :
                        rollback(true);
                        break;

                    case ResultConstants.TX_SAVEPOINT_NAME_RELEASE :
                        try {
                            String name = cmd.getMainString();

                            releaseSavepoint(name);
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }
                        break;

                    case ResultConstants.TX_SAVEPOINT_NAME_ROLLBACK :
                        try {
                            rollbackToSavepoint(cmd.getMainString());
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }
                        break;

                    case ResultConstants.PREPARECOMMIT :
                        try {
                            prepareCommit();
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }
                        break;
                }

                return Result.updateZeroResult;
            }
            case ResultConstants.SETCONNECTATTR : {
                switch (cmd.getConnectionAttrType()) {

                    case ResultConstants.SQL_ATTR_SAVEPOINT_NAME :
                        try {
                            savepoint(cmd.getMainString());
                        } catch (Throwable t) {
                            return Result.newErrorResult(t);
                        }

                    // case ResultConstants.SQL_ATTR_AUTO_IPD
                    //   - always true
                    // default: throw - case never happens
                }

                return Result.updateZeroResult;
            }
            case ResultConstants.REQUESTDATA : {
                return sessionData.getDataResultSlice(cmd.getResultId(),
                                                      cmd.getUpdateCount(),
                                                      cmd.getFetchSize());
            }
            case ResultConstants.DISCONNECT : {
                close();

                return Result.updateZeroResult;
            }
            default : {
                return Result.newErrorResult(
                    Error.runtimeError(ErrorCode.U_S0500, "Session"));
            }
        }
    }

    private Result performPostExecute(Result command, Result result) {

        if (result.mode == ResultConstants.DATA) {
            result = sessionData.getDataResultHead(command, result, isNetwork);
        }

/*
        else if (result.mode == ResultConstants.ERROR) {
            while (sessionContext.depth > 0) {
                sessionContext.pop();
            }
        }
*/
        if (sqlWarnings != null && sqlWarnings.size() > 0) {
            if (result.mode == ResultConstants.UPDATECOUNT) {
                result = new Result(ResultConstants.UPDATECOUNT,
                                    result.getUpdateCount());
            }

            HsqlException[] warnings = getAndClearWarnings();

            result.addWarnings(warnings);
        }

        return result;
    }

    public RowSetNavigatorClient getRows(long navigatorId, int offset,
                                         int blockSize) {
        return sessionData.getRowSetSlice(navigatorId, offset, blockSize);
    }

    public synchronized void closeNavigator(long id) {
        sessionData.closeNavigator(id);
    }

    public Result executeDirectStatement(Result cmd) {

        String        sql = cmd.getMainString();
        HsqlArrayList list;
        int           maxRows = cmd.getUpdateCount();

        if (maxRows == -1) {
            sessionContext.currentMaxRows = 0;
        } else if (sessionMaxRows == 0) {
            sessionContext.currentMaxRows = maxRows;
        } else {
            sessionContext.currentMaxRows = sessionMaxRows;
            sessionMaxRows                = 0;
        }

        try {
            list = parser.compileStatements(sql, cmd);
        } catch (Throwable e) {
            return Result.newErrorResult(e);
        }

        Result   result         = null;
        boolean  recompile      = false;
        HsqlName originalSchema = getCurrentSchemaHsqlName();

        for (int i = 0; i < list.size(); i++) {
            Statement cs = (Statement) list.get(i);

            if (i > 0) {
                if (cs.getCompileTimestamp()
                        > database.txManager.getGlobalChangeTimestamp()) {
                    recompile = true;
                }

                if (cs.getSchemaName() != null
                        && cs.getSchemaName() != originalSchema) {
                    recompile = true;
                }
            }

            if (recompile) {
                cs = compileStatement(cs.getSQL(), cmd.getExecuteProperties());
            }

            cs.setGeneratedColumnInfo(cmd.getGeneratedResultType(),
                                      cmd.getGeneratedResultMetaData());

            result = executeCompiledStatement(cs, ValuePool.emptyObjectArray,
                                              cmd.queryTimeout);

            if (result.mode == ResultConstants.ERROR) {
                break;
            }
        }

        return result;
    }

    public Result executeDirectStatement(String sql) {

        try {
            Statement cs = compileStatement(sql);
            Result result = executeCompiledStatement(cs,
                ValuePool.emptyObjectArray, 0);

            return result;
        } catch (HsqlException e) {
            return Result.newErrorResult(e);
        }
    }

    public Result executeCompiledStatement(Statement cs, Object[] pvals,
                                           int timeout) {

        Result r;

        if (abortTransaction) {
            return handleAbortTransaction();
        }

        if (sessionContext.depth > 0) {
            if (sessionContext.noSQL.booleanValue()
                    || cs.isAutoCommitStatement()) {
                return Result.newErrorResult(Error.error(ErrorCode.X_46000));
            }
        }

        if (cs.isAutoCommitStatement()) {
            if (isReadOnly()) {
                return Result.newErrorResult(Error.error(ErrorCode.X_25006));
            }

            try {

                /** special autocommit for backward compatibility */
                commit(false);
            } catch (HsqlException e) {
                database.logger.logInfoEvent("Exception at commit");
            }
        }

        sessionContext.currentStatement = cs;
        statementStartTimestamp =
            database.txManager.getGlobalChangeTimestamp();

        boolean isTX = cs.isTransactionStatement();

        if (!isTX) {
            actionTimestamp =
                database.txManager.getNextGlobalChangeTimestamp();

            sessionContext.setDynamicArguments(pvals);

            // statements such as DISCONNECT may close the session
            if (database.logger.getSqlEventLogLevel()
                    >= SimpleLog.LOG_NORMAL) {
                database.logger.logStatementEvent(this, cs, pvals,
                                                  Result.updateZeroResult,
                                                  SimpleLog.LOG_NORMAL);
            }

            r                               = cs.execute(this);
            sessionContext.currentStatement = null;
            abortAction                     = false;

            sessionData.persistentStoreCollection.clearStatementTables();

            return r;
        }

        timeoutManager.startTimeout(timeout);

        repeatLoop:
        while (true) {
            actionIndex = rowActionList.size();

            database.txManager.beginAction(this, cs);

            if (redoAction) {
                redoAction = false;

                continue;
            }

            cs = sessionContext.currentStatement;

            if (cs == null) {
                return Result.newErrorResult(Error.error(ErrorCode.X_07502));
            }

            if (abortTransaction) {
                return handleAbortTransaction();
            }

            boolean interrupted = false;

            while (true) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    interrupted = txInterruptRollback;

                    Thread.interrupted();

                    continue;
                }

                break;
            }

            if (abortAction) {
                r = Result.newErrorResult(Error.error(ErrorCode.X_40502));

                endAction(r);

                break repeatLoop;
            }

            if (abortTransaction || interrupted) {
                Result result = handleAbortTransaction();

                if (interrupted) {
                    Thread.currentThread().interrupt();
                }

                return result;
            }

            database.txManager.beginActionResume(this);

            //        tempActionHistory.add("sql execute " + cs.sql + " " + actionTimestamp + " " + rowActionList.size());
            sessionContext.setDynamicArguments(pvals);

            r = cs.execute(this);

            if (database.logger.getSqlEventLogLevel()
                    >= SimpleLog.LOG_NORMAL) {
                database.logger.logStatementEvent(this, cs, pvals, r,
                                                  SimpleLog.LOG_NORMAL);
            }

            //        tempActionHistory.add("sql execute end " + actionTimestamp + " " + rowActionList.size());
            endAction(r);

            if (abortTransaction) {
                break repeatLoop;
            }

            if (redoAction) {
                redoAction = false;

                while (true) {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        Thread.interrupted();

                        continue;
                    }

                    break;
                }
            } else {
                break repeatLoop;
            }
        }

        if (abortTransaction) {
            return handleAbortTransaction();
        }

        if (sessionContext.depth == 0
                && (sessionContext.isAutoCommit.booleanValue()
                    || cs.isAutoCommitStatement())) {
            try {
                if (r.mode == ResultConstants.ERROR) {
                    rollbackNoCheck(false);
                } else {
                    commit(false);
                }
            } catch (Exception e) {
                sessionContext.currentStatement = null;

                return Result.newErrorResult(Error.error(ErrorCode.X_40001,
                        e));
            }
        }

        sessionContext.currentStatement = null;

        return r;
    }

    private Result handleAbortTransaction() {

        rollbackNoCheck(false);

        sessionContext.currentStatement = null;

        return Result.newErrorResult(Error.error(ErrorCode.X_40001));
    }

    private Result executeCompiledBatchStatement(Result cmd) {

        long      csid;
        Statement cs;
        int[]     updateCounts;
        int       count;

        cs = cmd.statement;

        if (cs == null
                || cs.compileTimestamp
                   < database.schemaManager.schemaChangeTimestamp) {
            csid = cmd.getStatementID();
            cs   = statementManager.getStatement(this, csid);

            if (cs == null) {

                // invalid sql has been removed already
                return Result.newErrorResult(Error.error(ErrorCode.X_07502));
            }
        }

        count = 0;

        RowSetNavigator nav = cmd.initialiseNavigator();

        updateCounts = new int[nav.getSize()];

        Result generatedResult = null;

        if (cs.hasGeneratedColumns()) {
            generatedResult =
                Result.newGeneratedDataResult(cs.generatedResultMetaData());
        }

        Result error = null;

        while (nav.next()) {
            Object[] pvals = nav.getCurrent();
            Result in = executeCompiledStatement(cs, pvals, cmd.queryTimeout);

            // On the client side, iterate over the vals and throw
            // a BatchUpdateException if a batch status value of
            // esultConstants.EXECUTE_FAILED is encountered in the result
            if (in.isUpdateCount()) {
                if (cs.hasGeneratedColumns()) {
                    RowSetNavigator navgen =
                        in.getChainedResult().getNavigator();

                    while (navgen.next()) {
                        Object[] generatedRow = navgen.getCurrent();

                        generatedResult.getNavigator().add(generatedRow);
                    }
                }

                updateCounts[count++] = in.getUpdateCount();
            } else if (in.isData()) {

                // FIXME:  we don't have what it takes yet
                // to differentiate between things like
                // stored procedure calls to methods with
                // void return type and select statements with
                // a single row/column containing null
                updateCounts[count++] = ResultConstants.SUCCESS_NO_INFO;
            } else if (in.mode == ResultConstants.CALL_RESPONSE) {
                updateCounts[count++] = ResultConstants.SUCCESS_NO_INFO;
            } else if (in.mode == ResultConstants.ERROR) {
                updateCounts = ArrayUtil.arraySlice(updateCounts, 0, count);
                error        = in;

                break;
            } else {
                throw Error.runtimeError(ErrorCode.U_S0500, "Session");
            }
        }

        return Result.newBatchedExecuteResponse(updateCounts, generatedResult,
                error);
    }

    private Result executeDirectBatchStatement(Result cmd) {

        int[] updateCounts;
        int   count;

        count = 0;

        RowSetNavigator nav = cmd.initialiseNavigator();

        updateCounts = new int[nav.getSize()];

        Result error = null;

        while (nav.next()) {
            Result   in;
            Object[] data = nav.getCurrent();
            String   sql  = (String) data[0];

            try {
                Statement cs = compileStatement(sql);

                in = executeCompiledStatement(cs, ValuePool.emptyObjectArray,
                                              cmd.queryTimeout);
            } catch (Throwable t) {
                in = Result.newErrorResult(t);

                // if (t instanceof OutOfMemoryError) {
                // System.gc();
                // }
                // "in" already equals "err"
                // maybe test for OOME and do a gc() ?
                // t.printStackTrace();
            }

            if (in.isUpdateCount()) {
                updateCounts[count++] = in.getUpdateCount();
            } else if (in.isData()) {

                // FIXME:  we don't have what it takes yet
                // to differentiate between things like
                // stored procedure calls to methods with
                // void return type and select statements with
                // a single row/column containing null
                updateCounts[count++] = ResultConstants.SUCCESS_NO_INFO;
            } else if (in.mode == ResultConstants.CALL_RESPONSE) {
                updateCounts[count++] = ResultConstants.SUCCESS_NO_INFO;
            } else if (in.mode == ResultConstants.ERROR) {
                updateCounts = ArrayUtil.arraySlice(updateCounts, 0, count);
                error        = in;

                break;
            } else {
                throw Error.runtimeError(ErrorCode.U_S0500, "Session");
            }
        }

        return Result.newBatchedExecuteResponse(updateCounts, null, error);
    }

    /**
     * Retrieves the result of inserting, updating or deleting a row
     * from an updatable result.
     *
     * @return the result of executing the statement
     */
    private Result executeResultUpdate(Result cmd) {

        long   id         = cmd.getResultId();
        int    actionType = cmd.getActionType();
        Result result     = sessionData.getDataResult(id);

        if (result == null) {
            return Result.newErrorResult(Error.error(ErrorCode.X_24501));
        }

        Object[]       pvals     = (Object[]) cmd.valueData;
        Type[]         types     = cmd.metaData.columnTypes;
        StatementQuery statement = (StatementQuery) result.getStatement();

        sessionContext.rowUpdateStatement.setRowActionProperties(result,
                actionType, statement, types);

        Result resultOut =
            executeCompiledStatement(sessionContext.rowUpdateStatement, pvals,
                                     cmd.queryTimeout);

        return resultOut;
    }

// session DATETIME functions
    long                  currentTimestampSCN;
    long                  currentMillis;
    private TimestampData currentDate;
    private TimestampData currentTimestamp;
    private TimestampData localTimestamp;
    private TimestampData transactionUTC;
    boolean               transactionUTCSet;
    private TimeData      currentTime;
    private TimeData      localTime;

    /**
     * Returns the current date, unchanged for the duration of the current
     * execution unit (statement).<p>
     *
     * SQL standards require that CURRENT_DATE, CURRENT_TIME and
     * CURRENT_TIMESTAMP are all evaluated at the same point of
     * time in the duration of each SQL statement, no matter how long the
     * SQL statement takes to complete.<p>
     *
     * When this method or a corresponding method for CURRENT_TIME or
     * CURRENT_TIMESTAMP is first called in the scope of a system change
     * number, currentMillis is set to the current system time. All further
     * CURRENT_XXXX calls in this scope will use this millisecond value.
     * (fredt@users)
     */
    public synchronized TimestampData getCurrentDate() {

        resetCurrentTimestamp();

        if (currentDate == null) {
            currentDate = (TimestampData) Type.SQL_DATE.getValue(this,
                    currentMillis / 1000, 0, getZoneSeconds());
        }

        return currentDate;
    }

    /**
     * Returns the current time, unchanged for the duration of the current
     * execution unit (statement)
     */
    synchronized TimeData getCurrentTime(boolean withZone) {

        resetCurrentTimestamp();

        if (withZone) {
            if (currentTime == null) {
                int seconds =
                    (int) (HsqlDateTime.getNormalisedTime(
                        getCalendarGMT(), currentMillis)) / 1000;
                int nanos = (int) (currentMillis % 1000) * 1000000;

                currentTime = new TimeData(seconds, nanos, getZoneSeconds());
            }

            return currentTime;
        } else {
            if (localTime == null) {
                int seconds =
                    (int) (HsqlDateTime.getNormalisedTime(
                        getCalendarGMT(),
                        currentMillis + getZoneSeconds() * 1000L)) / 1000;
                int nanos = (int) (currentMillis % 1000) * 1000000;

                localTime = new TimeData(seconds, nanos, 0);
            }

            return localTime;
        }
    }

    /**
     * Returns the current timestamp, unchanged for the duration of the current
     * execution unit (statement)
     */
    synchronized TimestampData getCurrentTimestamp(boolean withZone) {

        resetCurrentTimestamp();

        if (withZone) {
            if (currentTimestamp == null) {
                int nanos = (int) (currentMillis % 1000) * 1000000;

                currentTimestamp = new TimestampData((currentMillis / 1000),
                                                     nanos, getZoneSeconds());
            }

            return currentTimestamp;
        } else {
            if (localTimestamp == null) {
                int nanos = (int) (currentMillis % 1000) * 1000000;

                localTimestamp = new TimestampData(currentMillis / 1000
                                                   + getZoneSeconds(), nanos,
                                                       0);
            }

            return localTimestamp;
        }
    }

    static TimestampData getSystemTimestamp(boolean withZone, boolean utc) {

        long millis  = System.currentTimeMillis();
        long seconds = millis / 1000;
        int  nanos   = (int) (millis % 1000) * 1000000;
        int  offset  = 0;

        if (!utc) {
            TimeZone zone = TimeZone.getDefault();

            offset = zone.getOffset(millis) / 1000;

            if (!withZone) {
                seconds += offset;
                offset  = 0;
            }
        }

        return new TimestampData(seconds, nanos, offset);
    }

    TimestampData getTransactionUTC() {

        if (!transactionUTCSet) {
            transactionUTC    = getSystemTimestamp(true, true);
            transactionUTCSet = true;
        }

        return transactionUTC;
    }

    private void resetCurrentTimestamp() {

        if (currentTimestampSCN != actionTimestamp) {
            currentTimestampSCN = actionTimestamp;
            currentMillis       = System.currentTimeMillis();
            currentDate         = null;
            currentTimestamp    = null;
            localTimestamp      = null;
            currentTime         = null;
            localTime           = null;
        }
    }

    private Result getAttributesResult(int id) {

        Result   r    = Result.newSessionAttributesResult();
        Object[] data = r.getSingleRowData();

        data[SessionInterface.INFO_ID] = ValuePool.getInt(id);

        switch (id) {

            case SessionInterface.INFO_ISOLATION :
                data[SessionInterface.INFO_INTEGER] =
                    ValuePool.getInt(isolationLevel);
                break;

            case SessionInterface.INFO_AUTOCOMMIT :
                data[SessionInterface.INFO_BOOLEAN] =
                    sessionContext.isAutoCommit;
                break;

            case SessionInterface.INFO_CONNECTION_READONLY :
                data[SessionInterface.INFO_BOOLEAN] =
                    sessionContext.isReadOnly;
                break;

            case SessionInterface.INFO_CATALOG :
                data[SessionInterface.INFO_VARCHAR] =
                    database.getCatalogName().name;
                break;
        }

        return r;
    }

    private Result setAttributes(Result r) {

        Object[] row = r.getSessionAttributes();
        int      id  = ((Integer) row[SessionInterface.INFO_ID]).intValue();

        try {
            switch (id) {

                case SessionInterface.INFO_AUTOCOMMIT : {
                    boolean value =
                        ((Boolean) row[SessionInterface.INFO_BOOLEAN])
                            .booleanValue();

                    this.setAutoCommit(value);

                    break;
                }
                case SessionInterface.INFO_CONNECTION_READONLY : {
                    boolean value =
                        ((Boolean) row[SessionInterface.INFO_BOOLEAN])
                            .booleanValue();

                    this.setReadOnlyDefault(value);

                    break;
                }
                case SessionInterface.INFO_ISOLATION : {
                    int value =
                        ((Integer) row[SessionInterface.INFO_INTEGER])
                            .intValue();

                    this.setIsolationDefault(value);

                    break;
                }
                case SessionInterface.INFO_CATALOG : {
                    String value =
                        ((String) row[SessionInterface.INFO_VARCHAR]);

                    this.setCatalog(value);
                }
            }
        } catch (HsqlException e) {
            return Result.newErrorResult(e);
        }

        return Result.updateZeroResult;
    }

    public synchronized Object getAttribute(int id) {

        switch (id) {

            case SessionInterface.INFO_ISOLATION :
                return ValuePool.getInt(isolationLevel);

            case SessionInterface.INFO_AUTOCOMMIT :
                return sessionContext.isAutoCommit;

            case SessionInterface.INFO_CONNECTION_READONLY :
                return isReadOnlyDefault ? Boolean.TRUE
                                         : Boolean.FALSE;

            case SessionInterface.INFO_CATALOG :
                return database.getCatalogName().name;
        }

        return null;
    }

    public synchronized void setAttribute(int id, Object object) {

        switch (id) {

            case SessionInterface.INFO_AUTOCOMMIT : {
                boolean value = ((Boolean) object).booleanValue();

                this.setAutoCommit(value);

                break;
            }
            case SessionInterface.INFO_CONNECTION_READONLY : {
                boolean value = ((Boolean) object).booleanValue();

                this.setReadOnlyDefault(value);

                break;
            }
            case SessionInterface.INFO_ISOLATION : {
                int value = ((Integer) object).intValue();

                this.setIsolationDefault(value);

                break;
            }
            case SessionInterface.INFO_CATALOG : {
                String value = ((String) object);

                this.setCatalog(value);
            }
        }
    }

    // lobs
    public BlobDataID createBlob(long length) {

        long lobID = database.lobManager.createBlob(this, length);

        if (lobID == 0) {
            throw Error.error(ErrorCode.X_0F502);
        }

        return new BlobDataID(lobID);
    }

    public ClobDataID createClob(long length) {

        long lobID = database.lobManager.createClob(this, length);

        if (lobID == 0) {
            throw Error.error(ErrorCode.X_0F502);
        }

        return new ClobDataID(lobID);
    }

    public void registerResultLobs(Result result) {
        sessionData.registerLobForResult(result);
    }

    public Result allocateResultLob(ResultLob result) {
        return sessionData.allocateLobForResult(result);
    }

    Result performLOBOperation(ResultLob cmd) {

        long id        = cmd.getLobID();
        int  operation = cmd.getSubType();

        switch (operation) {

            case ResultLob.LobResultTypes.REQUEST_GET_LOB : {
                return database.lobManager.getLob(id, cmd.getOffset(),
                                                  cmd.getBlockLength());
            }
            case ResultLob.LobResultTypes.REQUEST_GET_LENGTH : {
                return database.lobManager.getLength(id);
            }
            case ResultLob.LobResultTypes.REQUEST_GET_BYTES : {
                return database.lobManager.getBytes(
                    id, cmd.getOffset(), (int) cmd.getBlockLength());
            }
            case ResultLob.LobResultTypes.REQUEST_SET_BYTES : {
                return database.lobManager.setBytes(
                    id, cmd.getOffset(), cmd.getByteArray(),
                    (int) cmd.getBlockLength());
            }
            case ResultLob.LobResultTypes.REQUEST_GET_CHARS : {
                return database.lobManager.getChars(
                    id, cmd.getOffset(), (int) cmd.getBlockLength());
            }
            case ResultLob.LobResultTypes.REQUEST_SET_CHARS : {
                return database.lobManager.setChars(
                    id, cmd.getOffset(), cmd.getCharArray(),
                    (int) cmd.getBlockLength());
            }
            case ResultLob.LobResultTypes.REQUEST_TRUNCATE : {
                return database.lobManager.truncate(id, cmd.getOffset());
            }
            case ResultLob.LobResultTypes.REQUEST_DUPLICATE_LOB : {
                return database.lobManager.createDuplicateLob(id);
            }
            case ResultLob.LobResultTypes.REQUEST_CREATE_BYTES :
            case ResultLob.LobResultTypes.REQUEST_CREATE_CHARS :
            case ResultLob.LobResultTypes.REQUEST_GET_BYTE_PATTERN_POSITION :
            case ResultLob.LobResultTypes.REQUEST_GET_CHAR_PATTERN_POSITION : {
                throw Error.error(ErrorCode.X_0A501);
            }
            default : {
                throw Error.runtimeError(ErrorCode.U_S0500, "Session");
            }
        }
    }

    // DatabaseMetaData.getURL should work as specified for
    // internal connections too.
    public String getInternalConnectionURL() {
        return DatabaseURL.S_URL_PREFIX + database.getURI();
    }

    public Result cancel(Result result) {

        if (result.getType() == ResultConstants.SQLCANCEL) {
            if (result.getSessionRandomID() == randomId) {
                database.txManager.resetSession(
                    null, this, Long.MAX_VALUE,
                    TransactionManager.resetSessionStatement);
            }
        }

        return Result.updateZeroResult;
    }

    public boolean isProcessingScript() {
        return isProcessingScript;
    }

    public boolean isProcessingLog() {
        return isProcessingLog;
    }

    // schema object methods
    public void setSchema(String schema) {
        currentSchema = database.schemaManager.getSchemaHsqlName(schema);
    }

    public void setCatalog(String catalog) {

        if (database.getCatalogName().name.equals(catalog)) {
            return;
        }

        throw Error.error(ErrorCode.X_3D000);
    }

    /**
     * If schemaName is null, return the current schema name, else return
     * the HsqlName object for the schema. If schemaName does not exist,
     * throw.
     */
    HsqlName getSchemaHsqlName(String name) {
        return name == null ? currentSchema
                            : database.schemaManager.getSchemaHsqlName(name);
    }

    /**
     * Same as above, but return string
     */
    public String getSchemaName(String name) {
        return name == null ? currentSchema.name
                            : database.schemaManager.getSchemaName(name);
    }

    public void setCurrentSchemaHsqlName(HsqlName name) {
        currentSchema = name;
    }

    public HsqlName getCurrentSchemaHsqlName() {
        return currentSchema;
    }

    public int getResultMemoryRowCount() {
        return resultMaxMemoryRows;
    }

    public void setResultMemoryRowCount(int count) {

        if (database.logger.getTempDirectoryPath() != null) {
            if (count < 0) {
                count = 0;
            }

            resultMaxMemoryRows = count;
        }
    }

    // warnings
    HsqlDeque sqlWarnings;

    public void addWarning(HsqlException warning) {

        if (sqlWarnings == null) {
            sqlWarnings = new HsqlDeque();
        }

        if (sqlWarnings.size() > 9) {
            sqlWarnings.removeFirst();
        }

        int index = sqlWarnings.indexOf(warning);

        if (index >= 0) {
            sqlWarnings.remove(index);
        }

        sqlWarnings.add(warning);
    }

    public HsqlException[] getAndClearWarnings() {

        if (sqlWarnings == null) {
            return HsqlException.emptyArray;
        }

        HsqlException[] array = new HsqlException[sqlWarnings.size()];

        sqlWarnings.toArray(array);
        sqlWarnings.clear();

        return array;
    }

    public HsqlException getLastWarning() {

        if (sqlWarnings == null || sqlWarnings.size() == 0) {
            return null;
        }

        return (HsqlException) sqlWarnings.getLast();
    }

    public void clearWarnings() {

        if (sqlWarnings != null) {
            sqlWarnings.clear();
        }
    }

    // session zone
    private Calendar calendar;
    private Calendar calendarGMT;

    public int getZoneSeconds() {
        return timeZoneSeconds;
    }

    public void setZoneSeconds(int seconds) {
        timeZoneSeconds = seconds;
    }

    public Calendar getCalendar() {

        if (calendar == null) {
            if (zoneString == null) {
                calendar = new GregorianCalendar();
            } else {
                TimeZone zone = TimeZone.getTimeZone(zoneString);

                calendar = new GregorianCalendar(zone);
            }
        }

        return calendar;
    }

    public Calendar getCalendarGMT() {

        if (calendarGMT == null) {
            calendarGMT = new GregorianCalendar(TimeZone.getTimeZone("GMT"));

            calendarGMT.setFirstDayOfWeek(Calendar.MONDAY);
            calendarGMT.setMinimalDaysInFirstWeek(4);
        }

        return calendarGMT;
    }

    public SimpleDateFormat getSimpleDateFormatGMT() {

        if (simpleDateFormatGMT == null) {
            simpleDateFormatGMT = new SimpleDateFormat("MMMM", Locale.ENGLISH);

            simpleDateFormatGMT.setCalendar(getCalendarGMT());
        }

        return simpleDateFormatGMT;
    }

    // services
    TypedComparator  typedComparator = new TypedComparator(this);
    Scanner          secondaryScanner;
    SimpleDateFormat simpleDateFormat;
    SimpleDateFormat simpleDateFormatGMT;
    Random           randomGenerator = new Random();
    long             seed            = -1;
    public final int randomId = randomGenerator.nextInt(Integer.MAX_VALUE);

    //
    public TypedComparator getComparator() {
        return typedComparator;
    }

    public double random(long seed) {

        if (this.seed != seed) {
            randomGenerator.setSeed(seed);

            this.seed = seed;
        }

        return randomGenerator.nextDouble();
    }

    public double random() {
        return randomGenerator.nextDouble();
    }

    public Scanner getScanner() {

        if (secondaryScanner == null) {
            secondaryScanner = new Scanner();
        }

        return secondaryScanner;
    }

    // properties
    HsqlProperties clientProperties;

    public HsqlProperties getClientProperties() {

        if (clientProperties == null) {
            clientProperties = new HsqlProperties();

            clientProperties.setProperty(
                HsqlDatabaseProperties.jdbc_translate_tti_types,
                database.sqlTranslateTTI);
            clientProperties.setProperty(
                HsqlDatabaseProperties.sql_live_object,
                database.sqlLiveObject);
        }

        return clientProperties;
    }

    // logging and SEQUENCE current values
    void logSequences() {

        HashMap map = sessionData.sequenceUpdateMap;

        if (map == null || map.isEmpty()) {
            return;
        }

        Iterator it = map.keySet().iterator();

        for (int i = 0, size = map.size(); i < size; i++) {
            NumberSequence sequence = (NumberSequence) it.next();

            database.logger.writeSequenceStatement(this, sequence);
        }

        sessionData.sequenceUpdateMap.clear();
    }

    String getStartTransactionSQL() {

        StringBuilder sb = new StringBuilder();

        sb.append(Tokens.T_START).append(' ').append(Tokens.T_TRANSACTION);

        if (isolationLevel != isolationLevelDefault) {
            sb.append(' ');
            appendIsolationSQL(sb, isolationLevel);
        }

        return sb.toString();
    }

    String getTransactionIsolationSQL() {

        StringBuilder sb = new StringBuilder();

        sb.append(Tokens.T_SET).append(' ').append(Tokens.T_TRANSACTION);
        sb.append(' ');
        appendIsolationSQL(sb, isolationLevel);

        return sb.toString();
    }

    String getSessionIsolationSQL() {

        StringBuilder sb = new StringBuilder();

        sb.append(Tokens.T_SET).append(' ').append(Tokens.T_SESSION);
        sb.append(' ').append(Tokens.T_CHARACTERISTICS).append(' ');
        sb.append(Tokens.T_AS).append(' ').append(Tokens.T_TRANSACTION).append(
            ' ');
        appendIsolationSQL(sb, isolationLevelDefault);

        return sb.toString();
    }

    static void appendIsolationSQL(StringBuilder sb, int isolationLevel) {

        sb.append(Tokens.T_ISOLATION).append(' ');
        sb.append(Tokens.T_LEVEL).append(' ');
        sb.append(getIsolationString(isolationLevel));
    }

    static String getIsolationString(int isolationLevel) {

        switch (isolationLevel) {

            case SessionInterface.TX_READ_UNCOMMITTED :
            case SessionInterface.TX_READ_COMMITTED :
                StringBuilder sb = new StringBuilder();

                sb.append(Tokens.T_READ).append(' ');
                sb.append(Tokens.T_COMMITTED);

                return sb.toString();

            case SessionInterface.TX_REPEATABLE_READ :
            case SessionInterface.TX_SERIALIZABLE :
            default :
                return Tokens.T_SERIALIZABLE;
        }
    }

    String getSetSchemaStatement() {
        return "SET SCHEMA " + currentSchema.statementName;
    }

    // timeouts
    class TimeoutManager {

        AtomicInteger currentTimeout = new AtomicInteger();
        volatile long checkTimestamp;

        void startTimeout(int timeout) {

            currentTimeout.set(timeout);

            if (timeout == 0) {
                return;
            }

            checkTimestamp = Session.this.statementStartTimestamp;

            database.timeoutRunner.addSession(Session.this);
        }

        boolean endTimeout() {

            currentTimeout.set(0);

            return true;
        }

        public boolean checkTimeout() {

            if (currentTimeout.get() == 0) {
                return true;
            }

            int result = currentTimeout.decrementAndGet();

            if (result <= 0) {
                currentTimeout.set(0);
                database.txManager.resetSession(
                    null, Session.this, checkTimestamp,
                    TransactionManager.resetSessionStatement);

                return true;
            }

            return false;
        }
    }
}
