Seditio Source
Root |
./othercms/ips_4.3.4/system/Db/Db.php
<?php
/**
 * @brief        Database Class
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        18 Feb 2013
 */

namespace IPS;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * @brief    Database Class
 * @note    All functionality MUST be supported by MySQL 5.1.3 and higher. All references to the MySQL manual are therefore the 5.1 version.
 */
class _Db extends \mysqli
{
   
/**
     * SELECT flags
     */
   
const SELECT_DISTINCT = 1;
    const
SELECT_SQL_CALC_FOUND_ROWS = 2;
    const
SELECT_MULTIDIMENSIONAL_JOINS = 4;
    const
SELECT_FROM_WRITE_SERVER = 8;
   
   
/**
     * INSERT/UPDATE flags
     */
   
const LOW_PRIORITY = 1;
    const
IGNORE = 2;
   
   
/**
     * @brief    Datatypes
     */
   
public static $dataTypes = array(
       
'database_column_type_numeric'    => array(
           
'TINYINT'    => 'TINYINT [±127 ⊻ 255] [1B]',
           
'SMALLINT'    => 'SMALLINT [±3.3e4 ⊻ 6.6e4] [2B]',
           
'MEDIUMINT'    => 'MEDIUMINT [±8.4e6 ⊻ 1.7e7] [3B]',
           
'INT'        => 'INT [±2.1e9 ⊻ 4.3e9] [4B]',
           
'BIGINT'    => 'BIGINT [±9.2e18 ⊻ 1.8e19] [8B]',
           
'DECIMAL'    => 'DECIMAL',
           
'FLOAT'        => 'FLOAT',
           
'BIT'        => 'BIT',
           
        ),
       
'database_column_type_datetime'    => array(
           
'DATE'        => 'DATE',
           
'DATETIME'    => 'DATETIME',
           
'TIMESTAMP'    => 'TIMESTAMP',
           
'TIME'        => 'TIME',
           
'YEAR'        => 'YEAR',
        ),
       
'database_column_type_string'    => array(
           
'CHAR'        => 'CHAR [M≤6.6e4] [(M*w)B]',
           
'VARCHAR'    => 'VARCHAR [M≤6.6e4] [(L+(1∨2))B]',
           
'TINYTEXT'    => 'TINYTEXT [256B] [(L+1)B]',
           
'TEXT'        => 'TEXT [64kB] [(L+2)B]',
           
'MEDIUMTEXT'=> 'MEDIUMTEXT [16MB] [(L+3)B]',
           
'LONGTEXT'    => 'LONGTEXT [4GB] [(L+4)B]',
           
'BINARY'    => 'BINARY [M≤6.6e4] [(M)B]',
           
'VARBINARY'    => 'VARBINARY [M≤6.6e4] [(L+(1∨2))B]',
           
'TINYBLOB'    => 'TINYBLOB [256B] [(L+1)B]',
           
'BLOB'        => 'BLOB [64kB] [(L+2)B]',
           
'MEDIUMBLOB'=> 'MEDIUMBLOB [16MB] [(L+3)B]',
           
'BIGBLOB'    => 'BIGBLOB [4GB] [(L+4)B]',
           
'ENUM'        => 'ENUM [6.6e4] [(1∨2)B]',
           
'SET'        => 'SET [64] [(1∨2∨3∨4∨8)B]',
        )
    );

   
/**
     * @brief    Multiton Store
     */
   
protected static $multitons;

   
/**
     * Get instance
     *
     * @param    mixed    $identifier            Identifier
     * @param    array    $connectionSettings    Connection settings (use when initiating a new connection)
     * @return    \IPS\Db
     */
   
public static function i( $identifier=NULL, $connectionSettings=array() )
    {
       
/* Did we pass a null value? */
       
$identifier = ( $identifier === NULL ) ? '__MAIN' : $identifier;
   
       
/* Don't have an instance? */
       
if( !isset( static::$multitons[ $identifier ] ) )
        {
           
/* Load the default settings if necessary */
           
if( $identifier === '__MAIN' )
            {
                require( \
IPS\ROOT_PATH . '/conf_global.php' );
               
$connectionSettings = isset( $INFO ) ? $INFO : array();
            }
                       
           
/* Connect */
           
static::$multitons[ $identifier ] = static::_establishConnection(
               
get_called_class(),
                array(
                   
'host'        => $connectionSettings['sql_host'],
                   
'username'    => $connectionSettings['sql_user'],
                   
'password'    => $connectionSettings['sql_pass'],
                   
'database'    => $connectionSettings['sql_database'],
                   
'port'        => ( isset( $connectionSettings['sql_port'] ) and $connectionSettings['sql_port']) ? $connectionSettings['sql_port'] : NULL,
                   
'socket'    => ( isset( $connectionSettings['sql_socket'] ) and $connectionSettings['sql_socket'] ) ? $connectionSettings['sql_socket'] : NULL,
                ),
                isset(
$connectionSettings['sql_utf8mb4'] ) and $connectionSettings['sql_utf8mb4']
            );
                           
           
/* Set charset / collation properties */
           
if ( static::$multitons[ $identifier ]->getCharset() === 'utf8mb4' )
            {
                static::
$multitons[ $identifier ]->charset = 'utf8mb4';
                static::
$multitons[ $identifier ]->collation = 'utf8mb4_unicode_ci';
                static::
$multitons[ $identifier ]->binaryCollation = 'utf8mb4_bin';
            }
            else
            {
                static::
$multitons[ $identifier ]->charset = 'utf8';
                static::
$multitons[ $identifier ]->collation = 'utf8_unicode_ci';
                static::
$multitons[ $identifier ]->binaryCollation = 'utf8_bin';
            }

           
/* Set the prefix */
           
if ( isset( $connectionSettings['sql_tbl_prefix'] ) )
            {
                static::
$multitons[ $identifier ]->prefix = $connectionSettings['sql_tbl_prefix'];
            }
           
           
/* Read/Write Separation? */
           
if ( isset( $connectionSettings['sql_read_host'] ) and \IPS\READ_WRITE_SEPARATION )
            {
                static::
$multitons[ $identifier ]->reader = static::_establishConnection(
                   
'mysqli',
                    array(
                       
'host'        => $connectionSettings['sql_read_host'],
                       
'username'    => $connectionSettings['sql_read_user'],
                       
'password'    => $connectionSettings['sql_read_pass'],
                       
'database'    => $connectionSettings['sql_read_database'],
                       
'port'        => ( isset( $connectionSettings['sql_read_port'] ) and $connectionSettings['sql_read_port']) ? $connectionSettings['sql_read_port'] : NULL,
                       
'socket'    => ( isset( $connectionSettings['sql_read_socket'] ) and $connectionSettings['sql_read_socket'] ) ? $connectionSettings['sql_read_socket'] : NULL,
                    ),
                    isset(
$connectionSettings['sql_utf8mb4'] ) and $connectionSettings['sql_utf8mb4']
                );
            }
        }
       
       
/* Return */
       
return static::$multitons[ $identifier ];
    }
   
   
/**
     * Apparently, get_charset can be unavailable
     *
     * @return string
     */
   
public function getCharset()
    {
        if (
method_exists( $this, 'get_charset' ) )
        {
            return static::
get_charset()->charset;
        }
        else
        {
            return static::
character_set_name();
        }
    }
   
   
/**
     * Establish database connection
     *
     * @note    Credentials are passed as an array so that if there's an error the PHP error will say "Array" rather than reveal the credentials in any log or if display_errors is on
     * @param    string        $classname            Class to use
     * @param    string        $sqlCredentials        array( 'host' => MySQL Host, 'username' => MySQL Username, 'password' => MySQL Password, 'database' => MySQL Database Name, 'port' => MySQL Port or NULL, 'socket' => MySQL Socket or NULL )
     * @param    bool        $utf8Mb4        Use UTF8MB4?
     * @return    mysqli
     */
   
protected static function _establishConnection( $classname, $sqlCredentials, $utf8Mb4 = TRUE )
    {
       
/* Connect */
       
$object = @new $classname(
           
$sqlCredentials['host'],
           
$sqlCredentials['username'],
           
$sqlCredentials['password'],
           
$sqlCredentials['database'],
           
$sqlCredentials['port'],
           
$sqlCredentials['socket']
            );

       
/* If the connection failed, throw an exception */
       
if( $error = mysqli_connect_error() )
        {
           
$errorNumber = $object->connect_errno;
            throw new \
IPS\Db\Exception( $error, $errorNumber );
        }
       
       
/* Charset */
       
if ( $utf8Mb4 )
        {
            if (
$object->set_charset( 'utf8mb4' ) === FALSE )
            {
               
/* If setting utf8mb4 fails, then gracefully fallback to normal utf8 */
               
$object->set_charset( 'utf8' );
            }
        }
        else
        {
           
$object->set_charset( 'utf8' );
        }
       
       
/* Strict mode */
       
if ( \IPS\IN_DEV )
        {
           
$object->query( "SET sql_mode='STRICT_ALL_TABLES,ONLY_FULL_GROUP_BY,ANSI_QUOTES'" );
        }
       
       
/* Return */
       
return $object;
    }
   
   
/**
     * @brief    Charset
     */
   
public $charset = 'utf8';
   
   
/**
     * @brief    Collation
     */
   
public $collation = 'utf8_unicode_ci';
   
   
/**
     * @brief    Binary Collation
     */
   
public $binaryCollation = 'utf8_bin';
   
   
/**
     * @brief    Table Prefix
     */
   
public $prefix = '';
   
   
/**
     * @brief    Query log
     */
   
public $log = array();

   
/**
     * @brief    Return the query instead of executing it
     * @note    Only designed to work with methods that call query() vs prepared statements
     */
   
public $returnQuery    = FALSE;
       
   
/**
     * @brief    MySQLi object for reading, if using read/write separation
     */
   
protected $reader = NULL;
   
   
/**
     * @brief    Read/Write Separation Enabled
     * @todo    This is hacky. Do it properly later
     */
   
public $readWriteSeparation = TRUE;
   
   
/**
     * Run a query
     *
     * @param    string    $query    The query
     * @param    bool    $log    Should be logged?
     * @param    bool    $read    If TRUE and read/write separation is in use, will use the "read" connection
     * @return    mixed
     * @see        <a href="http://uk1.php.net/manual/en/mysqli.query.php">mysqli::query</a>
     * @throws    \IPS\Db\Exception
     */
   
public function query( $query, $log = TRUE, $read=FALSE )
    {
       
/* Should we return the query instead of executing it? */
       
if( $this->returnQuery === TRUE )
        {
           
$this->returnQuery    = FALSE;
            return
$query;
        }

       
/* Log */
       
if ( \IPS\QUERY_LOG and $log )
        {
           
$this->log( $query, ( $read and $this->readWriteSeparation ) ? 'read' : 'write' );
        }
       
       
/* Run */
       
if ( $read and $this->reader and $this->readWriteSeparation )
        {
           
$return = $this->reader->query( $query );
            if (
$return === FALSE )
            {
                throw new \
IPS\Db\Exception( $this->reader->error, $this->reader->errno );
            }
        }
        else
        {
           
$return = parent::query( $query );
            if (
$return === FALSE )
            {
                throw new \
IPS\Db\Exception( $this->error, $this->errno );
            }
        }
       
       
/* Return */
       
return $return;
    }

   
/**
     * Force a query to run regardless of $this->returnQuery
     *
     * @param    string    $query    The query
     * @param    bool    $log    Should be logged?
     * @param    bool    $read    If TRUE and read/write separation is in use, will use the "read" connection
     * @return    mixed
     * @see        <a href="http://uk1.php.net/manual/en/mysqli.query.php">mysqli::query</a>
     * @throws    \IPS\Db\Exception
     */
   
public function forceQuery( $query, $log = TRUE, $read=FALSE )
    {
       
$return = $this->returnQuery;
       
$this->returnQuery    = false;

       
$result    = $this->query( $query, $log, $read );

       
$this->returnQuery    = $return;

        return
$result;
    }

   
/**
     * Run Prepared SQL Statement
     *
     * @param    string    $query    SQL Statement
     * @param    array    $_binds    Variables to bind
     * @param    bool    $read    If TRUE and read/write separation is in use, will use the "read" connection
     * @return    \mysqli_stmt
     */
   
public function preparedQuery( $query, array $_binds, $read=FALSE )
    {
       
/* Init Bind object */
       
$bind = new Db\Bind();
       
       
/* Sort out subqueries */
       
$binds = array();
       
$i = 0;
        for (
$j = 0; $j < \strlen( $query ); $j++ )
        {
            if (
$query[ $j ] == '?' )
            {
                if (
array_key_exists( $i, $_binds ) )
                {
                    if (
$_binds[ $i ] instanceof \IPS\Db\Select )
                    {
                       
$query = \substr( $query, 0, $j ) . $_binds[ $i ]->query . \substr( $query, $j + 1);
                       
$j += \strlen( $_binds[ $i ]->query );

                        foreach (
$_binds[ $i ]->binds as $_bind )
                        {
                           
$binds[] = $_bind;
                        }
                    }
                    else
                    {
                       
$binds[] = $_binds[ $i ];
                    }
                   
                   
$i++;
                }
            }
        }
       
       
/* Store the original query before the bind checks are done as NULL replaces ? which throws out the order in the query log */
       
$queryForLog = $query;
       
       
/* Loop values to bind */
       
$i = 0;
       
$longThreshold = 1048576;
       
$sendAsLong = array();
        foreach (
$binds as $bindVal )
        {
            if( (
is_object( $bindVal ) OR is_string( $bindVal ) ) AND \strlen( (string) $bindVal ) > $longThreshold )
            {
               
$sendAsLong[ $i ] = (string) $bindVal;
            }

           
$i++;
            switch (
gettype( $bindVal ) )
            {
                case
'boolean':
                case
'integer':
                   
$bind->add( 'i', $bindVal );
                    break;
                   
                case
'double':
                   
$bind->add( 'd', $bindVal );
                    break;
                                               
                case
'string':
                    if( \
strlen( $bindVal ) > $longThreshold )
                    {
                       
$bind->add( 'b', NULL );
                    }
                    else
                    {
                       
$bind->add( 's', $bindVal );
                    }
                    break;
                   
                case
'object':
                    if(
method_exists( $bindVal, '__toString' ) )
                    {
                        if( \
strlen( $bindVal ) > $longThreshold )
                        {
                           
$bind->add( 'b', NULL );
                        }
                        else
                        {
                           
$bind->add( 's', (string) $bindVal );
                        }
                        break;
                    }
                   
// Deliberately no break
                   
               
case 'NULL':
                case
'array':
                case
'resource':
                case
'unknown type':
                default:
                   
/* For NULL values, you can't bind, so we adjust the query to actually pass a NULL value */
                   
$pos = 0;
                    for (
$j=0; $j<$i; $j++ )
                    {
                       
$pos = mb_strpos( $query, '?', $pos ) + 1;
                    }
                   
$query = mb_substr( $query, 0, $pos - 1 ) . 'NULL' . mb_substr( $query, $pos );
                   
$i--;
                    break;
            }
        }
               
       
/* Log */
       
if ( \IPS\QUERY_LOG )
        {
           
/* Log */
           
$this->log( static::_replaceBinds( $queryForLog, $binds ), ( $read and $this->readWriteSeparation ) ? 'read' : 'write' );    
        }

       
/* Return full query */
       
if( $this->returnQuery === TRUE )
        {
           
$this->returnQuery = FALSE;
            return static::
_replaceBinds( $queryForLog, $binds );
        }
       
       
/* Add a backtrace to the query so we know where it came from if it causes issues */
       
$comment = '??';
       
$line = '?';
        foreach(
debug_backtrace( FALSE ) as $b )
        {
            if ( isset(
$b['line'] ) )
            {
               
$line = $b['line'];
            }
           
            if( isset(
$b['class'] ) and !in_array( $b['class'], array( 'IPS\_Db', 'IPS\Db\_Select', 'IPS\Patterns\_ActiveRecord', 'IPS\Patterns\_ActiveRecordIterator' ) ) )
            {
               
$comment = "{$b['class']}::{$b['function']}:{$line}";
                break;
            }
        }
       
$_query = $query;
       
$query = "/*{$comment}*/ {$query}";
                   
       
/* Prepare */
       
if ( $read and $this->reader and $this->readWriteSeparation )
        {
           
$stmt = $this->reader->prepare( $query );
            if(
$stmt === FALSE )
            {
                throw new \
IPS\Db\Exception( $this->reader->error, $this->reader->errno, NULL, $queryForLog, $binds );
            }
        }
        else
        {
           
$stmt = parent::prepare( $query );
            if(
$stmt === FALSE )
            {
                throw new \
IPS\Db\Exception( $this->error, $this->errno, NULL, $queryForLog, $binds );
            }
        }

       
/* Bind values */
       
if( $bind->haveBinds() === TRUE )
        {
           
$stmt->bind_param( ...$bind->get() );

            if(
count( $sendAsLong ) )
            {
                foreach(
$sendAsLong as $index => $data )
                {
                   
$chunks = str_split( $data, $longThreshold - 1 );

                    foreach(
$chunks as $chunk )
                    {
                       
$stmt->send_long_data( $index, $chunk );
                    }
                }
            }
        }
       
       
/* Execute */
       
$stmt->execute();
       
       
/* Handle errors */
       
$count = 1;
        while (
$stmt->error )
        {
           
/* If we hit a deadlock, try again once */
           
if ( $stmt->errno === 1213 and $count < 2 )
            {
               
$stmt->execute();
               
$count++;
            }
                       
           
/* Throw error */
           
else
            {
                throw new \
IPS\Db\Exception( $stmt->error, $stmt->errno, NULL, $queryForLog, $binds );
            }
        }
               
       
/* Store result */
       
$stmt->store_result();
                       
       
/* Return a Statement object */
       
return $stmt;
    }

   
/**
     * Log
     *
     * @param    string    $logQuery    Query to log
     * @param    string    $server        Will be "read" or "write" to indicate which server was (or would be) used in read/write separation
     * @return    void
     */
   
protected function log( $logQuery, $server=NULL )
    {        
       
$this->log[] = array(
           
'query'        => $logQuery,
           
'server'    => $server,
           
'backtrace'    => var_export( debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ), TRUE ),
           
'extra'        => NULL,
        );
    }
       
   
