package org.apache.torque.util;

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

import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.torque.Column;
import org.apache.torque.Database;
import org.apache.torque.TooManyRowsException;
import org.apache.torque.Torque;
import org.apache.torque.TorqueException;
import org.apache.torque.adapter.Adapter;
import org.apache.torque.adapter.IDMethod;
import org.apache.torque.criteria.FromElement;
import org.apache.torque.map.ColumnMap;
import org.apache.torque.map.MapHelper;
import org.apache.torque.map.TableMap;
import org.apache.torque.oid.IdGenerator;
import org.apache.torque.om.NumberKey;
import org.apache.torque.om.ObjectKey;
import org.apache.torque.om.SimpleKey;
import org.apache.torque.om.StringKey;
import org.apache.torque.om.mapper.RecordMapper;
import org.apache.torque.sql.Query;
import org.apache.torque.sql.SqlBuilder;

/**
 * This is the base class for all Peer classes in the system.  Peer
 * classes are responsible for isolating all of the database access
 * for a specific business object.  They execute all of the SQL
 * against the database.  Over time this class has grown to include
 * utility methods which ease execution of cross-database queries and
 * the implementation of concrete Peers.
 *
 * @param <T> The data object class for this Peer.
 *
 * @author <a href="mailto:frank.kim@clearink.com">Frank Y. Kim</a>
 * @author <a href="mailto:jmcnally@collab.net">John D. McNally</a>
 * @author <a href="mailto:bmclaugh@algx.net">Brett McLaughlin</a>
 * @author <a href="mailto:stephenh@chase3000.com">Stephen Haberman</a>
 * @author <a href="mailto:mpoeschl@marmot.at">Martin Poeschl</a>
 * @author <a href="mailto:vido@ldh.org">Augustin Vidovic</a>
 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 * @version $Id: BasePeerImpl.java 1415362 2012-11-29 20:33:52Z tfischer $
 */
public class BasePeerImpl<T> implements Serializable
{
    /**
     * Serial Version
     */
    private static final long serialVersionUID = -7702123730779032381L;

    /** the log */
    private static final Log log = LogFactory.getLog(BasePeerImpl.class);

    /** An injected instance of a record mapper to map JDBC result sets to objects */
    private RecordMapper<T> recordMapper = null;

    /** An injected instance of a table map */
    private TableMap tableMap = null;

    /** An injected instance of the database name */
    private String databaseName = null;

    /**
     * Default constructor
     */
    public BasePeerImpl()
    {
        super();
    }

    /**
     * Constructor providing the objects to be injected as parameters.
     *
     * @param recordMapper a record mapper to map JDBC result sets to objects
     * @param tableMap the default table map
     * @param databaseName the name of the database
     */
    public BasePeerImpl(RecordMapper<T> recordMapper, TableMap tableMap, String databaseName)
    {
        this();
        setRecordMapper(recordMapper);
        setTableMap(tableMap);
        setDatabaseName(databaseName);
    }

    /**
     * Set the record mapper for this instance.
     *
     * @param recordMapper the recordMapper to set
     */
    public void setRecordMapper(RecordMapper<T> recordMapper)
    {
        this.recordMapper = recordMapper;
    }

    /**
     * Get the record mapper for this instance.
     *
     * @return the recordMapper
     */
    public RecordMapper<T> getRecordMapper() throws TorqueException
    {
        if (recordMapper == null)
        {
            throw new TorqueException("No record mapper injected");
        }

        return recordMapper;
    }

    /**
     * Set the default table map for this instance.
     *
     * @param tableMap the tableMap to set
     */
    public void setTableMap(TableMap tableMap)
    {
        this.tableMap = tableMap;
    }

    /**
     * Get the default table map for this instance.
     *
     * @return the tableMap
     */
    public TableMap getTableMap() throws TorqueException
    {
        if (tableMap == null)
        {
            throw new TorqueException("No table map injected");
        }

        return tableMap;
    }

    /**
     * Set the database name for this instance.
     *
     * @param databaseName the databaseName to set
     */
    public void setDatabaseName(String databaseName)
    {
        this.databaseName = databaseName;
    }

    /**
     * Get the database name for this instance.
     *
     * @return the databaseName
     */
    public String getDatabaseName() throws TorqueException
    {
        if (databaseName == null)
        {
            throw new TorqueException("No database name injected");
        }

        return databaseName;
    }

    /**
     * Convenience method to create a String array of criteria keys.
     *
     * @param tableName Name of table.
     * @param columnNames A String[].
     * @return A String[].
     *
     * @deprecated This method is not used any more and will be removed in a
     *             future version of Torque.
     */
    @Deprecated
    public String[] initCriteriaKeys(
        String tableName,
        String[] columnNames)
    {
        String[] keys = new String[columnNames.length];
        for (int i = 0; i < columnNames.length; i++)
        {
            keys[i] = tableName + "." + columnNames[i].toUpperCase();
        }
        return keys;
    }

