You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
648 lines
17 KiB
648 lines
17 KiB
<?php
|
|
/**
|
|
* Part of the Fuel framework.
|
|
*
|
|
* @package Fuel
|
|
* @version 1.8
|
|
* @author Fuel Development Team
|
|
* @license MIT License
|
|
* @copyright 2010 - 2016 Fuel Development Team
|
|
* @link http://fuelphp.com
|
|
*/
|
|
|
|
namespace Fuel\Core;
|
|
|
|
class Database_Schema
|
|
{
|
|
/**
|
|
* @var Database_Connection database connection instance
|
|
*/
|
|
protected $_connection;
|
|
|
|
/**
|
|
* @var string database connection config name
|
|
*/
|
|
protected $_name;
|
|
|
|
/**
|
|
* Stores the database instance to be used.
|
|
*
|
|
* @param string database connection instance
|
|
*/
|
|
public function __construct($name, $connection)
|
|
{
|
|
// Set the connection config name
|
|
$this->_name = $name;
|
|
|
|
// Set the connection instance
|
|
$this->_connection = $connection;
|
|
}
|
|
|
|
/**
|
|
* Creates a database. Will throw a Database_Exception if it cannot.
|
|
*
|
|
* @throws Fuel\Database_Exception
|
|
* @param string $database the database name
|
|
* @param string $charset the character set
|
|
* @param boolean $if_not_exists whether to add an IF NOT EXISTS statement.
|
|
* @return int the number of affected rows
|
|
*/
|
|
public function create_database($database, $charset = null, $if_not_exists = true)
|
|
{
|
|
$sql = 'CREATE DATABASE';
|
|
$sql .= $if_not_exists ? ' IF NOT EXISTS ' : ' ';
|
|
|
|
$sql .= $this->_connection->quote_identifier($database).$this->process_charset($charset, true);
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Drops a database. Will throw a Database_Exception if it cannot.
|
|
*
|
|
* @throws Fuel\Database_Exception
|
|
* @param string $database the database name
|
|
* @return int the number of affected rows
|
|
*/
|
|
public function drop_database($database)
|
|
{
|
|
$sql = 'DROP DATABASE ';
|
|
$sql .= $this->_connection->quote_identifier($database);
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Drops a table. Will throw a Database_Exception if it cannot.
|
|
*
|
|
* @throws Fuel\Database_Exception
|
|
* @param string $table the table name
|
|
* @return int the number of affected rows
|
|
*/
|
|
public function drop_table($table)
|
|
{
|
|
$sql = 'DROP TABLE IF EXISTS ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Renames a table. Will throw a Database_Exception if it cannot.
|
|
*
|
|
* @throws \Database_Exception
|
|
* @param string $table the old table name
|
|
* @param string $new_table_name the new table name
|
|
* @return int the number of affected
|
|
*/
|
|
public function rename_table($table, $new_table_name)
|
|
{
|
|
$sql = 'RENAME TABLE ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$sql .= ' TO ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($new_table_name));
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Creates a table.
|
|
*
|
|
* @throws \Database_Exception
|
|
* @param string $table the table name
|
|
* @param array $fields the fields array
|
|
* @param array $primary_keys an array of primary keys
|
|
* @param boolean $if_not_exists whether to add an IF NOT EXISTS statement.
|
|
* @param string|boolean $engine storage engine overwrite
|
|
* @param string $charset default charset overwrite
|
|
* @param array $foreign_keys an array of foreign keys
|
|
* @return int number of affected rows.
|
|
*/
|
|
public function create_table($table, $fields, $primary_keys = array(), $if_not_exists = true, $engine = false, $charset = null, $foreign_keys = array())
|
|
{
|
|
$sql = 'CREATE TABLE';
|
|
|
|
$sql .= $if_not_exists ? ' IF NOT EXISTS ' : ' ';
|
|
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table)).' (';
|
|
$sql .= $this->process_fields($fields, '');
|
|
if ( ! empty($primary_keys))
|
|
{
|
|
foreach ($primary_keys as $index => $primary_key)
|
|
{
|
|
$primary_keys[$index] = $this->_connection->quote_identifier($primary_key);
|
|
}
|
|
$sql .= ",\n\tPRIMARY KEY (".implode(', ', $primary_keys).')';
|
|
}
|
|
|
|
empty($foreign_keys) or $sql .= $this->process_foreign_keys($foreign_keys);
|
|
|
|
$sql .= "\n)";
|
|
$sql .= ($engine !== false) ? ' ENGINE = '.$engine.' ' : '';
|
|
$sql .= $this->process_charset($charset, true).";";
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Truncates a table.
|
|
*
|
|
* @throws Fuel\Database_Exception
|
|
* @param string $table the table name
|
|
* @return int the number of affected rows
|
|
*/
|
|
public function truncate_table($table)
|
|
{
|
|
$sql = 'TRUNCATE TABLE ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
|
|
return $this->_connection->query(\DB::DELETE, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Generic check if a given table exists.
|
|
*
|
|
* @throws \Database_Exception
|
|
* @param string $table Table name
|
|
* @return bool
|
|
*/
|
|
public function table_exists($table)
|
|
{
|
|
$sql = 'SELECT * FROM ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$sql .= ' LIMIT 1';
|
|
|
|
try
|
|
{
|
|
$this->_connection->query(\DB::SELECT, $sql, false);
|
|
return true;
|
|
}
|
|
catch (\Database_Exception $e)
|
|
{
|
|
// check if we have a DB connection at all
|
|
if ( ! $this->_connection->has_connection())
|
|
{
|
|
// if no connection could be made, re throw the exception
|
|
throw $e;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if given field(s) in a given table exists.
|
|
*
|
|
* @throws \Database_Exception
|
|
* @param string $table Table name
|
|
* @param string|array $columns columns to check
|
|
* @return bool
|
|
*/
|
|
public function field_exists($table, $columns)
|
|
{
|
|
if ( ! is_array($columns))
|
|
{
|
|
$columns = array($columns);
|
|
}
|
|
|
|
$sql = 'SELECT ';
|
|
$sql .= implode(', ', array_unique(array_map(array($this->_connection, 'quote_identifier'), $columns)));
|
|
$sql .= ' FROM ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$sql .= ' LIMIT 1';
|
|
|
|
try
|
|
{
|
|
$this->_connection->query(\DB::SELECT, $sql, false);
|
|
return true;
|
|
}
|
|
catch (\Database_Exception $e)
|
|
{
|
|
// check if we have a DB connection at all
|
|
if ( ! $this->_connection->has_connection())
|
|
{
|
|
// if no connection could be made, re throw the exception
|
|
throw $e;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an index on that table.
|
|
*
|
|
* @access public
|
|
* @param string $table
|
|
* @param string $index_name
|
|
* @param string $index_columns
|
|
* @param string $index (should be 'unique', 'fulltext', 'spatial' or 'nonclustered')
|
|
* @return bool
|
|
* @author Thomas Edwards
|
|
*/
|
|
public function create_index($table, $index_columns, $index_name = '', $index = '')
|
|
{
|
|
static $accepted_index = array('UNIQUE', 'FULLTEXT', 'SPATIAL', 'NONCLUSTERED', 'PRIMARY');
|
|
|
|
// make sure the index type is uppercase
|
|
$index !== '' and $index = strtoupper($index);
|
|
|
|
if (empty($index_name))
|
|
{
|
|
if (is_array($index_columns))
|
|
{
|
|
foreach ($index_columns as $key => $value)
|
|
{
|
|
if (is_numeric($key))
|
|
{
|
|
$index_name .= ($index_name == '' ? '' : '_').$value;
|
|
}
|
|
else
|
|
{
|
|
$index_name .= ($index_name == '' ? '' : '_').str_replace(array('(', ')', ' '), '', $key);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$index_name = $index_columns;
|
|
}
|
|
}
|
|
|
|
if ($index == 'PRIMARY')
|
|
{
|
|
$sql = 'ALTER TABLE ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$sql .= ' ADD PRIMARY KEY ';
|
|
if (is_array($index_columns))
|
|
{
|
|
$columns = '';
|
|
foreach ($index_columns as $key => $value)
|
|
{
|
|
if (is_numeric($key))
|
|
{
|
|
$columns .= ($columns=='' ? '' : ', ').$this->_connection->quote_identifier($value);
|
|
}
|
|
else
|
|
{
|
|
$columns .= ($columns=='' ? '' : ', ').$this->_connection->quote_identifier($key).' '.strtoupper($value);
|
|
}
|
|
}
|
|
$sql .= ' ('.$columns.')';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$sql = 'CREATE ';
|
|
|
|
$index !== '' and $sql .= (in_array($index, $accepted_index)) ? $index.' ' : '';
|
|
|
|
$sql .= 'INDEX ';
|
|
$sql .= $this->_connection->quote_identifier($index_name);
|
|
$sql .= ' ON ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
if (is_array($index_columns))
|
|
{
|
|
$columns = '';
|
|
foreach ($index_columns as $key => $value)
|
|
{
|
|
if (is_numeric($key))
|
|
{
|
|
$columns .= ($columns=='' ? '' : ', ').$this->_connection->quote_identifier($value);
|
|
}
|
|
else
|
|
{
|
|
$columns .= ($columns=='' ? '' : ', ').$this->_connection->quote_identifier($key).' '.strtoupper($value);
|
|
}
|
|
}
|
|
$sql .= ' ('.$columns.')';
|
|
}
|
|
else
|
|
{
|
|
$sql .= ' ('.$this->_connection->quote_identifier($index_columns).')';
|
|
}
|
|
}
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Drop an index from a table.
|
|
*
|
|
* @access public
|
|
* @param string $table
|
|
* @param string $index_name
|
|
* @return bool
|
|
* @author Thomas Edwards
|
|
*/
|
|
public function drop_index($table, $index_name)
|
|
{
|
|
if (strtoupper($index_name) == 'PRIMARY')
|
|
{
|
|
$sql = 'ALTER TABLE '.$this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$sql .= ' DROP PRIMARY KEY';
|
|
}
|
|
else
|
|
{
|
|
$sql = 'DROP INDEX '.$this->_connection->quote_identifier($index_name);
|
|
$sql .= ' ON '.$this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
}
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Adds a single foreign key to a table
|
|
*
|
|
* @param string $table the table name
|
|
* @param array $foreign_key a single foreign key
|
|
* @return int number of affected rows
|
|
*/
|
|
public function add_foreign_key($table, $foreign_key)
|
|
{
|
|
if ( ! is_array($foreign_key))
|
|
{
|
|
throw new \InvalidArgumentException('Foreign key for add_foreign_key() must be specified as an array');
|
|
}
|
|
|
|
$sql = 'ALTER TABLE ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table)).' ';
|
|
$sql .= 'ADD ';
|
|
$sql .= ltrim($this->process_foreign_keys(array($foreign_key), $this->_connection), ',');
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Drops a foreign key from a table
|
|
*
|
|
* @param string $table the table name
|
|
* @param string $fk_name the foreign key name
|
|
* @return int number of affected rows
|
|
*/
|
|
public function drop_foreign_key($table, $fk_name)
|
|
{
|
|
$sql = 'ALTER TABLE ';
|
|
$sql .= $this->_connection->quote_identifier($this->_connection->table_prefix($table)).' ';
|
|
$sql .= 'DROP FOREIGN KEY '.$this->_connection->quote_identifier($fk_name);
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/**
|
|
* Returns string of foreign keys
|
|
*
|
|
* @throws \Database_Exception
|
|
* @param array $foreign_keys Array of foreign key rules
|
|
* @return string the formatted foreign key string
|
|
*/
|
|
public function process_foreign_keys($foreign_keys)
|
|
{
|
|
if ( ! is_array($foreign_keys))
|
|
{
|
|
throw new \Database_Exception('Foreign keys on create_table() must be specified as an array');
|
|
}
|
|
|
|
$fk_list = array();
|
|
|
|
foreach($foreign_keys as $definition)
|
|
{
|
|
// some sanity checks
|
|
if (empty($definition['key']))
|
|
{
|
|
throw new \Database_Exception('Foreign keys on create_table() must specify a foreign key name');
|
|
}
|
|
if ( empty($definition['reference']))
|
|
{
|
|
throw new \Database_Exception('Foreign keys on create_table() must specify a foreign key reference');
|
|
}
|
|
if (empty($definition['reference']['table']) or empty($definition['reference']['column']))
|
|
{
|
|
throw new \Database_Exception('Foreign keys on create_table() must specify a reference table and column name');
|
|
}
|
|
|
|
$sql = '';
|
|
! empty($definition['constraint']) and $sql .= " CONSTRAINT ".$this->_connection->quote_identifier($definition['constraint']);
|
|
$sql .= " FOREIGN KEY (".$this->_connection->quote_identifier($definition['key']).')';
|
|
$sql .= " REFERENCES ".$this->_connection->quote_identifier($this->_connection->table_prefix($definition['reference']['table'])).' (';
|
|
if (is_array($definition['reference']['column']))
|
|
{
|
|
$sql .= implode(', ', $this->_connection->quote_identifier($definition['reference']['column']));
|
|
}
|
|
else
|
|
{
|
|
$sql .= $this->_connection->quote_identifier($definition['reference']['column']);
|
|
}
|
|
$sql .= ')';
|
|
! empty($definition['on_update']) and $sql .= " ON UPDATE ".$definition['on_update'];
|
|
! empty($definition['on_delete']) and $sql .= " ON DELETE ".$definition['on_delete'];
|
|
|
|
$fk_list[] = "\n\t".ltrim($sql);
|
|
}
|
|
|
|
return ', '.implode(',', $fk_list);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function alter_fields($type, $table, $fields)
|
|
{
|
|
$sql = 'ALTER TABLE '.$this->_connection->quote_identifier($this->_connection->table_prefix($table)).' ';
|
|
|
|
if ($type === 'DROP')
|
|
{
|
|
if ( ! is_array($fields))
|
|
{
|
|
$fields = array($fields);
|
|
}
|
|
|
|
$drop_fields = array();
|
|
foreach ($fields as $field)
|
|
{
|
|
$drop_fields[] = 'DROP '.$this->_connection->quote_identifier($field);
|
|
}
|
|
$sql .= implode(', ', $drop_fields);
|
|
}
|
|
else
|
|
{
|
|
$use_brackets = ! in_array($type, array('ADD', 'CHANGE', 'MODIFY'));
|
|
$use_brackets and $sql .= $type.' ';
|
|
$use_brackets and $sql .= '(';
|
|
$sql .= $this->process_fields($fields, (( ! $use_brackets) ? $type.' ' : ''));
|
|
$use_brackets and $sql .= ')';
|
|
}
|
|
|
|
return $this->_connection->query(0, $sql, false);
|
|
}
|
|
|
|
/*
|
|
* Executes table maintenance. Will throw FuelException when the operation is not supported.
|
|
*
|
|
* @throws FuelException
|
|
* @param string $table the table name
|
|
* @return bool whether the operation has succeeded
|
|
*/
|
|
public function table_maintenance($operation, $table)
|
|
{
|
|
$sql = $operation.' '.$this->_connection->quote_identifier($this->_connection->table_prefix($table));
|
|
$result = $this->_connection->query(\DB::SELECT, $sql, false);
|
|
|
|
$type = $result->get('Msg_type');
|
|
$message = $result->get('Msg_text');
|
|
$table = $result->get('Table');
|
|
|
|
if ($type === 'status' and in_array(strtolower($message), array('ok', 'table is already up to date')))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// make sure we have a type logger can handle
|
|
if (in_array($type, array('info', 'warning', 'error')))
|
|
{
|
|
$type = strtoupper($type);
|
|
}
|
|
else
|
|
{
|
|
$type = \Fuel::L_INFO;
|
|
}
|
|
|
|
logger($type, 'Table: '.$table.', Operation: '.$operation.', Message: '.$result->get('Msg_text'), 'DBUtil::table_maintenance');
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Formats the default charset.
|
|
*
|
|
* @param string $charset the character set
|
|
* @param bool $is_default whether to use default
|
|
* @param string $collation the collating sequence to be used
|
|
* @return string the formatted charset sql
|
|
*/
|
|
protected function process_charset($charset = null, $is_default = false, $collation = null)
|
|
{
|
|
$charset or $charset = \Config::get('db.'.$this->_name.'.charset', null);
|
|
|
|
if (empty($charset))
|
|
{
|
|
return '';
|
|
}
|
|
|
|
$collation or $collation = \Config::get('db.'.$this->_name.'.collation', null);
|
|
|
|
if (empty($collation) and ($pos = stripos($charset, '_')) !== false)
|
|
{
|
|
$collation = $charset;
|
|
$charset = substr($charset, 0, $pos);
|
|
}
|
|
|
|
$charset = 'CHARACTER SET '.$charset;
|
|
|
|
if ($is_default)
|
|
{
|
|
$charset = 'DEFAULT '.$charset;
|
|
}
|
|
|
|
if ( ! empty($collation))
|
|
{
|
|
if ($is_default)
|
|
{
|
|
$charset .= ' DEFAULT';
|
|
}
|
|
$charset .= ' COLLATE '.$collation;
|
|
}
|
|
|
|
return $charset;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
protected function process_fields($fields, $prefix = '')
|
|
{
|
|
$sql_fields = array();
|
|
|
|
foreach ($fields as $field => $attr)
|
|
{
|
|
$attr = array_change_key_case($attr, CASE_UPPER);
|
|
$_prefix = $prefix;
|
|
if(array_key_exists('NAME', $attr) and $field !== $attr['NAME'] and $_prefix === 'MODIFY ')
|
|
{
|
|
$_prefix = 'CHANGE ';
|
|
}
|
|
$sql = "\n\t".$_prefix;
|
|
$sql .= $this->_connection->quote_identifier($field);
|
|
$sql .= (array_key_exists('NAME', $attr) and $attr['NAME'] !== $field) ? ' '.$this->_connection->quote_identifier($attr['NAME']).' ' : '';
|
|
$sql .= array_key_exists('TYPE', $attr) ? ' '.$attr['TYPE'] : '';
|
|
|
|
if(array_key_exists('CONSTRAINT', $attr))
|
|
{
|
|
if(is_array($attr['CONSTRAINT']))
|
|
{
|
|
$sql .= "(";
|
|
foreach($attr['CONSTRAINT'] as $constraint)
|
|
{
|
|
$sql .= (is_string($constraint) ? "'".$constraint."'" : $constraint).", ";
|
|
}
|
|
$sql = rtrim($sql, ', '). ")";
|
|
}
|
|
else
|
|
{
|
|
$sql .= '('.$attr['CONSTRAINT'].')';
|
|
}
|
|
}
|
|
|
|
$sql .= array_key_exists('CHARSET', $attr) ? $this->process_charset($attr['CHARSET'], false) : '';
|
|
|
|
if (array_key_exists('UNSIGNED', $attr) and $attr['UNSIGNED'] === true)
|
|
{
|
|
$sql .= ' UNSIGNED';
|
|
}
|
|
|
|
if(array_key_exists('DEFAULT', $attr))
|
|
{
|
|
$sql .= ' DEFAULT '.(($attr['DEFAULT'] instanceof \Database_Expression) ? $attr['DEFAULT'] : $this->_connection->quote($attr['DEFAULT']));
|
|
}
|
|
|
|
if(array_key_exists('NULL', $attr) and $attr['NULL'] === true)
|
|
{
|
|
$sql .= ' NULL';
|
|
}
|
|
else
|
|
{
|
|
$sql .= ' NOT NULL';
|
|
}
|
|
|
|
if (array_key_exists('AUTO_INCREMENT', $attr) and $attr['AUTO_INCREMENT'] === true)
|
|
{
|
|
$sql .= ' AUTO_INCREMENT';
|
|
}
|
|
|
|
if (array_key_exists('PRIMARY_KEY', $attr) and $attr['PRIMARY_KEY'] === true)
|
|
{
|
|
$sql .= ' PRIMARY KEY';
|
|
}
|
|
|
|
if (array_key_exists('COMMENT', $attr))
|
|
{
|
|
$sql .= ' COMMENT '.$this->_connection->escape($attr['COMMENT']);
|
|
}
|
|
|
|
if (array_key_exists('FIRST', $attr) and $attr['FIRST'] === true)
|
|
{
|
|
$sql .= ' FIRST';
|
|
}
|
|
elseif (array_key_exists('AFTER', $attr) and strval($attr['AFTER']))
|
|
{
|
|
$sql .= ' AFTER '.$this->_connection->quote_identifier($attr['AFTER']);
|
|
}
|
|
|
|
$sql_fields[] = $sql;
|
|
}
|
|
|
|
return implode(',', $sql_fields);
|
|
}
|
|
|
|
}
|