/**
     * Build SELECT statement
     *
     * @param    array|string        $columns    The columns (as an array) to select or an expression
     * @param    array|string        $table        The table to select from. Either (string) table_name or (array) ( name, alias ) or \IPS\Db\Select object
     * @param    array|string|NULL    $where        WHERE clause - see \IPS\Db::compileWhereClause() for details
     * @param    string|NULL            $order        ORDER BY clause
     * @param    array|int            $limit        Rows to fetch or array( offset, limit )
     * @param    string|NULL|array    $group        Column(s) to GROUP BY
     * @param    array|string|NULL    $having        HAVING clause (same format as WHERE clause)
     * @param    int                    $flags        Bitwise flags
     *    @li    \IPS\Db::SELECT_DISTINCT                Will use SELECT DISTINCT
     *    @li    \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS        Will add SQL_CALC_FOUND_ROWS
     *    @li    \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS    Will return the result as a multidimensional array, with each joined table separately
     *    @li    \IPS\Db::SELECT_FROM_WRITE_SERVER        Will send the query to the write server (if read/write separation is enabled)
     * @return    \IPS\Db\Select
     *
     */
   
public function select( $columns=NULL, $table, $where=NULL, $order=NULL, $limit=NULL, $group=NULL, $having=NULL, $flags=0 )
    {
       
$binds = array();
       
$query = 'SELECT ';
       
       
/* Flags */
       
if ( $flags & static::SELECT_DISTINCT )
        {
           
$query .= 'DISTINCT ';
        }
        if (
$flags & static::SELECT_SQL_CALC_FOUND_ROWS )
        {
           
$query .= 'SQL_CALC_FOUND_ROWS ';
        }
       
       
/* Columns */
       
if ( is_string( $columns ) )
        {
           
$query .= $columns;
        }
        else
        {
           
$query .= implode( ', ', array_map( function( $col )
            {
                return (
mb_strpos( $col, '`' ) === FALSE ) ? ( '`' . $col . '`' ) : $col;
            },
$columns ) );
        }
       
       
/* Tables */
       
if ( $table instanceof \IPS\Db\Select )
        {
           
$tableQuery = $table->query;
           
$binds = $table->binds;
           
preg_match( '/FROM `(.+?)`( AS `(.+?)`)?/', $tableQuery, $matches );
           
$query .= isset( $matches[3] ) ? " FROM ( {$tableQuery} ) AS `{$matches[3]}`" : ( " FROM ( {$tableQuery} ) AS `" . md5(mt_rand()) . '`' );            
        }
        elseif (
is_array( $table ) )
        {
            if (
is_array( $table[0] ) and count( $table[0] ) )
            {
               
$tables = array();
                foreach(
$table as $item )
                {
                   
$tables[] = " `{$this->prefix}{$item[0]}` AS `{$item[1]}`";
                }
               
               
$query .= " FROM " . implode( ', ', $tables );
            }
            else
            {
               
$tableName = ( $table[0] instanceof \IPS\Db\Select ) ? '(' . $table[0] . ')' : '`' . $this->prefix . $table[0] . '`';
               
$query .= " FROM {$tableName} AS `{$table[1]}`";
            }
        }
        else
        {
           
$query .= $this->prefix ? " FROM `{$this->prefix}{$table}` AS `{$table}`" : " FROM `{$table}`";
        }
       
       
/* WHERE */
       
if ( $where )
        {
           
$where = $this->compileWhereClause( $where );
           
$query .= ' WHERE ' . $where['clause'];
           
$binds = array_merge( $binds, $where['binds'] );
        }
       
       
/* Group? */
       
if( $group )
        {
            if (
is_array( $group ) )
            {
               
$query .= " GROUP BY " . implode( ',', array_map( function( $val )
                {
                    if(
mb_strpos( $val, '.' ) !== FALSE )
                    {
                       
$pieces = explode( '.', $val );

                        foreach(
$pieces as $k => $piece )
                        {
                           
$pieces[ $k ] = '`' . $piece . '`';
                        }

                        return
implode( '.', $pieces );
                    }

                    return
"`{$val}`";
                },
$group ) );
            }
            else
            {
                if(
mb_strpos( $group, '.' ) !== FALSE )
                {
                   
$pieces = explode( '.', $group );

                    foreach(
$pieces as $k => $piece )
                    {
                       
$pieces[ $k ] = '`' . $piece . '`';
                    }

                   
$group = implode( '.', $pieces );
                }
                else
                {
                   
$group = "`{$group}`";
                }

               
$query .= " GROUP BY {$group}";
            }
        }
               
       
/* Having? */
       
if( $having )
        {
           
$having = $this->compileWhereClause( $having );
           
$query .= ' HAVING ' . $having['clause'];
           
$binds = array_merge( $binds, $having['binds'] );
        }
       
       
/* Order? */
       
if( $order )
        {
           
$query .= ' ORDER BY ' . $order;
        }
       
       
/* Limit */
       
if( $limit )
        {
           
$query .= $this->compileLimitClause( $limit );
        }
       
       
/* Return */
       
return new \IPS\Db\Select( $query, $binds, $this, $flags & static::SELECT_MULTIDIMENSIONAL_JOINS, $flags & static::SELECT_FROM_WRITE_SERVER );
    }
   
   
