<?php

/**
 * DB_Table is a database API and data type SQL abstraction class.
 * 
 * DB_Table provides database API abstraction, data type abstraction,
 * automated SELECT, INSERT, and UPDATE queries, automated table
 * creation, automated validation of inserted/updated column values,
 * and automated creation of QuickForm elements based on the column
 * definitions.
 * 
 * @category Database
 * @package DB_Table
 * @author Paul M. Jones <pmjones@php.net>
 * @author Mark Wiesemann <wiesemann@php.net>
 * 
 * @license http://www.gnu.org/copyleft/lesser.html LGPL
 * 
 * @version $Id: Table.php,v 1.87 2007/06/14 15:07:27 morse Exp $
 */

/**
 * Error code at instantiation time when the first parameter to the
 * constructor is not a PEAR DB object.
 */
define('DB_TABLE_ERR_NOT_DB_OBJECT',    -1);

/**
 * Error code at instantiation time when the PEAR DB/MDB2 $phptype is not
 * supported by DB_Table.
 */
define('DB_TABLE_ERR_PHPTYPE',          -2);

/**
 * Error code when you call select() or selectResult() and the first
 * parameter is a string that does not match any of the $this->sql keys.
 */
define('DB_TABLE_ERR_SQL_UNDEF',        -3);

/**
 * Error code when you call select*() or buildSQL() and the first
 * parameter is neither an array nor a string
 */
define('DB_TABLE_ERR_SQL_NOT_STRING',   -4);

/**
 * Error code when you try to insert data to a column that is not in the
 * $this->col array.
 */
define('DB_TABLE_ERR_INS_COL_NOMAP',    -5);

/**
 * Error code when you try to insert data, and that data does not have a
 * column marked as 'require' in the $this->col array.
 */
define('DB_TABLE_ERR_INS_COL_REQUIRED', -6);

/**
 * Error code when auto-validation fails on data to be inserted.
 */
define('DB_TABLE_ERR_INS_DATA_INVALID', -7);

/**
 * Error code when you try to update data to a column that is not in the
 * $this->col array.
 */
define('DB_TABLE_ERR_UPD_COL_NOMAP',    -8);

/**
 * Error code when you try to update data, and that data does not have a
 * column marked as 'require' in the $this->col array.
 */
define('DB_TABLE_ERR_UPD_COL_REQUIRED', -9);

/**
 * Error code when auto-validation fails on update data.
 */
define('DB_TABLE_ERR_UPD_DATA_INVALID', -10);

/**
 * Error code when you use a create() flag that is not recognized (must
 * be 'safe', 'drop', 'verify' or boolean false.
 */
define('DB_TABLE_ERR_CREATE_FLAG',      -11);

/**
 * Error code at create() time when you define an index in $this->idx
 * that has no columns.
 */
define('DB_TABLE_ERR_IDX_NO_COLS',      -12);

/**
 * Error code at create() time when you define an index in $this->idx
 * that refers to a column that does not exist in the $this->col array.
 */
define('DB_TABLE_ERR_IDX_COL_UNDEF',    -13);

/**
 * Error code at create() time when you define a $this->idx index type
 * that is not recognized (must be 'normal' or 'unique').
 */
define('DB_TABLE_ERR_IDX_TYPE',         -14);

/**
 * Error code at create() time when you have an error in a 'char' or
 * 'varchar' definition in $this->col (usually because 'size' is wrong).
 */
define('DB_TABLE_ERR_DECLARE_STRING',   -15);

/**
 * Error code at create() time when you have an error in a 'decimal'
 * definition (usually because the 'size' or 'scope' are wrong).
 */
define('DB_TABLE_ERR_DECLARE_DECIMAL',  -16);

/**
 * Error code at create() time when you define a column in $this->col
 * with an unrecognized 'type'.
 */
define('DB_TABLE_ERR_DECLARE_TYPE',     -17);

/**
 * Error code at validation time when a column in $this->col has an
 * unrecognized 'type'.
 */
define('DB_TABLE_ERR_VALIDATE_TYPE',    -18);

/**
 * Error code at create() time when you define a column in $this->col
 * with an invalid column name (usually because it's a reserved keyword).
 */
define('DB_TABLE_ERR_DECLARE_COLNAME',  -19);

/**
 * Error code at create() time when you define an index in $this->idx
 * with an invalid index name (usually because it's a reserved keyword).
 */
define('DB_TABLE_ERR_DECLARE_IDXNAME',  -20);

/**
 * Error code at create() time when you define an index in $this->idx
 * that refers to a CLOB column.
 */
define('DB_TABLE_ERR_IDX_COL_CLOB',     -21);

/**
 * Error code at create() time when you define a column name that is
 * more than 30 chars long (an Oracle restriction).
 */
define('DB_TABLE_ERR_DECLARE_STRLEN',   -22);

/**
 * Error code at create() time when the index name ends up being more
 * than 30 chars long (an Oracle restriction).
 */
define('DB_TABLE_ERR_IDX_STRLEN',       -23);

/**
 * Error code at create() time when the table name is more than 30 chars
 * long (an Oracle restriction).
 */
define('DB_TABLE_ERR_TABLE_STRLEN',     -24);

/**
 * Error code at nextID() time when the sequence name is more than 30
 * chars long (an Oracle restriction).
 */
define('DB_TABLE_ERR_SEQ_STRLEN',       -25);

/**
 * Error code at verify() time when the table does not exist in the
 * database.
 */
define('DB_TABLE_ERR_VER_TABLE_MISSING', -26);

/**
 * Error code at verify() time when the column does not exist in the
 * database table.
 */
define('DB_TABLE_ERR_VER_COLUMN_MISSING', -27);

/**
 * Error code at verify() time when the column type does not match the
 * type specified in the column declaration.
 */
define('DB_TABLE_ERR_VER_COLUMN_TYPE',  -28);

/**
 * Error code at instantiation time when the column definition array
 * does not contain at least one column.
 */
define('DB_TABLE_ERR_NO_COLS',          -29);

/**
 * Error code at verify() time when an index cannot be found in the
 * database table.
 */
define('DB_TABLE_ERR_VER_IDX_MISSING',   -30);

/**
 * Error code at verify() time when an index does not contain all
 * columns that it should contain.
 */
define('DB_TABLE_ERR_VER_IDX_COL_MISSING', -31);

/**
 * Error code at instantiation time when a creation mode
 * is not available for a phptype.
 */
define('DB_TABLE_ERR_CREATE_PHPTYPE', -32);

/**
 * Error code at create() time when you define more than one primary key
 * in $this->idx.
 */
define('DB_TABLE_ERR_DECLARE_PRIMARY', -33);

/**
 * Error code at create() time when a primary key is defined in $this->idx
 * and SQLite is used (SQLite does not support primary keys).
 */
define('DB_TABLE_ERR_DECLARE_PRIM_SQLITE', -34);

/**
 * Error code at alter() time when altering a table field is not possible
 * (e.g. because MDB2 has no support for the change or because the DBMS
 * does not support the change).
 */
define('DB_TABLE_ERR_ALTER_TABLE_IMPOS', -35);

/**
 * Error code at alter() time when altering a(n) index/constraint is not possible
 * (e.g. because MDB2 has no support for the change or because the DBMS
 * does not support the change).
 */
define('DB_TABLE_ERR_ALTER_INDEX_IMPOS', -36);

/**
 * Error code at insert() time due to invalid the auto-increment column
 * definition. This column must be an integer type and required.
 */
define('DB_TABLE_ERR_AUTO_INC_COL', -37);

/**
 * The DB_Table_Base parent class
 */
require_once 'DB/Table/Base.php';

/**
 * The PEAR class for errors
 */
require_once 'PEAR.php';

/**
 * The Date class for recasting date and time values
 */
require_once 'DB/Table/Date.php';


/**
 * DB_Table supports these RDBMS engines and their various native data
 * types; we need these here instead of in Manager.php because the
 * initial array key tells us what databases are supported.
 */
