Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+423 -144
View File
@@ -2,9 +2,9 @@
/**
* @brief Database Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 18 Feb 2013
* @version SVN_VERSION_NUMBER
*/
@@ -30,6 +30,7 @@ class _Db extends \mysqli
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
@@ -95,7 +96,7 @@ class _Db extends \mysqli
$identifier = ( $identifier === NULL ) ? '__MAIN' : $identifier;
/* Don't have an instance? */
if( !isset( self::$multitons[ $identifier ] ) )
if( !isset( static::$multitons[ $identifier ] ) )
{
/* Load the default settings if necessary */
if( $identifier === '__MAIN' )
@@ -103,57 +104,130 @@ class _Db extends \mysqli
require( \IPS\ROOT_PATH . '/conf_global.php' );
$connectionSettings = isset( $INFO ) ? $INFO : array();
}
/* Connect */
$classname = get_called_class();
self::$multitons[ $identifier ] = @new $classname(
$connectionSettings['sql_host'],
$connectionSettings['sql_user'],
$connectionSettings['sql_pass'],
$connectionSettings['sql_database'],
( isset( $connectionSettings['sql_port'] ) and $connectionSettings['sql_port']) ? $connectionSettings['sql_port'] : NULL,
( isset( $connectionSettings['sql_socket'] ) and $connectionSettings['sql_socket'] ) ? $connectionSettings['sql_socket'] : NULL
);
/* If the connection failed, throw an exception */
if( $error = mysqli_connect_error() )
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' )
{
throw new \IPS\Db\Exception( $error, self::$multitons[ $identifier ]->connect_errno );
static::$multitons[ $identifier ]->charset = 'utf8mb4';
static::$multitons[ $identifier ]->collation = 'utf8mb4_unicode_ci';
static::$multitons[ $identifier ]->binaryCollation = 'utf8mb4_bin';
}
/* UTF8MB4? */
if ( isset( $connectionSettings['sql_utf8mb4'] ) and $connectionSettings['sql_utf8mb4'] )
else
{
self::$multitons[ $identifier ]->charset = 'utf8mb4';
self::$multitons[ $identifier ]->collation = 'utf8mb4_unicode_ci';
self::$multitons[ $identifier ]->binaryCollation = 'utf8mb4_bin';
}
/* If we succeeded, set the charset */
if( !self::$multitons[ $identifier ]->set_charset( self::$multitons[ $identifier ]->charset ) )
{
/* But if setting that failed, fall back to UTF8 */
self::$multitons[ $identifier ]->charset = 'utf8';
self::$multitons[ $identifier ]->collation = 'utf8_unicode_ci';
self::$multitons[ $identifier ]->binaryCollation = 'utf8_bin';
self::$multitons[ $identifier ]->set_charset( self::$multitons[ $identifier ]->charset );
static::$multitons[ $identifier ]->charset = 'utf8';
static::$multitons[ $identifier ]->collation = 'utf8_unicode_ci';
static::$multitons[ $identifier ]->binaryCollation = 'utf8_bin';
}
if ( \IPS\IN_DEV )
{
self::$multitons[ $identifier ]->query( "SET sql_mode=STRICT_ALL_TABLES" );
}
/* Set the prefix */
if ( isset( $connectionSettings['sql_tbl_prefix'] ) )
{
self::$multitons[ $identifier ]->prefix = $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 self::$multitons[ $identifier ];
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;
}
/**
@@ -169,7 +243,7 @@ class _Db extends \mysqli
/**
* @brief Binary Collation
*/
protected $binaryCollation = 'utf8_bin';
public $binaryCollation = 'utf8_bin';
/**
* @brief Table Prefix
@@ -186,17 +260,29 @@ class _Db extends \mysqli
* @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 )
public function query( $query, $log = TRUE, $read=FALSE )
{
/* Should we return the query instead of executing it? */
if( $this->returnQuery === TRUE )
@@ -208,15 +294,28 @@ class _Db extends \mysqli
/* Log */
if ( \IPS\QUERY_LOG and $log )
{
$this->log( $query );
$this->log( $query, ( $read and $this->readWriteSeparation ) ? 'read' : 'write' );
}
/* Run */
$return = parent::query( $query );
if ( $return === FALSE )
if ( $read and $this->reader and $this->readWriteSeparation )
{
throw new \IPS\Db\Exception( $this->error, $this->errno );
$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;
}
@@ -225,16 +324,17 @@ class _Db extends \mysqli
*
* @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 )
public function forceQuery( $query, $log = TRUE, $read=FALSE )
{
$return = $this->returnQuery;
$this->returnQuery = false;
$result = $this->query( $query, $log );
$result = $this->query( $query, $log, $read );
$this->returnQuery = $return;
@@ -246,9 +346,10 @@ class _Db extends \mysqli
*
* @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 )
public function preparedQuery( $query, array $_binds, $read=FALSE )
{
/* Init Bind object */
$bind = new Db\Bind();
@@ -256,33 +357,46 @@ class _Db extends \mysqli
/* Sort out subqueries */
$binds = array();
$i = 0;
for ( $j = 0; $j < mb_strlen( $query ); $j++ )
for ( $j = 0; $j < \strlen( $query ); $j++ )
{
if ( $query[ $j ] == '?' )
{
if ( $_binds[ $i ] instanceof \IPS\Db\Select )
if ( array_key_exists( $i, $_binds ) )
{
$query = mb_substr( $query, 0, $j ) . $_binds[ $i ]->query . mb_substr( $query, $j + 1);
$j += mb_strlen( $_binds[ $i ]->query );
foreach ( $_binds[ $i ]->binds as $_bind )
if ( $_binds[ $i ] instanceof \IPS\Db\Select )
{
$binds[] = $_bind;
$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++;
}
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 ) )
{
@@ -296,13 +410,27 @@ class _Db extends \mysqli
break;
case 'string':
$bind->add( 's', $bindVal );
if( \strlen( $bindVal ) > $longThreshold )
{
$bind->add( 'b', NULL );
}
else
{
$bind->add( 's', $bindVal );
}
break;
case 'object':
if( method_exists( $bindVal, '__toString' ) )
{
$bind->add( 's', (string) $bindVal );
if( \strlen( $bindVal ) > $longThreshold )
{
$bind->add( 'b', NULL );
}
else
{
$bind->add( 's', (string) $bindVal );
}
break;
}
// Deliberately no break
@@ -328,14 +456,14 @@ class _Db extends \mysqli
if ( \IPS\QUERY_LOG )
{
/* Log */
$this->log( $this->_replaceBinds( $query, $binds ) );
$this->log( static::_replaceBinds( $queryForLog, $binds ), ( $read and $this->readWriteSeparation ) ? 'read' : 'write' );
}
/* Return full query */
if( $this->returnQuery === TRUE )
{
$this->returnQuery = FALSE;
return $this->_replaceBinds( $query, $binds );
return static::_replaceBinds( $queryForLog, $binds );
}
/* Add a backtrace to the query so we know where it came from if it causes issues */
@@ -356,26 +484,66 @@ class _Db extends \mysqli
}
$_query = $query;
$query = "/*{$comment}*/ {$query}";
/* Prepare */
$stmt = parent::prepare( $query );
if( $stmt === FALSE )
if ( $read and $this->reader and $this->readWriteSeparation )
{
throw new \IPS\Db\Exception( $this->error, $this->errno, NULL, $_query, $binds );
$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 )
{
call_user_func_array( array( $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();
if ( $stmt->error )
$stmt->execute();
/* Handle errors */
$count = 1;
while ( $stmt->error )
{
throw new \IPS\Db\Exception( $stmt->error, $stmt->errno, NULL, $_query, $binds );
/* 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 */
@@ -386,14 +554,16 @@ class _Db extends \mysqli
* 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 )
{
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
'extra' => NULL,
);
}
@@ -402,7 +572,7 @@ class _Db extends \mysqli
*
* @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 example)
* @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
@@ -411,6 +581,7 @@ class _Db extends \mysqli
* @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
*
*/
@@ -464,7 +635,8 @@ class _Db extends \mysqli
}
else
{
$query .= " FROM `{$this->prefix}{$table[0]}` AS `{$table[1]}`";
$tableName = ( $table[0] instanceof \IPS\Db\Select ) ? '(' . $table[0] . ')' : '`' . $this->prefix . $table[0] . '`';
$query .= " FROM {$tableName} AS `{$table[1]}`";
}
}
else
@@ -477,7 +649,7 @@ class _Db extends \mysqli
{
$where = $this->compileWhereClause( $where );
$query .= ' WHERE ' . $where['clause'];
$binds = $where['binds'];
$binds = array_merge( $binds, $where['binds'] );
}
/* Group? */
@@ -487,12 +659,40 @@ class _Db extends \mysqli
{
$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
{
$query .= " GROUP BY `{$group}`";
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}";
}
}
@@ -517,23 +717,24 @@ class _Db extends \mysqli
}
/* Return */
return new \IPS\Db\Select( $query, $binds, $this, $flags & static::SELECT_MULTIDIMENSIONAL_JOINS );
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)
* @li \IPS\DB::SELECT_SQL_CALC_FOUND_ROWS Will add SQL_CALC_FOUND_ROWS
* @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 )
public function union( $selects, $order, $limit, $group=NULL, $unionAll=FALSE, $flags=0, $where=NULL, $querySelect='*' )
{
/* Combine selects */
$query = array();
@@ -546,7 +747,7 @@ class _Db extends \mysqli
$union = $unionAll ? "UNION ALL" : "UNION";
$query = "SELECT " . ( $flags & static::SELECT_SQL_CALC_FOUND_ROWS ? "SQL_CALC_FOUND_ROWS " : "" ) . "* FROM( " . implode( ' ' . $union . ' ', $query ) . ") derivedTable ";
$query = "SELECT " . ( $flags & static::SELECT_SQL_CALC_FOUND_ROWS ? "SQL_CALC_FOUND_ROWS " : "" ) . $querySelect . " FROM( " . implode( ' ' . $union . ' ', $query ) . ") derivedTable ";
/* WHERE */
if ( $where )
@@ -610,8 +811,12 @@ class _Db extends \mysqli
{
return $stmt;
}
$insertId = $stmt->insert_id;
$stmt->close();
return $stmt->insert_id;
return $insertId;
}
/**
@@ -638,7 +843,11 @@ class _Db extends \mysqli
return $stmt;
}
return $getInsertId ? $stmt->insert_id : $stmt->affected_rows;
$return = $getInsertId ? $stmt->insert_id : $stmt->affected_rows;
$stmt->close();
return $return;
}
/**
@@ -704,13 +913,12 @@ class _Db extends \mysqli
/**
* Run UPDATE statement and return number of affected rows
*
* @see \IPS\Db::build
* @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::build for details)
* @param array $joins Tables to join (see \IPS\Db::build for details)
* @param int|array|null $limit LIMIT clause (see \IPS\Db::build for details)
* @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
@@ -787,19 +995,22 @@ class _Db extends \mysqli
return $stmt;
}
return $stmt->affected_rows;
$return = $stmt->affected_rows;
$stmt->close();
return $return;
}
/**
* Run DELETE statement and return number of affected rows
*
* @see \IPS\Db::build
* @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::build for details)
* @param string|null $order ORDER BY clause (see \IPS\Db::build for details)
* @param int|array|null $limit LIMIT clause (see \IPS\Db::build 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
* @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
*/
@@ -816,8 +1027,12 @@ class _Db extends \mysqli
{
return $stmt;
}
$return = $stmt->affected_rows;
$stmt->close();
return $stmt->affected_rows;
return $return;
}
/* Basic query */
@@ -864,13 +1079,26 @@ class _Db extends \mysqli
return $stmt;
}
return $stmt->affected_rows;
$return = $stmt->affected_rows;
$stmt->close();
return $return;
}
/**
* Compile WHERE clause
*
* @param string|array $data See \IPS\Db::build for details
* @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 )
@@ -941,10 +1169,10 @@ class _Db extends \mysqli
/**
* Compile LIMIT clause
*
* @param int|array $data See \IPS\Db::build for details
* @param int|array $data Rows to fetch or array( offset, limit )
* @return string
*/
protected function compileLimitClause( $data )
public function compileLimitClause( $data )
{
$limit = NULL;
if( is_array( $data ) )
@@ -996,7 +1224,7 @@ class _Db extends \mysqli
public function compileColumnDefinition( $data )
{
/* Specify name and type */
$definition = "`{$data['name']}` {$data['type']} ";
$definition = "`{$data['name']}` " . \strtoupper( $data['type'] ) . ' ';
/* Some types specify length */
if(
@@ -1082,7 +1310,7 @@ class _Db extends \mysqli
}
else
{
$defaultValue = in_array( $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'] );
$defaultValue = in_array( \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} ";
}
}
@@ -1103,11 +1331,11 @@ class _Db extends \mysqli
}
/* Comment */
if( isset( $data['comment'] ) )
if( isset( $data['comment'] ) and ! empty( $data['comment'] ) )
{
$definition .= "COMMENT '{$this->escape_string( $data['comment'] )}'";
}
/* Return */
return $definition;
}
@@ -1245,6 +1473,7 @@ class _Db extends \mysqli
public function _createTableQuery( $data )
{
$data = $this->updateDefinitionIndexLengths( $data );
$mysqlVersion = \IPS\Db::i()->server_info;
/* Start with a basic CREATE TABLE */
$query = 'CREATE ';
@@ -1273,7 +1502,7 @@ class _Db extends \mysqli
{
/* 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( \IPS\Db::i()->server_version < 50600 OR ( isset( $data['engine'] ) AND !in_array( \strtoupper( $data['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
if( !$this->_innoDbSupportsFulltextIndexes() OR ( isset( $data['engine'] ) AND !in_array( \strtoupper( $data['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
{
$data['engine'] = 'MYISAM';
}
@@ -1383,14 +1612,28 @@ class _Db extends \mysqli
return $engine['Engine'];
}
}
if( \IPS\Db::i()->server_version < 50600 )
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 ) )
{
return 'MyISAM';
$mariaVersion = $matches[3];
return version_compare( $mariaVersion, '10.0.5', '>=' );
}
/* Normal MySQL supports fulltext for InnoDB on versions higher than 5.6 */
else
{
return 'InnoDB';
return $this->server_version >= 50600;
}
}
@@ -1401,7 +1644,7 @@ class _Db extends \mysqli
* @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 void
* @return mixed
*/
public function dropTable( $table, $ifExists=FALSE, $temporary=FALSE )
{
@@ -1482,7 +1725,7 @@ class _Db extends \mysqli
if( $columnDefinition['type'] === 'ENUM' or $columnDefinition['type'] === 'SET' )
{
preg_match_all( "/'(.+?)'/", $matches[2], $enum );
preg_match_all( "/'(.*?)'/", $matches[2], $enum );
$columnDefinition['values'] = $enum[1];
}
else
@@ -1727,12 +1970,18 @@ class _Db extends \mysqli
* @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 )
public function buildIndex( $table, $definition, $data=NULL )
{
$indexName = $definition['name'];
$data = $this->getTableDefinition( $table, FALSE, TRUE );
if ( $data === NULL )
{
$data = $this->getTableDefinition( $table, FALSE, TRUE );
}
$engine = mb_strtolower( $data['engine'] );
/* Add the index to the table definition */
@@ -1767,7 +2016,7 @@ class _Db extends \mysqli
$indexes = array();
if( \IPS\DB::i()->returnQuery )
if( \IPS\Db::i()->returnQuery )
{
foreach( $index as $key => $col )
{
@@ -1788,7 +2037,19 @@ class _Db extends \mysqli
try
{
return $_index ? $this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` {$_index};" ) : '';
$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 )
{
@@ -1827,17 +2088,12 @@ class _Db extends \mysqli
$where[] = ( $reverse ? 'NOT ' : '' ) . "FIND_IN_SET('" . $this->real_escape_string( $i ) . "'," . $column . ")";
}
}
$return = array();
$statement = $reverse ? 'AND' : 'OR';
if ( ! empty( $where ) )
{
$return[] = '( ' . implode( " OR ", $where ) . ' )';
}
if ( count( $return ) )
{
return '( ' . implode( ' OR ', $return ) . ' )';
return '( ' . implode( " {$statement} ", $where ) . ' )';
}
else
{
@@ -1866,7 +2122,8 @@ class _Db extends \mysqli
foreach( $values as $i )
{
if ( $i != NULL and is_numeric( $i ) and ( is_int( $i ) or is_float( $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;
}
@@ -1875,7 +2132,7 @@ class _Db extends \mysqli
$in[] = "'" . $this->real_escape_string( $i ) . "'";
}
}
$return = array();
if ( ! empty( $in ) )
@@ -1929,8 +2186,11 @@ class _Db extends \mysqli
if ( isset( $definition['indexes'] ) )
{
foreach( $definition['indexes'] as $key => $index )
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 )
@@ -1950,7 +2210,16 @@ class _Db extends \mysqli
{
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( \strtoupper( $data['type'] ), array_keys( static::$dataTypes['database_column_type_numeric'] ) ) and ( ! in_array( \strtoupper( $data['type'] ), array( 'DECIMAL', 'FLOAT', 'BIT' ) ) ) and is_numeric( $data['default'] ) )
{
$definition['columns'][ $name ]['default'] = intval( $data['default'] );
}
}
return $definition;
}
@@ -1981,7 +2250,7 @@ class _Db extends \mysqli
{
/* 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( \IPS\Db::i()->server_version < 50600 OR ( isset( $definition['engine'] ) AND !in_array( \strtoupper( $definition['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
if( !$this->_innoDbSupportsFulltextIndexes() OR ( isset( $definition['engine'] ) AND !in_array( \strtoupper( $definition['engine'] ), array( 'INNODB', 'MYISAM' ) ) ) )
{
$definition['engine'] = 'myisam';
}
@@ -2074,8 +2343,11 @@ class _Db extends \mysqli
}
/* Recalculate value to multiply index sub lengths with (subtracting debt) */
$reduceEachBy = ( ( 100 / ($totalLength - $debt) ) * ( $maxChars - $debt ) ) / 100;
if ( $debt < $totalLength )
{
$reduceEachBy = ( ( 100 / ($totalLength - $debt) ) * ( $maxChars - $debt ) ) / 100;
}
foreach( $i as $x => $vals )
{
/* No length? */
@@ -2086,7 +2358,14 @@ class _Db extends \mysqli
if ( in_array( mb_strtoupper( $vals[2] ), array_keys( static::$dataTypes['database_column_type_numeric'] ) ) )
{
/* Preserve col len */
/* 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;
}
@@ -2099,7 +2378,7 @@ class _Db extends \mysqli
{
if ( $definition['columns'][ $definition['indexes'][ $key ]['columns'][ $x ] ]['length'] != $vals[1] )
{
$definition['indexes'][ $key ]['length'][ $x ] = $vals[1];
$definition['indexes'][ $key ]['length'][ $x ] = intval( $vals[1] );
}
else
{
@@ -2146,7 +2425,7 @@ class _Db extends \mysqli
* @param array $binds Any binds in the query
* @return string
*/
protected function _replaceBinds( $query, $binds )
public static function _replaceBinds( $query, $binds )
{
/* Replace ?s with the actual values */
if( is_array( $binds ) AND count( $binds ) )