/**
     * Build UNION statement
     *
     * @param    array                $selects        Array of \IPS\Db\Select objects
     * @param    string|NULL            $order            ORDER BY clause
     * @param    array|int            $limit            Rows to fetch or array( offset, limit )
     * @param    string|null            $group            Group by clause
     * @param    bool                $unionAll        TRUE to perform a UNION ALL, FALSE (default) to perform a regular UNION
     * @param    int                    $flags            Bitwise flags
     * @param    array|string|NULL    $where            WHERE clause (see example)
     * @param    string                $querySelect    Custom select for the outer query
     *    @li    \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS    Will add SQL_CALC_FOUND_ROWS
     * @return    \IPS\Db|Select
     */
   
public function union( $selects, $order, $limit, $group=NULL, $unionAll=FALSE, $flags=0, $where=NULL, $querySelect='*' )
    {
       
/* Combine selects */
       
$query = array();
       
$binds = array();
        foreach (
$selects as $s )
        {
           
$query[] = '( ' . $s->query . ' )';
           
$binds = array_merge( $binds, $s->binds );
        }

       
$union    = $unionAll ? "UNION ALL" : "UNION";
       
       
$query = "SELECT " . ( $flags & static::SELECT_SQL_CALC_FOUND_ROWS ? "SQL_CALC_FOUND_ROWS " : "" ) . $querySelect . " FROM( " . implode( ' ' . $union . ' ', $query ) . ") derivedTable ";
       
       
/* WHERE */
       
if ( $where )
        {
           
$where = $this->compileWhereClause( $where );
           
$query .= ' WHERE ' . $where['clause'];
           
$binds = array_merge( $binds, $where['binds'] );
        }

       
/* Group */
       
if( $group )
        {
           
$query.= " GROUP BY " . $group;
        }

       
/* Order? */
       
if( $order )
        {
           
$query .= ' ORDER BY ' . $order;
        }
       
       
/* Limit */
       
if( $limit )
        {
           
$query .= $this->compileLimitClause( $limit );
        }
       
       
/* Return */
       
return new \IPS\Db\Select( $query, $binds, $this );
    }
   
   
/**
     * Run INSERT statement and return insert ID
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/insert.html'>INSERT Syntax</a>
     * @param    string                    $table            Table name
     * @param    array|\IPS\Db\Select    $set            Values to insert or array of values to set for multiple rows (NB, if providing multiple rows, they MUST all contain the same columns) or a statement to do INSERT INTO SELECT FROM
     * @param    bool                    $odkUpdate        Append an ON DUPLICATE KEY UPDATE clause to the query.  Similar to the replace() method but updates if a record is found, instead of delete and reinsert.
     * @param    bool                    $ignoreErrors    Ignore errors?
     * @see        \IPS\Db::replace()
     * @return    int
     * @throws    \IPS\Db\Exception
     */
   
public function insert( $table, $set, $odkUpdate=FALSE, $ignoreErrors=FALSE )
    {
       
/* Build */
       
$query = $this->_buildInsertQuery( ( $ignoreErrors ? 'INSERT IGNORE' : 'INSERT' ), $table, $set );
       
       
/* Add "ON DUPLICATE KEY UPDATE" */
       
if( $odkUpdate )
        {
           
$query[0]    .= " ON DUPLICATE KEY UPDATE " . implode( ', ', array_map( function( $val ){ return "{$val}=VALUES({$val})"; }, $query[2] ) );
        }
       
       
/* Run */
       
$return = $this->returnQuery;

       
$stmt = $this->preparedQuery( $query[0], $query[1] );

        if(
$return === TRUE )
        {
            return
$stmt;
        }
       
       
$insertId = $stmt->insert_id;
       
       
$stmt->close();

        return
$insertId;
    }
   
   
/**
     * Run REPLACE statament and return number of affected rows OR inserted ID
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/replace.html'>REPLACE Syntax</a>
     * @param    string    $table            Table name
     * @param    array    $set            Values to insert
     * @param    bool    $getInsertId    If TRUE, returns the insert ID rather than the number of affected rows
     * @return    int
     * @throws    \IPS\Db\Exception
     */
   
public function replace( $table, $set, $getInsertId=false )
    {
       
/* Build */
       
$query = $this->_buildInsertQuery( 'REPLACE', $table, $set );

       
$return = $this->returnQuery;

       
$stmt = $this->preparedQuery( $query[0], $query[1] );

        if(
$return === TRUE )
        {
            return
$stmt;
        }

       
$return = $getInsertId ? $stmt->insert_id : $stmt->affected_rows;
       
       
$stmt->close();
       
        return
$return;
    }

   
/**
     * Build the replace or insert into query
     *
     * @param    string                    $type    INSERT|REPLACE
     * @param    string                    $table    Table name
     * @param    array|\IPS\Db\Select    $set    Values to insert or array of values to set for multiple rows (NB, if providing multiple rows, they MUST all contain the same columns) or a statement to do INSERT INTO SELECT FROM
     * @return    array    0 => query, 1 => binds, 2 => columns
     */
   
protected function _buildInsertQuery( $type, $table, $set )
    {
       
$columns    = NULL;

       
/* Is a statement? */
       
if ( $set instanceof \IPS\Db\Select )
        {
           
$query = "{$type} INTO `{$this->prefix}{$table}` " . $set->query;
           
$binds = $set->binds;
        }
        else
        {
           
/* Is this just one row? */
           
foreach ( $set as $k => $v )
            {
                if ( !
is_array( $v ) )
                {
                   
$set = array( $set );
                }
                break;
            }
           
           
/* Compile */
           
$columns = NULL;
           
$values = array();
           
$binds = array();
            if (
count( $set ) )
            {
                foreach (
$set as $row )
                {
                    if (
$columns === NULL )
                    {
                         
$columns = array_map( function( $val ){ return "`{$val}`"; }, array_keys( $row ) );
                    }
                   
                   
$binds = array_merge( $binds, array_values( $row ) );
                   
$values[] = '( ' . implode( ', ', array_fill( 0, count( $columns ), '?' ) ) . ' )';
                }
            }
            else
            {
               
$columns = array();
               
$values = array( '()' );
            }
           
           
/* Construct query */
           
$query = "{$type} INTO `{$this->prefix}{$table}` ( " . implode( ', ', $columns ) . ' ) VALUES ' . implode( ', ', $values );
        }

        return array(
0 => $query, 1 => $binds, 2 => $columns );
    }
   
   