$GLOBALS['_DB_TABLE']['type'] = array(
    'fbsql' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'LONGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'REAL',
        'double'    => 'DOUBLE PRECISION',
        'clob'      => 'CLOB',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'ibase' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'FLOAT',
        'double'    => 'DOUBLE PRECISION',
        'clob'      => 'BLOB SUB_TYPE 1',
        'date'      => 'DATE',
        'time'      => 'TIME',
        'timestamp' => 'TIMESTAMP'
    ),
    'mssql' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'REAL',
        'double'    => 'FLOAT',
        'clob'      => 'TEXT',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'mysql' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'FLOAT',
        'double'    => 'DOUBLE',
        'clob'      => 'LONGTEXT',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'mysqli' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'FLOAT',
        'double'    => 'DOUBLE',
        'clob'      => 'LONGTEXT',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'oci8' => array(
        'boolean'   => 'NUMBER(1)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR2',
        'smallint'  => 'NUMBER(6)',
        'integer'   => 'NUMBER(11)',
        'bigint'    => 'NUMBER(19)',
        'decimal'   => 'NUMBER',
        'single'    => 'REAL',
        'double'    => 'DOUBLE PRECISION',
        'clob'      => 'CLOB',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'pgsql' => array(
        'boolean'   => 'DECIMAL(1,0)',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'DECIMAL',
        'single'    => 'REAL',
        'double'    => 'DOUBLE PRECISION',
        'clob'      => 'TEXT',
        'date'      => 'CHAR(10)',
        'time'      => 'CHAR(8)',
        'timestamp' => 'CHAR(19)'
    ),
    'sqlite' => array(
        'boolean'   => 'BOOLEAN',
        'char'      => 'CHAR',
        'varchar'   => 'VARCHAR',
        'smallint'  => 'SMALLINT',
        'integer'   => 'INTEGER',
        'bigint'    => 'BIGINT',
        'decimal'   => 'NUMERIC',
        'single'    => 'FLOAT',
        'double'    => 'DOUBLE',
        'clob'      => 'CLOB',
        'date'      => 'DATE',
        'time'      => 'TIME',
        'timestamp' => 'TIMESTAMP'
    )
);


/** 
 * US-English default error messages. If you want to internationalize, you can
 * set the translated messages via $GLOBALS['_DB_TABLE']['error']. You can also
 * use DB_Table::setErrorMessage(). Examples:
 * 
 * <code>
 * (1) $GLOBALS['_DB_TABLE]['error'] = array(DB_TABLE_ERR_PHPTYPE   => '...',
 *                                           DB_TABLE_ERR_SQL_UNDEF => '...');
 * (2) DB_Table::setErrorMessage(DB_TABLE_ERR_PHPTYPE,   '...');
 *     DB_Table::setErrorMessage(DB_TABLE_ERR_SQL_UNDEF, '...');
 * (3) DB_Table::setErrorMessage(array(DB_TABLE_ERR_PHPTYPE   => '...');
 *                                     DB_TABLE_ERR_SQL_UNDEF => '...');
 * (4) $obj =& new DB_Table();
 *     $obj->setErrorMessage(DB_TABLE_ERR_PHPTYPE,   '...');
 *     $obj->setErrorMessage(DB_TABLE_ERR_SQL_UNDEF, '...');
 * (5) $obj =& new DB_Table();
 *     $obj->setErrorMessage(array(DB_TABLE_ERR_PHPTYPE   => '...');
 *                                 DB_TABLE_ERR_SQL_UNDEF => '...');
 * </code>
 * 
 * For errors that can occur with-in the constructor call (i.e. e.g. creating
 * or altering the database table), only the code from examples (1) to (3)
 * will alter the default error messages early enough. For errors that can
 * occur later, examples (4) and (5) are also valid.
 */
$GLOBALS['_DB_TABLE']['default_error'] = array(
    DB_TABLE_ERR_NOT_DB_OBJECT       => 'First parameter must be a DB/MDB2 object',
    DB_TABLE_ERR_PHPTYPE             => 'DB/MDB2 phptype (or dbsyntax) not supported',
    DB_TABLE_ERR_SQL_UNDEF           => 'Select query string not in a key of $sql. Key',
    DB_TABLE_ERR_SQL_NOT_STRING      => 'Select query is neither an array nor a string',
    DB_TABLE_ERR_INS_COL_NOMAP       => 'Insert column not in map',
    DB_TABLE_ERR_INS_COL_REQUIRED    => 'Insert data must be set and non-null for column',
    DB_TABLE_ERR_INS_DATA_INVALID    => 'Insert data not valid for column',
    DB_TABLE_ERR_UPD_COL_NOMAP       => 'Update column not in map',
    DB_TABLE_ERR_UPD_COL_REQUIRED    => 'Update column must be set and non-null',
    DB_TABLE_ERR_UPD_DATA_INVALID    => 'Update data not valid for column',
    DB_TABLE_ERR_CREATE_FLAG         => 'Create flag not valid',
    DB_TABLE_ERR_IDX_NO_COLS         => 'No columns for index',
    DB_TABLE_ERR_IDX_COL_UNDEF       => 'Column not in map for index',
    DB_TABLE_ERR_IDX_TYPE            => 'Type not valid for index',
    DB_TABLE_ERR_DECLARE_STRING      => 'String column declaration not valid',
    DB_TABLE_ERR_DECLARE_DECIMAL     => 'Decimal column declaration not valid',
    DB_TABLE_ERR_DECLARE_TYPE        => 'Column type not valid',
    DB_TABLE_ERR_VALIDATE_TYPE       => 'Cannot validate for unknown type on column',
    DB_TABLE_ERR_DECLARE_COLNAME     => 'Column name not valid',
    DB_TABLE_ERR_DECLARE_IDXNAME     => 'Index name not valid',
    DB_TABLE_ERR_DECLARE_TYPE        => 'Column type not valid',
    DB_TABLE_ERR_IDX_COL_CLOB        => 'CLOB column not allowed for index',
    DB_TABLE_ERR_DECLARE_STRLEN      => 'Column name too long, 30 char max',
    DB_TABLE_ERR_IDX_STRLEN          => 'Index name too long, 30 char max',
    DB_TABLE_ERR_TABLE_STRLEN        => 'Table name too long, 30 char max',
    DB_TABLE_ERR_SEQ_STRLEN          => 'Sequence name too long, 30 char max',
    DB_TABLE_ERR_VER_TABLE_MISSING   => 'Verification failed: table does not exist',
    DB_TABLE_ERR_VER_COLUMN_MISSING  => 'Verification failed: column does not exist',
    DB_TABLE_ERR_VER_COLUMN_TYPE     => 'Verification failed: wrong column type',
    DB_TABLE_ERR_NO_COLS             => 'Column definition array may not be empty',
    DB_TABLE_ERR_VER_IDX_MISSING     => 'Verification failed: index does not exist',
    DB_TABLE_ERR_VER_IDX_COL_MISSING => 'Verification failed: index does not contain all specified cols',
    DB_TABLE_ERR_CREATE_PHPTYPE      => 'Creation mode is not supported for this phptype',
    DB_TABLE_ERR_DECLARE_PRIMARY     => 'Only one primary key is allowed',
    DB_TABLE_ERR_DECLARE_PRIM_SQLITE => 'SQLite does not support primary keys',
    DB_TABLE_ERR_ALTER_TABLE_IMPOS   => 'Alter table failed: changing the field type not possible',
    DB_TABLE_ERR_ALTER_INDEX_IMPOS   => 'Alter table failed: changing the index/constraint not possible',
    DB_TABLE_ERR_AUTO_INC_COL        => 'Illegal auto-increment column definition'
);

// merge default and user-defined error messages
if (!isset($GLOBALS['_DB_TABLE']['error'])) {
    $GLOBALS['_DB_TABLE']['error'] = array();
}
foreach ($GLOBALS['_DB_TABLE']['default_error'] as $code => $message) {
    if (!array_key_exists($code, $GLOBALS['_DB_TABLE']['error'])) {
        $GLOBALS['_DB_TABLE']['error'][$code] = $message;
    }
}

/**
 * DB_Table is a database API and data type SQL abstraction class.
 * 
 * DB_Table provides database API abstraction, data type abstraction,
 * automated SELECT, INSERT, and UPDATE queries, automated table
 * creation, automated validation of inserted/updated column values,
 * and automated creation of QuickForm elemnts based on the column
 * definitions.
 * 
 * @category Database
 * @package DB_Table
 * @author Paul M. Jones <pmjones@php.net>
 * @author Mark Wiesemann <wiesemann@php.net>
 * 
 * @version @package_version@
 *
 */