    /**
     * Convenience method that uses straight JDBC to delete multiple
     * rows.
     *
     * @param con A Connection.
     * @param table The table to delete records from.
     * @param column The column in the where clause.
     * @param value The value of the column.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated The value is not SQL escaped.
     *             Better use doDelete(Criteria, String, Connection)
     *             for automatic escaping and more flexibility.
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public int deleteAll(
                Connection con,
                String table,
                String column,
                int value)
            throws TorqueException
    {
        Statement statement = null;
        try
        {
            statement = con.createStatement();

            StringBuffer query = new StringBuffer();
            query.append("DELETE FROM ")
                .append(table)
                .append(" WHERE ")
                .append(column)
                .append(" = ")
                .append(value);

            return statement.executeUpdate(query.toString());
        }
        catch (SQLException e)
        {
            throw new TorqueException(e);
        }
        finally
        {
            if (statement != null)
            {
                try
                {
                    statement.close();
                }
                catch (SQLException e)
                {
                    throw new TorqueException(e);
                }
            }
        }
    }

    /**
     * Convenience method that uses straight JDBC to delete multiple
     * rows. This method attempts to get the default database from
     * the pool.
     *
     * @param table The table to delete records from.
     * @param column The column in the where clause.
     * @param value The value of the column.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated The value is not SQL escaped.
     *             Better use doDelete(Criteria, String)
     *             for automatic escaping and more flexibility.
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public int deleteAll(String table, String column, int value)
        throws TorqueException
    {
        Connection con = null;
        try
        {
            con = Transaction.begin(Torque.getDefaultDB());
            int result = deleteAll(con, table, column, value);
            Transaction.commit(con);
            con = null;
            return result;
        }
        finally
        {
            if (con != null)
            {
                Transaction.safeRollback(con);
            }
        }
    }

    /**
     * Deletes rows from a database table.
     *
     * @param criteria defines the rows to be deleted, not null.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
     public int doDelete(org.apache.torque.criteria.Criteria criteria)
             throws TorqueException
     {
        Connection connection = null;
        try
        {
            setDbName(criteria);
            connection = Transaction.begin(criteria.getDbName());
            int deletedRows = doDelete(criteria, connection);
            Transaction.commit(connection);
            connection = null;
            return deletedRows;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
     }

    /**
     * Deletes rows from a table.  This method is to be used
     * during a transaction, otherwise use the doDelete(Criteria) method.
      *
     * @param criteria defines the rows to be deleted, not null.
     * @param connection the connection to use, not null.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
     public int doDelete(
             org.apache.torque.criteria.Criteria criteria,
             Connection connection)
        throws TorqueException
     {
        correctBooleans(criteria);
        setDbName(criteria);

        Query query = SqlBuilder.buildQuery(criteria);
        query.setType(Query.Type.DELETE);

        String fullTableName;
        if (tableMap == null)
        {
            fullTableName = SqlBuilder.guessFullTableFromCriteria(criteria);
        }
        else
        {
            fullTableName = SqlBuilder.getFullTableName(
                    tableMap.getFullyQualifiedTableName(),
                    criteria.getDbName());
        }
        boolean ownTableAdded = false;
        for (FromElement fromElement : query.getFromClause())
        {
            // Table names are case insensitive in known databases
            // so use case-insensitive compare
            if (fullTableName.equalsIgnoreCase(fromElement.getFromExpression()))
            {
                ownTableAdded = true;
                break;
            }
        }
        if (!ownTableAdded)
        {
            query.getFromClause().add(new FromElement(fullTableName));
        }
        String sql = query.toString();

        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = connection.prepareStatement(sql);
            List<Object> replacements = setPreparedStatementReplacements(
                    preparedStatement,
                    query.getPreparedStatementReplacements(),
                    0);
            long startTime = System.currentTimeMillis();
            log.debug("Executing delete " + sql
                    + ", parameters = "
                    + replacements);

            int affectedRows = preparedStatement.executeUpdate();
            long queryEndTime = System.currentTimeMillis();
            log.trace("delete took " + (queryEndTime - startTime)
                    + " milliseconds");

            preparedStatement.close();
            preparedStatement = null;
            return affectedRows;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing prepared statement", e);
                }
            }
        }
     }

    /**
     * Method to perform deletes based on conditions in a Criteria.
     *
     * @param criteria The criteria to use.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated This method causes unexpected results when joins are used.
     *             Please use doDelete(
     *                 org.apache.torque.criteria.Criteria, TableMap).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    protected int doDelete(Criteria criteria) throws TorqueException
    {
        Connection con = null;
        try
        {
            con = Transaction.begin(criteria.getDbName());
            int result = doDelete(criteria, con);
            Transaction.commit(con);
            con = null;
            return result;
        }
        finally
        {
            if (con != null)
            {
                Transaction.safeRollback(con);
            }
        }
    }

    /**
     * Method to perform deletes based on conditions a Criteria.
     *
     * @param criteria The criteria to use.
     * @param con the Connection to be used for deleting.
     *
     * @return the number of deleted rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated This method causes unexpected results when joins are used.
     *             Please use doDelete(
     *                 org.apache.torque.criteria.Criteria, TableMap, Connection).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    protected int doDelete(Criteria criteria, Connection con)
        throws TorqueException
    {
        if (criteria.values().isEmpty())
        {
            throw new TorqueException("No conditions found in Criteria");
        }
        Criteria.Criterion criterion
                = criteria.values().iterator().next();

        TableMap tableMapFromCriteria = MapHelper.getTableMap(
                criterion.getColumn(), criteria, null);
        if (tableMapFromCriteria == null)
        {
            throw new TorqueException("Unqualified column name in criteria"
                    + " or table name not found in database map");
        }

        Query query = SqlBuilder.buildQuery(criteria);
        query.setType(Query.Type.DELETE);

        String fullTableName = null;
        if (tableMap != null)
        {
            fullTableName = SqlBuilder.getFullTableName(
                    tableMap.getFullyQualifiedTableName(),
                    criteria.getDbName());
        }
        else
        {
            Column column = criteria.values().iterator().next().getColumn();
            fullTableName = SqlBuilder.getFullTableName(
                    column.getFullTableName(),
                    criteria.getDbName());
        }

        boolean ownTableAdded = false;
        for (FromElement fromElement : query.getFromClause())
        {
            // Table names are case insensitive in known databases
            // so use case-insensitive compare
            if (fullTableName.equalsIgnoreCase(fromElement.getFromExpression()))
            {
                ownTableAdded = true;
                break;
            }
        }
        if (!ownTableAdded)
        {
            query.getFromClause().add(new FromElement(fullTableName));
        }
        String sql = query.toString();

        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = con.prepareStatement(sql);
            List<Object> replacements = setPreparedStatementReplacements(
                    preparedStatement,
                    query.getPreparedStatementReplacements(),
                    0);
            long startTime = System.currentTimeMillis();
            log.debug("Executing delete " + sql
                    + ", parameters = "
                    + replacements);

            int affectedRows = preparedStatement.executeUpdate();
            long queryEndTime = System.currentTimeMillis();
            log.trace("delete took " + (queryEndTime - startTime)
                    + " milliseconds");

            preparedStatement.close();
            preparedStatement = null;
            return affectedRows;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing prepared statement", e);
                }
            }
        }
    }

    /**
     * Inserts a record into a database table.
     * <p>
     * If the primary key is included in Criteria, then that value will
     * be used to insert the row.
     * <p>
     * Otherwise, if the primary key can be generated automatically,
     * the generated key will be used for the insert and will be returned.
     * <p>
     * If no value is given for the primary key is defined and it cannot
     * be generated automatically or the table has no primary key,
     * the values will be inserted as specified and null will be returned.
     *
     * @param insertValues Contains the values to insert, not null.
     *
     * @return the primary key of the inserted row (if the table
     *         has a primary key) or null (if the table does not have
     *         a primary key).
     *
     * @throws TorqueException if a database error occurs.
     */
    public ObjectKey doInsert(ColumnValues insertValues)
          throws TorqueException
    {
        String databaseNameFromInsertValues = insertValues.getDbName();
        if (databaseNameFromInsertValues == null)
        {
            databaseNameFromInsertValues = getDatabaseName();
        }
        Connection connection = null;
        try
        {
            connection = Transaction.begin(databaseNameFromInsertValues);
            ObjectKey id = doInsert(insertValues, connection);
            Transaction.commit(connection);
            connection = null;
            return id;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Inserts a record into a database table.
     * <p>
     * If the primary key is included in Criteria, then that value will
     * be used to insert the row.
     * <p>
     * Otherwise, if the primary key can be generated automatically,
     * the generated key will be used for the insert and will be returned.
     * <p>
     * If no value is given for the primary key is defined and it cannot
     * be generated automatically or the table has no primary key,
     * the values will be inserted as specified and null will be returned.
     *
     * @param insertValues Contains the values to insert, not null.
     * @param connection the connection to use for the insert, not null.
     *
     * @return the primary key of the inserted row (if the table
     *         has a primary key) or null (if the table does not have
     *         a primary key).
     *
     * @throws TorqueException if a database error occurs.
     */
    public ObjectKey doInsert(
                ColumnValues insertValues,
                Connection connection)
            throws TorqueException
    {
        if (insertValues == null)
        {
            throw new TorqueException("insertValues is null");
        }
        if (connection == null)
        {
            throw new TorqueException("connection is null");
        }
        String databaseNameFromInsertValues = insertValues.getDbName();
        if (databaseNameFromInsertValues == null)
        {
            databaseNameFromInsertValues = getDatabaseName();
        }
        Database database = Torque.getDatabase(databaseNameFromInsertValues);
        Object keyInfo = getIdMethodInfo();
        IdGenerator keyGen = database.getIdGenerator(
                getTableMap().getPrimaryKeyMethod());

        SimpleKey id = null;
        // can currently generate only single column pks, therefore a single
        // columnMap is ok
        ColumnMap primaryKey = null;
        if (keyGen != null)
        {
            // fail on multiple pks
            primaryKey = getTableMap().getPrimaryKey();

            // primaryKey will be null if there is no primary key
            // defined for the table we're inserting into.
            if (keyGen.isPriorToInsert() && primaryKey != null
                    && !insertValues.containsKey(
                            primaryKey))
            {
                id = getId(primaryKey, keyGen, connection, keyInfo);
                insertValues.put(
                        primaryKey,
                        new JdbcTypedValue(id.getValue(), id.getJdbcType()));
            }
        }

        List<String> columnNames = new ArrayList<String>();
        List<JdbcTypedValue> replacementObjects
                = new ArrayList<JdbcTypedValue>();
        for (Map.Entry<Column, JdbcTypedValue> columnValue
                : insertValues.entrySet())
        {
            Column column = columnValue.getKey();
            columnNames.add(column.getColumnName());
            JdbcTypedValue value = columnValue.getValue();
            replacementObjects.add(value);
        }

        String fullTableName = SqlBuilder.getFullTableName(
                getTableMap().getFullyQualifiedTableName(),
                databaseNameFromInsertValues);
        StringBuilder query = new StringBuilder("INSERT INTO ")
            .append(fullTableName)
            .append("(")
            .append(StringUtils.join(columnNames, ","))
            .append(") VALUES (");
        for (int i = 0; i < columnNames.size(); ++i)
        {
            if (i != 0)
            {
                query.append(",");
            }
            query.append("?");
        }
        query.append(")");

        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = connection.prepareStatement(query.toString());
            int position = 1;
            for (JdbcTypedValue replacementObject : replacementObjects)
            {
                Object value = replacementObject.getValue();
                if (value != null)
                {
                    if (replacementObject.getJdbcType() != Types.BLOB
                            && replacementObject.getJdbcType() != Types.CLOB)
                    {
                        preparedStatement.setObject(
                                position,
                                value,
                                replacementObject.getJdbcType());
                    }
                    else
                    {
                        preparedStatement.setObject(
                                position,
                                value);
                    }
                }
                else
                {
                    preparedStatement.setNull(
                            position,
                            replacementObject.getJdbcType());
                }
                position++;
            }
            long startTime = System.currentTimeMillis();
            log.debug("Executing insert " + query.toString()
                    + " using parameters " + replacementObjects);

            preparedStatement.executeUpdate();
            long queryEndTime = System.currentTimeMillis();
            log.trace("insert took " + (queryEndTime - startTime)
                    + " milliseconds");

            preparedStatement.close();
            preparedStatement = null;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing prepared statement", e);
                }
            }
        }