/**
     * Run UPDATE statement and return number of affected rows
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/update.html'>UPDATE Syntax</a>
     * @param    string|array    $table        Table Name, or array( Table Name => Identifier )
     * @param    string|array    $set        Values to set (keys should be the table columns) or pre-formatted SET clause or \IPS\Db\Select object
     * @param    mixed            $where        WHERE clause (see \IPS\Db::compileWhereClause for details)
     * @param    array            $joins        Tables to join
     * @param    int|array|null    $limit        LIMIT clause (see \IPS\Db::select for details)
     * @param    int                $flags        Bitwise flags
     *    @li    \IPS\Db::LOW_PRIORITY            Will use LOW_PRIORITY
     *    @li    \IPS\Db::IGNORE                    Will use IGNORE
     * @return    int
     * @throws    \IPS\Db\Exception
     */
   
public function update( $table, $set, $where='', $joins=array(), $limit=NULL, $flags=0 )
    {
       
$binds = array();
       
       
/* Work out table */
       
$table = is_array( $table ) ? "`{$this->prefix}{$table[0]}` {$this->prefix}{$table[1]}" : "`{$this->prefix}{$table}` {$table}";

       
/* Work out joins */
       
$_joins    = array();
       
        foreach (
$joins as $join )
        {
           
$type = ( isset( $join['type'] ) and in_array( mb_strtoupper( $join['type'] ), array( 'LEFT', 'INNER', 'RIGHT' ) ) ) ? mb_strtoupper( $join['type'] ) : 'LEFT';
           
$_table = is_array( $join['from'] ) ? "`{$this->prefix}{$join['from'][0]}` {$this->prefix}{$join['from'][1]}" : "`{$this->prefix}{$join['from']}` {$join['from']}";

           
$on = $this->compileWhereClause( $join['where'] );
           
$binds = array_merge( $binds, $on['binds'] );

           
$_joins[] = "{$type} JOIN {$_table} ON {$on['clause']}";
        }
       
$joins = empty( $_joins ) ? '' : ( ' ' . implode( "\n", $_joins ) );
       
       
/* Work out SET clause */
       
if ( is_array( $set ) )
        {
           
$_set = array();
            foreach (
$set as $k => $v )
            {
               
$_set[] = "`{$k}`=" . ( is_object( $v ) ? '(?)' : '?' );
               
$binds[] = $v;
            }
           
$set = implode( ',', $_set );
        }
               
       
/* Compile where clause */
       
if ( $where !== '' )
        {
           
$_where = $this->compileWhereClause( $where );
           
$where = 'WHERE ' . $_where['clause'];
           
$binds = array_merge( $binds, $_where['binds'] );
        }
       
       
/* Build query */
       
$query = 'UPDATE ';
        if (
$flags & static::LOW_PRIORITY )
        {
           
$query .= 'LOW_PRIORITY ';
        }
        if (
$flags & static::IGNORE )
        {
           
$query .= 'IGNORE ';
        }
       
$query .= "{$table} {$joins} SET {$set} {$where} ";
       
       
/* Limit */
       
if( $limit !== NULL )
        {
           
$query .= $this->compileLimitClause( $limit );
        }
               
       
/* Run it */
       
$return = $this->returnQuery;

       
$stmt = $this->preparedQuery( $query, $binds );

        if(
$return === TRUE )
        {
            return
$stmt;
        }

       
$return = $stmt->affected_rows;
       
       
$stmt->close();
       
        return
$return;
    }
   
   
/**
     * Run DELETE statement and return number of affected rows
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/delete.html'>DELETE Syntax</a>
     * @param    string                                $table                Table Name
     * @param    string|array|\IPS\Db\Statement|null    $where                WHERE clause (see \IPS\Db::compileWhereClause for details)
     * @param    string|null                            $order                ORDER BY clause
     * @param    int|array|null                        $limit                LIMIT clause (see \IPS\Db::select for details)
     * @param    string|null                            $statementColumn    If \IPS\Db\Statement is passed, this is the name of the column that results are being loaded from
     * @return    \IPS\Db\Statement
     * @throws    \IPS\Db\Exception
     */
   
public function delete( $table, $where=NULL, $order=NULL, $limit=NULL, $statementColumn=NULL )
    {
       
/* TRUNCATE is faster, so use that if appropriate */
       
if ( $where === NULL and $limit === NULL )
        {
           
$return = $this->returnQuery;

           
$stmt = $this->preparedQuery( "TRUNCATE `{$this->prefix}{$table}`", array() );

            if(
$return === TRUE )
            {
                return
$stmt;
            }
           
           
$return = $stmt->affected_rows;
           
           
$stmt->close();

            return
$return;
        }
       
       
/* Basic query */
       
$query = "DELETE FROM `{$this->prefix}{$table}`";

       
/* Is a statement? */
       
if ( $where instanceof \IPS\Db\Statement )
        {
           
$query .= ' WHERE ' . $statementColumn . ' IN(' . $where->query . ')';
           
$binds = $where->binds;
        }

       
/* Add where clause */
       
else
        {
           
$binds = array();
            if (
$where !== NULL )
            {
               
$_where = $this->compileWhereClause( $where );
               
$query .= ' WHERE ' . $_where['clause'];
               
$binds = $_where['binds'];
            }
        }
       
       
/* Order? */
       
if( $order !== NULL )
        {
           
$query .= ' ORDER BY ' . $order;
        }
       
       
/* Limit */
       
if( $limit !== NULL )
        {
           
$query .= $this->compileLimitClause( $limit );
        }
       
       
/* Run it */
       
$return = $this->returnQuery;

       
$stmt = $this->preparedQuery( $query, $binds );

        if(
$return === TRUE )
        {
            return
$stmt;
        }

       
$return = $stmt->affected_rows;
       
       
$stmt->close();
       
        return
$return;
    }
           
   
/**
     * Compile WHERE clause
     *
     * @code
         // Single clause
         "foo IS NOT NULL"
         // Single clause with bound values (always bind values to ensure they are properly escaped)
         array( 'foo=?', 'fooValue' )
         array( 'foo=? OR bar=?', 'fooValue', 'barValue' )
         // Multiple clauses (will be joined with AND) with bound values
         array( array( 'foo=?, 'fooValue' ), array( 'bar=?', 'barValue' ) )
     * @endcode
     * @param    string|array    $data    See examples
     * @return    array    Array containing the WHERE clause and the values to be bound - array( 'clause' => '1=1', 'binds' => array() )
     */
   
public function compileWhereClause( $data )
    {
       
$return = array( 'clause' => '1=1', 'binds' => array() );
       
        if(
is_string( $data ) )
        {
           
$return['clause'] = $data;
        }
        elseif (
is_array( $data ) and ! empty( $data ) )
        {
            if (
is_string( $data[0] ) )
            {
               
$data = array( $data );
            }
       
           
$clauses = array();
            foreach (
$data as $bit )
            {
                if( !
is_array( $bit ) )
                {
                   
$clauses[] = $bit;
                }
                else
                {
                   
$clause = array_shift( $bit );
                   
                   
$binds = $bit;
                   
$i = 0;
                    foreach (
$binds as $k => $v )
                    {
                       
$i++;
                        if (
$v === NULL )
                        {
                           
$pos = 0;
                            for (
$j=0; $j<$i; $j++ )
                            {
                               
$pos = mb_strpos( $clause, '?', $pos ) + 1;
                            }

                            if(
mb_substr( $clause, $pos - 3, 3 ) == '!=?' )
                            {
                               
$clause = mb_substr( $clause, 0, $pos - 3 ) . ' IS NOT NULL' . mb_substr( $clause, $pos );
                            }
                            else
                            {
                               
$clause = mb_substr( $clause, 0, $pos - 2 ) . ' IS NULL' . mb_substr( $clause, $pos );
                            }

                           
$i--;
                            unset(
$binds[$k] );
                        }

                    }
                           
                   
$clauses[] = $clause;
                   
$return['binds'] = array_merge( $return['binds'], $binds );
                }
            }
           
           
$return['clause'] = implode( ' AND ', $clauses );
        }
       
        return
$return;
    }
   
   
/**
     * Compile LIMIT clause
     *
     * @param    int|array    $data    Rows to fetch or array( offset, limit )
     * @return    string
     */
   
public function compileLimitClause( $data )
    {
       
$limit = NULL;
        if(
is_array( $data ) )
        {
           
$offset = intval( $data[0] );
           
$limit  = intval( $data[1] );
        }
        else
        {
           
$offset = intval( $data );
        }

        if(
$limit !== NULL )
        {
            return
" LIMIT {$offset},{$limit}";
        }
        else
        {
            return
" LIMIT {$offset}";
        }
    }
   
   
/**
     * Compile column definition
     *
     * @code
         \IPS\Db::i()->compileColumnDefinition( array(
             'name'            => 'column_name',        // Column name
             'type'            => 'VARCHAR',            // Data type (do not specify length, etc. here)
             'length'        => 255,                    // Length. May be required or optional depending on data type.
             'decimals'        => 2,                    // Decimals. May be required or optional depending on data type.
             'values'        => array( 0, 1 ),        // Acceptable values. Required for ENUM and SET data types.
             'allow_null'    => FALSE,                // (Optional) Specifies whether or not NULL vavlues are allowed. Defaults to TRUE.
             'default'        => 'Default Value',        // (Optional) Default value
             'comment'        => 'Column Comment',    // (Optional) Column comment
             'unsigned'        => TRUE,                // (Optional) Will specify UNSIGNED for numeric types. Defaults to FALSE.
             'zerofill'        => TRUE,                // (Optional) Will specify ZEROFILL for numeric types. Defaults to FALSE.
             'auto_increment'=> TRUE,                // (Optional) Will specify auto_increment. Defaults to FALSE.
             'binary'        => TRUE,                // (Optional) Will specify BINARY for TEXT types. Defaults to FALSE.
             'primary'        => TRUE,                // (Optional) Will specify PRIMARY KEY. Defaults to FALSE.
             'unqiue'        => TRUE,                // (Optional) Will specify UNIQUE. Defaults to FALSE.
             'key'            => TRUE,                // (Optional) Will specify KEY. Defaults to FALSE.
         ) );
     * @endcode
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/create-table.html'>MySQL CREATE TABLE syntax</a>
     * @param    array    $data    Column Data (see \IPS\Db::createTable for details)
     * @return    string
     */
   