class DB_Table extends DB_Table_Base 
{
    
    /**
     * The table or view in the database to which this object binds.
     * 
     * @access public
     * @var string
     */
    var $table = null;
    
    /**
     * DB_Table_Database instance that this table belongs to.
     * 
     * @access private
     * @var object
     */
    var $_database = null;


    /**
     * Associative array of column definitions.
     * 
     * @access public
     * @var array
     */
    var $col = array();
    
    
    /**
     * Associative array of index definitions.
     * 
     * @access public
     * @var array
     */
    var $idx = array();
    
    /**
     * Name of an auto-increment column, if any. Null otherwise.
     *
     * A table can contain at most one auto-increment column. 
     * Auto-incrementing is implemented in the insert() method,
     * using a sequence accessed by the nextID() method.
     *
     * @access public
     * @var string
     */
    var $auto_inc_col = null;


    /**
     * Boolean flag to turn on (true) or off (false) auto-incrementing.
     * 
     * Auto-increment column $auto_inc_col upon insertion only if $_auto_inc is
     * true and the value of that column is null in the data to be inserted.
     *
     * @var bool
     * @access private
     */
    var $_auto_inc = true;


    /**
     * Whether or not to automatically validate data at insert-time.
     * 
     * @var bool
     * @access private
     */
    var $_valid_insert = true;
    
    /**
     * Whether or not to automatically validate data at update-time.
     * 
     * @var bool
     * @access private
     */
    var $_valid_update = true;
    

    /**
     * Whether or not to automatically recast data at insert- and update-time.
     * 
     * @var    bool
     * @access private
     */
    var $_auto_recast = true;
    
    
    /**
     * Constructor.
     *
     * The constructor returns a DB_Table object that wraps an
     * instance $db DB or MDB2, and that binds to a specific database
     * table named $table. It can optionally create the database table
     * or verify that its schema matches that declared in the $col and
     * $idx parameters, depending on the value of the $create parameter.
     *
     * If there is an error on instantiation, $this->error will be 
     * populated with the PEAR_Error.
     * 
     * @param object &$db A PEAR DB/MDB2 object.
     * 
     * @param string $table The table name to connect to in the database.
     * 
     * @param mixed $create The automatic table creation mode to pursue:
     * - boolean false to not attempt creation
     * - 'safe' to create the table only if it does not exist
     * - 'drop' to drop any existing table with the same name and re-create it
     * - 'verify' to check whether the table exists, whether all the columns
     *   exist, whether the columns have the right type, and whether the indexes
     *   exist and have the right type
     * - 'alter' does the same as 'safe' if the table does not exist; if it
     *   exists, a verification for columns existence, the column types, the
     *   indexes existence, and the indexes types will be performed and the
     *   table schema will be modified if needed
     * 
     * @return object DB_Table
     * @access public
     */
    function DB_Table(&$db, $table, $create = false)
    {
        // is the first argument a DB/MDB2 object?
        $this->backend = null;
        if (is_subclass_of($db, 'db_common')) {
            $this->backend = 'db';
        } elseif (is_subclass_of($db, 'mdb2_driver_common')) {
            $this->backend = 'mdb2';
        }

        if (is_null($this->backend)) {
            $this->error =& DB_Table::throwError(DB_TABLE_ERR_NOT_DB_OBJECT);
            return;
        }
        
        // set the class properties
        $this->db =& $db;
        $this->table = $table;

        // Identify the class for error handling by parent class
        $this->_primary_subclass = 'DB_TABLE';
        
        // is the RDBMS supported?
        $phptype = $db->phptype;
        $dbsyntax = $db->dbsyntax;
        if (! DB_Table::supported($phptype, $dbsyntax)) {
            $this->error =& DB_Table::throwError(
                DB_TABLE_ERR_PHPTYPE,
                "({$db->phptype})"
            );
            return;
        }

        // load MDB2_Extended module
        if ($this->backend == 'mdb2') {
            $this->db->loadModule('Extended', null, false);
        }

        // should we attempt table creation?
        if ($create) {

            if ($this->backend == 'mdb2') {
                $this->db->loadModule('Manager');
            }

            // check whether the chosen mode is supported
            $mode_supported = DB_Table::modeSupported($create, $phptype);
            if (PEAR::isError($mode_supported)) {
                $this->error =& $mode_supported;
                return;
            }
            if (!$mode_supported) {
                $this->error =& $this->throwError(
                    DB_TABLE_ERR_CREATE_PHPTYPE,
                    "('$create', '$phptype')"
                );
                return;
            }

            include_once 'DB/Table/Manager.php';

            switch ($create) {

                case 'alter':
                    $result = $this->alter();
                    break;

                case 'drop':
                case 'safe':
                    $result = $this->create($create);
                    break;

                case 'verify':
                    $result = $this->verify();
                    break;
            }
            
            if (PEAR::isError($result)) {
                // problem creating/altering/verifing the table
                $this->error =& $result;
                return;
            }
        }
    }
    
    
    /**
     * Is a particular RDBMS supported by DB_Table?
     * 
     * @static
     * @param string $phptype The RDBMS type for PHP.
     * @param string $dbsyntax The chosen database syntax.
     * @return bool  True if supported, false if not.
     * @access public
     */
    
    function supported($phptype, $dbsyntax = '')
    {
        // only Firebird is supported, not its ancestor Interbase
        if ($phptype == 'ibase' && $dbsyntax != 'firebird') {
            return false;
        }
        $supported = array_keys($GLOBALS['_DB_TABLE']['type']);
        return in_array(strtolower($phptype), $supported);
    }


    /**
     * Is a creation mode supported for a RDBMS by DB_Table?
     * 
     * @param string $mode The chosen creation mode.
     * @param string $phptype The RDBMS type for PHP.
     * @return bool  True if supported, false if not (PEAR_Error on failure)
     *
     * @throws PEAR_Error if
     *     Unknown creation mode is specified (DB_TABLE_ERR_CREATE_FLAG)
     * 
     * @access public
     */
    function modeSupported($mode, $phptype)
    {
        // check phptype for validity
        $supported = array_keys($GLOBALS['_DB_TABLE']['type']);
        if (!in_array(strtolower($phptype), $supported)) {
            return false;
        }

        switch ($mode) {
            case 'drop':
            case 'safe':
                // supported for all RDBMS
                return true;

            case 'alter':
            case 'verify':
                // not supported for fbsql and mssql (yet)
                switch ($phptype) {
                    case 'fbsql':
                    case 'mssql':
                        return false;
                    default:
                        return true;
                }

            default:
                // unknown creation mode
                return $this->throwError(
                    DB_TABLE_ERR_CREATE_FLAG,
                    "('$mode')"
                );
        }
    }


    /**
     * Overwrite one or more error messages, e.g. to internationalize them.
     * 
     * @param mixed $code If string, the error message with code $code will
     * be overwritten by $message. If array, the error messages with code
     * of each array key will be overwritten by the key's value.
     * 
     * @param string $message Only used if $key is not an array.
     * @return void
     * @access public
     */
    function setErrorMessage($code, $message = null) {
        if (is_array($code)) {
            foreach ($code as $single_code => $single_message) {
                $GLOBALS['_DB_TABLE']['error'][$single_code] = $single_message;
            }
        } else {
            $GLOBALS['_DB_TABLE']['error'][$code] = $message;
        }
    }


