646 lines
13 KiB
PHP
646 lines
13 KiB
PHP
|
<?php defined('SYSPATH') or die('No direct script access.');
|
||
|
/**
|
||
|
* Database wrapper.
|
||
|
*
|
||
|
* $Id: Database.php 4709 2009-12-10 05:09:35Z isaiah $
|
||
|
*
|
||
|
* @package Kohana
|
||
|
* @author Kohana Team
|
||
|
* @copyright (c) 2008-2009 Kohana Team
|
||
|
* @license http://kohanaphp.com/license
|
||
|
*/
|
||
|
abstract class Database_Core {
|
||
|
|
||
|
const SELECT = 1;
|
||
|
const INSERT = 2;
|
||
|
const UPDATE = 3;
|
||
|
const DELETE = 4;
|
||
|
const CROSS_REQUEST = 5;
|
||
|
const PER_REQUEST = 6;
|
||
|
|
||
|
protected static $instances = array();
|
||
|
|
||
|
// Global benchmarks
|
||
|
public static $benchmarks = array();
|
||
|
|
||
|
// Last execute query
|
||
|
protected $last_query;
|
||
|
|
||
|
// Configuration array
|
||
|
protected $config;
|
||
|
|
||
|
// Required configuration keys
|
||
|
protected $config_required = array();
|
||
|
|
||
|
// Raw server connection
|
||
|
protected $connection;
|
||
|
|
||
|
// Cache (Cache object for cross-request, array for per-request)
|
||
|
protected $cache;
|
||
|
|
||
|
// Quote character to use for identifiers (tables/columns/aliases)
|
||
|
protected $quote = '"';
|
||
|
|
||
|
/**
|
||
|
* Returns a singleton instance of Database.
|
||
|
*
|
||
|
* @param string Database name
|
||
|
* @return Database_Core
|
||
|
*/
|
||
|
public static function instance($name = 'default')
|
||
|
{
|
||
|
if ( ! isset(Database::$instances[$name]))
|
||
|
{
|
||
|
// Load the configuration for this database group
|
||
|
$config = Kohana::config('database.'.$name);
|
||
|
|
||
|
if (is_string($config['connection']))
|
||
|
{
|
||
|
// Parse the DSN into connection array
|
||
|
$config['connection'] = Database::parse_dsn($config['connection']);
|
||
|
}
|
||
|
|
||
|
// Set the driver class name
|
||
|
$driver = 'Database_'.ucfirst($config['connection']['type']);
|
||
|
|
||
|
// Create the database connection instance
|
||
|
Database::$instances[$name] = new $driver($config);
|
||
|
}
|
||
|
|
||
|
return Database::$instances[$name];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Constructs a new Database object
|
||
|
*
|
||
|
* @param array Database config array
|
||
|
* @return Database_Core
|
||
|
*/
|
||
|
protected function __construct(array $config)
|
||
|
{
|
||
|
// Store the config locally
|
||
|
$this->config = $config;
|
||
|
|
||
|
if ($this->config['cache'] !== FALSE)
|
||
|
{
|
||
|
if (is_string($this->config['cache']))
|
||
|
{
|
||
|
// Use Cache library
|
||
|
$this->cache = new Cache($this->config['cache']);
|
||
|
}
|
||
|
elseif ($this->config['cache'] === TRUE)
|
||
|
{
|
||
|
// Use array
|
||
|
$this->cache = array();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function __destruct()
|
||
|
{
|
||
|
$this->disconnect();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Connects to the database
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
abstract public function connect();
|
||
|
|
||
|
/**
|
||
|
* Disconnects from the database
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
abstract public function disconnect();
|
||
|
|
||
|
/**
|
||
|
* Sets the character set
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
abstract public function set_charset($charset);
|
||
|
|
||
|
/**
|
||
|
* Executes the query
|
||
|
*
|
||
|
* @param string SQL
|
||
|
* @return Database_Result
|
||
|
*/
|
||
|
abstract public function query_execute($sql);
|
||
|
|
||
|
/**
|
||
|
* Escapes the given value
|
||
|
*
|
||
|
* @param mixed Value
|
||
|
* @return mixed Escaped value
|
||
|
*/
|
||
|
abstract public function escape($value);
|
||
|
|
||
|
/**
|
||
|
* List constraints for the given table
|
||
|
*
|
||
|
* @param string Table name
|
||
|
* @return array
|
||
|
*/
|
||
|
abstract public function list_constraints($table);
|
||
|
|
||
|
/**
|
||
|
* List fields for the given table
|
||
|
*
|
||
|
* @param string Table name
|
||
|
* @return array
|
||
|
*/
|
||
|
abstract public function list_fields($table);
|
||
|
|
||
|
/**
|
||
|
* List tables for the given connection (checks for prefix)
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
abstract public function list_tables();
|
||
|
|
||
|
/**
|
||
|
* Converts the given DSN string to an array of database connection components
|
||
|
*
|
||
|
* @param string DSN string
|
||
|
* @return array
|
||
|
*/
|
||
|
public static function parse_dsn($dsn)
|
||
|
{
|
||
|
$db = array
|
||
|
(
|
||
|
'type' => FALSE,
|
||
|
'user' => FALSE,
|
||
|
'pass' => FALSE,
|
||
|
'host' => FALSE,
|
||
|
'port' => FALSE,
|
||
|
'socket' => FALSE,
|
||
|
'database' => FALSE
|
||
|
);
|
||
|
|
||
|
// Get the protocol and arguments
|
||
|
list ($db['type'], $connection) = explode('://', $dsn, 2);
|
||
|
|
||
|
if ($connection[0] === '/')
|
||
|
{
|
||
|
// Strip leading slash
|
||
|
$db['database'] = substr($connection, 1);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$connection = parse_url('http://'.$connection);
|
||
|
|
||
|
if (isset($connection['user']))
|
||
|
{
|
||
|
$db['user'] = $connection['user'];
|
||
|
}
|
||
|
|
||
|
if (isset($connection['pass']))
|
||
|
{
|
||
|
$db['pass'] = $connection['pass'];
|
||
|
}
|
||
|
|
||
|
if (isset($connection['port']))
|
||
|
{
|
||
|
$db['port'] = $connection['port'];
|
||
|
}
|
||
|
|
||
|
if (isset($connection['host']))
|
||
|
{
|
||
|
if ($connection['host'] === 'unix(')
|
||
|
{
|
||
|
list($db['socket'], $connection['path']) = explode(')', $connection['path'], 2);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$db['host'] = $connection['host'];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (isset($connection['path']) AND $connection['path'])
|
||
|
{
|
||
|
// Strip leading slash
|
||
|
$db['database'] = substr($connection['path'], 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $db;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the last executed query for this database
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function last_query()
|
||
|
{
|
||
|
return $this->last_query;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Executes the given query, returning the cached version if enabled
|
||
|
*
|
||
|
* @param string SQL query
|
||
|
* @return Database_Result
|
||
|
*/
|
||
|
public function query($sql)
|
||
|
{
|
||
|
// Start the benchmark
|
||
|
$start = microtime(TRUE);
|
||
|
|
||
|
if (is_array($this->cache))
|
||
|
{
|
||
|
$hash = $this->query_hash($sql);
|
||
|
|
||
|
if (isset($this->cache[$hash]))
|
||
|
{
|
||
|
// Use cached result
|
||
|
$result = $this->cache[$hash];
|
||
|
|
||
|
// It's from cache
|
||
|
$sql .= ' [CACHE]';
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// No cache, execute query and store in cache
|
||
|
$result = $this->cache[$hash] = $this->query_execute($sql);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Execute the query, cache is off
|
||
|
$result = $this->query_execute($sql);
|
||
|
}
|
||
|
|
||
|
// Stop the benchmark
|
||
|
$stop = microtime(TRUE);
|
||
|
|
||
|
if ($this->config['benchmark'] === TRUE)
|
||
|
{
|
||
|
// Benchmark the query
|
||
|
Database::$benchmarks[] = array('query' => $sql, 'time' => $stop - $start, 'rows' => count($result));
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Performs the query on the cache (and caches it if it's not found)
|
||
|
*
|
||
|
* @param string query
|
||
|
* @param int time-to-live (NULL for Cache default)
|
||
|
* @return Database_Cache_Result
|
||
|
*/
|
||
|
public function query_cache($sql, $ttl)
|
||
|
{
|
||
|
if ( ! $this->cache instanceof Cache)
|
||
|
{
|
||
|
throw new Database_Exception('Database :name has not been configured to use the Cache library.');
|
||
|
}
|
||
|
|
||
|
// Start the benchmark
|
||
|
$start = microtime(TRUE);
|
||
|
|
||
|
$hash = $this->query_hash($sql);
|
||
|
|
||
|
if (($data = $this->cache->get($hash)) !== NULL)
|
||
|
{
|
||
|
// Found in cache, create result
|
||
|
$result = new Database_Cache_Result($data, $sql, $this->config['object']);
|
||
|
|
||
|
// It's from the cache
|
||
|
$sql .= ' [CACHE]';
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Run the query and return the full array of rows
|
||
|
$data = $this->query_execute($sql)->as_array(TRUE);
|
||
|
|
||
|
// Set the Cache
|
||
|
$this->cache->set($hash, $data, NULL, $ttl);
|
||
|
|
||
|
// Create result
|
||
|
$result = new Database_Cache_Result($data, $sql, $this->config['object']);
|
||
|
}
|
||
|
|
||
|
// Stop the benchmark
|
||
|
$stop = microtime(TRUE);
|
||
|
|
||
|
if ($this->config['benchmark'] === TRUE)
|
||
|
{
|
||
|
// Benchmark the query
|
||
|
Database::$benchmarks[] = array('query' => $sql, 'time' => $stop - $start, 'rows' => count($result));
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generates a hash for the given query
|
||
|
*
|
||
|
* @param string SQL query string
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function query_hash($sql)
|
||
|
{
|
||
|
return sha1(str_replace("\n", ' ', trim($sql)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears the internal query cache.
|
||
|
*
|
||
|
* @param mixed clear cache by SQL statement, NULL for all, or TRUE for last query
|
||
|
* @param integer Type of cache to clear, Database::CROSS_REQUEST or Database::PER_REQUEST
|
||
|
* @return Database
|
||
|
*/
|
||
|
public function clear_cache($sql = NULL, $type = NULL)
|
||
|
{
|
||
|
if ($this->cache instanceof Cache AND ($type == NULL OR $type == Database::CROSS_REQUEST))
|
||
|
{
|
||
|
// Using cross-request Cache library
|
||
|
if ($sql === TRUE)
|
||
|
{
|
||
|
$this->cache->delete($this->query_hash($this->last_query));
|
||
|
}
|
||
|
elseif (is_string($sql))
|
||
|
{
|
||
|
$this->cache->delete($this->query_hash($sql));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$this->cache->delete_all();
|
||
|
}
|
||
|
}
|
||
|
elseif (is_array($this->cache) AND ($type == NULL OR $type == Database::PER_REQUEST))
|
||
|
{
|
||
|
// Using per-request memory cache
|
||
|
if ($sql === TRUE)
|
||
|
{
|
||
|
unset($this->cache[$this->query_hash($this->last_query)]);
|
||
|
}
|
||
|
elseif (is_string($sql))
|
||
|
{
|
||
|
unset($this->cache[$this->query_hash($sql)]);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
$this->cache = array();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quotes the given value
|
||
|
*
|
||
|
* @param mixed value
|
||
|
* @return mixed
|
||
|
*/
|
||
|
public function quote($value)
|
||
|
{
|
||
|
if ( ! $this->config['escape'])
|
||
|
return $value;
|
||
|
|
||
|
if ($value === NULL)
|
||
|
{
|
||
|
return 'NULL';
|
||
|
}
|
||
|
elseif ($value === TRUE)
|
||
|
{
|
||
|
return 'TRUE';
|
||
|
}
|
||
|
elseif ($value === FALSE)
|
||
|
{
|
||
|
return 'FALSE';
|
||
|
}
|
||
|
elseif (is_int($value))
|
||
|
{
|
||
|
return (int) $value;
|
||
|
}
|
||
|
elseif ($value instanceof Database_Expression)
|
||
|
{
|
||
|
return (string) $value;
|
||
|
}
|
||
|
|
||
|
return '\''.$this->escape($value).'\'';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quotes a table, adding the table prefix
|
||
|
* Reserved characters not allowed in table names for the builder are [ .*] (space, dot, asterisk)
|
||
|
*
|
||
|
* @param string|array table name or array - 'users u' or array('u' => 'users') both valid
|
||
|
* @param string table alias
|
||
|
* @return string
|
||
|
*/
|
||
|
public function quote_table($table, $alias = NULL)
|
||
|
{
|
||
|
if (is_array($table))
|
||
|
{
|
||
|
// Using array('u' => 'user')
|
||
|
list($alias, $table) = each($table);
|
||
|
}
|
||
|
elseif (strpos(' ', $table) !== FALSE)
|
||
|
{
|
||
|
// Using format 'user u'
|
||
|
list($table, $alias) = explode(' ', $table);
|
||
|
}
|
||
|
|
||
|
if ($table instanceof Database_Expression)
|
||
|
{
|
||
|
if ($alias)
|
||
|
{
|
||
|
if ($this->config['escape'])
|
||
|
{
|
||
|
$alias = $this->quote.$alias.$this->quote;
|
||
|
}
|
||
|
|
||
|
return $table.' AS '.$alias;
|
||
|
}
|
||
|
|
||
|
return (string) $table;
|
||
|
}
|
||
|
|
||
|
if ($this->config['table_prefix'])
|
||
|
{
|
||
|
$table = $this->config['table_prefix'].$table;
|
||
|
}
|
||
|
|
||
|
if ($alias)
|
||
|
{
|
||
|
if ($this->config['escape'])
|
||
|
{
|
||
|
$table = $this->quote.$table.$this->quote;
|
||
|
$alias = $this->quote.$alias.$this->quote;
|
||
|
}
|
||
|
|
||
|
return $table.' AS '.$alias;
|
||
|
}
|
||
|
|
||
|
if ($this->config['escape'])
|
||
|
{
|
||
|
$table = $this->quote.$table.$this->quote;
|
||
|
}
|
||
|
|
||
|
return $table;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quotes column or table.column, adding the table prefix if necessary
|
||
|
* Reserved characters not allowed in table names for the builder are [ .*] (space, dot, asterisk)
|
||
|
* Complex column names must have table/columns in double quotes, e.g. array('mycount' => 'COUNT("users.id")')
|
||
|
*
|
||
|
* @param string|array column name or array('u' => 'COUNT("*")')
|
||
|
* @param string column alias
|
||
|
* @return string
|
||
|
*/
|
||
|
public function quote_column($column, $alias = NULL)
|
||
|
{
|
||
|
if ($column === '*')
|
||
|
return $column;
|
||
|
|
||
|
if (is_array($column))
|
||
|
{
|
||
|
list($alias, $column) = each($column);
|
||
|
}
|
||
|
|
||
|
if ($column instanceof Database_Expression)
|
||
|
{
|
||
|
if ($alias)
|
||
|
{
|
||
|
if ($this->config['escape'])
|
||
|
{
|
||
|
$alias = $this->quote.$alias.$this->quote;
|
||
|
}
|
||
|
|
||
|
return $column.' AS '.$alias;
|
||
|
}
|
||
|
|
||
|
return (string) $column;
|
||
|
}
|
||
|
|
||
|
if ($this->config['table_prefix'] AND strpos($column, '.') !== FALSE)
|
||
|
{
|
||
|
if (strpos($column, '"') !== FALSE)
|
||
|
{
|
||
|
// Find "table.column" and replace them with "[prefix]table.column"
|
||
|
$column = preg_replace('/"([^.]++)\.([^"]++)"/', '"'.$this->config['table_prefix'].'$1.$2"', $column);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Attach table prefix if table.column format
|
||
|
$column = $this->config['table_prefix'].$column;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($this->config['escape'])
|
||
|
{
|
||
|
if (strpos($column, '"') === FALSE)
|
||
|
{
|
||
|
// Quote the column
|
||
|
$column = $this->quote.$column.$this->quote;
|
||
|
}
|
||
|
elseif ($this->quote !== '"')
|
||
|
{
|
||
|
// Replace double quotes
|
||
|
$column = str_replace('"', $this->quote, $column);
|
||
|
}
|
||
|
|
||
|
// Replace . with "."
|
||
|
$column = str_replace('.', $this->quote.'.'.$this->quote, $column);
|
||
|
|
||
|
// Unescape any asterisks
|
||
|
$column = str_replace($this->quote.'*'.$this->quote, '*', $column);
|
||
|
|
||
|
if ($alias)
|
||
|
{
|
||
|
// Quote the alias
|
||
|
return $column.' AS '.$this->quote.$alias.$this->quote;
|
||
|
}
|
||
|
|
||
|
return $column;
|
||
|
}
|
||
|
|
||
|
// Strip double quotes
|
||
|
$column = str_replace('"', '', $column);
|
||
|
|
||
|
if ($alias)
|
||
|
return $column.' AS '.$alias;
|
||
|
|
||
|
return $column;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the table prefix
|
||
|
*
|
||
|
* @param string Optional new table prefix to set
|
||
|
* @return string
|
||
|
*/
|
||
|
public function table_prefix($new_prefix = NULL)
|
||
|
{
|
||
|
$prefix = $this->config['table_prefix'];
|
||
|
|
||
|
if ($new_prefix !== NULL)
|
||
|
{
|
||
|
// Set a new prefix
|
||
|
$this->config['table_prefix'] = $new_prefix;
|
||
|
}
|
||
|
|
||
|
return $prefix;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fetches SQL type information about a field, in a generic format.
|
||
|
*
|
||
|
* @param string field datatype
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function sql_type($str)
|
||
|
{
|
||
|
static $sql_types;
|
||
|
|
||
|
if ($sql_types === NULL)
|
||
|
{
|
||
|
// Load SQL data types
|
||
|
$sql_types = Kohana::config('sql_types');
|
||
|
}
|
||
|
|
||
|
$str = trim($str);
|
||
|
|
||
|
if (($open = strpos($str, '(')) !== FALSE)
|
||
|
{
|
||
|
// Closing bracket
|
||
|
$close = strpos($str, ')', $open);
|
||
|
|
||
|
// Length without brackets
|
||
|
$length = substr($str, $open + 1, $close - 1 - $open);
|
||
|
|
||
|
// Type without the length
|
||
|
$type = substr($str, 0, $open).substr($str, $close + 1);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// No length
|
||
|
$type = $str;
|
||
|
}
|
||
|
|
||
|
if (empty($sql_types[$type]))
|
||
|
throw new Database_Exception('Undefined field type :type', array(':type' => $str));
|
||
|
|
||
|
// Fetch the field definition
|
||
|
$field = $sql_types[$type];
|
||
|
|
||
|
$field['sql_type'] = $type;
|
||
|
|
||
|
if (isset($length))
|
||
|
{
|
||
|
// Add the length to the field info
|
||
|
$field['length'] = $length;
|
||
|
}
|
||
|
|
||
|
return $field;
|
||
|
}
|
||
|
|
||
|
} // End Database
|