public function compileColumnDefinition( $data )
    {
       
/* Specify name and type */
       
$definition = "`{$data['name']}` " . mb_strtoupper( $data['type'] ) . ' ';
       
       
/* Some types specify length */
       
if(
           
in_array( mb_strtoupper( $data['type'] ), array( 'VARCHAR', 'VARBINARY' ) )
            or
            (
                isset(
$data['length'] ) and $data['length']
                and
               
in_array( mb_strtoupper( $data['type'] ), array( 'BIT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC', 'CHAR', 'BINARY' ) )
            )
        ) {
           
$definition .= "({$data['length']}";
           
           
/* And some of those specify decimals (which may or may not be optional) */
           
if( in_array( mb_strtoupper( $data['type'] ), array( 'REAL', 'DOUBLE', 'FLOAT' ) ) or ( in_array( mb_strtoupper( $data['type'] ), array( 'DECIMAL', 'NUMERIC' ) ) and isset( $data['decimals'] ) ) )
            {
               
$definition .= ',' . $data['decimals'];
            }
           
           
$definition .= ') ';
        }
       
       
/* Numeric types can be UNSIGNED and ZEROFILL */
       
if( in_array( mb_strtoupper( $data['type'] ), array( 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC' ) ) )
        {
            if( isset(
$data['unsigned'] ) and $data['unsigned'] === TRUE )
            {
               
$definition .= 'UNSIGNED ';
            }
            if( isset(
$data['zerofill'] ) and $data['zerofill'] === TRUE )
            {
               
$definition .= 'ZEROFILL ';
            }
        }
       
       
/* ENUM and SETs have values */
       
if( in_array( mb_strtoupper( $data['type'] ), array( 'ENUM', 'SET' ) ) )
        {
           
$values = array();
            foreach (
$data['values'] as $v )
            {
               
$values[] = "'{$this->escape_string( $v )}'";
            }
           
           
$definition .= '(' . implode( ',', $values ) . ') ';
        }
               
       
/* Some types can be binary or not */
       
if( isset( $data['binary'] ) and $data['binary'] === TRUE and in_array( mb_strtoupper( $data['type'] ), array( 'CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT' ) ) )
        {
           
$definition .= 'BINARY ';
        }
       
       
/* Text types specify a character set and collation */
       
if( in_array( mb_strtoupper( $data['type'] ), array( 'CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'ENUM', 'SET' ) ) )
        {
           
$definition .= "CHARACTER SET {$this->charset} COLLATE {$this->collation} ";
        }
       
       
/* NULL? */
       
if( isset( $data['allow_null'] ) and $data['allow_null'] === FALSE )
        {
           
$definition .= 'NOT NULL ';
        }
        else
        {
           
$definition .= 'NULL ';
        }
       
       
/* auto_increment? */
       
if( isset( $data['auto_increment'] ) and $data['auto_increment'] === TRUE )
        {
           
$definition .= 'AUTO_INCREMENT ';
        }
        else
        {
           
/* Default value */
           
if( isset( $data['default'] ) and !in_array( mb_strtoupper( $data['type'] ), array( 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'BLOB', 'MEDIUMBLOB', 'BIGBLOB', 'LONGBLOB' ) ) )
            {
                if(
$data['type'] == 'BIT' )
                {
                   
$definition .= "DEFAULT {$data['default']} ";
                }
                else
                {
                   
$defaultValue = in_array( mb_strtoupper( $data['type'] ), array( 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC' ) ) ? floatval( $data['default'] ) : ( ! in_array( $data['default'], array( 'CURRENT_TIMESTAMP', 'BIT' ) ) ? '\'' . $this->escape_string( $data['default'] ) . '\'' : $data['default'] );
                   
$definition .= "DEFAULT {$defaultValue} ";
                }
            }
        }
       
       
/* Index? */
       
if( isset( $data['primary'] ) )
        {
           
$definition .= 'PRIMARY KEY ';
        }
        elseif( isset(
$data['unique'] ) )
        {
           
$definition .= 'UNIQUE ';
        }
        if( isset(
$data['key'] ) )
        {
           
$definition .= 'KEY ';
        }
       
       
/* Comment */
       
if( isset( $data['comment'] ) and ! empty( $data['comment'] ) )
        {
           
$definition .= "COMMENT '{$this->escape_string( $data['comment'] )}'";
        }
                                   
       
/* Return */
       
return $definition;
    }
   
   
/**
     * Compile index definition
     *
     * @code
         \IPS\Db::i()->compileIndexDefinition( array(
             'type'        => 'key',                // "primary", "unique", "fulltext" or "key"
             'name'        => 'index_name',        // Index name. Not required if type is "primary"
             'length'    => 200,                    // Index length (used when taking part of a text field, for example)
             'columns'    => array( 'column' )    // Columns to be in the index
         ) );
     * @endcode
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/create-index.html'>MySQL CREATE INDEX syntax</a>
     * @see        \IPS\Db::createTable
     * @param    array    $data    Index Data (see \IPS\Db::createTable for details)
     * @return    string
     */
   
public function compileIndexDefinition( $data )
    {
       
$definition = '';
       
       
/* Specify type */
       
switch ( \strtolower( $data['type'] ) )
        {
            case
'primary':
               
$definition .= 'PRIMARY KEY ';
                break;
               
            case
'unique':
               
$definition .= "UNIQUE KEY `{$data['name']}` ";
                break;
               
            case
'fulltext':
               
$definition .= "FULLTEXT KEY `{$data['name']}` ";
                break;
               
            default:
               
$definition .= "KEY `{$data['name']}` ";
                break;
        }
       
       
/* Specify columns */
       
$definition .= '(' . implode( ',', array_map( function ( $val, $len )
        {
            return ( ! empty(
$len ) ) ? "`{$val}`({$len})" : "`{$val}`";
        },
$data['columns'], ( ( isset( $data['length'] ) AND is_array( $data['length'] ) ) ? $data['length'] : array_fill( 0, count( $data['columns'] ), null ) ) ) ) . ')';
       
       
/* Return */
       
return $definition;
    }
   
   
/**
     * Does table exist?
     *
     * @param    string    $name    Table Name
     * @return    bool
     */
   
public function checkForTable( $name )
    {
        return (
$this->forceQuery( "SHOW TABLES LIKE '". $this->escape_string( "{$this->prefix}{$name}" ) . "'" )->num_rows > 0 );
    }

   
/**
     * Does column exist?
     *
     * @param    string    $name    Table Name
     * @param    string    $column    Column Name
     * @return    bool
     */
   
public function checkForColumn( $name, $column )
    {
        return (
$this->forceQuery( "SHOW COLUMNS FROM `". $this->escape_string( "{$this->prefix}{$name}" ) . "` LIKE '". $this->escape_string( $column ) . "'" )->num_rows > 0 );
    }

   
/**
     * Does index exist?
     *
     * @param    string    $name    Table Name
     * @param    string    $index    Index Name
     * @return    bool
     */
   
public function checkForIndex( $name, $index )
    {
        return (
$this->forceQuery( "SHOW INDEXES FROM `". $this->escape_string( "{$this->prefix}{$name}" ) . "` WHERE Key_name LIKE '". $this->escape_string( $index ) . "'" )->num_rows > 0 );
    }
   
   
/**
     * Create Table
     *
     * @code
         \IPS\Db::createTable( array(
             'name'            => 'table_name',    // Table name
             'columns'        => array( ... ),    // Column data - see \IPS\Db::compileColumnDefinition for details
             'indexes'        => array( ... ),    // (Optional) Index data - see \IPS\Db::compileIndexDefinition for details
             'comment'        => '...',            // (Optional) Table comment
             'engine'        => 'MEMORY',        // (Optional) Engine to use - will default to not specifying one, unless a FULLTEXT index is specified, in which case MyISAM is forced
             'temporary'        => TRUE,            // (Optional) Will sepcify CREATE TEMPORARY TABLE - defaults to FALSE
             'if_not_exists'    => TRUE,            // (Optional) Will sepcify CREATE TABLE name IF NOT EXISTS - defaults to FALSE
         ) );
     * @endcode
     * @param    array    $data    Table Definition (see code sample for details)
     * @throws    \IPS\Db\Exception
     * @return    void|string
     */
   
public function createTable( $data )
    {
        return
$this->query( $this->_createTableQuery( $data ) );
    }
   
   
/**
     * Create copy of table structure
     *
     * @param    string    $table            The table name
     * @param    string    $newTableName    Name of table to create
     * @throws    \IPS\Db\Exception
     * @return    void|string
     */
   
public function duplicateTableStructure( $table, $newTableName )
    {
        return
$this->query( "CREATE TABLE `{$this->prefix}{$newTableName}` LIKE `{$this->prefix}{$table}`" );
    }
   
   
/**
     * Create Table Query
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/create-table.html'>MySQL CREATE TABLE syntax</a>
     * @see        \IPS\Db::compileColumnDefinition
     * @see        \IPS\Db::compileIndexDefinition
     * @param    array    $data    Table Definition (see code sample for details)
     * @return    string
     */
   
public function _createTableQuery( $data )
    {
       
$data = $this->updateDefinitionIndexLengths( $data );
       
$mysqlVersion = \IPS\Db::i()->server_info;
       
       
/* Start with a basic CREATE TABLE */
       
$query = 'CREATE ';
        if( isset(
$data['temporary'] ) and $data['temporary'] === TRUE )
        {
           
$query.= 'TEMPORARY ';
        }
       
$query .= 'TABLE ';
        if( isset(
$data['if_not_exists'] ) and $data['if_not_exists'] === TRUE )
        {
           
$query.= 'IF NOT EXISTS ';
        }
               
       
/* Add in our create definition */
       
$query .= "`{$this->prefix}{$data['name']}` (\n\t";
       
$createDefinitons = array();
        foreach (
$data['columns'] as $field )
        {
           
$createDefinitons[] = $this->compileColumnDefinition( $field );
        }
        if( isset(
$data['indexes'] ) )
        {
            foreach (
$data['indexes'] as $index )
            {
                if(
$index['type'] === 'fulltext' )
                {
                   
/* If this is a fulltext index, set engine to myisam but only if engine is something besides innodb or myisam OR the mysql version is less than 5.6 - in
                        this case we assume the default engine is most likely either myisam or innodb */
                   
if( !$this->_innoDbSupportsFulltextIndexes() OR ( isset( $data['engine'] ) AND !in_array( mb_strtoupper( $data['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
                    {
                       
$data['engine'] = 'MYISAM';
                    }
                }

               
$createDefinitons[] = $this->compileIndexDefinition( $index );
            }
        }
       
$query .= implode( ",\n\t", $createDefinitons );
       
$query .= "\n)\n";
       
       
/* Specifying a particular engine? */
       
if( isset( $data['engine'] ) and $data['engine'] )
        {
           
$query .= "ENGINE {$data['engine']} ";
        }
       
       
/* Specify UTF8 */
       
$query .= "CHARACTER SET {$this->charset} COLLATE {$this->collation} ";
       
       
/* Add comment */
       
if( isset( $data['comment'] ) )
        {
           
$query .= "COMMENT '{$this->escape_string( $data['comment'] )}'";
        }
       
       
/* Return */
       
return $query;
    }
   
   
/**
     * Rename table
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/rename-table.html'>Rename Table</a>
     * @param    string    $oldName    The current table name
     * @param    string    $newName    The new name
     * @return    void
     * @see        <a href='http://stackoverflow.com/questions/12856783/best-practice-with-mysql-innodb-to-rename-huge-table-when-table-with-same-name-a'>Renaming huge InnoDB tables</a>
     * @see        <a href='http://www.percona.com/blog/2011/02/03/performance-problem-with-innodb-and-drop-table/'>Performance problem dropping huge InnoDB tables</a>
     * @note    A race condition can occur sometimes with InnoDB + innodb_file_per_table so we can't drop then rename...see above links
     */
   
public function renameTable( $oldName, $newName )
    {
       
/* Find out if the table we are renaming *to* already exists */
       
$cleanUp    = FALSE;
       
$query        = "`{$this->prefix}{$this->escape_string( $oldName )}` TO `{$this->prefix}{$this->escape_string( $newName )}`";

        if(
$this->checkForTable( $newName ) )
        {
           
$query    = "`{$this->prefix}{$this->escape_string( $newName )}` TO `{$this->prefix}{$this->escape_string( $newName )}_DROP`, " . $query;
           
$cleanUp = TRUE;
        }

       
$result = $this->query( "RENAME TABLE " . $query );

        if(
$cleanUp )
        {
           
$this->dropTable( $newName . '_DROP', TRUE );
        }

        return
$result;
    }
   
   
/**
     * Alter Table
     * Can only update the comment and engine
     * @note This will not examine key lengths and adjust.
     *
     * @param    string            $table        Table name
     * @param    string|null        $comment    Table comment. NULL to not change
     * @param    string|null        $engine        Engine to use. NULL to not change
     * @return    void
     */
   
public function alterTable( $table, $comment=NULL, $engine=NULL )
    {
        if (
$comment === NULL and $engine === NULL )
        {
            return;
        }
       
       
$query = "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` ";
        if (
$comment !== NULL )
        {
           
$query .= "COMMENT='{$this->escape_string( $comment )}' ";
        }
        if (
$engine !== NULL )
        {
           
$query .= "ENGINE={$engine}";
        }
               
        return
$this->query( $query );
    }

   
/**
     * Find out the default storage engine
     *
     * @return    string
     */
   
public function defaultEngine()
    {
       
$result = $this->forceQuery( "SHOW ENGINES" );

        while(
$engine = $result->fetch_assoc() )
        {
            if(
mb_strtoupper( $engine['Support'] ) == 'DEFAULT' )
            {
                return
$engine['Engine'];
            }
        }
       
        return
$this->_innoDbSupportsFulltextIndexes() ? 'InnoDB' : 'MyISAM';
    }
   
   
/**
     * Is InnoDB supported for fulltext indexes?
     *
     * @return    bool
     */
   
protected function _innoDbSupportsFulltextIndexes()
    {
       
/* MariaDB supports fulltext for InnoDB on versions higher than 10.0.5 */
       
if ( preg_match( '/^(\d*\.\d*(\.\d*)-)?(\d*\.\d*(\.\d*))-MariaDB/', $this->server_info, $matches ) )
        {
           
$mariaVersion = $matches[3];
            return
version_compare( $mariaVersion, '10.0.5', '>=' );
        }
       
       
/* Normal MySQL supports fulltext for InnoDB on versions higher than 5.6 */
       
else
        {
            return
$this->server_version >= 50600;
        }
    }
   
   
/**
     * Drop table
     *
     * @see        <a href='http://dev.mysql.com/doc/refman/5.1/en/drop-table.html'>DROP TABLE Syntax</a>
     * @param    string|array    $table        Table Name(s)
     * @param    bool            $ifExists    Adds an "IF EXISTS" clause to the query
     * @param    bool            $temporary    Table is temporary?
     * @return    mixed
     */
   
public function dropTable( $table, $ifExists=FALSE, $temporary=FALSE )
    {
       
$prefix = $this->prefix;
       
        return
$this->query(
             
'DROP '
           
. ( $temporary ? 'TEMPORARY ' : '' )
            .
'TABLE '
           
. ( $ifExists ? 'IF EXISTS ' :'' )
            .
implode( ', ', array_map(
                function(
$val ) use ( $prefix )
                {
                    return
'`' . $prefix . $val . '`';
                },
                (
is_array( $table ) ? $table : array( $table ) )
            ) )
        );
    }

   
/**
     * Get database tables
     *
     * @param    string|NULL        $prefix        Optional table prefix to filter by
     * @return    array
     */
   
public function getTables( $prefix=NULL )
    {
       
$stmt = static::i()->prepare( "SHOW TABLES" );
       
$stmt->execute();
       
$stmt->bind_result( $name );

       
$tables = array();

        while (
$stmt->fetch() === true )
        {
            if (
$prefix === NULL OR mb_substr( $name, 0, \strlen( $prefix ) ) === $prefix )
            {
               
$tables[] = $name;
            }
        }

       
$stmt->close();

        return
$tables;
    }
   
   
/**
     * Get the table definition for an existing table
     *
     * @see        \IPS\Db::createTable
     * @param    string    $table    Table Name
     * @param    boolean    $columnsOnly    Fetch columns only
     * @param    boolean    $getCollation    Get column collations
     * @return    array    Table definition - see IPS\Db::createTable for details
     * @throws    \OutOfRangeException
     * @throws    \IPS\Db\Exception
     */
   
public function getTableDefinition( $table, $columnsOnly=FALSE, $getCollation=FALSE )
    {
       
/* Set name */
       
$definition = array(
           
'name'        => $table,
        );
   
       
/* Fetch columns */
       
if( !$this->checkForTable( $table ) )
        {
            throw new \
OutOfRangeException;
        }
       
$query = $this->forceQuery( "SHOW FULL COLUMNS FROM `{$this->prefix}" . $this->escape_string( $table ) . '`' );
        if (
$query->num_rows === 0 )
        {
            throw new \
OutOfRangeException;
        }
        while (
$row = $query->fetch_assoc() )
        {
           
/* Set basic information */
           
$columnDefinition = array(
               
'name' => $row['Field'],
               
'type'        => '',
               
'length'    => 0,
               
'decimals'    => NULL,
               
'values'    => array()
            );
           
            if (
$getCollation and isset( $row['Collation'] ) )
            {
               
$columnDefinition['collation'] = $row['Collation'];
            }
           
           
/* Parse the type */
           
if( mb_strpos( $row['Type'], '(' ) !== FALSE )
            {
               
/* First, we need to protect the enum options as they may have spaces before splitting */
               
preg_match( '/(.+?)\((.+?)\)/', $row['Type'], $matches );
               
$options = $matches[2];
               
$type = preg_replace( '/(.+?)\((.+?)\)/', "$1(___TEMP___)", $row['Type'] );
               
$typeInfo = explode( ' ', $type );
               
$typeInfo[0] = str_replace( "___TEMP___", $options, $typeInfo[0] );

               
/* Now we match out the options */
               
preg_match( '/(.+?)\((.+?)\)/', $typeInfo[0], $matches );
               
$columnDefinition['type'] = mb_strtoupper( $matches[1] );
               
                if(
$columnDefinition['type'] === 'ENUM' or $columnDefinition['type'] === 'SET' )
                {
                   
preg_match_all( "/'(.*?)'/", $matches[2], $enum );
                   
$columnDefinition['values'] = $enum[1];
                }
                else
                {                        
                   
$lengthInfo = explode( ',', $matches[2] );
                   
$columnDefinition['length'] = intval( $lengthInfo[0] );
                    if( isset(
$lengthInfo[1] ) )
                    {
                       
$columnDefinition['decimals'] = intval( $lengthInfo[1] );
                    }
                }
            }
            else
            {
               
$typeInfo = explode( ' ', $row['Type'] );

               
$columnDefinition['type'] = mb_strtoupper( $typeInfo[0] );
               
$columnDefinition['length'] = 0;
            }
           
           
/* unsigned? */
           
$columnDefinition['unsigned'] = in_array( 'unsigned', $typeInfo );
           
           
/* zerofill? */
           
$columnDefinition['zerofill'] = in_array( 'zerofill', $typeInfo );
           
           
/* binary? */
           
$columnDefinition['binary'] = ( $row['Collation'] === $this->binaryCollation );
           
           
/* Allow NULL? */
           
$columnDefinition['allow_null'] = ( $row['Null'] === 'YES' );
                       
           
/* Default value */
           
$columnDefinition['default'] = $row['Default'];
            if (
$columnDefinition['default'] === NULL and !$columnDefinition['allow_null'] and mb_strpos( $row['Extra'], 'auto_increment' ) === FALSE )
            {
               
$columnDefinition['default'] = '';
            }
           
           
/* auto_increment */
           
$columnDefinition['auto_increment'] = mb_strpos( $row['Extra'], 'auto_increment' ) !== FALSE;
           
           
/* Comment */
           
$columnDefinition['comment'] = $row['Comment'] ?: '';
           
           
/* Add it in the defintion */
           
ksort( $columnDefinition );
           
$definition['columns'][ $columnDefinition['name'] ] = $columnDefinition;
        }
       
        if( !
$columnsOnly )
        {
           
/* Fetch indexes */
           
$indexes = array();
           
$query = $this->forceQuery( "SHOW INDEXES FROM `{$this->prefix}{$table}`" );
            while (
$row = $query->fetch_assoc() )
            {
               
$length = ( isset( $row['Sub_part'] ) AND ! empty( $row['Sub_part'] ) ) ? intval( $row['Sub_part'] ) : null;
               
                if( isset(
$indexes[ $row['Key_name'] ] ) )
                {
                   
$indexes[ $row['Key_name'] ]['length'][] = $length;
                   
$indexes[ $row['Key_name'] ]['columns'][] = $row['Column_name'];
                }
                else
                {
                   
$type = 'key';
                    if(
$row['Key_name'] === 'PRIMARY' )
                    {
                       
$type = 'primary';
                    }
                    elseif(
$row['Index_type'] === 'FULLTEXT' )
                    {
                       
$type = 'fulltext';
                    }
                    elseif( !
$row['Non_unique'] )
                    {
                       
$type = 'unique';
                    }
                   
                   
$indexes[ $row['Key_name'] ] = array(
                       
'type'        => $type,
                       
'name'        => $row['Key_name'],
                       
'length'    => array( $length ),
                       
'columns'    => array( $row['Column_name'] )
                        );
                }
            }
           
$definition['indexes'] = $indexes;
           
           
/* Finally, get the table comment and engine */
           
$row = $this->forceQuery( "SHOW TABLE STATUS LIKE '{$this->prefix}" . $this->escape_string( $table ) . "'" )->fetch_assoc();

            if(
$row['Comment'] )
            {
               
$definition['comment']    = $row['Comment'];
            }

            if(
$row['Collation'] )
            {
               
$definition['collation'] = $row['Collation'];
            }
   
            if(
$row['Engine'] )
            {
               
$definition['engine']    = $row['Engine'];
            }
               
        }
       
       
/* Return */
       
return $definition;
    }
   
   
/**
     * Add column to table in database
     *
     * @see        \IPS\Db::compileColumnDefinition
     * @param    string    $table            Table name
     * @param    array    $definition        Column Definition (see \IPS\Db::compileColumnDefinition for details)
     * @return    void
     */
   
public function addColumn( $table, $definition )
    {
        return
$this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` ADD COLUMN {$this->compileColumnDefinition( $definition )}" );
    }
   
   
/**
     * Modify an existing column
     *
     * @see        \IPS\Db::compileColumnDefinition
     * @param    string    $table            Table name
     * @param    string    $column            Column name
     * @param    array    $definition        New column definition (see \IPS\Db::compileColumnDefinition for details)
     * @return    void
     */
   
public function changeColumn( $table, $column, $definition )
    {
        return
$this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` CHANGE COLUMN `{$this->escape_string( $column )}` {$this->compileColumnDefinition( $definition )}" );
    }
   
   
/**
     * Drop a column
     *
     * @param    string            $table            Table name
     * @param    string|array    $column            Column name
     * @return    void
     */
   
public function dropColumn( $table, $column )
    {
        if(
is_array( $column ) )
        {
           
$drops    = array();

            foreach(
$column as $_column )
            {
               
$drops[]    = "DROP COLUMN `{$this->escape_string( $_column )}`";
            }

           
$statement    = implode( ", ", $drops );
        }
        else
        {
           
$statement = "DROP COLUMN `{$this->escape_string( $column )}`";
        }

        return
$this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` {$statement};" );
    }
   
   
/**
     * Add index to table in database
     *
     * @see        \IPS\Db::compileIndexDefinition
     * @param    string    $table                Table name
     * @param    array    $definition            Index Definition (see \IPS\Db::compileIndexDefinition for details)
     * @param    bool    $discardDuplicates    If adding a unique index, should duplicates be discarded? (If FALSE and there are any, an exception will be thrown)
     * @return    void
     */
   
public function addIndex( $table, $definition, $discardDuplicates=TRUE )
    {
       
/* If it's a unique index, make sure there won't be any duplicates */
       
if ( $discardDuplicates and in_array( $definition['type'], array( 'primary', 'unique' ) ) AND $this->returnQuery === FALSE )
        {
           
$this->duplicateTableStructure( $table, "{$table}_temp" );
           
$this->addIndex( "{$table}_temp", $definition, FALSE );
           
$this->insert( "{$table}_temp", \IPS\Db::i()->select( '*', $table ), FALSE, TRUE );
           
$this->dropTable( $table );
           
$this->renameTable( "{$table}_temp", $table );
        }
       
/* Otherwise just do it normally */
       
else
        {
            return
$this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` {$this->buildIndex( $table, $definition )}" );
        }
    }
   
   
/**
     * Modify an existing index
     *
     * @see        \IPS\Db::compileIndexDefinition
     * @param    string    $table            Table name
     * @param    string    $index            Index name
     * @param    array    $definition        New index definition (see \IPS\Db::compileIndexDefinition for details)
     * @return    void
     */
   
public function changeIndex( $table, $index, $definition )
    {
       
$returnQuery = $this->returnQuery;
       
$return = NULL;

        if(
$this->checkForIndex( $table, $index ) )
        {
           
$query = $this->dropIndex( $table, $index );
       
            if(
$returnQuery === TRUE )
            {
               
$return = $query;
            }
        }
       
        if (
$returnQuery )
        {
           
$this->returnQuery = TRUE;
        }
       
       
$query = $this->addIndex( $table, $definition );
       
        if(
$returnQuery === TRUE )
        {
           
$this->returnQuery = FALSE;
           
$return .= $query;
   
            return
$return;
        }

        return
$query;
    }

   
/**
     * Build an index query for add/change
     *
     * @see        \IPS\Db::compileIndexDefinition
     * @param    string    $table            Table name
     * @param    array    $definition        New index definition (see \IPS\Db::compileIndexDefinition for details)
     * @param    array|NULL    $data        Table definition, or null to pull from database
     * @return    void
     */
   
public function buildIndex( $table, $definition, $data=NULL )
    {
       
$indexName    = $definition['name'];
       
        if (
$data === NULL )
        {
           
$data    = $this->getTableDefinition( $table, FALSE, TRUE );
        }
       
       
$engine        = mb_strtolower( $data['engine'] );
       
       
/* Add the index to the table definition */
       
$data['indexes'][ $indexName ] = $definition;
       
       
/* Reduce sub_part if required */
       
$data    = $this->updateDefinitionIndexLengths( $data );
       
$return = '';

       
/* Do we need to adjust the engine because it's a fulltext index? */
       
if( $engine !== mb_strtolower( $data['engine'] ) )
        {
           
$return = "ENGINE={$data['engine']}, ";
        }

       
/* Extract the key we want to add */
       
$definition = $data['indexes'][ $indexName ];

        return
$return . "ADD {$this->compileIndexDefinition( $definition )}";
    }
   
   
/**
     * Drop an index
     *
     * @param    string            $table            Table name
     * @param    string|array    $index            Column name
     * @return    mixed
     */
   
public function dropIndex( $table, $index )
    {
       
$index = ( is_array( $index ) ) ? $index : array( $index );

       
$indexes    = array();

        if( \
IPS\Db::i()->returnQuery )
        {
            foreach(
$index as $key => $col )
            {
                if ( !
$this->checkForIndex( $table, $col ) )
                {
                    unset(
$index[$key] );
                }
            }
        }

        foreach(
$index as $col )
        {
           
$indexes[]    = ( $col == 'PRIMARY KEY' ) ? "DROP " . $col : "DROP INDEX `" . $this->escape_string( $col ) . "`";
        }

       
$_index    = implode( ', ', $indexes );


        try
        {
           
$return = '';
           
            if (
$_index )
            {
               
$return = $this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` {$_index};" );
            }
            else
            {
               
/* Even if we do not run a query here, we need to reset this */
               
\IPS\Db::i()->returnQuery = FALSE;
            }
           
            return
$return;
        }
        catch( \
IPS\Db\Exception $e )
        {
           
/* No need to stop here if index doesn't exist */
           
if ( $e->getCode() !== 1091 )
            {
                throw
$e;
            }

            return
0;
        }
    }

   
/**
     * FIND_IN_SET
     * Generates a WHERE clause to determine if any value from a column containing a comma-delimined list matches any value from an array
     *
     * @param    string    $column        Column name (which contains a comma-delimited list)
     * @param    array    $values        Acceptable values
     * @param    bool    $reverse    If true, will match cases where NO values from $column match any from $values
     * @return     string    Where clause
     * @see        \IPS\Db::in()        More efficient equivilant for columns that do not contain comma-delimited lists
     */
   
public function findInSet( $column, $values, $reverse=FALSE )
    {
       
$where = array();

        foreach(
$values as $i )
        {
            if (
$i != NULL and is_numeric( $i ) )
            {
               
$where[] = ( $reverse ? 'NOT ' : '' ) . "FIND_IN_SET(" . $i . "," . $column . ")";
            }
            else if (
$i != NULL and is_string( $i ) )
            {
               
$where[] = ( $reverse ? 'NOT ' : '' ) . "FIND_IN_SET('" . $this->real_escape_string( $i ) . "'," . $column . ")";
            }
        }

       
$statement = $reverse ? 'AND' : 'OR';
       
        if ( ! empty(
$where ) )
        {
            return
'( ' . implode( " {$statement} ", $where ) . ' )';
        }
        else
        {
            return
$reverse ? '1=1' : '1=0';
        }
    }

   
/**
     * IN
     * Generates a WHERE clause to determine if the value of a column matches any value from an array
     *
     * @param    string    $column            Column name
     * @param    array    $values            Acceptable values
     * @param    bool    $reverse        If true, will match cases where $column does NOT match $values
     * @return     string    Where clause
     * @see        \IPS\Db::findInSet()    For columns that contain comma-delimited lists
     */
   
public function in( $column, $values, $reverse=FALSE )
    {
       
$in    = array();

        if( !
is_array( $values ) )
        {
           
$values = array( $values );
        }
       
        foreach(
$values as $i )
        {
           
/* We must use the !== comparison so that 0 is not treated the same as NULL */
           
if ( $i !== NULL and is_numeric( $i ) and ( is_int( $i ) or is_float( $i ) ) )
            {
               
$in[] = $i;
            }
            else if (
$i != NULL and is_string( $i ) )
            {
               
$in[] = "'" . $this->real_escape_string( $i ) . "'";
            }
        }
   
       
$return = array();

        if ( ! empty(
$in ) )
        {
           
$return[] = $column . ( $reverse ? ' NOT' : '' ) . ' IN(' . implode( ',', $in ) . ')';
        }
       
        if (
count( $return ) )
        {
            return
'( ' . implode( ' OR ', $return ) . ' )';
        }
        else
        {
            return
$reverse ? '1=1' : '1=0';
        }
    }
   
   
/**
     * Bitwise WHERE clause
     *
     * @param    array    $definition        Bitwise keys as defined by the class
     * @param    string    $key            The key to check for
     * @param    bool    $value            Value to check for
     * @return    string
     * @throws    \InvalidArgumentException
     */
   
public function bitwiseWhere( $definition, $key, $value=TRUE )
    {
       
$operator = $value ? '& ' : '& ~';
        foreach (
$definition as $column => $keys )
        {
            if ( isset(
$keys[ $key ] ) )
            {
                return
"(`{$column}` {$operator}{$keys[ $key ]} ) != 0";
            }
        }
       
        throw new \
InvalidArgumentException;
    }

   
/**
     * Strip index lengths in the schema definitions - useful for a better comparison of the definitions
     * since different engines and charsets require different storage.  Also, strip engine and collation.
     *
     * @param    array|string    $data        Table definition (array) or table name (string)
     * @return    array
     */
   
public function normalizeDefinition( $data )
    {
       
$definition  = ( is_array( $data ) ) ? $data : $this->getTableDefinition( $data, FALSE, TRUE );

        if ( isset(
$definition['indexes'] ) )
        {
            foreach(
$definition['indexes'] as $key => &$index )
            {
               
/* Make sure the keys are in the correct order otherwise normal variances trigger differences just because 'columns' can come before 'length', etc */
               
ksort( $index );
               
                if( isset(
$index['length'] ) )
                {
                    foreach(
$index['length'] as $_key => $length )
                    {
                       
$definition['indexes'][ $key ]['length'][ $_key ] = null;
                    }
                }
            }
        }

        if( isset(
$definition['collation'] ) )
        {
            unset(
$definition['collation'] );
        }

        if( isset(
$definition['engine'] ) )
        {
            unset(
$definition['engine'] );
        }
       
       
/* Prevent conflicts when schema says DEFAULT '0' but it is DEFAULT 0 and an INT type column as this is always set as a 0 anyway */
       
foreach( $definition['columns'] as $name => $data )
        {
            if (
in_array( mb_strtoupper( $data['type'] ), array_keys( static::$dataTypes['database_column_type_numeric'] ) ) and ( ! in_array( mb_strtoupper( $data['type'] ), array( 'DECIMAL', 'FLOAT', 'BIT' ) ) ) and is_numeric( $data['default'] ) )
            {
               
$definition['columns'][ $name ]['default'] = intval( $data['default'] );
            }
        }
       
        return
$definition;
    }

   
/**
     * Attempt to fix issues with keys longer than maximum allowed by DB engine
     * which is 1000 bytes for MyISAM and 767 for InnoDB taking into consideration the
     * multiplier (4 bytes per character for utf8mb4 and 3 bytes per character for UTF8)
     *
     * @param    array|string    $data        Table definition (array) or table name (string)
     * @return    array
     */
   
public function updateDefinitionIndexLengths( $data )
    {
       
$definition  = ( is_array( $data ) ) ? $data : $this->getTableDefinition( $data, FALSE, TRUE );
       
$length      = 0;
       
/* We use 4 for utf8mb4 to prevent issues if the client attempts to switch or forgets to mark conf_global.php that utf8mb4 is used */
       
$multiplier     = 4;
       
$needsFixing = array();
       
$maxLen      = 1000;
       
        if ( ( ! isset(
$definition['engine'] ) OR mb_strtolower( $definition['engine'] ) == 'innodb' ) and isset( $definition['indexes'] ) )
        {
           
$definition['engine'] = $this->defaultEngine();
           
           
/* Any FULLTEXT fields? */
           
foreach( $definition['indexes'] as $key => $data )
            {
                if (
$data['type'] === 'fulltext' )
                {
                   
/* If this is a fulltext index, set engine to myisam but only if engine is something besides innodb or myisam OR the mysql version is less than 5.6 - in
                        this case we assume the default engine is most likely either myisam or innodb */
                   
if( !$this->_innoDbSupportsFulltextIndexes() OR ( isset( $definition['engine'] ) AND !in_array( mb_strtoupper( $definition['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
                    {
                       
$definition['engine'] = 'myisam';
                    }
                }
            }
        }
       
        if ( \
mb_strtolower( $definition['engine'] ) === 'innodb' )
        {
           
$maxLen = 767;
        }
       
        if ( isset(
$definition['indexes'] ) )
        {
            foreach(
$definition['indexes'] as $key => $index )
            {
               
$thisLength = null;
               
$hasText    = false;
               
                foreach(
$index['columns'] as $i => $column )
                {
                   
$thisLength    = ( isset( $index['length'][ $i ] ) ) ? $index['length'][ $i ] : ( ( (int) $definition['columns'][ $column ]['length'] or empty( $definition['columns'][ $column ]['length'] ) ) ? $definition['columns'][ $column ]['length'] : 250 );
                   
                   
$isText = in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'mediumtext', 'text' ) );
                   
                    if (
$hasText === false and $isText === true )
                    {
                       
$hasText = true;
                    }
                   
                    if ( isset(
$definition['columns'][ $column ] ) and ( ( ! empty( $thisLength ) or $isText ) ) )
                    {
                       
$length += $thisLength;
                    }
                }
           
                if ( (
$length * $multiplier > $maxLen ) or $hasText )
                {
                    foreach(
$index['columns'] as $i => $column )
                    {
                       
$thisLength    = ( isset( $index['length'][ $i ] ) ) ? $index['length'][ $i ] : ( (int) $definition['columns'][ $column ]['length'] ? $definition['columns'][ $column ]['length'] : 250 );

                        if ( isset(
$definition['columns'][ $column ] ) and ( ( ! empty( $thisLength ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'mediumtext', 'text' ) ) ) ) )
                        {
                           
/* Column name, column length, column type  */
                           
$needsFixing[ $key ][ $i ] = array( $column, $thisLength, $definition['columns'][ $column ]['type'] );
                        }
                    }
                }
                       
               
$length = 0;
            }
        }
       
        if (
count( $needsFixing ) )
        {
            foreach(
$needsFixing as $key => $i )
            {
               
$totalLength    = 0;
               
$maxChars        = $maxLen / $multiplier;

                foreach(
$i as $vals )
                {
                   
$totalLength += $vals[1];
                }

                if (
$totalLength > $maxChars )
                {
                   
/* Check each column can be reduced by the amount we need reducing */
                   
$debt = 0;
 
                   
$reduceEachBy = ( ( 100 / $totalLength ) * $maxChars) / 100;
 
                   
/* Apply debt if we have any. We do not reduce integers */
                   
foreach( $i as $x => $vals )
                    {
                        if (
in_array( mb_strtoupper( $vals[2] ), array_keys( static::$dataTypes['database_column_type_numeric'] ) ) )
                        {
                           
$debt += $vals[1];
                        }
                    }

                   
/* Recalculate value to multiply index sub lengths with (subtracting debt) */
                   
if ( $debt < $totalLength )
                    {
                       
$reduceEachBy = ( ( 100 / ($totalLength - $debt) ) * ( $maxChars - $debt ) ) / 100;
                    }
                   
                    foreach(
$i as $x => $vals )
                    {
                       
/* No length? */
                       
if ( empty( $vals[1] ) )
                        {
                           
$vals[1] = 250;
                        }

                        if (
in_array( mb_strtoupper( $vals[2] ), array_keys( static::$dataTypes['database_column_type_numeric'] ) ) )
                        {
                           
/* Preserve col len where possible but if the column length is greater than subpart allowed, NULL the length
                               otherwise MySQL will complain as you cannot use subpart on non-string column. */
                           
if ( $vals[1] > floor( $maxLen / $multiplier ) )
                            {
                               
$vals[1] = NULL;
                               
$i[ $x ] = $vals;
                            }
                           
                            continue;
                        }

                       
$vals[1] = floor( $vals[1] * $reduceEachBy );    
                       
$i[ $x ] = $vals;
                    }
                }

                foreach(
$i as $x => $vals )
                {
                    if (
$definition['columns'][ $definition['indexes'][ $key ]['columns'][ $x ] ]['length'] != $vals[1] )
                    {
                       
$definition['indexes'][ $key ]['length'][ $x ] = intval( $vals[1] );
                    }
                    else
                    {
                       
$definition['indexes'][ $key ]['length'][ $x ] = NULL;
                    }
                }
            }
        }

        return
$definition;
    }
   
   
/**
     * Create database
     *
     * @param    string    $name    Database Name
     * @return    bool
     */
   
public function createDatabase( $name )
    {
        return (
$this->query( "CREATE DATABASE ". $this->escape_string( "{$name}" ) ) );
    }

   
/**
     * Strip comments from a .sql file
     *
     * @param    string    $contents    Contents from SQL file
     * @return    string
     */
   
public static function stripComments( $contents )
    {
       
$contents = preg_replace( '/\/\*.+?\*\//', '', $contents );
       
$contents = preg_replace( '/#.*/', '', $contents );
       
$contents = preg_replace( '/--.*/', '', $contents );
       
$contents = trim( $contents );

        return
$contents;
    }

   
/**
     * Replace binds in a prepared query to get the "full" query
     *
     * @param    string    $query    Query
     * @param    array     $binds    Any binds in the query
     * @return    string
     */
   
public static function _replaceBinds( $query, $binds )
    {
       
/* Replace ?s with the actual values */
       
if( is_array( $binds ) AND count( $binds ) )
        {
            foreach (
$binds as $b )
            {
               
$query = preg_replace( '/\?/', var_export( $b, TRUE ), $query, 1 );
            }
        }

        return
$query;
    }
}