    /**
     * 
     * Returns all or part of the $this->col property array.
     * 
     * @param mixed $col If null, returns the $this->col property array
     * as it is.  If string, returns that column name from the $this->col
     * array. If an array, returns those columns named as the array
     * values from the $this->col array as an array.
     *
     * @return mixed All or part of the $this->col property array, or
     *               boolean false if no matching column names are found.
     * @access public
     */
    function getColumns($col = null)
    {
        // by default, return all column definitions
        if (is_null($col)) {
            return $this->col;
        }
        
        // if the param is a string, only return the column definition
        // named by the that string
        if (is_string($col)) {
            if (isset($this->col[$col])) {
                return $this->col[$col];
            } else {
                return false;
            }
        }
        
        // if the param is a sequential array of column names,
        // return only those columns named in that array
        if (is_array($col)) {
            $set = array();
            foreach ($col as $name) {
                $set[$name] = $this->getColumns($name);
            }
            
            if (count($set) == 0) {
                return false;
            } else {
                return $set;
            }
        }
        
        // param was not null, string, or array
        return false;
    }
    
    
    /**
     * Returns all or part of the $this->idx property array.
     * 
     * @param mixed $idx Index name (key in $this->idx), or array of
     *                   index name strings.
     * 
     * @return mixed All or part of the $this->idx property array, 
     *               or boolean false if $idx is not null but invalid
     * 
     * @access public
     */
    function getIndexes($idx = null)
    {
        // by default, return all index definitions
        if (is_null($idx)) {
            return $this->idx;
        }
        
        // if the param is a string, only return the index definition
        // named by the that string
        if (is_string($idx)) {
            if (isset($this->idx[$idx])) {
                return $this->idx[$idx];
            } else {
                return false;
            }
        }
        
        // if the param is a sequential array of index names,
        // return only those indexes named in that array
        if (is_array($idx)) {
            $set = array();
            foreach ($idx as $name) {
                $set[$name] = $this->getIndexes($name);
            }
            
            if (count($set) == 0) {
                return false;
            } else {
                return $set;
            }
        }
        
        // param was not null, string, or array
        return false;
    }
    
    
    /**
     * Connect or disconnect a DB_Table_Database instance to this table
     * instance.
     * 
     * Used to re-connect this DB_Table object to a parent DB_Table_Database
     * object during unserialization. Can also disconnect if the $database 
     * parameter is null. Use the DB_Table_Database::addTable method instead 
     * to add a table to a new DB_Table_Database.
     * 
     * @param object &$database DB_Table_Database instance that this table
     *               belongs to (or null to disconnect from instance).
     * 
     * @return void
     * @access public
     */
    function setDatabaseInstance(&$database)
    {
        if (is_a($database, 'DB_Table_Database')) {
            $this->_database =& $database;
        } elseif (is_null($database)) {
            $this->_database = null;
        }
    }

    
    /**
     * Inserts a single table row.
     *
     * Inserts data from associative array $data, in which keys are column
     * names and values are column values. All required columns (except an
     * auto-increment column) must be included in the data array. Columns
     * values that are not set or null are inserted as SQL NULL values. 
     *
     * If an auto-increment column is declared (by setting $this->auto_inc_col),
     * and the value of that column in $data is not set or null, then a new
     * sequence value will be generated and inserted.
     *
     * If auto-recasting is enabled (if $this->_auto_recast), the method will
     * try, if necessary to recast $data to proper column types, with recast().
     *
     * If auto-validation is enabled (if $this->_valid_insert), the method
     * will validates column types with validInsert() before insertion.
     *
     * @access public
     * 
     * @param array $data An associative array of key-value pairs where
     * the key is the column name and the value is the column value. 
     * This is the data that will be inserted into the table.  
     * 
     * @return mixed Void on success (PEAR_Error on failure)
     *
     * @throws PEAR_Error if:
     *     - Error in auto_inc_col declaration (DB_TABLE_ERR_AUTO_INC_COL)
     *     - Error returned by DB/MDB2::autoExecute() (Error bubbled up)
     *
     * @see validInsert()
     * @see DB::autoExecute()
     * @see MDB2::autoExecute()
     */
    function insert($data)
    {
        // Auto-increment if enabled and input value is null or not set
        if ($this->_auto_inc 
            && !is_null($this->auto_inc_col) 
            && !isset($data[$this->auto_inc_col]) 
           ) {
            $column = $this->auto_inc_col;
            // check that the auto-increment column exists
            if (!array_key_exists($column, $this->col)) {
                return $this->throwError(
                        DB_TABLE_ERR_AUTO_INC_COL,
                        ": $column does not exist");
            }
            // check that the column is integer 
            if (!in_array($this->col[$column]['type'],
                           array('integer','smallint','bigint'))) {
                return $this->throwError(
                        DB_TABLE_ERR_AUTO_INC_COL,
                        ": $column is not an integer");
            }
            // check that the column is required
            // Note: The insert method will replace a null input value 
            // of $data[$column] with a sequence value. This makes 
            // the column effectively 'not null'. This column must be
            // 'required' for consistency, to make this explicit.
            if (!$this->isRequired($column)) {
                return $this->throwError(
                        DB_TABLE_ERR_AUTO_INC_COL,
                        ": $column is not required");
            }
            // set the value
            $id = $this->nextID();
            if (PEAR::isError($id)) {
                return $id;
            }
            $data[$column] = $id;
        }

        // forcibly recast the data elements to their proper types?
        if ($this->_auto_recast) {
            $this->recast($data);
        }

        // validate the data if auto-validation is turned on
        if ($this->_valid_insert) {
            $result = $this->validInsert($data);
            if (PEAR::isError($result)) {
                return $result;
            }
        }

        // Does a parent DB_Table_Database object exist?
        if ($this->_database) {

            $_database = $this->_database;
  
            // Validate foreign key values (if enabled)
            if ($_database->_check_fkey) {
               $result = $_database->validForeignKeys($this->table, $data);
               if (PEAR::isError($result)) {
                   return $result;
               }
            }
    
        }
       
        // Do insertion
        if ($this->backend == 'mdb2') {
            $result = $this->db->extended->autoExecute($this->table, $data,
                MDB2_AUTOQUERY_INSERT);
        } else {
            $result = $this->db->autoExecute($this->table, $data,
                DB_AUTOQUERY_INSERT);
        }
        return $result;
    }
    
    
    /**
     * Turns on or off auto-incrementing of $auto_inc_col column (if any)
     * 
     * For auto-incrementing to work, an $auto_inc_col column must be declared,
     * auto-incrementing must be enabled (by this method), and the value of
     * the $auto_inc_col column must be not set or null in the $data passed to
     * the insert method. 
     * 
     * @param  bool $flag True to turn on auto-increment, false to turn off.
     * @return void
     * @access public
     */
    function setAutoInc($flag = true)
    {
        if ($flag) {
            $this->_auto_inc = true;
        } else {
            $this->_auto_inc = false;
        }
    }
    
    
    /**
     * Turns on (or off) automatic validation of inserted data.
     * 
     * Enables (if $flag is true) or disables (if $flag is false) automatic 
     * validation of data types prior to actual insertion into the database 
     * by the DB_Table::insert() method.
     *
     * @param  bool $flag True to turn on auto-validation, false to turn off.
     * @return void
     * @access public
     */
    function autoValidInsert($flag = true)
    {
        if ($flag) {
            $this->_valid_insert = true;
        } else {
            $this->_valid_insert = false;
        }
    }
    
    
    /**
     * Validates an array for insertion into the table.
     * 
     * @param array $data An associative array of key-value pairs where
     * the key is the column name and the value is the column value.  This
     * is the data that will be inserted into the table.  Data is checked
     * against the column data type for validity.
     * 
     * @return boolean true on success (PEAR_Error on failure)
     *
     * @throws PEAR_Error if:
     *     - Invalid column name key in $data (DB_TABLE_ERR_INS_COL_NOMAP)
     *     - Missing required column value    (DB_TABLE_ERR_INS_COL_NOMAP)
     *     - Column value doesn't match type  (DB_TABLE_ERR_INS_DATA_INVALID)
     *
     * @access public
     * 
     * @see insert()
     */
    function validInsert(&$data)
    {
        // loop through the data, and disallow insertion of unmapped
        // columns
        foreach ($data as $col => $val) {
            if (! isset($this->col[$col])) {
                return $this->throwError(
                    DB_TABLE_ERR_INS_COL_NOMAP,
                    "('$col')"
                );
            }
        }
        
        // loop through each column mapping, and check the data to be
        // inserted into it against the column data type. we loop through
        // column mappings instead of the insert data to make sure that
        // all necessary columns are being inserted.
        foreach ($this->col as $col => $val) {
            
            // is the value allowed to be null?
            if (isset($val['require']) &&
                $val['require'] == true &&
                (! isset($data[$col]) || is_null($data[$col]))) {
                return $this->throwError(
                    DB_TABLE_ERR_INS_COL_REQUIRED,
                    "'$col'"
                );
            }
            
            // does the value to be inserted match the column data type?
            if (isset($data[$col]) &&
                ! $this->isValid($data[$col], $col)) {
                return $this->throwError(
                    DB_TABLE_ERR_INS_DATA_INVALID,
                    "'$col' ('$data[$col]')"
                );
            }
        }
        
        return true;
    }
    
    
    /**
     * Update table row or rows that match a custom WHERE clause
     *
     * Constructs and submits an SQL UPDATE command to update columns whose
     * names are keys in the $data array parameter, in all rows that match
     * the logical condition given by the $where string parameter.
     * 
     * If auto-recasting is enabled (if $this->_auto_recast), update() will
     * try, if necessary, to recast $data to proper column types, with recast().
     *
     * If auto-validation is enabled (if $this->_valid_insert), update() 
     * validates column types with validUpdate() before insertion.
     *
     * @param array $data An associative array of key-value pairs where the
     * key is the column name and the value is the column value. These are
     * the columns that will be updated with new values.
     * 
     * @param string $where An SQL WHERE clause limiting which records are
     * are to be updated.
     * 
     * @return mixed Void on success, a PEAR_Error object on failure.
     *
     * @throws PEAR_Error if:
     *     - Data fails type validation (bubbles error returned by validUpdate)
     *     - Error thrown by DB/MDB2::autoexecute()
     *
     * @access public
     * 
     * @see validUpdate()
     * @see DB::autoExecute()
     * @see MDB2::autoExecute()
     */
    function update($data, $where)
    {
        // forcibly recast the data elements to their proper types?
        if ($this->_auto_recast) {
            $this->recast($data);
        }
        
        // validate the data if auto-validation is turned on
        if ($this->_valid_update) {
            $result = $this->validUpdate($data);
            if (PEAR::isError($result)) {
                return $result;
            }
        }

        // Does a parent DB_Table_Database object exist?
        if ($this->_database) {
  
            $_database =& $this->_database;

            // Validate foreign key values (if enabled)
            if ($_database->_check_fkey) {
               $result = $_database->validForeignKeys($this->table, $data);
               if (PEAR::isError($result)) {
                   return $result;
               } 
            }
    
            // Implement any relevant ON UPDATE actions
            $result = $_database->onUpdateAction($this, $data, $where);
            if (PEAR::isError($result)) {
                return $result;
            }

        }
       
        // Submit update command 
        if ($this->backend == 'mdb2') {
            $result = $this->db->extended->autoExecute($this->table, $data,
                MDB2_AUTOQUERY_UPDATE, $where);
        } else {
            $result = $this->db->autoExecute($this->table, $data,
                DB_AUTOQUERY_UPDATE, $where);
        }
        return $result;

    }
    
    
    /**
     * Turns on (or off) automatic validation of updated data.
     * 
     * Enables (if $flag is true) or disables (if $flag is false) automatic 
     * validation of data types prior to updating rows in the database by
     * the {@link update()} method.
     *
     * @param  bool $flag True to turn on auto-validation, false to turn off.
     * @return void
     * @access public
     */
    function autoValidUpdate($flag = true)
    {
        if ($flag) {
            $this->_valid_update = true;
        } else {
            $this->_valid_update = false;
        }
    }
    
    
    /**
     * Validates an array for updating the table.
     * 
     * @param array $data An associative array of key-value pairs where
     * the key is the column name and the value is the column value.  This
     * is the data that will be inserted into the table.  Data is checked
     * against the column data type for validity.
     * 
     * @return mixed Boolean true on success (PEAR_Error object on failure)
     *
     * @throws PEAR_Error if
     *     - Invalid column name key in $data (DB_TABLE_ERR_UPD_COL_NOMAP)
     *     - Missing required column value    (DB_TABLE_ERR_UPD_COL_NOMAP)
     *     - Column value doesn't match type  (DB_TABLE_ERR_UPD_DATA_INVALID)
     *
     * @access public
     * 
     * @see update()
     */
    function validUpdate(&$data)
    {
        // loop through each data element, and check the
        // data to be updated against the column data type.
        foreach ($data as $col => $val) {
            
            // does the column exist?
            if (! isset($this->col[$col])) {
                return $this->throwError(
                    DB_TABLE_ERR_UPD_COL_NOMAP,
                    "('$col')"
                );
            }
            
            // the column definition
            $defn = $this->col[$col];
            
            // is it allowed to be null?
            if (isset($defn['require']) &&
                $defn['require'] == true &&
                isset($data[$col]) &&
                is_null($data[$col])) {
                return $this->throwError(
                    DB_TABLE_ERR_UPD_COL_REQUIRED,
                    $col
                );
            }
            
            // does the value to be inserted match the column data type?
            if (! $this->isValid($data[$col], $col)) {
                return $this->throwError(
                    DB_TABLE_ERR_UPD_DATA_INVALID,
                    "$col ('$data[$col]')"
                );
            }
        }
        
        return true;
    }
    
    
    /**
     * Deletes table rows matching a custom WHERE clause.
     * 
     * Constructs and submits and SQL DELETE command with the specified WHERE 
     * clause. Command is submitted by DB::query() or MDB2::exec().
     *
     * If a reference to a DB_Table_Database instance exists, carry out any
     * ON DELETE actions declared in that instance before actual insertion, 
     * if emulation of ON DELETE actions is enabled in that instance.
     *
     * @param string $where Logical condition in the WHERE clause of the 
     *                      delete command.
     *
     * @return mixed void on success (PEAR_Error on failure)
     *
     * @throws PEAR_Error if
     *     DB::query() or MDB2::exec() returns error (bubbles up)
     *
     * @access public
     * 
     * @see DB::query()
     * @see MDB2::exec()
     */
    function delete($where)
    {
        // Does a parent DB_Table_Database object exist?
        if ($this->_database) {
  
            $_database =& $this->_database;

            // Implement any relevant ON DELETE actions
            $result = $_database->onDeleteAction($this, $where);
            if (PEAR::isError($result)) {
                return $result;
            }

        }
       
        if ($this->backend == 'mdb2') {
            $result = $this->db->exec("DELETE FROM $this->table WHERE $where");
        } else {
            $result = $this->db->query("DELETE FROM $this->table WHERE $where");
        }
        return $result;
    }
    
    
    /**
     *
     * Generates and returns a sequence value.
     *
     * Generates a sequence value by calling the DB or MDB2::nextID() method. The
     * sequence name defaults to the table name, or may be specified explicitly.
     * 
     * @param  string  $seq_name The sequence name; defaults to table_id.
     * 
     * @return integer The next value in the sequence (PEAR_Error on failure)
     *
     * @throws PEAR_Error if
     *     Sequence name too long (>26 char + _seq) (DB_TABLE_ERR_SEQ_STRLEN)
     *
     * @access public
     * 
     * @see DB::nextID()
     * @see MDB2::nextID()
     */
    function nextID($seq_name = null)
    {
        if (is_null($seq_name)) {
            $seq_name = "{$this->table}";
        } else {
            $seq_name = "{$this->table}_{$seq_name}";
        }
        
        // the maximum length is 30, but PEAR DB/MDB2 will add "_seq" to the
        // name, so the max length here is less 4 chars. we have to
        // check here because the sequence will be created automatically
        // by PEAR DB/MDB2, which will not check for length on its own.
        if (strlen($seq_name) > 26) {
            return DB_Table::throwError(
                DB_TABLE_ERR_SEQ_STRLEN,
                " ('$seq_name')"
            );
            
        }
        return $this->db->nextId($seq_name);
    }
    
    
    /**
     * Escapes and enquotes a value for use in an SQL query.
     * 
     * Simple wrapper for DB_Common::quoteSmart() or MDB2::quote(), which 
     * returns the value of one of these functions. Helps makes user input 
     * safe against SQL injection attack.
     * 
     * @param mixed $val The value to be quoted
     *
     * @return string The value with quotes escaped, inside single quotes if 
     *                non-numeric.
     *
     * @throws PEAR_Error if
     *     DB_Common::quoteSmart() or MDB2::quote() returns Error (bubbled up)
     * 
     * @access public
     * 
     * @see DB_Common::quoteSmart()
     * @see MDB2::quote()
     */
    function quote($val)
    {
        if ($this->backend == 'mdb2') {
            $val = $this->db->quote($val);
        } else {
            $val = $this->db->quoteSmart($val);
        }
        return $val;
    }
    
    
    /**
     * Returns a blank row array based on the column map.
     * 
     * The array keys are the column names, and all values are set to null.
     * 
     * @return array An associative array where keys are column names and
     *               all values are null.
     * @access public
     */
    function getBlankRow()
    {
        $row = array();
        
        foreach ($this->col as $key => $val) {
            $row[$key] = null;
        }
        
        $this->recast($row);
        
        return $row;
    }
    
    
    /**
     * Turns on (or off) automatic recasting of insert and update data.
     * 
     * Turns on (if $flag is true) or off (if $flag is false) automatic forcible 
     * recasting of data to the declared data type, if required, prior to inserting 
     * or updating.  The recasting is done by calling the DB_Table::recast() 
     * method from within the DB_Table::insert() and DB_Table::update().
     * 
     * @param bool $flag True to automatically recast insert and update data,
     *                   false to not do so.
     * @return void
     * @access public
     */
    function autoRecast($flag = true)
    {
        if ($flag) {
            $this->_auto_recast = true;
        } else {
            $this->_auto_recast = false;
        }
    }
    
    
    /**
     * Forces array elements to the proper types for their columns.
     * 
     * This will not valiate the data, and will forcibly change the data
     * to match the recast-type.
     * 
     * The date, time, and timestamp recasting has special logic for
     * arrays coming from an HTML_QuickForm object so that the arrays
     * are converted into properly-formatted strings.
     * 
     * @todo If a column key holds an array of values (say from a multiple
     * select) then this method will not work properly; it will recast the
     * value to the string 'Array'.  Is this bad?
     * 
     * @param array   &$data The data array to re-cast.
     * 
     * @return void
     * 
     * @access public
     */
    function recast(&$data)
    {
        $keys = array_keys($data);
        
        $null_if_blank = array('date', 'time', 'timestamp', 'smallint',
            'integer', 'bigint', 'decimal', 'single', 'double');
        
        foreach ($keys as $key) {
        
            if (! isset($this->col[$key])) {
                continue;
            }
            
            unset($val);
            $val =& $data[$key];
            
            // convert blanks to null for non-character field types
            $convert = in_array($this->col[$key]['type'], $null_if_blank);
            if (is_array($val)) {  // if one of the given array values is
                                   // empty, null will be the new value if
                                   // the field is not required
                $tmp_val = implode('', $val);
                foreach ($val as $array_val) {
                    if (trim((string) $array_val) == '') {
                        $tmp_val = '';
                        break;
                    }
                }
            } else {
                $tmp_val = $val;
            }
            if ($convert && trim((string) $tmp_val) == '' && (
                !isset($this->col[$key]['require']) ||
                $this->col[$key]['require'] === false
              )
            ) {
                $val = null;
            }
            
            // skip explicit NULL values
            if (is_null($val)) {
                continue;
            }
            
            // otherwise, recast to the column type
            switch ($this->col[$key]['type']) {
            
            case 'boolean':
                $val = ($val) ? 1 : 0;
                break;
                
            case 'char':
            case 'varchar':
            case 'clob':
                settype($val, 'string');
                break;
                
            case 'date':

                // smart handling of non-standard (i.e. Y-m-d) date formats,
                // this allows to use two-digit years (y) and short (M) or
                // long (F) names of months without having to recast the
                // date value yourself
                if (is_array($val)) {
                    if (isset($val['y'])) {
                        $val['Y'] = $val['y'];
                    }
                    if (isset($val['F'])) {
                        $val['m'] = $val['F'];
                    }
                    if (isset($val['M'])) {
                        $val['m'] = $val['M'];
                    }
                }

                if (is_array($val) &&
                    isset($val['Y']) &&
                    isset($val['m']) &&
                    isset($val['d'])) {
                    
                    // the date is in HTML_QuickForm format,
                    // convert into a string
                    $y = (strlen($val['Y']) < 4)
                        ? str_pad($val['Y'], 4, '0', STR_PAD_LEFT)
                        : $val['Y'];
                    
                    $m = (strlen($val['m']) < 2)
                        ? '0'.$val['m'] : $val['m'];
                        
                    $d = (strlen($val['d']) < 2)
                        ? '0'.$val['d'] : $val['d'];
                        
                    $val = "$y-$m-$d";
                    
                } else {
                
                    // convert using the Date class
                    $tmp =& new DB_Table_Date($val);
                    $val = $tmp->format('%Y-%m-%d');
                    
                }
                
                break;
            
            case 'time':
            
                if (is_array($val) &&
                    isset($val['H']) &&
                    isset($val['i']) &&
                    isset($val['s'])) {
                    
                    // the time is in HTML_QuickForm format,
                    // convert into a string
                    $h = (strlen($val['H']) < 2)
                        ? '0' . $val['H'] : $val['H'];
                    
                    $i = (strlen($val['i']) < 2)
                        ? '0' . $val['i'] : $val['i'];
                        
                    $s = (strlen($val['s']) < 2)
                        ? '0' . $val['s'] : $val['s'];
                        
                        
                    $val = "$h:$i:$s";
                    
                } else {
                    // date does not matter in this case, so
                    // pre 1970 and post 2040 are not an issue.
                    $tmp = strtotime(date('Y-m-d') . " $val");
                    $val = date('H:i:s', $tmp);
                }
                
                break;
                
            case 'timestamp':

                // smart handling of non-standard (i.e. Y-m-d) date formats,
                // this allows to use two-digit years (y) and short (M) or
                // long (F) names of months without having to recast the
                // date value yourself
                if (is_array($val)) {
                    if (isset($val['y'])) {
                        $val['Y'] = $val['y'];
                    }
                    if (isset($val['F'])) {
                        $val['m'] = $val['F'];
                    }
                    if (isset($val['M'])) {
                        $val['m'] = $val['M'];
                    }
                }

                if (is_array($val) &&
                    isset($val['Y']) &&
                    isset($val['m']) &&
                    isset($val['d']) &&
                    isset($val['H']) &&
                    isset($val['i']) &&
                    isset($val['s'])) {
                    
                    // timestamp is in HTML_QuickForm format,
                    // convert each element to a string. pad
                    // with zeroes as needed.
                
                    $y = (strlen($val['Y']) < 4)
                        ? str_pad($val['Y'], 4, '0', STR_PAD_LEFT)
                        : $val['Y'];
                    
                    $m = (strlen($val['m']) < 2)
                        ? '0'.$val['m'] : $val['m'];
                        
                    $d = (strlen($val['d']) < 2)
                        ? '0'.$val['d'] : $val['d'];
                        
                    $h = (strlen($val['H']) < 2)
                        ? '0' . $val['H'] : $val['H'];
                    
                    $i = (strlen($val['i']) < 2)
                        ? '0' . $val['i'] : $val['i'];
                        
                    $s = (strlen($val['s']) < 2)
                        ? '0' . $val['s'] : $val['s'];
                        
                    $val = "$y-$m-$d $h:$i:$s";
                    
                } else {
                    // convert using the Date class
                    $tmp =& new DB_Table_Date($val);
                    $val = $tmp->format('%Y-%m-%d %H:%M:%S');
                }
                
                break;
            
            case 'smallint':
            case 'integer':
            case 'bigint':
                settype($val, 'integer');
                break;
            
            case 'decimal':
            case 'single':
            case 'double':
                settype($val, 'float');
                break;

            }
        }
    }
    
    
    /**
     * Creates the table based on $this->col and $this->idx.
     * 
     * @param string $flag The automatic table creation mode to pursue:
     * - 'safe' to create the table only if it does not exist
     * - 'drop' to drop any existing table with the same name and re-create it
     * 
     * @return mixed Boolean true if the table was successfully created,
     *               false if there was no need to create the table, or
     *               a PEAR_Error if the attempted creation failed.
     *
     * @throws PEAR_Error if
     *     - DB_Table_Manager::tableExists() returns Error (bubbles up)
     *     - DB_Table_Manager::create() returns Error (bubbles up)
     * 
     * @access public
     * 
     * @see DB_Table_Manager::tableExists()
     * @see DB_Table_Manager::create()
     */
    function create($flag)
    {
        include_once 'DB/Table/Manager.php';

        // are we OK to create the table?
        $ok = false;
        
        // check the create-flag
        switch ($flag) {

            case 'drop':
                // drop only if table exists
                $table_exists = DB_Table_Manager::tableExists($this->db,
                                                              $this->table);
                if (PEAR::isError($table_exists)) {
                    return $table_exists;
                }
                if ($table_exists) {
                    // forcibly drop an existing table
                    if ($this->backend == 'mdb2') {
                        $this->db->manager->dropTable($this->table);
                    } else {
                        $this->db->query("DROP TABLE {$this->table}");
                    }
                }
                $ok = true;
                break;

            case 'safe':
                // create only if table does not exist
                $table_exists = DB_Table_Manager::tableExists($this->db,
                                                              $this->table);
                if (PEAR::isError($table_exists)) {
                    return $table_exists;
                }
                // ok to create only if table does not exist
                $ok = !$table_exists;
                break;

        }

        // are we going to create the table?
        if (! $ok) {
            return false;
        }

        return DB_Table_Manager::create(
            $this->db, $this->table, $this->col, $this->idx
        );
    }
    
    
    /**
     * Alters the table to match schema declared in $this->col and $this->idx.
     *
     * If the table does not exist, create it instead.
     * 
     * @return boolean true if altering is successful (PEAR_Error on failure)
     *
     * @throws PEAR_Error if
     *     - DB_Table_Manager::tableExists() returns Error (bubbles up)
     *     - DB_Table_Manager::create() returns Error (bubbles up)
     *     - DB_Table_Manager::alter() returns Error (bubbles up)
     *
     * @access public
     *
     * @see DB_Table_Manager::tableExists()
     * @see DB_Table_Manager::create()
     * @see DB_Table_Manager::alter()
     */
    function alter()
    {
        $create = false;
        
        // alter the table columns and indexes if the table exists
        $table_exists = DB_Table_Manager::tableExists($this->db,
                                                      $this->table);
        if (PEAR::isError($table_exists)) {
            return $table_exists;
        }
        if (!$table_exists) {
            // table does not exist => just create the table, there is
            // nothing that could be altered
            $create = true;
        }

        if ($create) {
            return DB_Table_Manager::create(
                $this->db, $this->table, $this->col, $this->idx
            );
        }

        return DB_Table_Manager::alter(
            $this->db, $this->table, $this->col, $this->idx
        );
    }
    
    
    /**
     * Verifies the table based on $this->col and $this->idx.
     * 
     * @return boolean true if verification succees (PEAR_Error on failure).
     *
     * @throws PEAR_Error if
     *     DB_Table_Manager::verify() returns Error (bubbles up)
     *
     * @access public
     * 
     * @see DB_Table_Manager::verify()
     */
    function verify()
    {
        return DB_Table_Manager::verify(
            $this->db, $this->table, $this->col, $this->idx
        );
    }
    
    
    /**
     * Checks if a value validates against the DB_Table data type for a
     * given column. This only checks that it matches the data type; it
     * does not do extended validation.
     * 
     * @param array $val A value to check against the column's DB_Table
     * data type.
     * 
     * @param array $col A column name from $this->col.
     * 
     * @return boolean True if $val validates against data type, false if not
     *
     * @throws PEAR_Error if
     *     Invalid column type in $this->col (DB_TABLE_ERR_VALIDATE_TYPE)
     *
     * @access public
     * 
     * @see DB_Table_Valid
     */
    function isValid($val, $col)
    {
        // is the value null?
        if (is_null($val)) {
            // is the column required?
            if ($this->isRequired($col)) {
                // yes, so not valid
                return false;
            } else {
                // not required, so it's valid
                return true;
            }
        }
        
        // make sure we have the validation class
        include_once 'DB/Table/Valid.php';
        
        // validate values per the column type.  we use sqlite
        // as the single authentic list of allowed column types,
        // regardless of the actual rdbms being used.
        $map = array_keys($GLOBALS['_DB_TABLE']['type']['sqlite']);
        
        // is the column type on the map?
        if (! in_array($this->col[$col]['type'], $map)) {
            return $this->throwError(
                DB_TABLE_ERR_VALIDATE_TYPE,
                "'$col' ('{$this->col[$col]['type']}')"
            );
        }
        
        // validate for the type
        switch ($this->col[$col]['type']) {
        
        case 'char':
        case 'varchar':
            $result = DB_Table_Valid::isChar(
                $val,
                $this->col[$col]['size']
            );
            break;
        
        case 'decimal':
            $result = DB_Table_Valid::isDecimal(
                $val,
                $this->col[$col]['size'],
                $this->col[$col]['scope']
            );
            break;
            
        default:
            $result = call_user_func(
                array(
                    'DB_Table_Valid',
                    'is' . ucwords($this->col[$col]['type'])
                ),
                $val
            );
            break;

        }
        
        // have we passed the check so far, and should we
        // also check for allowed values?
        if ($result && isset($this->col[$col]['qf_vals'])) {
            $keys = array_keys($this->col[$col]['qf_vals']);
            
            $result = in_array(
                $val,
                array_keys($this->col[$col]['qf_vals'])
            );
        }
        
        return $result;
    }
    
    
    /**
     * Is a specific column required to be set and non-null?
     * 
     * @param mixed $column The column to check against.
     * @return boolean      True if required, false if not.
     * @access public
     */
    function isRequired($column)
    {
        if (isset($this->col[$column]['require']) &&
            $this->col[$column]['require'] == true) {
            return true;
        } else {
            return false;
        }
    }
    
    
    /**
     * 
     * Creates and returns a QuickForm object based on table columns.
     *
     * @param array $columns A sequential array of column names to use in
     * the form; if null, uses all columns.
     *
     * @param string $array_name By default, the form will use the names
     * of the columns as the names of the form elements.  If you pass
     * $array_name, the column names will become keys in an array named
     * for this parameter.
     * 
     * @param array $args An associative array of optional arguments to
     * pass to the QuickForm object.  The keys are...
     *
     * 'formName' : String, name of the form; defaults to the name of this
     * table.
     * 
     * 'method' : String, form method; defaults to 'post'.
     * 
     * 'action' : String, form action; defaults to
     * $_SERVER['REQUEST_URI'].
     * 
     * 'target' : String, form target target; defaults to '_self'
     * 
     * 'attributes' : Associative array, extra attributes for <form>
     * tag; the key is the attribute name and the value is attribute
     * value.
     * 
     * 'trackSubmit' : Boolean, whether to track if the form was
     * submitted by adding a special hidden field
     * 
     * @param string $clientValidate By default, validation will match
     * the 'qf_client' value from the column definition.  However, if
     * you set $clientValidate to true or false, this will override the
     * value from the column definition.
     *
     * @param array $formFilters An array with filter function names or
     * callbacks that will be applied to all form elements.
     *
     * @return object HTML_QuickForm
     * 
     * @access public
     *
     * @see HTML_QuickForm
     * @see DB_Table_QuickForm
     */
    function &getForm($columns = null, $array_name = null, $args = array(),
        $clientValidate = null, $formFilters = null)
    {
        include_once 'DB/Table/QuickForm.php';
        $coldefs = $this->_getFormColDefs($columns);
        $form =& DB_Table_QuickForm::getForm($coldefs, $array_name, $args,
            $clientValidate, $formFilters);
        return $form;
    }
    
    
    /**
     * Adds elements and rules to a pre-existing HTML_QuickForm object.
     * 
     * By default, the form will use the names of the columns as the names 
     * of the form elements.  If you pass $array_name, the column names 
     * will become keys in an array named for this parameter.
     *
     * @param object &$form      An HTML_QuickForm object.
     * 
     * @param array $columns     A sequential array of column names to use in
     *                           the form; if null, uses all columns.
     *
     * @param string $array_name Name of array of column names
     *
     * @param clientValidate
     * 
     * @return void
     * 
     * @access public
     * 
     * @see HTML_QuickForm
     * 
     * @see DB_Table_QuickForm
     */
    function addFormElements(&$form, $columns = null, $array_name = null,
        $clientValidate = null)
    {
        include_once 'DB/Table/QuickForm.php';
        $coldefs = $this->_getFormColDefs($columns);
        DB_Table_QuickForm::addElements($form, $coldefs, $array_name);
        DB_Table_QuickForm::addRules($form, $coldefs, $array_name, 
           $clientValidate);
    }


