This repository has been archived on 2021-04-26. You can view files and clone it, but cannot push or open issues or pull requests.
gallery3-debian/system/libraries/Database_Builder.php

1242 lines
26 KiB
PHP
Raw Normal View History

2013-04-04 09:26:00 +00:00
<?php defined('SYSPATH') or die('No direct script access.');
/**
* The Database Query Builder provides methods for creating database agnostic queries and
* data manipulation.
*
* ##### A basic select query
*
* $builder = new Database_Builder;
* $kohana = $builder
* ->select()
* ->where('name', '=', 'Kohana')
* ->from('frameworks')
* ->execute();
*
* @package Kohana
* @author Kohana Team
* @copyright (c) 2008-2009 Kohana Team
* @license http://kohanaphp.com/license
*/
class Database_Builder_Core {
// Valid ORDER BY directions
protected $order_directions = array('ASC', 'DESC', 'RAND()');
// Database object
protected $db;
// Builder members
protected $select = array();
protected $from = array();
protected $join = array();
protected $where = array();
protected $group_by = array();
protected $having = array();
protected $order_by = array();
protected $limit = NULL;
protected $offset = NULL;
protected $set = array();
protected $columns = array();
protected $values = array();
protected $type;
protected $distinct = FALSE;
protected $reset = TRUE;
// TTL for caching (using Cache library)
protected $ttl = FALSE;
public function __construct($db = 'default')
{
$this->db = $db;
}
/**
* Compiles the builder object into a SQL query. Useful for debugging
*
* ##### Example
*
* echo $builder->select()->from('products');
* // Output: SELECT * FROM `products`
*
* @return string Compiled query
*/
public function __toString()
{
return $this->compile();
}
/**
* Creates a `SELECT` query with support for column aliases, database functions,
* subqueries or a [Database_Expression]
*
* ##### Examples
*
* // Simple select
* echo $builder->select()->from('products');
*
* // Select with database function
* echo $builder->select(array('records_found' => 'COUNT("*")'))->from('products');
*
* // Select with sub query
* echo $builder->select(array('field', 'test' => db::select('test')->from('table')))->from('products');
*
* @chainable
* @param string|array column name or array(alias => column)
* @return Database_Builder
*/
public function select($columns = NULL)
{
$this->type = Database::SELECT;
if ($columns === NULL)
{
$columns = array('*');
}
elseif ( ! is_array($columns))
{
$columns = func_get_args();
}
$this->select = array_merge($this->select, $columns);
return $this;
}
/**
* Creates a `DISTINCT SELECT` query. For more information see see [Database_Builder::select].
*
* @chainable
* @param string|array column name or array(alias => column)
* @return Database_Builder
*/
public function select_distinct($columns = NULL)
{
$this->select($columns);
$this->distinct = TRUE;
return $this;
}
/**
* Add tables to the FROM portion of the builder
*
* ##### Example
*
* $builder->select()->from('products')
* ->from(array('other' => 'other_table'));
* // Output: SELECT * FROM `products`, `other_table` AS `other`
*
* @chainable
* @param string|array table name or array(alias => table)
* @return Database_Builder
*/
public function from($tables)
{
if ( ! is_array($tables))
{
$tables = func_get_args();
}
$this->from = array_merge($this->from, $tables);
return $this;
}
/**
* Add conditions to the `WHERE` clause. Alias for [Database_Builder::and_where].
*
* @chainable
* @param mixed Column name or array of columns => vals
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function where($columns, $op = '=', $value = NULL)
{
return $this->and_where($columns, $op, $value);
}
/**
* Add conditions to the `WHERE` clause separating multiple conditions with `AND`.
* This function supports all `WHERE` operators including `LIKE` and `IN`. It can
* also be used with a [Database_Expression] or subquery.
*
* ##### Examples
*
* // Basic where condition
* $builder->where('field', '=', 'value');
*
* // Multiple conditions with an array (you can also chain where() function calls)
* $builder->where(array(array('field', '=', 'value'), array(...)));
*
* // With a database expression
* $builder->where('field', '=', db::expr('field + 1'));
* // or a function
* $builder->where('field', '=', db::expr('UNIX_TIMESTAMP()'));
*
* // With a subquery
* $builder->where('field', 'IN', db::select('id')->from('table'));
*
* [!!] You must manually escape all data you pass into a database expression!
*
* @chainable
* @param mixed Column name or array of triplets
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function and_where($columns, $op = '=', $value = NULL)
{
if (is_array($columns))
{
foreach ($columns as $column)
{
$this->where[] = array('AND' => $column);
}
}
else
{
$this->where[] = array('AND' => array($columns, $op, $value));
}
return $this;
}
/**
* Add conditions to the `WHERE` clause separating multiple conditions with `OR`.
* For more information about building a `WHERE` clause see [Database_Builder::and_where]
*
* @chainable
* @param mixed Column name or array of triplets
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function or_where($columns, $op = '=', $value = NULL)
{
if (is_array($columns))
{
foreach ($columns as $column)
{
$this->where[] = array('OR' => $column);
}
}
else
{
$this->where[] = array('OR' => array($columns, $op, $value));
}
return $this;
}
/**
* Join tables to the builder
*
* ##### Example
*
* // Basic join
* db::select()->from('products')
* ->join('reviews', 'reviews.product_id', 'products.id');
*
* // Advanced joins
* echo db::select()->from('products')
* ->join('reviews', 'field', db::expr('advanced condition here'), 'RIGHT');
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @param string Join type (LEFT, RIGHT, INNER, etc.)
* @return Database_Builder
*/
public function join($table, $keys, $value = NULL, $type = NULL)
{
if (is_string($keys))
{
$keys = array($keys => $value);
}
if ($type !== NULL)
{
$type = strtoupper($type);
}
$this->join[] = array($table, $keys, $type);
return $this;
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `LEFT`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function left_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'LEFT');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `RIGHT`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function right_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'RIGHT');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `INNER`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function inner_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'INNER');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `OUTER`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function outer_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'OUTER');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `FULL`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function full_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'FULL');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `LEFT INNER`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function left_inner_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'LEFT INNER');
}
/**
* This function is an alias for [Database_Builder::join]
* with the join type set to `RIGHT INNER`.
*
* @chainable
* @param mixed Table name
* @param mixed Key, or an array of key => value pair, for join condition (can be a Database_Expression)
* @param mixed Value if $keys is not an array or Database_Expression
* @return Database_Builder
*/
public function right_inner_join($table, $keys, $value = NULL)
{
return $this->join($table, $keys, $value, 'RIGHT INNER');
}
/**
* Add fields to the GROUP BY portion
*
* ##### Example
*
* db::select()->from('products')
* ->group_by(array('name', 'cat_id'));
* // Output: SELECT * FROM `products` GROUP BY `name`, `cat_id`
*
* @chainable
* @param mixed Field names or an array of fields
* @return Database_Builder
*/
public function group_by($columns)
{
if ( ! is_array($columns))
{
$columns = func_get_args();
}
$this->group_by = array_merge($this->group_by, $columns);
return $this;
}
/**
* Add conditions to the HAVING clause (AND)
*
* @chainable
* @param mixed Column name or array of columns => vals
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function having($columns, $op = '=', $value = NULL)
{
return $this->and_having($columns, $op, $value);
}
/**
* Add conditions to the HAVING clause (AND)
*
* @chainable
* @param mixed Column name or array of triplets
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function and_having($columns, $op = '=', $value = NULL)
{
if (is_array($columns))
{
foreach ($columns as $column)
{
$this->having[] = array('AND' => $column);
}
}
else
{
$this->having[] = array('AND' => array($columns, $op, $value));
}
return $this;
}
/**
* Add conditions to the HAVING clause (OR)
*
* @chainable
* @param mixed Column name or array of triplets
* @param string Operation to perform
* @param mixed Value
* @return Database_Builder
*/
public function or_having($columns, $op = '=', $value = NULL)
{
if (is_array($columns))
{
foreach ($columns as $column)
{
$this->having[] = array('OR' => $column);
}
}
else
{
$this->having[] = array('OR' => array($columns, $op, $value));
}
return $this;
}
/**
* Add fields to the ORDER BY portion
*
* @chainable
* @param mixed Field names or an array of fields (field => direction)
* @param string Direction or NULL for ascending
* @return Database_Builder
*/
public function order_by($columns, $direction = NULL)
{
if (is_array($columns))
{
foreach ($columns as $column => $direction)
{
if (is_string($column))
{
$this->order_by[] = array($column => $direction);
}
else
{
// $direction is the column name when the array key is numeric
$this->order_by[] = array($direction => NULL);
}
}
}
else
{
$this->order_by[] = array($columns => $direction);
}
return $this;
}
/**
* Limit rows returned
*
* @chainable
* @param int Number of rows
* @return Database_Builder
*/
public function limit($number)
{
$this->limit = (int) $number;
return $this;
}
/**
* Offset into result set
*
* @chainable
* @param int Offset
* @return Database_Builder
*/
public function offset($number)
{
$this->offset = (int) $number;
return $this;
}
/**
* Alias for [Database_Builder::and_open]
*
* @chainable
* @param string Clause (WHERE OR HAVING)
* @return Database_Builder
*/
public function open($clause = 'WHERE')
{
return $this->and_open($clause);
}
/**
* Open new **ANDs** parenthesis set
*
* @chainable
* @param string Clause (WHERE OR HAVING)
* @return Database_Builder
*/
public function and_open($clause = 'WHERE')
{
if ($clause === 'WHERE')
{
$this->where[] = array('AND' => '(');
}
else
{
$this->having[] = array('AND' => '(');
}
return $this;
}
/**
* Open new **OR** parenthesis set
*
* @chainable
* @param string Clause (WHERE OR HAVING)
* @return Database_Builder
*/
public function or_open($clause = 'WHERE')
{
if ($clause === 'WHERE')
{
$this->where[] = array('OR' => '(');
}
else
{
$this->having[] = array('OR' => '(');
}
return $this;
}
/**
* Close close parenthesis set
*
* @chainable
* @param string Clause (WHERE OR HAVING)
* @return Database_Builder
*/
public function close($clause = 'WHERE')
{
if ($clause === 'WHERE')
{
$this->where[] = array(')');
}
else
{
$this->having[] = array(')');
}
return $this;
}
/**
* Set values for UPDATE
*
* @chainable
* @param mixed Column name or array of columns => vals
* @param mixed Value (can be a Database_Expression)
* @return Database_Builder
*/
public function set($keys, $value = NULL)
{
if (is_string($keys))
{
$keys = array($keys => $value);
}
$this->set = array_merge($keys, $this->set);
return $this;
}
/**
* Columns used for INSERT queries
*
* @chainable
* @param array Columns
* @return Database_Builder
*/
public function columns($columns)
{
if ( ! is_array($columns))
{
$columns = func_get_args();
}
$this->columns = $columns;
return $this;
}
/**
* Values used for INSERT queries
*
* @chainable
* @param array Values
* @return Database_Builder
*/
public function values($values)
{
if ( ! is_array($values))
{
$values = func_get_args();
}
$this->values[] = $values;
return $this;
}
/**
* Set caching for the query
*
* @chainable
* @param mixed Time-to-live (FALSE to disable, NULL for Cache default, seconds otherwise)
* @return Database_Builder
*/
public function cache($ttl = NULL)
{
$this->ttl = $ttl;
return $this;
}
/**
* Resets the database builder after execution. By default after you `execute()` a query
* the database builder will reset to its default state. You can use `reset(FALSE)`
* to stop this from happening. This is useful for pagination when you might want to
* apply a limit to the previous query.
*
* ##### Example
*
* $db = new Database_Builder;
* $all_results = $db->select()
* ->where('id', '=', 3)
* ->from('products')
* ->reset(FALSE)
* ->execute();
*
* // Run the query again with a limit of 10
* $ten_results = $db->limit(10)
* ->execute();
* @chainable
* @param bool reset builder
* @return Database_Builder
*/
public function reset($reset = TRUE)
{
$this->reset = (bool) $reset;
return $this;
}
/**
* Compiles the given clause's conditions
*
* @param array Clause conditions
* @return string
*/
protected function compile_conditions($groups)
{
$last_condition = NULL;
$sql = '';
foreach ($groups as $group)
{
// Process groups of conditions
foreach ($group as $logic => $condition)
{
if ($condition === '(')
{
if ( ! empty($sql) AND $last_condition !== '(')
{
// Include logic operator
$sql .= ' '.$logic.' ';
}
$sql .= '(';
}
elseif ($condition === ')')
{
$sql .= ')';
}
else
{
list($columns, $op, $value) = $condition;
// Stores each individual condition
$vals = array();
if ($columns instanceof Database_Expression)
{
// Add directly to condition list
$vals[] = (string) $columns;
}
else
{
$op = strtoupper($op);
if ( ! is_array($columns))
{
$columns = array($columns => $value);
}
foreach ($columns as $column => $value)
{
if ($value instanceof Database_Builder)
{
// Using a subquery
$value->db = $this->db;
$value = '('.(string) $value.')';
}
elseif (is_array($value))
{
if ($op === 'BETWEEN' OR $op === 'NOT BETWEEN')
{
// Falls between two values
$value = $this->db->quote($value[0]).' AND '.$this->db->quote($value[1]);
}
else
{
// Return as list
$value = array_map(array($this->db, 'quote'), $value);
$value = '('.implode(', ', $value).')';
}
}
else
{
$value = $this->db->quote($value);
}
if ( ! empty($column))
{
// Ignore blank columns
$column = $this->db->quote_column($column);
}
// Add to condition list
$vals[] = $column.' '.$op.' '.$value;
}
}
if ( ! empty($sql) AND $last_condition !== '(')
{
// Add the logic operator
$sql .= ' '.$logic.' ';
}
// Join the condition list items together by the given logic operator
$sql .= implode(' '.$logic.' ', $vals);
}
$last_condition = $condition;
}
}
return $sql;
}
/**
* Compiles the columns portion of the query for INSERT
*
* @return string
*/
protected function compile_columns()
{
return '('.implode(', ', array_map(array($this->db, 'quote_column'), $this->columns)).')';
}
/**
* Compiles the VALUES portion of the query for INSERT
*
* @return string
*/
protected function compile_values()
{
$values = array();
foreach ($this->values as $group)
{
// Each set of values to be inserted
$values[] = '('.implode(', ', array_map(array($this->db, 'quote'), $group)).')';
}
return implode(', ', $values);
}
/**
* Create an UPDATE query
*
* @chainable
* @param string Table name
* @param array Array of Keys => Values
* @param array WHERE conditions
* @return Database_Builder
*/
public function update($table = NULL, $set = NULL, $where = NULL)
{
$this->type = Database::UPDATE;
if (is_array($set))
{
$this->set($set);
}
if ($where !== NULL)
{
$this->where($where);
}
if ($table !== NULL)
{
$this->from($table);
}
return $this;
}
/**
* Create an INSERT query. Use 'columns' and 'values' methods for multi-row inserts
*
* @chainable
* @param string Table name
* @param array Array of Keys => Values
* @return Database_Builder
*/
public function insert($table = NULL, $set = NULL)
{
$this->type = Database::INSERT;
if (is_array($set))
{
$this->columns(array_keys($set));
$this->values(array_values($set));
}
if ($table !== NULL)
{
$this->from($table);
}
return $this;
}
/**
* Create a DELETE query
*
* @chainable
* @param string Table name
* @param array WHERE conditions
* @return Database_Builder
*/
public function delete($table, $where = NULL)
{
$this->type = Database::DELETE;
if ($where !== NULL)
{
$this->where($where);
}
if ($table !== NULL)
{
$this->from($table);
}
return $this;
}
/**
* Count records for a given table
*
* @param string Table name
* @param array WHERE conditions
* @return int
*/
public function count_records($table = FALSE, $where = NULL)
{
if (count($this->from) < 1)
{
if ($table === FALSE)
throw new Database_Exception('Database count_records requires a table');
$this->from($table);
}
if ($where !== NULL)
{
$this->where($where);
}
// Grab the count AS records_found
$result = $this->select(array('records_found' => 'COUNT("*")'))->execute();
return $result->get('records_found');
}
/**
* Executes the built query
*
* @param mixed Database name or object
* @return Database_Result
*/
public function execute($db = NULL)
{
if ($db !== NULL)
{
$this->db = $db;
}
if ( ! is_object($this->db))
{
// Get the database instance
$this->db = Database::instance($this->db);
}
$query = $this->compile();
if ($this->reset)
{
// Reset the query after executing
$this->_reset();
}
if ($this->ttl !== FALSE AND $this->type === Database::SELECT)
{
// Return result from cache (only allowed with SELECT)
return $this->db->query_cache($query, $this->ttl);
}
else
{
// Load the result (no caching)
return $this->db->query($query);
}
}
/**
* Compiles the builder object into a SQL query
*
* @return string Compiled query
*/
protected function compile()
{
if ( ! is_object($this->db))
{
// Use default database for compiling to string if none is given
$this->db = Database::instance($this->db);
}
if ($this->type === Database::SELECT)
{
// SELECT columns FROM table
$sql = $this->distinct ? 'SELECT DISTINCT ' : 'SELECT ';
$sql .= $this->compile_select();
if ( ! empty($this->from))
{
$sql .= "\nFROM ".$this->compile_from();
}
}
elseif ($this->type === Database::UPDATE)
{
$sql = 'UPDATE '.$this->compile_from()."\n".'SET '.$this->compile_set();
}
elseif ($this->type === Database::INSERT)
{
$sql = 'INSERT INTO '.$this->compile_from()."\n".$this->compile_columns()."\nVALUES ".$this->compile_values();
}
elseif ($this->type === Database::DELETE)
{
$sql = 'DELETE FROM '.$this->compile_from();
}
if ( ! empty($this->join))
{
$sql .= $this->compile_join();
}
if ( ! empty($this->where))
{
$sql .= "\n".'WHERE '.$this->compile_conditions($this->where);
}
if ( ! empty($this->group_by))
{
$sql .= "\n".'GROUP BY '.$this->compile_group_by();
}
if ( ! empty($this->having))
{
$sql .= "\n".'HAVING '.$this->compile_conditions($this->having);
}
if ( ! empty($this->order_by))
{
$sql .= "\nORDER BY ".$this->compile_order_by();
}
if (is_int($this->limit))
{
$sql .= "\nLIMIT ".$this->limit;
}
if (is_int($this->offset))
{
$sql .= "\nOFFSET ".$this->offset;
}
return $sql;
}
/**
* Compiles the SELECT portion of the query
*
* @return string
*/
protected function compile_select()
{
$vals = array();
foreach ($this->select as $alias => $name)
{
if ($name instanceof Database_Builder)
{
// Using a subquery
$name->db = $this->db;
$vals[] = '('.(string) $name.') AS '.$this->db->quote_column($alias);
}
elseif (is_string($alias))
{
$vals[] = $this->db->quote_column($name, $alias);
}
else
{
$vals[] = $this->db->quote_column($name);
}
}
return implode(', ', $vals);
}
/**
* Compiles the FROM portion of the query
*
* @return string
*/
protected function compile_from()
{
$vals = array();
foreach ($this->from as $alias => $name)
{
if (is_string($alias))
{
// Using AS format so escape both
$vals[] = $this->db->quote_table($name, $alias);
}
else
{
// Just using the table name itself
$vals[] = $this->db->quote_table($name);
}
}
return implode(', ', $vals);
}
/**
* Compiles the JOIN portion of the query
*
* @return string
*/
protected function compile_join()
{
$sql = '';
foreach ($this->join as $join)
{
list($table, $keys, $type) = $join;
if ($type !== NULL)
{
// Join type
$sql .= ' '.$type;
}
$sql .= ' JOIN '.$this->db->quote_table($table);
$condition = '';
if ($keys instanceof Database_Expression)
{
$condition = (string) $keys;
}
elseif (is_array($keys))
{
// ON condition is an array of matches
foreach ($keys as $key => $value)
{
if ( ! empty($condition))
{
$condition .= ' AND ';
}
$condition .= $this->db->quote_column($key).' = '.$this->db->quote_column($value);
}
}
if ( ! empty($condition))
{
// Add ON condition
$sql .= ' ON ('.$condition.')';
}
}
return $sql;
}
/**
* Compiles the GROUP BY portion of the query
*
* @return string
*/
protected function compile_group_by()
{
$vals = array();
foreach ($this->group_by as $column)
{
// Escape the column
$vals[] = $this->db->quote_column($column);
}
return implode(', ', $vals);
}
/**
* Compiles the ORDER BY portion of the query
*
* @return string
*/
protected function compile_order_by()
{
$ordering = array();
foreach ($this->order_by as $column => $order_by)
{
list($column, $direction) = each($order_by);
$column = $this->db->quote_column($column);
if ($direction !== NULL)
{
$direction = ' '.$direction;
}
$ordering[] = $column.$direction;
}
return implode(', ', $ordering);
}
/**
* Compiles the SET portion of the query for UPDATE
*
* @return string
*/
protected function compile_set()
{
$vals = array();
foreach ($this->set as $key => $value)
{
// Using an UPDATE so Key = Val
$vals[] = $this->db->quote_column($key).' = '.$this->db->quote($value);
}
return implode(', ', $vals);
}
/**
* Resets all query components
*/
protected function _reset()
{
$this->select = array();
$this->from = array();
$this->join = array();
$this->where = array();
$this->group_by = array();
$this->having = array();
$this->order_by = array();
$this->limit = NULL;
$this->offset = NULL;
$this->set = array();
$this->values = array();
$this->type = NULL;
$this->distinct = FALSE;
$this->reset = TRUE;
$this->ttl = FALSE;
}
} // End Database_Builder