        // If the primary key column is auto-incremented, get the id
        // now.
        if (keyGen != null && keyGen.isPostInsert()
                && primaryKey != null
                && !insertValues.containsKey(
                        primaryKey))
        {
            id = getId(primaryKey, keyGen, connection, keyInfo);
        }

        return id;
    }

    /**
     * Returns the idMethodInfo for the table for this Peer class.
     *
     * @return the idMethodInfo, not null.
     *
     * @throws TorqueException if the database adapter for the table's database
     *         needs to be accessed but is not configured.
     */
    private Object getIdMethodInfo()
            throws TorqueException
    {
        IDMethod idMethod = tableMap.getPrimaryKeyMethod();
        if (IDMethod.NATIVE == idMethod)
        {
            Adapter adapter = Torque.getAdapter(getDatabaseName());
            if (adapter == null)
            {
               throw new TorqueException(
                   "missing adapter configuration for database "
                       + getDatabaseName()
                       + "check the Torque configuration");
            }
            idMethod = adapter.getIDMethodType();
        }
        Object keyInfo = tableMap.getPrimaryKeyMethodInfo(idMethod);
        return keyInfo;
    }

    /**
     * Create an Id for insertion in the Criteria
     *
     * @param pk ColumnMap for the Primary key
     * @param keyGen The Id Generator object
     * @param con The SQL Connection to run the id generation under
     * @param keyInfo KeyInfo Parameter from the Table map
     *
     * @return A simple Key representing the new Id value
     * @throws TorqueException Possible errors get wrapped in here.
     */
    private SimpleKey getId(
                ColumnMap pk,
                IdGenerator keyGen,
                Connection con,
                Object keyInfo)
            throws TorqueException
    {
        SimpleKey id = null;

        if (pk != null && keyGen != null)
        {
            if (pk.getType() instanceof Number)
            {
                id = new NumberKey(
                        keyGen.getIdAsBigDecimal(con, keyInfo));
            }
            else
            {
                id = new StringKey(keyGen.getIdAsString(con, keyInfo));
            }
        }

        return id;
    }

    /**
     * Add all the columns needed to create a new object.
     *
     * @param criteria the Criteria to which the select columns should
     *        be added.
     */
    public void addSelectColumns(org.apache.torque.criteria.Criteria criteria)
    {
        ColumnMap[] columns = this.tableMap.getColumns();

        for (ColumnMap c : columns)
        {
            criteria.addSelectColumn(c);
        }
    }

    /**
     * Add all the columns needed to create a new object.
     *
     * @param criteria the Criteria to which the select columns should
     *        be added.
     *
     * @deprecated Please use addSelectColumns(
     *                 org.apache.torque.criteria.Criteria).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public void addSelectColumns(Criteria criteria)
    {
        ColumnMap[] columns = this.tableMap.getColumns();

        for (ColumnMap c : columns)
        {
            criteria.addSelectColumn(c);
        }
    }

    /**
     * Selects objects from a database.
     *
     * @param criteria object used to create the SELECT statement.
     *
     * @return the list of selected objects, not null.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated Please use doSelect(org.apache.torque.criteria.Criteria).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public List<T> doSelect(Criteria criteria)
            throws TorqueException
    {
        if (criteria.getSelectColumns().size() == 0)
        {
            addSelectColumns(criteria);
        }
        setDbName(criteria);

        return doSelect(
            criteria,
            getRecordMapper());
    }

    /**
     * Selects objects from a database.
     *
     * @param criteria object used to create the SELECT statement.
     *
     * @return the list of selected objects, not null.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    public List<T> doSelect(org.apache.torque.criteria.Criteria criteria)
            throws TorqueException
    {
        if (criteria.getSelectColumns().size() == 0)
        {
            addSelectColumns(criteria);
        }
        setDbName(criteria);

        return doSelect(
            criteria,
            getRecordMapper());
    }

    /**
     * Selects objects from a database
     * within a transaction.
     *
     * @param criteria object used to create the SELECT statement.
     * @param connection the connection to use, not null.
     *
     * @return the list of selected objects, not null.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     *
     * @deprecated Please use doSelect(org.apache.torque.criteria.Criteria,
     *                 Connection).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public List<T> doSelect(
                Criteria criteria,
                Connection connection)
            throws TorqueException
    {
        if (criteria.getSelectColumns().size() == 0)
        {
            addSelectColumns(criteria);
        }
        setDbName(criteria);

        return doSelect(
                criteria,
                getRecordMapper(),
                connection);
    }

    /**
     * Selects objects from a database
     * within a transaction.
     *
     * @param criteria object used to create the SELECT statement.
     * @param connection the connection to use, not null.
     *
     * @return the list of selected objects, not null.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    public List<T> doSelect(
                org.apache.torque.criteria.Criteria criteria,
                Connection connection)
            throws TorqueException
    {
        if (criteria.getSelectColumns().size() == 0)
        {
            addSelectColumns(criteria);
        }
        setDbName(criteria);

        return doSelect(
                criteria,
                getRecordMapper(),
                connection);
    }

    /**
     * Selects at most one object from a database.
     *
     * @param criteria object used to create the SELECT statement.
     *
     * @return the selected Object, or null if no object was selected.
     *
     * @throws TorqueException If more than one record is selected or if
     *         an error occurs when processing the query.
     */
    public T doSelectSingleRecord(org.apache.torque.criteria.Criteria criteria)
            throws TorqueException
    {
        List<T> recordList = doSelect(criteria);
        T record = null;
        if (recordList.size() > 1)
        {
            throw new TooManyRowsException("Criteria " + criteria
                + " matched more than one record");
        }
        if (!recordList.isEmpty())
        {
            record = recordList.get(0);
        }
        return record;
    }

    /**
     * Selects at most one object from a database
     * within a transaction.
     *
     * @param criteria object used to create the SELECT statement.
     * @param connection the connection holding the transaction, not null.
     *
     * @return the selected Object, or null if no object was selected.
     *
     * @throws TorqueException If more than one record is selected or if
     *         an error occurs when processing the query.
     */
    public T doSelectSingleRecord(
                org.apache.torque.criteria.Criteria criteria,
                Connection connection)
            throws TorqueException
    {
        List<T> recordList = doSelect(criteria, connection);
        T record = null;
        if (recordList.size() > 1)
        {
            throw new TooManyRowsException("Criteria " + criteria
                + " matched more than one record");
        }
        if (!recordList.isEmpty())
        {
            record = recordList.get(0);
        }
        return record;
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     *
     * @deprecated Please use doSelect(org.apache.torque.criteria.Criteria,
     *                 RecordMapper).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public <TT> List<TT> doSelect(
                Criteria criteria,
                RecordMapper<TT> mapper)
            throws TorqueException
    {
        Connection connection = null;
        try
        {
            connection = Transaction.begin(criteria.getDbName());

            List<TT> result = doSelect(
                    criteria,
                    mapper,
                    connection);

            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     */
    public <TT> List<TT> doSelect(
                org.apache.torque.criteria.Criteria criteria,
                RecordMapper<TT> mapper)
            throws TorqueException
    {
        Connection connection = null;
        try
        {
            connection = Transaction.begin(criteria.getDbName());

            List<TT> result = doSelect(
                    criteria,
                    mapper,
                    connection);

            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param query the sql query to execute, not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     */
    public List<T> doSelect(String query)
            throws TorqueException
    {
        return doSelect(
                query,
                getRecordMapper(),
                getDatabaseName());
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param query the SQL Query to execute, not null.
     * @param connection the database connection, not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     */
    public List<T> doSelect(
                String query,
                Connection connection)
            throws TorqueException
    {
        return doSelect(
                query,
                getRecordMapper(),
                connection);
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param query the sql query to execute, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     * @param dbName The name of the database to create the connection for,
     *        or null for the default DB.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     */
    public <TT> List<TT> doSelect(
                String query,
                RecordMapper<TT> mapper,
                String dbName)
            throws TorqueException
    {
        Connection connection = null;

        try
        {
            connection = Transaction.begin((dbName == null)
                    ? Torque.getDefaultDB()
                    : dbName);

            List<TT> result = doSelect(
                    query,
                    mapper,
                    connection);

            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Selects rows from a database an maps them to objects.
     *
     * @param query the SQL Query to execute, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     * @param connection the database connection, not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException if querying the database fails.
     */
    public <TT> List<TT> doSelect(
                String query,
                RecordMapper<TT> mapper,
                Connection connection)
            throws TorqueException
    {
        if (connection == null)
        {
            throw new NullPointerException("connection is null");
        }

        List<TT> result = new ArrayList<TT>();
        Statement statement = null;
        ResultSet resultSet = null;
        try
        {
            statement = connection.createStatement();
            long startTime = System.currentTimeMillis();
            log.debug("Executing query " + query);

            resultSet = statement.executeQuery(query.toString());
            long queryEndTime = System.currentTimeMillis();
            log.trace("query took " + (queryEndTime - startTime)
                    + " milliseconds");

            while (resultSet.next())
            {
                TT rowResult = mapper.processRow(resultSet, 0, null);
                result.add(rowResult);
            }
            long mappingEndTime = System.currentTimeMillis();
            log.trace("mapping took " + (mappingEndTime - queryEndTime)
                    + " milliseconds");
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (resultSet != null)
            {
                try
                {
                    resultSet.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing resultSet", e);
                }
            }
            if (statement != null)
            {
                try
                {
                    statement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing statement", e);
                }
            }
        }
        return result;
    }

    /**
     * Performs a SQL <code>select</code> using a PreparedStatement.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     * @param connection the database connection for selecting records,
     *        not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException Error performing database query.
     *
     * @deprecated Please use doSelect(org.apache.torque.criteria.Criteria,
     *                 RecordMapper, Connection).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public <TT> List<TT> doSelect(
            Criteria criteria,
            RecordMapper<TT> mapper,
            Connection connection)
        throws TorqueException
    {
        correctBooleans(criteria);

        Query query = SqlBuilder.buildQuery(criteria);
        if (query.getFromClause().isEmpty())
        {
            String tableName = SqlBuilder.getFullTableName(
                    getTableMap().getFullyQualifiedTableName(),
                    criteria.getDbName());
            query.getFromClause().add(new FromElement(tableName));
        }

        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try
        {
            statement = connection.prepareStatement(query.toString());

            List<Object> replacements = setPreparedStatementReplacements(
                    statement,
                    query.getPreparedStatementReplacements(),
                    0);

            long startTime = System.currentTimeMillis();
            log.debug("Executing query " + query
                    + ", parameters = "
                    + replacements);

            resultSet = statement.executeQuery();
            long queryEndTime = System.currentTimeMillis();
            log.trace("query took " + (queryEndTime - startTime)
                    + " milliseconds");

            long offset;
            Database database = Torque.getDatabase(criteria.getDbName());
            if (database.getAdapter().supportsNativeOffset())
            {
                offset = 0; //database takes care of offset
            }
            else
            {
                offset = criteria.getOffset();
            }

            long limit;
            if (database.getAdapter().supportsNativeLimit())
            {
                limit = -1; //database takes care of offset
            }
            else
            {
                if (database.getAdapter().supportsNativeOffset())
                {
                    limit = criteria.getLimit();
                }
                else
                {
                    if (criteria.getLimit() == -1)
                    {
                        limit = criteria.getLimit();
                    }
                    else
                    {
                        limit = offset + criteria.getLimit();
                    }
                }
            }

            List<TT> result = new ArrayList<TT>();
            int rowNumber = 0;
            while (resultSet.next())
            {
                if (rowNumber < offset)
                {
                    rowNumber++;
                    continue;
                }
                if (limit >= 0 && rowNumber >= limit)
                {
                    break;
                }

                TT rowResult = mapper.processRow(resultSet, 0, criteria);
                result.add(rowResult);

                rowNumber++;
            }
            long mappingEndTime = System.currentTimeMillis();
            log.trace("mapping took " + (mappingEndTime - queryEndTime)
                    + " milliseconds");

            if (criteria.isSingleRecord() && result.size() > 1)
            {
                throw new TooManyRowsException(
                        "Criteria expected single Record and "
                        + "Multiple Records were selected");
            }
            return result;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (resultSet != null)
            {
                try
                {
                    resultSet.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing resultSet", e);
                }
            }
            if (statement != null)
            {
                try
                {
                    statement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing statement", e);
                }
            }
        }
    }

    /**
     * Performs a SQL <code>select</code> using a PreparedStatement.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     * @param connection the database connection for selecting records,
     *        not null.
     *
     * @return The results of the query, not null.
     *
     * @throws TorqueException Error performing database query.
     */
    public <TT> List<TT> doSelect(
                org.apache.torque.criteria.Criteria criteria,
                RecordMapper<TT> mapper,
                Connection connection)
            throws TorqueException
    {
        correctBooleans(criteria);

        Query query = SqlBuilder.buildQuery(criteria);
        if (query.getFromClause().isEmpty())
        {
            String tableName = SqlBuilder.getFullTableName(
                    getTableMap().getFullyQualifiedTableName(),
                    criteria.getDbName());
            query.getFromClause().add(new FromElement(tableName));
        }

        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try
        {
            statement = connection.prepareStatement(query.toString());
            if (query.getFetchSize() != null)
            {
                statement.setFetchSize(query.getFetchSize());
            }

            List<Object> replacements = setPreparedStatementReplacements(
                    statement,
                    query.getPreparedStatementReplacements(),
                    0);

            long startTime = System.currentTimeMillis();
            log.debug("Executing query " + query
                    + ", parameters = "
                    + replacements);

            resultSet = statement.executeQuery();
            long queryEndTime = System.currentTimeMillis();
            log.trace("query took " + (queryEndTime - startTime)
                    + " milliseconds");

            long offset;
            Database database = Torque.getDatabase(criteria.getDbName());
            if (database.getAdapter().supportsNativeOffset())
            {
                offset = 0; //database takes care of offset
            }
            else
            {
                offset = criteria.getOffset();
            }

            long limit;
            if (database.getAdapter().supportsNativeLimit())
            {
                limit = -1; //database takes care of offset
            }
            else
            {
                if (database.getAdapter().supportsNativeOffset())
                {
                    limit = criteria.getLimit();
                }
                else
                {
                    if (criteria.getLimit() == -1)
                    {
                        limit = criteria.getLimit();
                    }
                    else
                    {
                        limit = offset + criteria.getLimit();
                    }
                }
            }

            List<TT> result = new ArrayList<TT>();
            int rowNumber = 0;
            while (resultSet.next())
            {
                if (rowNumber < offset)
                {
                    rowNumber++;
                    continue;
                }
                if (limit >= 0 && rowNumber >= limit)
                {
                    break;
                }

                TT rowResult = mapper.processRow(resultSet, 0, criteria);
                result.add(rowResult);

                rowNumber++;
            }
            long mappingEndTime = System.currentTimeMillis();
            log.trace("mapping took " + (mappingEndTime - queryEndTime)
                    + " milliseconds");

            if (criteria.isSingleRecord() && result.size() > 1)
            {
                throw new TooManyRowsException(
                        "Criteria expected single Record and "
                        + "Multiple Records were selected");
            }
            return result;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (resultSet != null)
            {
                try
                {
                    resultSet.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing resultSet", e);
                }
            }
            if (statement != null)
            {
                try
                {
                    statement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing statement", e);
                }
            }
        }
    }

    /**
     * Selects at most a single row from a database an maps them to objects.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     *
     * @return The selected row, or null if no records was selected.
     *
     * @throws TorqueException if querying the database fails.
     */
    public <TT> TT doSelectSingleRecord(
                org.apache.torque.criteria.Criteria criteria,
                RecordMapper<TT> mapper)
            throws TorqueException
    {
        List<TT> resultList = doSelect(criteria, mapper);
        TT result = null;
        if (resultList.size() > 1)
        {
            throw new TooManyRowsException("Criteria " + criteria
                + " matched more than one record");
        }
        if (!resultList.isEmpty())
        {
            result = resultList.get(0);
        }
        return result;
    }

    /**
     * Selects at most a single row from a database an maps them to objects.
     *
     * @param criteria A Criteria specifying the records to select, not null.
     * @param mapper The mapper creating the objects from the resultSet,
     *        not null.
     * @param connection the database connection, not null.
     *
     * @return The selected row, or null if no records was selected.
     *
     * @throws TorqueException if querying the database fails.
     */
    public <TT> TT doSelectSingleRecord(
                org.apache.torque.criteria.Criteria criteria,
                RecordMapper<TT> mapper,
                Connection connection)
            throws TorqueException
    {
        List<TT> resultList = doSelect(
                criteria,
                mapper,
                connection);
        TT result = null;
        if (resultList.size() > 1)
        {
            throw new TooManyRowsException("Criteria " + criteria
                + " matched more than one record");
        }
        if (!resultList.isEmpty())
        {
            result = resultList.get(0);
        }
        return result;
    }

    /**
     * Convenience method used to update rows in the DB.  Checks if a
     * <i>single</i> primary key is specified in the Criteria
     * object and uses it to perform the update.  If no primary key is
     * specified or the table has multiple primary keys,
     * an Exception will be thrown.
     * <p>
     * Use this method for performing an update of the kind:
     * <p>
     * "WHERE primary_key_id = someValue"
     * <p>
     * To perform an update on a table with multiple primary keys or
     * an update with non-primary key fields in the WHERE
     * clause, use doUpdate(ColumnValues, Criteria).
     *
     * @param updateValues Which columns to update with which values
     *        for which primary key value, not null.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    public int doUpdate(ColumnValues updateValues)
            throws TorqueException
    {
        String databaseNameFromUpdateValues = updateValues.getDbName();
        if (databaseNameFromUpdateValues == null)
        {
            databaseNameFromUpdateValues = getDatabaseName();
        }
        Connection connection = null;
        try
        {
            connection = Transaction.begin(databaseNameFromUpdateValues);
            int result = doUpdate(updateValues, connection);
            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Convenience method used to update rows in the DB.  Checks if a
     * <i>single</i> primary key is specified in the Criteria
     * object and uses it to perform the update.  If no primary key is
     * specified or the table has multiple primary keys,
     * an Exception will be thrown.
     * <p>
     * Use this method for performing an update of the kind:
     * <p>
     * "WHERE primary_key_id = someValue"
     * <p>
     * To perform an update on a table with multiple primary keys or
     * an update with non-primary key fields in the WHERE
     * clause, use doUpdate(ColumnValues, Criteria, Connection).
     *
     * @param updateValues Which columns to update with which values
     *        for which primary key value, not null.
     * @param connection the database connection to use.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    public int doUpdate(
                ColumnValues updateValues,
                Connection connection)
            throws TorqueException
    {
        ColumnMap pk = getTableMap().getPrimaryKey();
        org.apache.torque.criteria.Criteria selectCriteria = null;

        if (pk != null && updateValues.containsKey(pk.getSqlExpression()))
        {
            selectCriteria = new org.apache.torque.criteria.Criteria();
            selectCriteria.where(pk,
                updateValues.remove(pk.getSqlExpression()));
        }
        else
        {
            throw new TorqueException("No PK specified for database update");
        }

        return doUpdate(selectCriteria, updateValues, connection);
    }

    /**
     * Executes an update against the database. The rows to be updated
     * are selected using <code>criteria</code> and updated using the values
     * in <code>updateValues</code>.
     *
     * @param selectCriteria selects which rows of which table
     *        should be updated, not null.
     * @param updateValues Which columns to update with which values, not null.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException if updating fails.
     *
     * @deprecated Please use doUpdate(
     *                 org.apache.torque.criteria.Criteria, ColumnValues).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public int doUpdate(
                Criteria selectCriteria,
                ColumnValues updateValues)
            throws TorqueException
    {
        String databaseNameFromUpdateValues = updateValues.getDbName();
        if (databaseNameFromUpdateValues == null)
        {
            databaseNameFromUpdateValues = getDatabaseName();
        }
        Connection connection = null;
        try
        {
            connection = Transaction.begin(databaseNameFromUpdateValues);
            int result = doUpdate(selectCriteria, updateValues, connection);
            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Executes an update against the database. The rows to be updated
     * are selected using <code>criteria</code> and updated using the values
     * in <code>updateValues</code>.
     *
     * @param selectCriteria selects which rows of which table
     *        should be updated, not null.
     * @param updateValues Which columns to update with which values, not null.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException if updating fails.
     */
    public int doUpdate(
                org.apache.torque.criteria.Criteria selectCriteria,
                ColumnValues updateValues)
            throws TorqueException
    {
        String databaseNameFromUpdateValues = updateValues.getDbName();
        if (databaseNameFromUpdateValues == null)
        {
            databaseNameFromUpdateValues = getDatabaseName();
        }
        Connection connection = null;
        try
        {
            connection = Transaction.begin(databaseNameFromUpdateValues);
            int result = doUpdate(selectCriteria, updateValues, connection);
            Transaction.commit(connection);
            connection = null;
            return result;
        }
        finally
        {
            if (connection != null)
            {
                Transaction.safeRollback(connection);
            }
        }
    }

    /**
     * Executes an update against the database. The rows to be updated
     * are selected using <code>criteria</code> and updated using the values
     * in <code>updateValues</code>.
     *
     * @param criteria selects which rows of which table should be updated.
     * @param updateValues Which columns to update with which values, not null.
     * @param connection the database connection to use, not null.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException if updating fails.
     *
     * @deprecated Please use doUpdate(org.apache.torque.criteria.Criteria,
     *                 ColumnValues, Connection).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public int doUpdate(
                Criteria criteria,
                ColumnValues updateValues,
                Connection connection)
            throws TorqueException
    {
        Query query = SqlBuilder.buildQuery(criteria);
        query.setType(Query.Type.UPDATE);

        query.getFromClause().clear();
        String fullTableName = SqlBuilder.getFullTableName(
                getTableMap().getFullyQualifiedTableName(),
                criteria.getDbName());
        query.getFromClause().add(new FromElement(fullTableName));

        List<JdbcTypedValue> replacementObjects
                = new ArrayList<JdbcTypedValue>();
        for (Map.Entry<Column, JdbcTypedValue> updateValue
                : updateValues.entrySet())
        {
            Column column = updateValue.getKey();
            query.getSelectClause().add(column.getColumnName());
            replacementObjects.add(updateValue.getValue());
        }

        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = connection.prepareStatement(query.toString());
            int position = 1;
            for (JdbcTypedValue replacementObject : replacementObjects)
            {
                Object value = replacementObject.getValue();
                if (value != null)
                {
                    preparedStatement.setObject(position, value);
                }
                else
                {
                    preparedStatement.setNull(
                            position,
                            replacementObject.getJdbcType());
                }
                position++;
            }
            List<Object> replacements = setPreparedStatementReplacements(
                    preparedStatement,
                    query.getPreparedStatementReplacements(),
                    position - 1);
            long startTime = System.currentTimeMillis();
            log.debug("Executing update " + query.toString()
                    + " using update parameters " + replacementObjects
                    + " and query parameters "
                    + replacements);

            int affectedRows = preparedStatement.executeUpdate();
            long queryEndTime = System.currentTimeMillis();
            log.trace("update took " + (queryEndTime - startTime)
                    + " milliseconds");

            preparedStatement.close();
            preparedStatement = null;
            return affectedRows;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing prepared statement", e);
                }
            }
        }
    }

    /**
     * Executes an update against the database. The rows to be updated
     * are selected using <code>criteria</code> and updated using the values
     * in <code>updateValues</code>.
     *
     * @param criteria selects which rows of which table should be updated.
     * @param updateValues Which columns to update with which values, not null.
     * @param connection the database connection to use, not null.
     *
     * @return the number of affected rows.
     *
     * @throws TorqueException if updating fails.
     */
    public int doUpdate(
                org.apache.torque.criteria.Criteria criteria,
                ColumnValues updateValues,
                Connection connection)
            throws TorqueException
    {
        Query query = SqlBuilder.buildQuery(criteria);
        query.setType(Query.Type.UPDATE);

        query.getFromClause().clear();
        String fullTableName = SqlBuilder.getFullTableName(
                getTableMap().getFullyQualifiedTableName(),
                criteria.getDbName());
        query.getFromClause().add(new FromElement(fullTableName));

        List<JdbcTypedValue> replacementObjects
                = new ArrayList<JdbcTypedValue>();
        for (Map.Entry<Column, JdbcTypedValue> updateValue
                : updateValues.entrySet())
        {
            Column column = updateValue.getKey();
            query.getSelectClause().add(column.getColumnName());
            replacementObjects.add(updateValue.getValue());
        }

        PreparedStatement preparedStatement = null;
        try
        {
            preparedStatement = connection.prepareStatement(query.toString());
            int position = 1;
            for (JdbcTypedValue replacementObject : replacementObjects)
            {
                Object value = replacementObject.getValue();
                if (value != null)
                {
                    preparedStatement.setObject(position, value);
                }
                else
                {
                    preparedStatement.setNull(
                            position,
                            replacementObject.getJdbcType());
                }
                position++;
            }
            List<Object> replacements = setPreparedStatementReplacements(
                    preparedStatement,
                    query.getPreparedStatementReplacements(),
                    position - 1);
            long startTime = System.currentTimeMillis();
            log.debug("Executing update " + query.toString()
                    + " using update parameters " + replacementObjects
                    + " and query parameters "
                    + replacements);

            int affectedRows = preparedStatement.executeUpdate();
            long queryEndTime = System.currentTimeMillis();
            log.trace("update took " + (queryEndTime - startTime)
                    + " milliseconds");

            preparedStatement.close();
            preparedStatement = null;
            return affectedRows;
        }
        catch (SQLException e)
        {
            throw ExceptionMapper.getInstance().toTorqueException(e);
        }
        finally
        {
            if (preparedStatement != null)
            {
                try
                {
                    preparedStatement.close();
                }
                catch (SQLException e)
                {
                    log.warn("error closing prepared statement", e);
                }
            }
        }
    }

    /**
     * Utility method which executes a given sql statement
     * as prepared statement.
     * This method should be used for update, insert, and delete statements.
     * Use executeQuery() for selects.
     *
     * @param statementString A String with the sql statement to execute.
     *
     * @return The number of rows affected.
     *
     * @throws TorqueException if executing the statement fails
     *         or no database connection can be established.
     */
    public int executeStatement(String statementString) throws TorqueException
    {
        return executeStatement(statementString, Torque.getDefaultDB(), null);
    }

    /**
     * Utility method which executes a given sql statement
     * as prepared statement.
     * This method should be used for update, insert, and delete statements.
     * Use executeQuery() for selects.
     *
     * @param statementString A String with the sql statement to execute.
     * @param replacementValues values to use as placeholders in the query.
     *        or null or empty if no placeholders need to be filled.
     *
     * @return The number of rows affected.
     *
     * @throws TorqueException if executing the statement fails
     *         or no database connection can be established.
     */
    public int executeStatement(
                String statementString,
                List<JdbcTypedValue> replacementValues)
            throws TorqueException
    {
        return executeStatement(
                statementString,
                Torque.getDefaultDB(),
                replacementValues);
    }

    /**
     * Utility method which executes a given sql statement
     * as prepared statement.
     * This method should be used for update, insert, and delete statements.
     * Use executeQuery() for selects.
     *
     * @param statementString A String with the sql statement to execute.
     * @param dbName The name of the database to execute the statement against,
     *        or null for the default DB.
     * @param replacementValues values to use as placeholders in the query.
     *        or null or empty if no placeholders need to be filled.
     *
     * @return The number of rows affected.
     *
     * @throws TorqueException if executing the statement fails
     *         or no database connection can be established.
     */
    public int executeStatement(
            String statementString,
            String dbName,
            List<JdbcTypedValue> replacementValues)
        throws TorqueException
    {
        Connection con = null;
        try
        {
            con = Transaction.begin(dbName);
            int rowCount = executeStatement(
                    statementString,
                    con,
                    replacementValues);
            Transaction.commit(con);
            con = null;
            return rowCount;
        }
        finally
        {
            if (con != null)
            {
                Transaction.safeRollback(con);
            }
        }
    }

    /**
     * Utility method which executes a given sql statement
     * as prepared statement.
     * This method should be used for update, insert, and delete statements.
     * Use executeQuery() for selects.
     *
     * @param statementString A String with the sql statement to execute.
     * @param con The database connection to use.
     * @param replacementValues values to use as placeholders in the query.
     *        or null or empty if no placeholders need to be filled.
     *
     * @return The number of rows affected.
     *
     * @throws TorqueException if executing the statement fails.
     */
    public int executeStatement(
            String statementString,
            Connection con,
            List<JdbcTypedValue> replacementValues)
        throws TorqueException
    {
        int rowCount = -1;
        PreparedStatement statement = null;
        try
        {
            statement = con.prepareStatement(statementString);
            if (replacementValues != null)
            {
                int position = 1;
                for (JdbcTypedValue replacementValue : replacementValues)
                {
                    if (replacementValue.getValue() == null)
                    {
                        statement.setNull(
                                position,
                                replacementValue.getJdbcType());
                    }
                    else
                    {
                        statement.setObject(
                                position,
                                replacementValue.getValue(),
                                replacementValue.getJdbcType());
                    }
                    ++position;
                }
            }
            rowCount = statement.executeUpdate();
        }
        catch (SQLException e)
        {
            throw new TorqueException(e);
        }
        finally
        {
            if (statement != null)
            {
                try
                {
                    statement.close();
                }
                catch (SQLException e)
                {
                    throw new TorqueException(e);
                }
            }
        }
        return rowCount;
    }

    /**
     * Sets the prepared statement replacements into a query, possibly
     * modifying the type if required by DB Drivers.
     *
     * @param statement the statement to set the parameters in, not null.
     * @param replacements the replacements to set, not null.
     * @param offset the offset on the parameters, 0 for no offset.
     *
     * @return the parameters set.
     *
     * @throws SQLException if setting the parameter fails.
     */
    private List<Object> setPreparedStatementReplacements(
                PreparedStatement statement,
                List<Object> replacements,
                int offset)
            throws SQLException
    {
        List<Object> result = new ArrayList<Object>(replacements.size());
        int i = 1 + offset;
        for (Object param : replacements)
        {
            if (param instanceof java.sql.Timestamp)
            {
                statement.setTimestamp(i, (java.sql.Timestamp) param);
                result.add(param);
            }
            else if (param instanceof java.sql.Date)
            {
                statement.setDate(i, (java.sql.Date) param);
                result.add(param);
            }
            else if (param instanceof java.util.Date)
            {
                java.sql.Timestamp sqlDate = new java.sql.Timestamp(
                        ((java.util.Date) param).getTime());
                statement.setTimestamp(i, sqlDate);
                result.add(sqlDate);
            }
            else if (param instanceof NumberKey)
            {
                BigDecimal bigDecimal = ((NumberKey) param).getBigDecimal();
                statement.setBigDecimal(i, bigDecimal);
                result.add(bigDecimal);
            }
            else if (param instanceof Integer)
            {
                statement.setInt(i, ((Integer) param).intValue());
                result.add(param);
            }
            else if (param instanceof Long)
            {
                statement.setLong(i, ((Long) param).longValue());
                result.add(param);
            }
            else if (param instanceof BigDecimal)
            {
                statement.setBigDecimal(i, (BigDecimal) param);
                result.add(param);
            }
            else if (param instanceof Boolean)
            {
                statement.setBoolean(i, ((Boolean) param).booleanValue());
                result.add(param);
            }
            else
            {
                statement.setString(i, param.toString());
                result.add(param.toString());
            }
            ++i;
        }
        return result;
    }

    /**
     * Changes the boolean values in the criteria to the appropriate type,
     * whenever a booleanchar or booleanint column is involved.
     * This enables the user to create criteria using Boolean values
     * for booleanchar or booleanint columns.
     *
     * @param criteria the criteria in which the boolean values should be
     *        corrected.
     * @throws TorqueException if the database map for the criteria cannot be
     *         obtained.
     *
     * @deprecated Please use correctBooleans(
     *                 org.apache.torque.criteria.Criteria).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    public void correctBooleans(Criteria criteria)
            throws TorqueException
    {
        for (Object criterionObject : criteria.values())
        {
            Criteria.Criterion criterion = (Criteria.Criterion) criterionObject;
            correctBooleans(criteria, criterion);
       }
    }

    /**
     * Checks all columns in the criteria to see whether
     * booleanchar and booleanint columns are queried with a boolean.
     * If yes, the query values are mapped onto values the database
     * does understand, i.e. 0 and 1 for booleanints and N and Y for
     * booleanchar columns.
     *
     * @param criteria The criteria to which teh criterion belongs.
     * @param criterion The criterion to be checked for booleanint
     *        and booleanchar columns.
     *
     * @throws TorqueException if the database map for the criteria cannot be
     *         retrieved.
     *
     * @deprecated
     */
    @Deprecated
    private void correctBooleans(
                Criteria criteria,
                Criteria.Criterion criterion)
            throws TorqueException
    {
        Column column = criterion.getColumn();
        TableMap tableMapFromCriteria = MapHelper.getTableMap(
                column,
                criteria,
                tableMap);
        // if no description of table available, do not modify anything
        if (tableMapFromCriteria != null)
        {
            String columnName = column.getColumnName();
            ColumnMap columnMap = tableMapFromCriteria.getColumn(columnName);
            if (columnMap != null)
            {
                if ("BOOLEANINT".equals(columnMap.getTorqueType()))
                {
                    replaceBooleanValues(
                            criterion,
                            Integer.valueOf(1),
                            Integer.valueOf(0));
                }
                else if ("BOOLEANCHAR".equals(columnMap.getTorqueType()))
                {
                    replaceBooleanValues(criterion, "Y", "N");
                 }
            }
        }
        for (Criteria.Criterion attachedCriterion : criterion.getClauses())
        {
            correctBooleans(criteria, attachedCriterion);
        }
    }

    /**
     * Checks all columns in the criteria to see whether
     * booleanchar and booleanint columns are queried with a boolean.
     * If yes, the query values are mapped onto values the database
     * does understand, i.e. 0 and 1 for booleanints and N and Y for
     * booleanchar columns.
     *
     * @param criteria The criteria to be checked for booleanint and booleanchar
     *        columns.
     *
     * @throws TorqueException if the database map for the criteria cannot be
     *         retrieved.
     */
    public void correctBooleans(
                org.apache.torque.criteria.Criteria criteria)
            throws TorqueException
    {
       correctBooleans(
               criteria,
               criteria.getTopLevelCriterion());
    }

    private void correctBooleans(
                org.apache.torque.criteria.Criteria criteria,
                org.apache.torque.criteria.Criterion criterion)
            throws TorqueException
    {
        if (criterion == null)
        {
            return;
        }
        if (criterion.isComposite())
        {
            for (org.apache.torque.criteria.Criterion part
                    : criterion.getParts())
            {
                correctBooleans(criteria, part);
            }
            return;
        }

        Object possibleColumn = criterion.getLValue();
        TableMap tableMapForColumn = MapHelper.getTableMap(
                possibleColumn,
                criteria,
                tableMap);
        // if no description of table available, do not modify anything
        if (tableMapForColumn == null)
        {
            return;
        }
        String columnName = ((Column) possibleColumn).getColumnName();
        ColumnMap columnMap = tableMapForColumn.getColumn(columnName);
        if (columnMap != null)
        {
            if ("BOOLEANINT".equals(columnMap.getTorqueType()))
            {
                replaceBooleanValues(
                        criterion,
                        Integer.valueOf(1),
                        Integer.valueOf(0));
            }
            else if ("BOOLEANCHAR".equals(columnMap.getTorqueType()))
            {
                replaceBooleanValues(criterion, "Y", "N");
             }
        }
    }

    /**
     * Replaces any Boolean value in the criterion and its attached Criterions
     * by trueValue if the Boolean equals <code>Boolean.TRUE</code>
     * and falseValue if the Boolean equals <code>Boolean.FALSE</code>.
     *
     * @param criterion the criterion to replace Boolean values in.
     * @param trueValue the value by which Boolean.TRUE should be replaced.
     * @param falseValue the value by which Boolean.FALSE should be replaced.
     *
     * @deprecated
     */
    @Deprecated
    private void replaceBooleanValues(
            Criteria.Criterion criterion,
            Object trueValue,
            Object falseValue)
    {
        // attachedCriterions also contains the criterion itself,
        // so no additional treatment is needed for the criterion itself.
        Criteria.Criterion[] attachedCriterions
            = criterion.getAttachedCriterion();
        for (int i = 0; i < attachedCriterions.length; ++i)
        {
            Object criterionValue
                    = attachedCriterions[i].getValue();
            if (criterionValue instanceof Boolean)
            {
                Boolean booleanValue = (Boolean) criterionValue;
                attachedCriterions[i].setValue(
                        Boolean.TRUE.equals(booleanValue)
                                ? trueValue
                                : falseValue);
            }

        }
    }

    /**
     * Replaces any Boolean value in the criterion and its attached Criterions
     * by trueValue if the Boolean equals <code>Boolean.TRUE</code>
     * and falseValue if the Boolean equals <code>Boolean.FALSE</code>.
     *
     * @param criterion the criterion to replace Boolean values in.
     *        May not be a composite criterion.
     * @param trueValue the value by which Boolean.TRUE should be replaced.
     * @param falseValue the value by which Boolean.FALSE should be replaced.
     */
    private void replaceBooleanValues(
            org.apache.torque.criteria.Criterion criterion,
            Object trueValue,
            Object falseValue)
    {
        Object rValue = criterion.getRValue();
        if (rValue instanceof Boolean)
        {
            Boolean booleanValue = (Boolean) rValue;
            criterion.setRValue(
                    Boolean.TRUE.equals(booleanValue)
                            ? trueValue
                            : falseValue);
        }
        Object lValue = criterion.getLValue();
        if (lValue instanceof Boolean)
        {
            Boolean booleanValue = (Boolean) lValue;
            criterion.setLValue(
                    Boolean.TRUE.equals(booleanValue)
                            ? trueValue
                            : falseValue);
        }
    }

    /**
     * Checks all columns in the criteria to see whether
     * booleanchar and booleanint columns are queried with a boolean.
     * If yes, the query values are mapped onto values the database
     * does understand, i.e. 0 and 1 for booleanints and N and Y for
     * booleanchar columns.
     *
     * @param columnValues The value to be checked for booleanint
     *        and booleanchar columns.
     * @throws TorqueException if the database map for the criteria cannot be
     *         retrieved.
     */
    public void correctBooleans(
            ColumnValues columnValues)
        throws TorqueException
    {
        for (Map.Entry<Column, JdbcTypedValue> entry : columnValues.entrySet())
        {
            String columnName = entry.getKey().getColumnName();
            ColumnMap column = getTableMap().getColumn(columnName);
            if (column != null)
            {
                JdbcTypedValue columnValue = entry.getValue();
                if ("BOOLEANINT".equals(column.getTorqueType()))
                {
                    if (Boolean.TRUE.equals(columnValue.getValue()))
                    {
                        entry.setValue(new JdbcTypedValue(1, Types.INTEGER));
                    }
                    else if (Boolean.FALSE.equals(columnValue.getValue()))
                    {
                        entry.setValue(new JdbcTypedValue(0, Types.INTEGER));
                    }
                    else if (columnValue.getValue() == null)
                    {
                        entry.setValue(new JdbcTypedValue(null, Types.INTEGER));
                    }
                }
                else if ("BOOLEANCHAR".equals(column.getTorqueType()))
                {
                    if (Boolean.TRUE.equals(columnValue.getValue()))
                    {
                        entry.setValue(new JdbcTypedValue("Y", Types.CHAR));
                    }
                    else if (Boolean.FALSE.equals(columnValue.getValue()))
                    {
                        entry.setValue(new JdbcTypedValue("N", Types.CHAR));
                    }
                    else if (columnValue.getValue() == null)
                    {
                        entry.setValue(new JdbcTypedValue(null, Types.CHAR));
                    }
                 }
            }
        }
    }

    /**
     * Sets the database name in the passed criteria to the table's default,
     * if it is not already set.
     *
     * @param crit the criteria to set the database name in, not null.
     */
    protected void setDbName(org.apache.torque.criteria.Criteria crit)
            throws TorqueException
    {
        if (crit.getDbName() == null)
        {
            crit.setDbName(getDatabaseName());
        }
    }

    /**
     * Sets the database name in the passed criteria to the table's default,
     * if it is not already set.
     *
     * @param crit the criteria to set the database name in, not null.
     *
     * @deprecated Please use addSelectColumns(
     *                 org.apache.torque.criteria.Criteria).
     *             This method will be removed in a future version of Torque.
     */
    @Deprecated
    protected void setDbName(Criteria crit) throws TorqueException
    {
        if (crit.getDbName() == null)
        {
            crit.setDbName(getDatabaseName());
        }
    }
}