    /**
     * Adds static form elements like 'header', 'static', 'submit' or 'reset' 
     * to a pre-existing HTML_QuickForm object. The form elements needs to be
     * defined in a property called $frm.
     * 
     * @param object &$form An HTML_QuickForm object.
     * @return void
     * @access public
     * 
     * @see HTML_QuickForm
     * @see DB_Table_QuickForm
     */
    function addStaticFormElements(&$form)
    {
        include_once 'DB/Table/QuickForm.php';
        DB_Table_QuickForm::addStaticElements($form, $this->frm);
    }


    /**
     * 
     * Creates and returns an array of QuickForm elements based on an
     * array of DB_Table column names.
     * 
     * @param array $columns A sequential array of column names to use in
     * the form; if null, uses all columns.
     * 
     * @param string $array_name By default, the form will use the names
     * of the columns as the names of the form elements.  If you pass
     * $array_name, the column names will become keys in an array named
     * for this parameter.
     * 
     * @return array An array of HTML_QuickForm_Element objects.
     * 
     * @access public
     * 
     * @see HTML_QuickForm
     * @see DB_Table_QuickForm
     */
    function &getFormGroup($columns = null, $array_name = null)
    {
        include_once 'DB/Table/QuickForm.php';
        $coldefs = $this->_getFormColDefs($columns);
        $group =& DB_Table_QuickForm::getGroup($coldefs, $array_name);
        return $group;
    }
    
    
    /**
     * Creates and returns a single QuickForm element based on a DB_Table
     * column name.
     * 
     * @param string $column   A DB_Table column name.
     * @param string $elemname The name to use for the generated QuickForm
     *                         element.
     * 
     * @return object HTML_QuickForm_Element
     * 
     * @access public
     * 
     * @see HTML_QuickForm
     * @see DB_Table_QuickForm
     */
    
