Merge branch 'db-migrations'
authorChuck Hagenbuch <chuck@horde.org>
Sun, 13 Dec 2009 17:06:35 +0000 (12:06 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Sun, 13 Dec 2009 17:06:35 +0000 (12:06 -0500)
Conflicts:
framework/Db/lib/Horde/Db/Adapter/Base/TableDefinition.php
framework/Db/test/Horde/Db/AllTests.php

1  2 
framework/Db/lib/Horde/Db/Adapter/Base.php
framework/Db/lib/Horde/Db/Adapter/Base/Column.php
framework/Db/lib/Horde/Db/Adapter/Base/TableDefinition.php
framework/Db/lib/Horde/Db/Adapter/Sqlite/Column.php
framework/Db/lib/Horde/Db/Adapter/Sqlite/Schema.php
framework/Db/test/Horde/Db/Adapter/Pdo/SqliteTest.php

index 1d05a68,0000000..59d72a7
mode 100644,000000..100644
--- /dev/null
@@@ -1,623 -1,0 +1,623 @@@
-             . (empty($runtime) ? '' : " ($runtime ms)");
 +<?php
 +/**
 + * Copyright 2007 Maintainable Software, LLC
 + * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
 + *
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
 +
 +/**
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
 +abstract class Horde_Db_Adapter_Base
 +{
 +    /**
 +     * Config options
 +     * @var array
 +     */
 +    protected $_config = array();
 +
 +    /**
 +     * @var mixed
 +     */
 +    protected $_connection = null;
 +
 +    /**
 +     * @var boolean
 +     */
 +    protected $_transactionStarted = false;
 +
 +    /**
 +     * @var int
 +     */
 +    protected $_rowCount = null;
 +
 +    /**
 +     * @var int
 +     */
 +    protected $_runtime = null;
 +
 +    /**
 +     * @var boolean
 +     */
 +    protected $_active = null;
 +
 +    /**
 +     * @var Cache object
 +     */
 +    protected $_cache = null;
 +
 +    /**
 +     * @var Logger
 +     */
 +    protected $_logger = null;
 +
 +    /**
 +     * @var Horde_Db_Adapter_Base_Schema
 +     */
 +    protected $_schema = null;
 +
 +    /**
 +     * @var string
 +     */
 +    protected $_schemaClass = null;
 +
 +    /**
 +     * @var array
 +     */
 +    protected $_schemaMethods = array();
 +
 +
 +    /*##########################################################################
 +    # Construct/Destruct
 +    ##########################################################################*/
 +
 +    /**
 +     * @param array $config Configuration options and optional objects (logger,
 +     * cache, etc.)
 +     */
 +    public function __construct($config)
 +    {
 +        // Create a stub if we don't have a useable cache.
 +        if (isset($config['cache'])
 +            && is_callable(array($config['cache'], 'get'))
 +            && is_callable(array($config['cache'], 'set'))) {
 +            $this->_cache = $config['cache'];
 +            unset($config['cache']);
 +        } else {
 +            $this->_cache = new Horde_Support_Stub;
 +        }
 +
 +        // Create a stub if we don't have a useable logger.
 +        if (isset($config['logger'])
 +            && is_callable(array($config['logger'], 'log'))) {
 +            $this->_logger = $config['logger'];
 +            unset($config['logger']);
 +        } else {
 +            $this->_logger = new Horde_Support_Stub;
 +        }
 +
 +        // Default to UTF-8
 +        if (!isset($config['charset'])) {
 +            $config['charset'] = 'UTF-8';
 +        }
 +
 +        $this->_config  = $config;
 +        $this->_runtime = 0;
 +
 +        // Create the database-specific (but not adapter specific) schema
 +        // object.
 +        if (!$this->_schemaClass)
 +            $this->_schemaClass = get_class($this).'_Schema';
 +        $this->_schema = new $this->_schemaClass($this, array(
 +            'cache' => $this->_cache,
 +            'logger' => $this->_logger));
 +        $this->_schemaMethods = array_flip(get_class_methods($this->_schema));
 +
 +        $this->connect();
 +    }
 +
 +    /**
 +     * Free any resources that are open.
 +     */
 +    public function __destruct()
 +    {
 +        $this->disconnect();
 +    }
 +
 +
 +    /*##########################################################################
 +    # Object factory
 +    ##########################################################################*/
 +
 +    /**
 +     * Delegate calls to the schema object.
 +     *
 +     * @param  string  $method
 +     * @param  array   $args
 +     */
 +    public function componentFactory($component, $args)
 +    {
 +        $class = str_replace('_Schema', '', $this->_schemaClass) . '_' . $component;
 +        if (class_exists($class)) {
 +            $class = new ReflectionClass($class);
 +        } else {
 +            $class = new ReflectionClass('Horde_Db_Adapter_Base_' . $component);
 +        }
 +
 +        return $class->newInstanceArgs($args);
 +    }
 +
 +
 +    /*##########################################################################
 +    # Object composition
 +    ##########################################################################*/
 +
 +    /**
 +     * Delegate calls to the schema object.
 +     *
 +     * @param  string  $method
 +     * @param  array   $args
 +     */
 +    public function __call($method, $args)
 +    {
 +        if (isset($this->_schemaMethods[$method])) {
 +            return call_user_func_array(array($this->_schema, $method), $args);
 +        }
 +
 +        throw new BadMethodCallException('Call to undeclared method "'.$method.'"');
 +    }
 +
 +
 +    /*##########################################################################
 +    # Public
 +    ##########################################################################*/
 +
 +    /**
 +     * Returns the human-readable name of the adapter.  Use mixed case - one
 +     * can always use downcase if needed.
 +     *
 +     * @return  string
 +     */
 +    public function adapterName()
 +    {
 +        return 'Base';
 +    }
 +
 +    /**
 +     * Does this adapter support migrations?  Backend specific, as the
 +     * abstract adapter always returns +false+.
 +     *
 +     * @return  boolean
 +     */
 +    public function supportsMigrations()
 +    {
 +        return false;
 +    }
 +
 +    /**
 +     * Does this adapter support using DISTINCT within COUNT?  This is +true+
 +     * for all adapters except sqlite.
 +     *
 +     * @return  boolean
 +     */
 +    public function supportsCountDistinct()
 +    {
 +        return true;
 +    }
 +
 +    /**
 +     * Should primary key values be selected from their corresponding
 +     * sequence before the insert statement?  If true, next_sequence_value
 +     * is called before each insert to set the record's primary key.
 +     * This is false for all adapters but Firebird.
 +     */
 +    public function prefetchPrimaryKey($tableName = null)
 +    {
 +        return false;
 +    }
 +
 +    /**
 +     * Reset the timer
 +     *
 +     * @return  int
 +     */
 +    public function resetRuntime()
 +    {
 +        $runtime = $this->_runtime;
 +        $this->_runtime = 0;
 +        return $this->_runtime;
 +    }
 +
 +
 +    /*##########################################################################
 +    # Connection Management
 +    ##########################################################################*/
 +
 +    /**
 +     * Connect to the db
 +     */
 +    abstract public function connect();
 +
 +    /**
 +     * Is the connection active
 +     *
 +     * @return  boolean
 +     */
 +    public function isActive()
 +    {
 +        return $this->_active;
 +    }
 +
 +    /**
 +     * Reconnect to the db
 +     */
 +    public function reconnect()
 +    {
 +        $this->disconnect();
 +        $this->connect();
 +    }
 +
 +    /**
 +     * Disconnect from db
 +     */
 +    public function disconnect()
 +    {
 +        $this->_connection = null;
 +        $this->_active = false;
 +    }
 +
 +    /**
 +     * Provides access to the underlying database connection. Useful for when
 +     * you need to call a proprietary method such as postgresql's lo_* methods
 +     *
 +     * @return  resource
 +     */
 +    public function rawConnection()
 +    {
 +        return $this->_connection;
 +    }
 +
 +
 +    /*##########################################################################
 +    # Database Statements
 +    ##########################################################################*/
 +
 +    /**
 +     * Returns an array of records with the column names as keys, and
 +     * column values as values.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     * @return  Traversable
 +     */
 +    public function select($sql, $arg1 = null, $arg2 = null)
 +    {
 +        return $this->execute($sql, $arg1, $arg2);
 +    }
 +
 +    /**
 +     * Returns an array of record hashes with the column names as keys and
 +     * column values as values.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function selectAll($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $rows = array();
 +        $result = $this->select($sql, $arg1, $arg2);
 +        if ($result) {
 +            foreach ($result as $row) {
 +                $rows[] = $row;
 +            }
 +        }
 +        return $rows;
 +    }
 +
 +    /**
 +     * Returns a record hash with the column names as keys and column values
 +     * as values.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     * @return  array
 +     */
 +    public function selectOne($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $result = $this->selectAll($sql, $arg1, $arg2);
 +        return $result ? next($result) : array();
 +    }
 +
 +    /**
 +     * Returns a single value from a record
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     * @return  string
 +     */
 +    public function selectValue($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $result = $this->selectOne($sql, $arg1, $arg2);
 +        return $result ? next($result) : null;
 +    }
 +
 +    /**
 +     * Returns an array of the values of the first column in a select:
 +     *   selectValues("SELECT id FROM companies LIMIT 3") => [1,2,3]
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function selectValues($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $result = $this->selectAll($sql, $arg1, $arg2);
 +        $values = array();
 +        foreach ($result as $row) {
 +            $values[] = next($row);
 +        }
 +        return $values;
 +    }
 +
 +    /**
 +     * Returns an array where the keys are the first column of a select, and the
 +     * values are the second column:
 +     *
 +     *   selectAssoc("SELECT id, name FROM companies LIMIT 3") => [1 => 'Ford', 2 => 'GM', 3 => 'Chrysler']
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function selectAssoc($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $result = $this->selectAll($sql, $arg1, $arg2);
 +        $values = array();
 +        foreach ($result as $row) {
 +            $values[next($row)] = next($row);
 +        }
 +        return $values;
 +    }
 +
 +    /**
 +     * Executes the SQL statement in the context of this connection.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function execute($sql, $arg1 = null, $arg2 = null)
 +    {
 +        if (is_array($arg1)) {
 +            $sql = $this->_replaceParameters($sql, $arg1);
 +            $name = $arg2;
 +        } else {
 +            $name = $arg1;
 +        }
 +
 +        $t = new Horde_Support_Timer;
 +        $t->push();
 +
 +        try {
 +            $stmt = $this->_connection->query($sql);
 +        } catch (Exception $e) {
 +            $this->_logInfo($sql, 'QUERY FAILED: ' . $e->getMessage());
 +            $this->_logInfo($sql, $name);
 +            throw new Horde_Db_Exception((string)$e->getMessage(), (int)$e->getCode());
 +        }
 +
 +        $this->_logInfo($sql, $name, $t->pop());
 +
 +        $this->_rowCount = $stmt ? $stmt->rowCount() : 0;
 +        return $stmt;
 +    }
 +
 +    /**
 +     * Returns the last auto-generated ID from the affected table.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     * @param   string  $pk
 +     * @param   int     $idValue
 +     * @param   string  $sequenceName
 +     */
 +    public function insert($sql, $arg1 = null, $arg2 = null, $pk = null, $idValue = null, $sequenceName = null)
 +    {
 +        $this->execute($sql, $arg1, $arg2);
 +        return isset($idValue) ? $idValue : $this->_connection->lastInsertId();
 +    }
 +
 +    /**
 +     * Executes the update statement and returns the number of rows affected.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function update($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $this->execute($sql, $arg1, $arg2);
 +        return $this->_rowCount;
 +    }
 +
 +    /**
 +     * Executes the delete statement and returns the number of rows affected.
 +     *
 +     * @param   string  $sql
 +     * @param   mixed   $arg1  Either an array of bound parameters or a query name.
 +     * @param   string  $arg2  If $arg1 contains bound parameters, the query name.
 +     */
 +    public function delete($sql, $arg1 = null, $arg2 = null)
 +    {
 +        $this->execute($sql, $arg1, $arg2);
 +        return $this->_rowCount;
 +    }
 +
 +    /**
 +     * Check if a transaction has been started
 +     */
 +    public function transactionStarted()
 +    {
 +        return $this->_transactionStarted;
 +    }
 +
 +    /**
 +     * Begins the transaction (and turns off auto-committing).
 +     */
 +    public function beginDbTransaction()
 +    {
 +        $this->_transactionStarted = true;
 +        $this->_connection->beginTransaction();
 +    }
 +
 +    /**
 +     * Commits the transaction (and turns on auto-committing).
 +     */
 +    public function commitDbTransaction()
 +    {
 +        $this->_connection->commit();
 +        $this->_transactionStarted = false;
 +    }
 +
 +    /**
 +     * Rolls back the transaction (and turns on auto-committing). Must be
 +     * done if the transaction block raises an exception or returns false.
 +     */
 +    public function rollbackDbTransaction()
 +    {
 +        if (! $this->_transactionStarted) { return; }
 +
 +        $this->_connection->rollBack();
 +        $this->_transactionStarted = false;
 +    }
 +
 +    /**
 +     * Appends +LIMIT+ and +OFFSET+ options to a SQL statement.
 +     *
 +     * @param   string  $sql
 +     * @param   array   $options
 +     * @return  string
 +     */
 +    public function addLimitOffset($sql, $options)
 +    {
 +        if (isset($options['limit']) && $limit = $options['limit']) {
 +            if (isset($options['offset']) && $offset = $options['offset']) {
 +                $sql .= " LIMIT $offset, $limit";
 +            } else {
 +                $sql .= " LIMIT $limit";
 +            }
 +        }
 +        return $sql;
 +    }
 +
 +    public function sanitizeLimit($limit)
 +    {
 +        if (strpos($limit, ',') !== false) {
 +            return implode(',', array_map(create_function('$i', 'return (int)$i;'), explode(',', $limit)));
 +        } else return (int)$limit;
 +    }
 +
 +    /**
 +     * Appends a locking clause to an SQL statement.
 +     * This method *modifies* the +sql+ parameter.
 +     *   # SELECT * FROM suppliers FOR UPDATE
 +     *   add_lock! 'SELECT * FROM suppliers', :lock => true
 +     *   add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
 +     */
 +    public function addLock(&$sql, $options = array())
 +    {
 +        if (isset($options['lock']) && is_string($options['lock'])) {
 +            $sql .= ' ' . $lock;
 +        } else {
 +            $sql .= ' FOR UPDATE';
 +        }
 +    }
 +
 +    /**
 +     * Inserts the given fixture into the table. Overridden in adapters that
 +     * require something beyond a simple insert (eg. Oracle).
 +     */
 +    public function insertFixture($fixture, $tableName)
 +    {
 +        /*@TODO*/
 +        return $this->execute("INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert');
 +    }
 +
 +    public function emptyInsertStatement($tableName)
 +    {
 +        return 'INSERT INTO '.$this->quoteTableName($tableName).' VALUES(DEFAULT)';
 +    }
 +
 +
 +    /*##########################################################################
 +    # Protected
 +    ##########################################################################*/
 +
 +    /**
 +     * Replace ? in a SQL statement with quoted values from $args
 +     *
 +     * @param   string  $sql
 +     * @param   array   $args
 +     */
 +    protected function _replaceParameters($sql, $args)
 +    {
 +        $paramCount = substr_count($sql, '?');
 +        if (count($args) != $paramCount) {
 +            throw new Horde_Db_Exception('Parameter count mismatch');
 +        }
 +
 +        $sqlPieces = explode('?', $sql);
 +        $sql = array_shift($sqlPieces);
 +        while (count($sqlPieces)) {
 +            $sql .= $this->quote(array_shift($args)) . array_shift($sqlPieces);
 +        }
 +        return $sql;
 +    }
 +
 +    /**
 +     * Logs the SQL query for debugging.
 +     *
 +     * @param   string  $sql
 +     * @param   string  $name
 +     * @param   float   $runtime
 +     */
 +    protected function _logInfo($sql, $name, $runtime = null)
 +    {
 +        /*@TODO */
 +        $name = (empty($name) ? '' : $name)
++              . (empty($runtime) ? '' : sprintf(" (%.4fs)", $runtime));
 +        $this->_logger->info($this->_formatLogEntry($name, $sql));
 +    }
 +
 +    /**
 +     * Formats the log entry.
 +     *
 +     * @param   string  $message
 +     * @param   string  $sql
 +     */
 +    protected function _formatLogEntry($message, $sql)
 +    {
 +        $sql = preg_replace("/\s+/", ' ', $sql);
 +        $sql = "\n\t".wordwrap($sql, 70, "\n\t  ", 1);
 +        return "SQL $message  $sql";
 +    }
 +
 +}
index f2f0c51,0000000..5d0ce69
mode 100644,000000..100644
--- /dev/null
@@@ -1,359 -1,0 +1,360 @@@
-         $this->_type      = $this->_simplifiedType($sqlType);
 +<?php
 +/**
 + * Copyright 2007 Maintainable Software, LLC
 + * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
 + *
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
 +
 +/**
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
 +class Horde_Db_Adapter_Base_Column
 +{
 +    protected $_name;
 +    protected $_type;
 +    protected $_null;
 +    protected $_limit;
 +    protected $_precision;
 +    protected $_scale;
 +    protected $_default;
 +    protected $_sqlType;
 +    protected $_isText;
 +    protected $_isNumber;
 +
 +
 +    /*##########################################################################
 +    # Construct/Destruct
 +    ##########################################################################*/
 +
 +    /**
 +     * Construct
 +     *
 +     * @param   string  $name     The column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
 +     * @param   string  $default  The type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
 +     * @param   string  $sqlType  Only used to extract the column's length, if necessary. For example +60+ in <tt>company_name varchar(60)</tt>.
 +     * @param   boolean $null     Determines if this column allows +NULL+ values.
 +     */
 +    public function __construct($name, $default, $sqlType = null, $null = true)
 +    {
 +        $this->_name      = $name;
 +        $this->_sqlType   = $sqlType;
 +        $this->_null      = $null;
++
++        $this->_type      = $this->_simplifiedType($sqlType);
++        $this->_isText    = $this->_type == 'text'  || $this->_type == 'string';
++        $this->_isNumber  = $this->_type == 'float' || $this->_type == 'integer' || $this->_type == 'decimal';
++
 +        $this->_limit     = $this->_extractLimit($sqlType);
 +        $this->_precision = $this->_extractPrecision($sqlType);
 +        $this->_scale     = $this->_extractScale($sqlType);
-         $this->_isText    = $this->_type == 'text'  || $this->_type == 'string';
-         $this->_isNumber  = $this->_type == 'float' || $this->_type == 'integer' || $this->_type == 'decimal';
 +        $this->_default   = $this->extractDefault($default);
 +    }
 +
 +    /**
 +     * @return  boolean
 +     */
 +    public function isText()
 +    {
 +        return $this->_isText;
 +    }
 +
 +    /**
 +     * @return  boolean
 +     */
 +    public function isNumber()
 +    {
 +        return $this->_isNumber;
 +    }
 +
 +    /**
 +     * Casts value (which is a String) to an appropriate instance.
 +     */
 +    public function typeCast($value)
 +    {
 +        if ($value === null) return null;
 +
 +        switch ($this->_type) {
 +        case 'string':
 +        case 'text':
 +            return $value;
 +        case 'integer':
 +            return strlen($value) ? (int)$value : null;
 +        case 'float':
 +            return strlen($value) ? (float)$value : null;
 +        case 'decimal':
 +            return $this->valueToDecimal($value);
 +        case 'datetime':
 +        case 'timestamp':
 +            return $this->stringToTime($value);
 +        case 'time':
 +            return $this->stringToDummyTime($value);
 +        case 'date':
 +            return $this->stringToDate($value);
 +        case 'binary':
 +            return $this->binaryToString($value);
 +        case 'boolean':
 +            return $this->valueToBoolean($value);
 +        default:
 +            return $value;
 +        }
 +    }
 +
 +    public function extractDefault($default)
 +    {
 +        return $this->typeCast($default);
 +    }
 +
 +
 +    /*##########################################################################
 +    # Accessor
 +    ##########################################################################*/
 +
 +    /**
 +     * @return  string
 +     */
 +    public function getName()
 +    {
 +        return $this->_name;
 +    }
 +
 +    /**
 +     * @return  string
 +     */
 +    public function getDefault()
 +    {
 +        return $this->_default;
 +    }
 +
 +    /**
 +     * @return  string
 +     */
 +    public function getType()
 +    {
 +        return $this->_type;
 +    }
 +
 +    /**
 +     * @return  int
 +     */
 +    public function getLimit()
 +    {
 +        return $this->_limit;
 +    }
 +
 +    /**
 +     * @return  int
 +     */
 +    public function precision()
 +    {
 +        return $this->_precision;
 +    }
 +
 +    /**
 +     * @return  int
 +     */
 +    public function scale()
 +    {
 +        return $this->_scale;
 +    }
 +
 +    /**
 +     * @return  boolean
 +     */
 +    public function isNull()
 +    {
 +        return $this->_null;
 +    }
 +
 +    /**
 +     * @return  string
 +     */
 +    public function getSqlType()
 +    {
 +        return $this->_sqlType;
 +    }
 +
 +
 +    /*##########################################################################
 +    # Type Juggling
 +    ##########################################################################*/
 +
 +    /**
 +     * Used to convert from Strings to BLOBs
 +     *
 +     * @return  string
 +     */
 +    public function stringToBinary($value)
 +    {
 +        return $value;
 +    }
 +
 +    /**
 +     * Used to convert from BLOBs to Strings
 +     *
 +     * @return  string
 +     */
 +    public function binaryToString($value)
 +    {
 +        return $value;
 +    }
 +
 +    /**
 +     * @param   string  $string
 +     * @return  Horde_Date
 +     */
 +    public function stringToDate($string)
 +    {
 +        if (empty($string) ||
 +            // preserve '0000-00-00' (http://bugs.php.net/bug.php?id=45647)
 +            preg_replace('/[^\d]/', '', $string) == 0) {
 +            return null;
 +        }
 +
 +        $d = new Horde_Date($string);
 +        $d->setDefaultFormat('Y-m-d');
 +
 +        return $d;
 +    }
 +
 +    /**
 +     * @param   string  $string
 +     * @return  Horde_Date
 +     */
 +    public function stringToTime($string)
 +    {
 +        if (empty($string) ||
 +            // preserve '0000-00-00 00:00:00' (http://bugs.php.net/bug.php?id=45647)
 +            preg_replace('/[^\d]/', '', $string) == 0) {
 +            return null;
 +        }
 +
 +        return new Horde_Date($string);
 +    }
 +
 +    /**
 +     * @TODO Return a Horde_Date object instead?
 +     *
 +     * @param   string  $string
 +     * @return  Horde_Date
 +     */
 +    public function stringToDummyTime($value)
 +    {
 +        if (empty($string)) {
 +            return null;
 +        }
 +        return $this->stringToTime('2000-01-01 ' . $string);
 +    }
 +
 +    /**
 +     * @param   mixed  $value
 +     * @return  boolean
 +     */
 +    public function valueToBoolean($value)
 +    {
 +        if ($value === true || $value === false) {
 +            return $value;
 +        }
 +
 +        $value = strtolower($value);
 +        return $value == 'true' || $value == 't' || $value == '1';
 +    }
 +
 +    /**
 +     * @param   mixed  $value
 +     * @return  decimal
 +     */
 +    public function valueToDecimal($value)
 +    {
 +        return (float)$value;
 +    }
 +
 +
 +    /*##########################################################################
 +    # Protected
 +    ##########################################################################*/
 +
 +    /**
 +     * @param   string  $sqlType
 +     * @return  int
 +     */
 +    protected function _extractLimit($sqlType)
 +    {
 +        if (preg_match("/\((.*)\)/", $sqlType, $matches)) {
 +            return (int)$matches[1];
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * @param   string  $sqlType
 +     * @return  int
 +     */
 +    protected function _extractPrecision($sqlType)
 +    {
 +        if (preg_match("/^(numeric|decimal|number)\((\d+)(,\d+)?\)/i", $sqlType, $matches)) {
 +            return (int)$matches[2];
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * @param   string  $sqlType
 +     * @return  int
 +     */
 +    protected function _extractScale($sqlType)
 +    {
 +        switch (true) {
 +            case preg_match("/^(numeric|decimal|number)\((\d+)\)/i", $sqlType):
 +                return 0;
 +            case preg_match("/^(numeric|decimal|number)\((\d+)(,(\d+))\)/i",
 +                            $sqlType, $match):
 +                return (int)$match[4];
 +        }
 +    }
 +
 +    /**
 +     * @param   string  $fieldType
 +     * @return  string
 +     */
 +    protected function _simplifiedType($fieldType)
 +    {
 +        switch (true) {
 +            case preg_match('/int/i', $fieldType):
 +                return 'integer';
 +            case preg_match('/float|double/i', $fieldType):
 +                return 'float';
 +            case preg_match('/decimal|numeric|number/i', $fieldType):
 +                return $this->_scale == 0 ? 'integer' : 'decimal';
 +            case preg_match('/datetime/i', $fieldType):
 +                return 'datetime';
 +            case preg_match('/timestamp/i', $fieldType):
 +                return 'timestamp';
 +            case preg_match('/time/i', $fieldType):
 +                return 'time';
 +            case preg_match('/date/i', $fieldType):
 +                return 'date';
 +            case preg_match('/clob|text/i', $fieldType):
 +                return 'text';
 +            case preg_match('/blob|binary/i', $fieldType):
 +                return 'binary';
 +            case preg_match('/char|string/i', $fieldType):
 +                return 'string';
 +            case preg_match('/boolean/i', $fieldType):
 +                return 'boolean';
 +        }
 +    }
 +
 +}
index a5af543,0000000..e27ee1b
mode 100644,000000..100644
--- /dev/null
@@@ -1,235 -1,0 +1,245 @@@
- class Horde_Db_Adapter_Base_TableDefinition implements ArrayAccess
 +<?php
 +/**
 + * Copyright 2007 Maintainable Software, LLC
 + * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
 + *
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
 +
 +/**
 + * @author     Mike Naberezny <mike@maintainable.com>
 + * @author     Derek DeVries <derek@maintainable.com>
 + * @author     Chuck Hagenbuch <chuck@horde.org>
 + * @license    http://opensource.org/licenses/bsd-license.php
 + * @category   Horde
 + * @package    Horde_Db
 + * @subpackage Adapter
 + */
-     /**
++class Horde_Db_Adapter_Base_TableDefinition implements ArrayAccess, IteratorAggregate
 +{
 +    protected $_name    = null;
 +    protected $_base    = null;
 +    protected $_options = null;
 +    protected $_columns = null;
 +
 +    /**
 +     * Class Constructor
 +     *
 +     * @param  string  $name
 +     * @param  Horde_Db_Adapter_Base_Schema  $base
 +     * @param  array   $options
 +     */
 +    public function __construct($name, $base, $options=array())
 +    {
 +        $this->_name    = $name;
 +        $this->_base    = $base;
 +        $this->_options = $options;
 +        $this->_columns = array();
 +    }
 +
 +    /**
 +     * @return  string
 +     */
 +    public function getName()
 +    {
 +        return $this->_name;
 +    }
 +
 +    /**
 +     * @return  array
 +     */
 +    public function getOptions()
 +    {
 +        return $this->_options;
 +    }
 +
++    /**v
 +     * @param   string  $name
 +     */
 +    public function primaryKey($name)
 +    {
 +        $natives = $this->_native();
 +        $this->column($name, $natives['primaryKey']);
 +    }
 +
 +    /**
 +     * Instantiates a new column for the table.
 +     * The +type+ parameter must be one of the following values:
 +     * <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
 +     * <tt>:integer</tt>, <tt>:float</tt>, <tt>:datetime</tt>,
 +     * <tt>:timestamp</tt>, <tt>:time</tt>, <tt>:date</tt>,
 +     * <tt>:binary</tt>, <tt>:boolean</tt>.
 +     *
 +     * Available options are (none of these exists by default):
 +     * * <tt>:limit</tt>:
 +     *   Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
 +     *   <tt>:binary</tt> or <tt>:integer</tt> columns only)
 +     * * <tt>:default</tt>:
 +     *   The column's default value.  You cannot explicitely set the default
 +     *   value to +NULL+.  Simply leave off this option if you want a +NULL+
 +     *   default value.
 +     * * <tt>:null</tt>:
 +     *   Allows or disallows +NULL+ values in the column.  This option could
 +     *   have been named <tt>:null_allowed</tt>.
 +     *
 +     * This method returns <tt>self</tt>.
 +     *
 +     * ===== Examples
 +     *  # Assuming def is an instance of TableDefinition
 +     *  def.column(:granted, :boolean)
 +     *    #=> granted BOOLEAN
 +     *
 +     *  def.column(:picture, :binary, :limit => 2.megabytes)
 +     *    #=> picture BLOB(2097152)
 +     *
 +     *  def.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
 +     *    #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
 +     *
 +     * @return  TableDefinition
 +     */
 +    public function column($name, $type, $options=array())
 +    {
 +        if ($this[$name]) {
 +            $column = $this[$name];
 +        } else {
 +            $column = $this->_base->componentFactory('ColumnDefinition', array(
 +                $this->_base, $name, $type));
 +        }
 +
 +        $natives = $this->_native();
 +        $opt = $options;
 +
 +        if (isset($opt['limit']) || isset($natives[$type])) {
 +            $nativeLimit = isset($natives[$type]['limit']) ? $natives[$type]['limit'] : null;
 +            $column->setLimit(isset($opt['limit']) ? $opt['limit'] : $nativeLimit);
 +        }
 +
 +        $column->setPrecision(isset($opt['precision']) ? $opt['precision'] : null);
 +        $column->setScale(isset($opt['scale'])         ? $opt['scale']     : null);
 +        $column->setDefault(isset($opt['default'])     ? $opt['default']   : null);
 +        $column->setNull(isset($opt['null'])           ? $opt['null']      : null);
 +
 +        $this[$name] ? $this[$name] = $column : $this->_columns[] = $column;
 +        return $this;
 +    }
 +
 +    /**
 +     * Wrap up table creation block & create the table
 +     */
 +    public function end()
 +    {
 +        return $this->_base->endTable($this);
 +    }
 +
 +    /**
 +     * Returns a String whose contents are the column definitions
 +     * concatenated together.  This string can then be pre and appended to
 +     * to generate the final SQL to create the table.
 +     *
 +     * @return  string
 +     */
 +    public function toSql()
 +    {
 +        $cols = array();
 +        foreach ($this->_columns as $col) { $cols[] = $col->toSql(); }
 +
 +        return "  ".implode(", \n  ", $cols);
 +    }
 +
 +
 +    /*##########################################################################
 +    # ArrayAccess
 +    ##########################################################################*/
 +
 +    /**
 +     * ArrayAccess: Check if the given offset exists
 +     *
 +     * @param   int     $offset
 +     * @return  boolean
 +     */
 +    public function offsetExists($offset)
 +    {
 +        foreach ($this->_columns as $column) {
 +            if ($column->getName() == $offset) return true;
 +        }
 +        return false;
 +    }
 +
 +    /**
 +     * ArrayAccess: Return the value for the given offset.
 +     *
 +     * @param   int     $offset
 +     * @return  object  {@link {@Horde_Db_Adapter_Base_ColumnDefinition}
 +     */
 +    public function offsetGet($offset)
 +    {
 +        if (!$this->offsetExists($offset)) {
 +            return null;
 +        }
 +
 +        foreach ($this->_columns as $column) {
 +            if ($column->getName() == $offset) {
 +                return $column;
 +            }
 +        }
 +    }
 +
 +    /**
 +     * ArrayAccess: Set value for given offset
 +     *
 +     * @param   int     $offset
 +     * @param   mixed   $value
 +     */
 +    public function offsetSet($offset, $value)
 +    {
 +        foreach ($this->_columns as $key=>$column) {
 +            if ($column->getName() == $offset) {
 +                $this->_columns[$key] = $value;
 +            }
 +        }
 +    }
 +
 +    /**
 +     * ArrayAccess: remove element
 +     *
 +     * @param   int     $offset
 +     */
 +    public function offsetUnset($offset)
 +    {
 +        foreach ($this->_columns as $key=>$column) {
 +            if ($column->getName() == $offset) {
 +                unset($this->_columns[$key]);
 +            }
 +        }
 +    }
 +
 +
 +    /*##########################################################################
++    # ArrayAccess
++    ##########################################################################*/
++
++    public function getIterator()
++    {
++        return new ArrayIterator($this->_columns);
++    }
++
++
++    /*##########################################################################
 +    # Protected
 +    ##########################################################################*/
 +
 +    /**
 +     * Get the types
 +     */
 +    protected function _native()
 +    {
 +        return $this->_base->nativeDatabaseTypes();
 +    }
 +
 +}