<?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 IPSUtf8;

/**
 * @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
{
	/**
	 * @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;
	
	/**
	 * @brief	Return Query
	 */
	public $returnQuery = FALSE;

	/**
	 * Get instance
	 *
	 * @param	mixed	$identifier			Identifier
	 * @param	array	$connectionSettings	Connection settings (use when initiating a new connection)
	 * @return	IPSUtf8\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( self::$multitons[ $identifier ] ) )
		{
			/* Load the default settings if necessary */
			if ( $identifier === '__MAIN' OR $identifier == 'utf8' )
			{
				require( ROOT_PATH . '/conf_global.php' );
				$connectionSettings = $INFO;
			}
			
			if ( isset( $connectionSettings['sql_tbl_prefix'] ) )
			{
				$connectionSettings['sql_tbl_prefix'] = str_replace( 'x_utf_', '', $connectionSettings['sql_tbl_prefix'] );
			}
			
			/* UTF8 Connection settings */
			if ( mb_substr( $identifier, 0, 4 ) == 'utf8' )
			{
				$connectionSettings['sql_tbl_prefix'] = 'x_utf_' . $connectionSettings['sql_tbl_prefix'];
			}
			
			/* 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() )
			{
				throw new \IPSUtf8\Db\Exception( $error, self::$multitons[ $identifier ]->connect_errno );
			}
			
			/* If we succeeded, set the charset */
			if ( mb_substr( $identifier, 0, 4 ) == 'utf8' )
			{
				/* UTF8MB4? */
				if ( isset( $connectionSettings['sql_utf8mb4'] ) and $connectionSettings['sql_utf8mb4'] )
				{
					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 ) )
				{
					/* Fallback 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( 'utf8' );
				}
			}
			
			/* Set the prefix */
			if ( isset( $connectionSettings['sql_tbl_prefix'] ) )
			{
				self::$multitons[ $identifier ]->prefix = $connectionSettings['sql_tbl_prefix'];
			}
		}
		
		/* Return */
		return self::$multitons[ $identifier ];
	}
	
	/**
	 * @brief	Charset
	 */
	protected $charset = 'utf8';
	
	/**
	 * @brief	Collation
	 */
	protected $collation = 'utf8_unicode_ci';
	
	/**
	 * @brief	Binary Collation
	 */
	protected $binaryCollation = 'utf8_bin';
	
	/**
	 * @brief	Default MySQL Engine
	 */
	protected $defaultEngine = NULL;

	/**
	 * @brief	Table Prefix
	 */
	public $prefix = '';
	
	/**
	 * Run a query
	 *
	 * @param	string	$query	The query
	 * @return	mixed
	 * @see		<a href="http://uk1.php.net/manual/en/mysqli.query.php">mysqli::query</a>
	 * @throws	\IPS\Db\Exception
	 */
	public function query( string $query, int $result_mode = MYSQLI_STORE_RESULT )
	{
		/* Should we return the query instead of executing it? */
		if( $this->returnQuery === TRUE )
		{
			$this->returnQuery	= FALSE;
			return $query;
		}

		$return = parent::query( $query );
		if ( $return === FALSE )
		{
			throw new \IPSUtf8\Db\Exception( $this->error, $this->errno );
		}
		return $return;
	}
	
	/**
	 * 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();
		}
	}
	
	/**
	 * Overload this method so we can change the charset if required
	 *
	 * @param	string	Charset
	 */
	public function set_charset( $charset )
	{
		if ( mb_substr( $charset, 0, 4 ) == 'utf8' )
		{
			if ( $charset === 'utf8mb4' )
			{
				$this->charset         = 'utf8mb4';
				$this->collation       = 'utf8mb4_unicode_ci';
				$this->binaryCollation = 'utf8mb4_bin';
			}
			else
			{
				$this->charset			= 'utf8';
				$this->collation		= 'utf8_unicode_ci';
				$this->binaryCollation	= 'utf8_bin';
			}
		}
		
		return parent::set_charset( $charset );
	}
	
	/** 
	 * Run Prepared SQL Statement
	 *
	 * @param	string	$query	SQL Statement
	 * @param	array	$_binds	Variables to bind
	 * @return	\mysqli_stmt
	 */
	public function preparedQuery( $query, array $_binds )
	{
		/* Init Bind object */
		$bind = new Db\Bind();
		
		/* Sort out subqueries */
		$binds = array();
		$i = 0;
		foreach ( $_binds as $bindVal )
		{
			$i++;
			if ( $bindVal instanceof \IPSUtf8\Db\Select )
			{
				$pos = 0;
				for ( $j=0; $j<$i; $j++ )
				{
					$pos = mb_strpos( $query, '?', $pos ) + 1;
				}					
				$query = mb_substr( $query, 0, $pos - 1 ) . $bindVal->query . mb_substr( $query, $pos );
				$i--;
				
				foreach ( $bindVal->binds as $v )
				{
					$binds[] = $v;
				}
			}
			else
			{
				$binds[] = $bindVal;
			}
		}
		
		/* Loop values to bind */
		$i = 0;
		foreach ( $binds as $bindVal )
		{
			$i++;
			switch ( \gettype( $bindVal ) )
			{
				case 'boolean':
				case 'integer':
					$bind->add( 'i', $bindVal );
					break;
					
				case 'double':
					$bind->add( 'd', $bindVal );
					break;
												
				case 'string':
					$bind->add( 's', $bindVal );
					break;
					
				case 'object':
					if( method_exists( $bindVal, '__toString' ) )
					{
						$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;
			}
		}
		
		/* 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 $b['class'] !== 'IPSUtf8\_Db' )
			{
				$comment = "{$b['class']}::{$b['function']}:{$line}";
				break;
			}
		}
		$_query = $query;
		$query = "/*{$comment}*/ {$query}";
		
		/* Prepare */		
		$stmt = parent::prepare( $query );
		if( $stmt === FALSE )
		{
			throw new \IPSUtf8\Db\Exception( $this->error, $this->errno, NULL, $_query, $binds );
		}
		
		/* Bind values */
		if( $bind->haveBinds() === TRUE )
		{
			$stmt->bind_param( ...$bind->get() );
		}
		
		/* Execute */
		$stmt->execute();
		if ( $stmt->error )
		{
			throw new \IPSUtf8\Db\Exception( $stmt->error, $stmt->errno, NULL, $_query, $binds );
		}
		$stmt->store_result();
				
		/* Return a Statement object */
		return $stmt;
	}
	
	const SELECT_DISTINCT = 1;
	
	/**
	 * 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 )
	 * @param	array|string|NULL	$where		WHERE clause (see example)
	 * @param	string|NULL			$order		ORDER BY clause
	 * @param	array|int			$limit		Rows to fetch or array( offset, limit )
	 * @param	string|NULL			$group		Column to GROUP BY
	 * @param	array|string|NULL	$having		HAVING clause (same format as WHERE clause)
	 * @param	int					$flags		Bitwise flags
	 *	@li	\IPSUtf8\Db::SELECT_DISTINCT			Will use SELECT DISTINCT
	 * @return	\IPSUtf8\Db\Select
	 * 
	 */
	public function select( $columns, $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 ';
		}
		
		/* Columns */
		if ( \is_string( $columns ) )
		{
			$query .= $columns;
		}
		else
		{
			$query .= implode( ', ', array_map( function( $col )
			{
				return '`' . $col . '`';
			}, $columns ) );
		}
		
		/* Tables */
		if ( \is_array( $table ) )
		{
			$query .= " FROM `{$this->prefix}{$table[0]}` AS `{$table[1]}`";
		}
		else
		{
			$query .= " FROM `{$this->prefix}{$table}` AS `{$table}`";
		}
		
		/* WHERE */
		if ( $where )
		{
			$where = $this->compileWhereClause( $where );
			$query .= ' WHERE ' . $where['clause'];
			$binds = $where['binds'];
		}
		
		/* Group? */
		if( $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 \IPSUtf8\Db\Select( $query, $binds, $this );
	}
	
	/**
	 * Build UNION statement
	 *
	 * @param	array			$selects	Array of \IPSUtf8\Db\Select objects
	 * @param	string|NULL		$order		ORDER BY clause
	 * @param	array|int		$limit		Rows to fetch or array( offset, limit )
	 * @return	\IPSUtf8\Db|Select
	 */
	public function union( $selects, $order, $limit )
	{
		/* Combine selects */
		$query = array();
		$binds = array();
		foreach ( $selects as $s )
		{
			$query[] = '( ' . $s->query . ' )';
			$binds = array_merge( $binds, $s->binds );
		}
		$query = implode( ' UNION ', $query );
		
		/* Order? */
		if( $order )
		{
			$query .= ' ORDER BY ' . $order;
		}
		
		/* Limit */
		if( $limit )
		{
			$query .= $this->compileLimitClause( $limit );
		}
		
		/* Return */
		return new \IPSUtf8\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|\IPSUtf8\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.
	 * @see		\IPSUtf8\Db::replace()
	 * @param	bool					$bulkInsertNoPrep	Turns off prepared statements for bulk inserts
	 * @return	int
	 * @throws	\IPSUtf8\Db\Exception
	 */
	public function insert( $table, $set, $odkUpdate=FALSE, $bulkInsertNoPrep=FALSE, $tableDefinition=NULL )
	{
		/* Is a statement? */
		if ( $set instanceof \IPSUtf8\Db\Select )
		{
			$query = "INSERT " . ( $odkUpdate ? " IGNORE " : '' ) . " INTO `{$this->prefix}{$table}` " . $set->query;
			$binds = $set->binds;
			$odkUpdate = false;
		}
		
		/* Nope, normal array */
		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();
			foreach ( $set as $row )
			{
				if ( $columns === NULL )
				{
					$columns = array_map( function( $val ){ return "`{$val}`"; }, array_keys( $row ) );
				}
				
				if ( \count( $set ) > 1 AND $bulkInsertNoPrep )
				{
					if ( $tableDefinition === NULL )
					{
						$tableDefinition = $this->getTableDefinition( $table );
					}
					
					foreach( $row as $k => $v )
					{
						if ( isset( $tableDefinition['definition']['columns'][ $k ] ) )
						{
							$isInt = \in_array( \mb_strtolower( $tableDefinition['definition']['columns'][ $k ]['type'] ), array( 'integer', 'int', 'smallint', 'tinyint', 'mediumint', 'bigint', 'decimal', 'numeric', 'float', 'double' ) );
							
							if ( $isInt )
							{
								if ( empty( $v ) )
								{
									
									if ( ! empty( $tableDefinition['definition']['columns'][ $k ]['allow_null'] ) AND $v !== 0 )
									{
										$row[ $k ] = 'null';
									}
									else
									{
										$row[ $k ] = 0;
									}
								}
							}
							else
							{
								if ( empty( $v ) )
								{
									if ( ! empty( $tableDefinition['definition']['columns'][ $k ]['allow_null'] ) AND $v !== '' )
									{
										$row[ $k ] = 'null';
									}
									else
									{
										$row[ $k ] = "''";
									}
								}
								else
								{
									$row[ $k ] = "'" . $this->real_escape_string( $v ) . "'";
								}
							}
						}
						else
						{
							if ( ! ctype_digit( (string) $v ) )
							{
								if ( empty( $v ) )
								{
									$row[ $k ] = 'null';
								}
								else
								{
									$row[ $k ] = "'" . $this->real_escape_string( $v ) . "'";
								}
							}
						}
					}
					
					$values[] = '(' . implode( ', ', $row ) . ')';
				}
				else
				{
					$binds    = array_merge( $binds, array_values( $row ) );
					$values[] = '( ' . implode( ', ', array_fill( 0, \count( $columns ), '?' ) ) . ' )';
				}
			}
			
			/* Construct query */
			$query = "INSERT INTO `{$this->prefix}{$table}` ( " . implode( ', ', $columns ) . ' ) VALUES ' . implode( ', ', $values );
		}
		
		/* Add "ON DUPLICATE KEY UPDATE" */
		if( $odkUpdate )
		{
			$query	.= " ON DUPLICATE KEY UPDATE " . implode( ', ', array_map( function( $val ){ return "{$val}=VALUES({$val})"; }, $columns ) );
		}
		
		/* Run */
		if ( \count( $set ) > 1 AND $bulkInsertNoPrep )
		{
			$stmt = $this->query( $query );
			return $this->insert_id;
		}
		else
		{
			$stmt = $this->preparedQuery( $query, $binds );
			return $stmt->insert_id;
		}
	}
	
	/**
	 * Run REPLACE statament and return number of affected rows
	 *
	 * @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
	 * @return	\IPSUtf8\Db\Statement
	 * @throws	\IPSUtf8\Db\Exception
	 */
	public function replace( $table, $set )
	{
		$columns = implode( ', ', array_map( function( $val ){ return "`{$val}`"; }, array_keys( $set ) ) );
		$query = "REPLACE INTO `{$this->prefix}{$table}` ( " . $columns . " ) VALUES ( " . implode( ', ', array_fill( 0, \count( $set ), '?' ) ) . " )";

		$stmt = $this->preparedQuery( $query, array_values( $set ) );
		return $stmt->affected_rows;
	}
	
	/**
	 * Run UPDATE statement and return number of affected rows
	 *
	 * @see		\IPSUtf8\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
	 * @param	mixed			$where		WHERE clause (see \IPSUtf8\Db::build for details)
	 * @param	array			$joins		Tables to join (see \IPSUtf8\Db::build for details)
	 * @return	int
	 * @throws	\IPSUtf8\Db\Exception
	 */
	public function update( $table, $set, $where='', $joins=array() )
	{
		$binds = array();
		
		/* Work out table */
		$table = \is_array( $table ) ? "`{$this->prefix}{$table[0]}` {$this->prefix}{$table[1]}" : "`{$this->prefix}{$table}`";
		
		/* Work out joins */
		foreach ( $joins as $join )
		{
			$type = ( isset( $join['type'] ) and \in_array( strtoupper( $join['type'] ), array( 'LEFT', 'INNER', 'RIGHT' ) ) ) ? strtoupper( $join['type'] ) : 'LEFT';
			$_table = \is_array( $join['from'] ) ? "`{$this->prefix}{$join['from'][0]}` {$this->prefix}{$join['from'][1]}" : $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}`=?";
				$binds[] = $v;
			}
			$set = implode( ',', $_set );
		}
		
		/* Compile where clause */
		if ( $where !== '' )
		{
			$_where = $this->compileWhereClause( $where );
			$where = 'WHERE ' . $_where['clause'];
			$binds = array_merge( $binds, $_where['binds'] );
		}
				
		/* Run it */
		$stmt = $this->preparedQuery( "UPDATE {$table} {$joins} SET {$set} {$where}", $binds );
		return $stmt->affected_rows;
	}
	
	/**
	 * Run DELETE statement and return number of affected rows
	 *
	 * @see		\IPSUtf8\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|\IPSUtf8\Db\Statement|null	$where		WHERE clause (see \IPSUtf8\Db::build for details)
	 * @param	string|null			$order		ORDER BY clause (see \IPSUtf8\Db::build for details)
	 * @param	int|array|null		$limit		LIMIT clause (see \IPSUtf8\Db::build for details)
	 * @param	string|null			$statementColumn	If \IPSUtf8\Db\Statement is passed, this is the name of the column that results are being loaded from
	 * @return	\IPSUtf8\Db\Statement
	 * @throws	\IPSUtf8\Db\Exception
	 */
	public function delete( $table, $where=NULL, $order=NULL, $limit=NULL, $statementColumn=NULL )
	{
		/* Basic query */
		$query = "DELETE FROM `{$this->prefix}{$table}`";

		/* Is a statement? */
		if ( $where instanceof \IPSUtf8\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 */
		$stmt = $this->preparedQuery( $query, $binds );		
		return $stmt->affected_rows;
	}
			
	/**
	 * Compile WHERE clause
	 *
	 * @param	string|array	$data	See \IPSUtf8\Db::build for details
	 * @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
				{
					$clauses[] = array_shift( $bit );
					$return['binds'] = array_merge( $return['binds'], $bit );
				}
			}
			
			$return['clause'] = implode( ' AND ', $clauses );
		}
		
		return $return;
	}
	
	/**
	 * Compile LIMIT clause
	 *
	 * @param	int|array	$data	See \IPSUtf8\Db::build for details
	 * @return	string
	 */
	protected 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
	 	\IPSUtf8\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.
	 		'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 \IPSUtf8\Db::createTable for details)
	 * @return	string
	 */	
	public function compileColumnDefinition( $data )
	{
		/* Specify name and type */
		$definition = "`{$data['name']}` {$data['type']} ";
		
		/* Some types specify length */
		if(
			\in_array( $data['type'], array( 'VARCHAR', 'VARBINARY' ) )
			or
			(
				isset( $data['length'] ) and $data['length']
				and
				\in_array( $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( $data['type'], array( 'REAL', 'DOUBLE', 'FLOAT' ) ) or ( \in_array( $data['type'], array( 'DECIMAL', 'NUMERIC' ) ) and isset( $data['decimals'] ) ) )
			{
				$definition .= ',' . $data['decimals'];
			}
			
			$definition .= ') ';
		}
		
		/* Numeric types can be UNSIGNED and ZEROFILL */
		if( \in_array( $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( $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( $data['type'], array( 'CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT' ) ) )
		{
			$definition .= 'BINARY ';
		}
		
		/* Text types specify a character set and collation */
		if( \in_array( $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 ';
		}
				
		/* Default value */
		if( isset( $data['default'] ) and !\in_array( $data['type'], array( 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'BLOB', 'MEDIUMBLOB', 'BIGBLOB' ) ) )
		{
			$defaultValue = \in_array( $data['type'], array( 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'INTEGER', 'BIGINT', 'REAL', 'DOUBLE', 'FLOAT', 'DECIMAL', 'NUMERIC', 'BIT' ) ) ? \floatval( $data['default'] ) : ( ! \in_array( $data['default'], array( 'CURRENT_TIMESTAMP' ) ) ? '\'' . $this->escape_string( $data['default'] ) . '\'' : $data['default'] );
			
			/* Strict Mode isn't nice */
			$toTest = trim( $defaultValue, "'" );
			if ( \in_array( $data['type'], array( 'DATETIME' ) ) AND empty( $toTest ) ) # array here in case we need to expand later
			{
				$defaultValue = '\'0000-00-00 00:00:00\'';
			}
			
			$definition .= "DEFAULT {$defaultValue} ";
		}
		
		/* auto_increment? */
		if( isset( $data['auto_increment'] ) and $data['auto_increment'] === TRUE )
		{
			$definition .= 'AUTO_INCREMENT ';
		}
		
		/* 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'] ) )
		{
			$definition .= "COMMENT '{$this->escape_string( $data['comment'] )}'";
		}
							
		/* Return */
		return $definition;
	}
	
	/**
	 * Compile index definition
	 *
	 * @code
	 	\IPSUtf8\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		\IPSUtf8\Db::createTable
	 * @param	array	$data	Index Data (see \IPSUtf8\Db::createTable for details)
	 * @return	string
	 */
	public function compileIndexDefinition( $data )
	{
		$definition = '';
		
		/* Specify type */
		switch ( $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->query( "SHOW TABLES LIKE '". $this->escape_string( "{$this->prefix}{$name}" ) . "'" )->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->query( "SHOW INDEXES FROM ". $this->escape_string( "{$this->prefix}{$name}" ) . " WHERE Key_name LIKE '". $this->escape_string( $index ) . "'" )->num_rows > 0 );
	}
	
	/**
	 * Create Table
	 *
	 * @code
	 	\IPSUtf8\Db::createTable( array(
	 		'name'			=> 'table_name',	// Table name
	 		'columns'		=> array( ... ),	// Column data - see \IPSUtf8\Db::compileColumnDefinition for details
	 		'indexes'		=> array( ... ),	// (Optional) Index data - see \IPSUtf8\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 
	 * @see		<a href='http://dev.mysql.com/doc/refman/5.1/en/create-table.html'>MySQL CREATE TABLE syntax</a>
	 * @see		\IPSUtf8\Db::compileColumnDefinition
	 * @see		\IPSUtf8\Db::compileIndexDefinition
	 * @param	array	$data	Table Definition (see code sample for details)
	 * @throws	\IPSUtf8\Db\Exception
	 * @return	void
	 */
	public function createTable( $data )
	{
		/* 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' )
				{
					$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'] )}'";
		}

		/* Do it */
		return $this->query( $query );
	}
	
	/**
	 * Rename table
	 *
	 * @see		<a href='http://dev.mysql.com/doc/refman/5.1/en/rename-table.html'>
	 * @param	string	$oldName	The current table name
	 * @param	string	$newName	The new name
	 * @return	void
	 */
	public function renameTable( $oldName, $newName )
	{
		return $this->query( "RENAME TABLE `{$this->prefix}{$this->escape_string( $oldName )}` TO `{$this->prefix}{$this->escape_string( $newName )}`" );
	}
	
	/**
	 * Alter Table
	 * Can only update the comment and engine
	 *
	 * @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 );
	}
	
	/**
	 * 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	void
	 */
	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 the table definition for an existing table
	 *
	 * @see		\IPSUtf8\Db::createTable
	 * @param	string	$table	Table Name
	 * @return	array	Table definition - see IPSUtf8\Db::createTable for details
	 * @throws	\OutOfRangeException
	 * @throws	\IPSUtf8\Db\Exception
	 */
	public function getTableDefinition( $table )
	{
		/* Set name */
		$definition = array(
			'name'		=> $table,
		);
	
		/* Fetch columns */
		$query = $this->query( "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()
			);
				
			/* 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['type'] != 'DATETIME' 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'] ?: '';
			
			/* Collation */
			$columnDefinition['collation'] = $row['Collation'] ?: NULL;
			
			/* Add it in the definition */
			ksort( $columnDefinition );
			$definition['columns'][ $columnDefinition['name'] ] = $columnDefinition;
		}
		
		/* Fetch indexes */
		$indexes = array();
		$query = $this->query( "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' )
				{
					$definition['engine'] = 'MYISAM';
					$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 */
		$row = $this->query( "SHOW TABLE STATUS LIKE '{$table}'" )->fetch_assoc();
		
		$definition['comment']   = $row['Comment'];
		$definition['collation'] = $row['Collation'];
		$definition['engine']	 = $row['Engine'];
		
		if ( ! isset( $definition['engine'] ) )
		{
			$definition['engine'] = $this->defaultEngine();
		}
		
		/* Return */
		return $definition;
	}
	
	/**
	 * Fetches the default engine
	 *
	 * @return	string
	 */
	public function defaultEngine()
	{
		if ( $this->defaultEngine === NULL )
		{
			/* If this is an IPB, we should use what is defined in conf_global.php */
			if ( file_exists( ROOT_PATH . '/conf_global.php' ) )
			{
				require( ROOT_PATH . '/conf_global.php' );
				if ( isset( $INFO['mysql_tbl_type'] ) )
				{
					if ( mb_strtolower( $INFO['mysql_tbl_type'] ) == 'myisam' )
					{
						$this->defaultEngine = 'MyISAM';
					}
					else
					{
						$this->defaultEngine = 'InnoDB';
					}
					
					return $this->defaultEngine;
				}
			}
			
			$query = $this->query( "SHOW ENGINES" );
			
			while ( $row = $query->fetch_assoc() )
			{
				if ( \mb_strtolower( $row['Support'] ) === 'default' )
				{
					$this->defaultEngine = \mb_strtolower( $row['Engine'] );
					break;
				}
			}
		}
		
		return $this->defaultEngine;
	}
	
	/**
	 * Add column to table in database
	 *
	 * @see		\IPSUtf8\Db::compileColumnDefinition
	 * @param	string	$table			Table name
	 * @param	array	$definition		Column Definition (see \IPSUtf8\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		\IPSUtf8\Db::compileColumnDefinition
	 * @param	string	$table			Table name
	 * @param	string	$column			Column name
	 * @param	array	$definition		New column definition (see \IPSUtf8\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	$column			Column name
	 * @return	void
	 */
	public function dropColumn( $table, $column )
	{
		return $this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` DROP COLUMN `{$this->escape_string( $column )}`;" );
	}
	
	/**
	 * Add index to table in database
	 *
	 * @see		\IPSUtf8\Db::compileIndexDefinition
	 * @param	string	$table			Table name
	 * @param	array	$definition		Index Definition (see \IPSUtf8\Db::compileIndexDefinition for details)
	 * @return	void
	 */
	public function addIndex( $table, $definition )
	{	
		return $this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` ADD {$this->compileIndexDefinition( $definition )}" );
	}
	
	/**
	 * Modify an existing index
	 *
	 * @see		\IPSUtf8\Db::compileIndexDefinition
	 * @param	string	$table			Table name
	 * @param	string	$index			Index name
	 * @param	array	$definition		New index definition (see \IPSUtf8\Db::compileIndexDefinition for details)
	 * @return	void
	 */
	public function changeIndex( $table, $index, $definition )
	{
		$this->dropIndex( $table, $index );
		$this->addIndex( $table, $definition );
	}
	
	/**
	 * Drop an index
	 *
	 * @param	string	$table			Table name
	 * @param	string	$index			Column name
	 * @return	void
	 */
	public function dropIndex( $table, $index )
	{
		return $this->query( "ALTER TABLE `{$this->prefix}{$this->escape_string( $table )}` DROP INDEX `{$this->escape_string( $index )}`;" );
	}
	
	/**
	 * Find In Set
	 *
	 * @param	string	$column	Column name
	 * @param	array	$values	Acceptable values
	 * @return 	string	Where clause
	 */
	public function in( $column, $values )
	{
		$where = array();
		$in	= array();
		
		foreach( $values as $i )
		{
			if ( $i and \is_numeric( $i ) )
			{
				$where[] = "FIND_IN_SET(" . $i . "," . $column . ")";
			}
			else if ( $i and \is_string( $i ) )
			{
				$in[] = "'" . $this->real_escape_string( $i ) . "'";
			}
		}
		
		$return = array();
		
		if ( ! empty( $where ) )
		{
			$return[] = '( ' . implode( " OR ", $where ) . ' )';
		}
		
		if ( ! empty( $in ) )
		{
			$return[] = $column . ' IN(' . implode( ',', $in ) . ')';
		}
		
		if ( \count( $return ) )
		{
			return '(' . implode( ' OR ', $return ) . ')';
		}
		else
		{
			return '1=1';
		}
	}

}