    function &getFormElement($column, $elemname)
    {
        include_once 'DB/Table/QuickForm.php';
        $coldef = $this->_getFormColDefs($column);
        DB_Table_QuickForm::fixColDef($coldef[$column], $elemname);
        $element =& DB_Table_QuickForm::getElement($coldef[$column],
            $elemname);
        return $element;
    }

    /**
     * Creates and returns an array of QuickForm elements based on a DB_Table
     * column name.
     * 
     * @author Ian Eure <ieure@php.net>
     * 
     * @param array $cols        Array of DB_Table column names
     * @param string $array_name The name to use for the generated QuickForm
     *                           elements.
     * @return object HTML_QuickForm_Element
     * 
     * @access public
     * 
     * @see HTML_QuickForm
     * @see DB_Table_QuickForm
     */
    function &getFormElements($cols, $array_name = null)
    {
        include_once 'DB/Table/QuickForm.php';
        $elements =& DB_Table_QuickForm::getElements($cols, $array_name);
        return $elements;
    }
    
    
    /**
     * Creates a column definition array suitable for DB_Table_QuickForm.
     * 
     * @param string|array $column_set A string column name, a sequential
     * array of columns names, or an associative array where the key is a
     * column name and the value is the default value for the generated
     * form element.  If null, uses all columns for this class.
     * 
     * @return array An array of column definitions suitable for passing
     *               to DB_Table_QuickForm.
     *
     * @access public
     * 
     */
    function _getFormColDefs($column_set = null)
    {
        if (is_null($column_set)) {
            // no columns or columns+values; just return the $this->col
            // array.
            return $this->getColumns($column_set);
        }
        
        // check to see if the keys are sequential integers.  if so,
        // the $column_set is just a list of columns.
        settype($column_set, 'array');
        $keys = array_keys($column_set);
        $all_integer = true;
        foreach ($keys as $val) {
            if (! is_integer($val)) {
                $all_integer = false;
                break;
            }
        }
        
        if ($all_integer) {
        
            // the column_set is just a list of columns; get back the $this->col
            // array elements matching this list.
            $coldefs = $this->getColumns($column_set);
            
        } else {
            
            // the columns_set is an associative array where the key is a
            // column name and the value is the form element value.
            $coldefs = $this->getColumns($keys);
            foreach ($coldefs as $key => $val) {
                $coldefs[$key]['qf_setvalue'] = $column_set[$key];
            }
            
        }
        
        return $coldefs;
    }

