diff options
Diffstat (limited to 'system/libraries/Database.php')
| -rw-r--r-- | system/libraries/Database.php | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/system/libraries/Database.php b/system/libraries/Database.php new file mode 100644 index 0000000..253bb15 --- /dev/null +++ b/system/libraries/Database.php @@ -0,0 +1,648 @@ +<?php defined('SYSPATH') or die('No direct script access.'); +/** + * Database wrapper. + * + * @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; + } + elseif (is_float($value)) + { + // Convert to non-locale aware float to prevent possible commas + return sprintf('%F', $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 |