    /**
     * Returns XML string representation of the table
     *
     * @param  string $indent string of whitespace
     * @return string XML string
     * @access public
     */
    function toXML($indent = '') {
        require_once 'DB/Table/XML.php';
        $s = array();
        $s[] = DB_Table_XML::openTag('table', $indent);
        $s[] = DB_Table_XML::lineElement('name', $this->table, $indent);
        $s[] = DB_Table_XML::openTag('declaration', $indent);
        // Column declarations
        foreach ($this->col as $name => $col) {
            $type     = (isset($col['type'])) ? $col['type'] : null;
            $size     = (isset($col['size'])) ? $col['size'] : null;
            $scope    = (isset($col['scope'])) ? $col['scope'] : null;
            $require  = (isset($col['require'])) ? $col['require'] : null;
            $default  = (isset($col['set default'])) ? $col['set default'] : null;
            $line = '   ' . $name . '  ' . $type;
            $s[] = DB_Table_XML::openTag('field', $indent);
            $s[] = DB_Table_XML::lineElement('name', $name, $indent);
            $s[] = DB_Table_XML::lineElement('type', $type, $indent);
            if ($size) {
                $s[] = DB_Table_XML::lineElement('length', $size, $indent);
            }
            if ($require) {
                $require = (int) $require;
                $s[] = DB_Table_XML::lineElement('notnull', $require, $indent);
            }
            if (!($default === null)) {
               $s[] = DB_Table_XML::lineElement('set default', $default, $indent);
            }
            if ($this->auto_inc_col == $name) {
               $s[] = DB_Table_XML::lineElement('autoincrement', '1', $indent);
            }
            $s[] = DB_Table_XML::closeTag('field', $indent);
        }
        // Index declarations
        foreach ($this->idx as $name => $idx) {
            $s[] = DB_Table_XML::openTag('index', $indent);
            $cols = $idx['cols'];
            $type = $idx['type'];
            if (is_string($name)) {
                $s[] = DB_Table_XML::lineElement('name', $name, $indent);
            }
            if ($type == 'primary') {
                $s[] = DB_Table_XML::lineElement('primary', '1', $indent);
            } elseif ($type == 'unique') {
                $s[] = DB_Table_XML::lineElement('unique', '1', $indent);
            }
            if (is_string($cols)) {
                $cols = array($cols);
            }
            foreach ($cols as $col) {
                $s[] = DB_Table_XML::lineElement('field', $col, $indent);
            }
            $s[] = DB_Table_XML::closeTag('index', $indent);
        }
        // Foreign key references (if $this->_database is not null)
        if ($this->_database) {
            if (isset($this->_database->_ref[$this->table])) {
                $refs = $this->_database->_ref[$this->table];
                foreach ($refs as $rtable => $def) {
                    $fkey = $def['fkey']; // foreign key of referencing table
                    $rkey = $def['rkey']; // referenced/primary key
                    if (is_string($fkey)) {
                        $fkey = array($fkey);
                    }
                    if (is_string($rkey)) {
                        $rkey = array($rkey);
                    }
                    $on_delete = $def['on_delete']; // on-delete action
                    $on_update = $def['on_update']; // on-update action
                    $s[] = DB_Table_XML::openTag('foreign', $indent);
                    foreach ($fkey as $fcol) {
                        $s[] = DB_Table_XML::lineElement('field', $fcol, $indent);
                    }
                    $s[] = DB_Table_XML::openTag('references', $indent);
                    $s[] = DB_Table_XML::lineElement('table', $rtable, $indent);
                    if ($rkey) {
                        foreach ($rkey as $rcol) {
                            $s[] = DB_Table_XML::lineElement('field', $rcol,
                                                             $indent);
                        }
                    }
                    $s[] = DB_Table_XML::closeTag('references', $indent);
                    if ($on_delete) {
                        $s[] = DB_Table_XML::lineElement('ondelete', $on_delete,
                                                         $indent);
                    }
                    if ($on_update) {
                        $s[] = DB_Table_XML::lineElement('onupdate', $on_update,
                                                         $indent);
                    }
                    $s[] = DB_Table_XML::closeTag('foreign', $indent);
                }
            }
        }
        $s[] = DB_Table_XML::closeTag('declaration', $indent);
        $s[] = DB_Table_XML::closeTag('table', $indent);
        return implode("\n", $s);
    }

}
?>
