Newer
Older
Dries Buytaert
committed
<?php
namespace Drupal\Core\Database;
Dries Buytaert
committed
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\PlaceholderInterface;
Angie Byron
committed
Alex Pott
committed
/**
* Provides a base implementation for Database Schema.
*/
abstract class Schema implements PlaceholderInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
Dries Buytaert
committed
protected $connection;
/**
* The placeholder counter.
Jess
committed
*
* @var int
*/
protected $placeholder = 0;
/**
* Definition of prefixInfo array structure.
*
* Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
* by defining the defaultSchema variable only MySQL has to re-write the
* method.
*
* @see DatabaseSchema::getPrefixInfo()
Jess
committed
*
* @var string
*/
protected $defaultSchema = 'public';
Angie Byron
committed
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
Dries Buytaert
committed
public function __construct($connection) {
Angie Byron
committed
$this->uniqueIdentifier = uniqid('', TRUE);
Dries Buytaert
committed
$this->connection = $connection;
}
Angie Byron
committed
/**
* Implements the magic __clone function.
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
}
/**
* {@inheritdoc}
Angie Byron
committed
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* {@inheritdoc}
Angie Byron
committed
*/
public function nextPlaceholder() {
return $this->placeholder++;
}
Dries Buytaert
committed
* Get information about the table name and schema from the prefix.
*
* @param
* Name of table to look prefix up for. Defaults to 'default' because that's
Dries Buytaert
committed
* default key for prefix.
Angie Byron
committed
* @param $add_prefix
* Boolean that indicates whether the given table name should be prefixed.
*
* @return
* A keyed array with information about the schema, table name and prefix.
*/
Angie Byron
committed
protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
$info = [
'schema' => $this->defaultSchema,
'prefix' => $this->connection->tablePrefix($table),
];
Angie Byron
committed
if ($add_prefix) {
$table = $info['prefix'] . $table;
}
// If the prefix contains a period in it, then that means the prefix also
// contains a schema reference in which case we will change the schema key
// to the value before the period in the prefix. Everything after the dot
// will be prefixed onto the front of the table.
Angie Byron
committed
if (($pos = strpos($table, '.')) !== FALSE) {
// Grab everything before the period.
Angie Byron
committed
$info['schema'] = substr($table, 0, $pos);
// Grab everything after the dot.
$info['table'] = substr($table, ++$pos);
}
else {
Angie Byron
committed
$info['table'] = $table;
}
return $info;
}
/**
* Create names for indexes, primary keys and constraints.
*
* This prevents using {} around non-table names like indexes and keys.
*/
public function prefixNonTable($table) {
$args = func_get_args();
$info = $this->getPrefixInfo($table);
$args[0] = $info['table'];
return implode('_', $args);
}
/**
* Build a condition to match a table name against a standard information_schema.
*
* The information_schema is a SQL standard that provides information about the
* database server and the databases, schemas, tables, columns and users within
* it. This makes information_schema a useful tool to use across the drupal
* database drivers and is used by a few different functions. The function below
* describes the conditions to be meet when querying information_schema.tables
* for drupal tables or information associated with drupal tables. Even though
* this is the standard method, not all databases follow standards and so this
* method should be overwritten by a database driver if the database provider
* uses alternate methods. Because information_schema.tables is used in a few
* different functions, a database driver will only need to override this function
Nate Lampton
committed
* to make all the others work. For example see
* core/includes/databases/mysql/schema.inc.
*
* @param $table_name
* The name of the table in question.
* @param $operator
* The operator to apply on the 'table' part of the condition.
Angie Byron
committed
* @param $add_prefix
* Boolean to indicate whether the table name needs to be prefixed.
Jennifer Hodgdon
committed
* @return \Drupal\Core\Database\Query\Condition
* A Condition object.
*/
Angie Byron
committed
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
Angie Byron
committed
$info = $this->connection->getConnectionOptions();
// Retrieve the table name and schema
Angie Byron
committed
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
$condition = new Condition('AND');
Angie Byron
committed
$condition->condition('table_catalog', $info['database']);
$condition->condition('table_schema', $table_info['schema']);
$condition->condition('table_name', $table_info['table'], $operator);
return $condition;
}
Dries Buytaert
committed
/**
* Check if a table exists.
* @param $table
* The name of the table in drupal (no prefixing).
* @return
Dries Buytaert
committed
* TRUE if the given table exists, otherwise FALSE.
Dries Buytaert
committed
*/
public function tableExists($table) {
$condition = $this->buildTableNameCondition($table);
$condition->compile($this->connection, $this);
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
Dries Buytaert
committed
return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
/**
* Finds all tables that are like the specified base table name.
*
* @param string $table_expression
* An SQL expression, for example "cache_%" (without the quotes).
* @return array
* Both the keys and the values are the matching tables.
*/
public function findTables($table_expression) {
// Load all the tables up front in order to take into account per-table
// prefixes. The actual matching is done at the bottom of the method.
$condition = $this->buildTableNameCondition('%', 'LIKE');
$condition->compile($this->connection, $this);
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
$default_prefix = $this->connection->tablePrefix();
$default_prefix_length = strlen($default_prefix);
$tables = [];
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
$results = $this->connection->query("SELECT table_name as table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
foreach ($results as $table) {
// Take into account tables that have an individual prefix.
if (isset($individually_prefixed_tables[$table->table_name])) {
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
}
elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
// This table name does not start the default prefix, which means that
// it is not managed by Drupal so it should be excluded from the result.
continue;
}
else {
$prefix_length = $default_prefix_length;
}
// Remove the prefix from the returned tables.
$unprefixed_table_name = substr($table->table_name, $prefix_length);
// The pattern can match a table which is the same as the prefix. That
// will become an empty string when we remove the prefix, which will
// probably surprise the caller, besides not being a prefixed table. So
// remove it.
if (!empty($unprefixed_table_name)) {
$tables[$unprefixed_table_name] = $unprefixed_table_name;
}
}
// Convert the table expression from its SQL LIKE syntax to a regular
// expression and escape the delimiter that will be used for matching.
$table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/'));
$tables = preg_grep('/^' . $table_expression . '$/i', $tables);
return $tables;
}
Dries Buytaert
committed
/**
* Check if a column exists in the given table.
Dries Buytaert
committed
*
* @param $table
* The name of the table in drupal (no prefixing).
* @param $name
* The name of the column.
Dries Buytaert
committed
* @return
* TRUE if the given column exists, otherwise FALSE.
Dries Buytaert
committed
*/
Dries Buytaert
committed
public function fieldExists($table, $column) {
$condition = $this->buildTableNameCondition($table);
$condition->condition('column_name', $column);
$condition->compile($this->connection, $this);
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.columns table.
Dries Buytaert
committed
return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
Dries Buytaert
committed
/**
* Returns a mapping of Drupal schema field names to DB-native field types.
*
* Because different field types do not map 1:1 between databases, Drupal has
* its own normalized field type names. This function returns a driver-specific
* mapping table from Drupal names to the native names for each database.
*
* @return array
* An array of Schema API field types to driver-specific field types.
*/
Dries Buytaert
committed
abstract public function getFieldTypeMap();
Dries Buytaert
committed
/**
* Rename a table.
*
* @param $table
* The table to be renamed.
* @param $new_name
* The new name for the table.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If a table with the specified new name already exists.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function renameTable($table, $new_name);
Dries Buytaert
committed
/**
* Drop a table.
*
* @param $table
* The table to be dropped.
Dries Buytaert
committed
* @return
* TRUE if the table was successfully dropped, FALSE if there was no table
* by that name to begin with.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function dropTable($table);
Dries Buytaert
committed
/**
* Add a new field to a table.
*
* @param $table
* Name of the table to be altered.
* @param $field
* Name of the field to be added.
* @param $spec
* The field specification array, as taken from a schema definition.
* The specification may also contain the key 'initial', the newly
* created field will be set to the value of the key in all rows.
* This is most useful for creating NOT NULL columns with no default
* value in existing tables.
Alex Pott
committed
* Alternatively, the 'initial_form_field' key may be used, which will
* auto-populate the new field with values from the specified field.
Dries Buytaert
committed
* @param $keys_new
Jennifer Hodgdon
committed
* (optional) Keys and indexes specification to be created on the
Dries Buytaert
committed
* table along with adding the field. The format is the same as a
Dries Buytaert
committed
* table specification but without the 'fields' element. If you are
Dries Buytaert
committed
* adding a type 'serial' field, you MUST specify at least one key
Dries Buytaert
committed
* or index including it in this array. See db_change_field() for more
Dries Buytaert
committed
* explanation why.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified table already has a field by that name.
Dries Buytaert
committed
*/
abstract public function addField($table, $field, $spec, $keys_new = []);
Dries Buytaert
committed
/**
* Drop a field.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be dropped.
Dries Buytaert
committed
* @return
* TRUE if the field was successfully dropped, FALSE if there was no field
* by that name to begin with.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function dropField($table, $field);
Dries Buytaert
committed
/**
* Set the default value for a field.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be altered.
* @param $default
* Default value to be set. NULL for 'default NULL'.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table or field doesn't exist.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function fieldSetDefault($table, $field, $default);
Dries Buytaert
committed
/**
* Set a field to have no default value.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be altered.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table or field doesn't exist.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function fieldSetNoDefault($table, $field);
Dries Buytaert
committed
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Checks if an index exists in the given table.
Dries Buytaert
committed
*
* @param $table
Dries Buytaert
committed
* The name of the table in drupal (no prefixing).
Dries Buytaert
committed
* @param $name
Dries Buytaert
committed
* The name of the index in drupal (no prefixing).
Dries Buytaert
committed
* @return
Dries Buytaert
committed
* TRUE if the given index exists, otherwise FALSE.
Dries Buytaert
committed
*/
abstract public function indexExists($table, $name);
Dries Buytaert
committed
/**
* Add a primary key.
*
* @param $table
* The table to be altered.
* @param $fields
* Fields for the primary key.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified table already has a primary key.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function addPrimaryKey($table, $fields);
Dries Buytaert
committed
/**
* Drop the primary key.
*
* @param $table
* The table to be altered.
Dries Buytaert
committed
* @return
* TRUE if the primary key was successfully dropped, FALSE if there was no
* primary key on this table to begin with.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function dropPrimaryKey($table);
Dries Buytaert
committed
/**
* Finds the primary key columns of a table, from the database.
*
* @param string $table
* The name of the table.
*
* @return string[]|false
* A simple array with the names of the columns composing the table's
* primary key, or FALSE if the table does not exist.
*
* @throws \RuntimeException
* If the driver does not override this method.
*/
protected function findPrimaryKeyColumns($table) {
if (!$this->tableExists($table)) {
return FALSE;
}
throw new \RuntimeException("The '" . $this->connection->driver() . "' database driver does not implement " . __METHOD__);
}
Dries Buytaert
committed
/**
* Add a unique key.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the key.
* @param $fields
* An array of field names.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified table already has a key by that name.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function addUniqueKey($table, $name, $fields);
Dries Buytaert
committed
/**
* Drop a unique key.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the key.
Dries Buytaert
committed
* @return
* TRUE if the key was successfully dropped, FALSE if there was no key by
* that name to begin with.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function dropUniqueKey($table, $name);
Dries Buytaert
committed
/**
* Add an index.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the index.
* @param $fields
Alex Pott
committed
* An array of field names or field information; if field information is
* passed, it's an array whose first element is the field name and whose
* second is the maximum length in the index. For example, the following
* will use the full length of the `foo` field, but limit the `bar` field to
* 4 characters:
* @code
* $fields = ['foo', ['bar', 4]];
* @endcode
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
* @param array $spec
* The table specification for the table to be altered. This is used in
* order to be able to ensure that the index length is not too long.
* This schema definition can usually be obtained through hook_schema(), or
* in case the table was created by the Entity API, through the schema
* handler listed in the entity class definition. For reference, see
* SqlContentEntityStorageSchema::getDedicatedTableSchema() and
* SqlContentEntityStorageSchema::getSharedTableFieldSchema().
*
* In order to prevent human error, it is recommended to pass in the
* complete table specification. However, in the edge case of the complete
* table specification not being available, we can pass in a partial table
* definition containing only the fields that apply to the index:
* @code
* $spec = [
* // Example partial specification for a table:
* 'fields' => [
* 'example_field' => [
* 'description' => 'An example field',
* 'type' => 'varchar',
* 'length' => 32,
* 'not null' => TRUE,
* 'default' => '',
* ],
* ],
* 'indexes' => [
* 'table_example_field' => ['example_field'],
* ],
* ];
* @endcode
* Note that the above is a partial table definition and that we would
* usually pass a complete table definition as obtained through
* hook_schema() instead.
*
* @see schemaapi
* @see hook_schema()
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified table already has an index by that name.
*
* @todo remove the $spec argument whenever schema introspection is added.
Dries Buytaert
committed
*/
abstract public function addIndex($table, $name, $fields, array $spec);
Dries Buytaert
committed
/**
* Drop an index.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the index.
Dries Buytaert
committed
* @return
* TRUE if the index was successfully dropped, FALSE if there was no index
* by that name to begin with.
Dries Buytaert
committed
*/
Dries Buytaert
committed
abstract public function dropIndex($table, $name);
Dries Buytaert
committed
/**
* Change a field definition.
*
* IMPORTANT NOTE: To maintain database portability, you have to explicitly
* recreate all indices and primary keys that are using the changed field.
*
* That means that you have to drop all affected keys and indexes with
* db_drop_{primary_key,unique_key,index}() before calling db_change_field().
* To recreate the keys and indices, pass the key definitions as the
* optional $keys_new argument directly to db_change_field().
*
* For example, suppose you have:
* @code
* $schema['foo'] = array(
* 'fields' => array(
* 'bar' => array('type' => 'int', 'not null' => TRUE)
* ),
* 'primary key' => array('bar')
* );
* @endcode
* and you want to change foo.bar to be type serial, leaving it as the
Dries Buytaert
committed
* primary key. The correct sequence is:
Dries Buytaert
committed
* @code
Dries Buytaert
committed
* db_drop_primary_key('foo');
* db_change_field('foo', 'bar', 'bar',
Dries Buytaert
committed
* array('type' => 'serial', 'not null' => TRUE),
* array('primary key' => array('bar')));
* @endcode
*
* The reasons for this are due to the different database engines:
*
* On PostgreSQL, changing a field definition involves adding a new field
* and dropping an old one which* causes any indices, primary keys and
* sequences (from serial-type fields) that use the changed field to be dropped.
*
* On MySQL, all type 'serial' fields must be part of at least one key
Dries Buytaert
committed
* or index as soon as they are created. You cannot use
Dries Buytaert
committed
* db_add_{primary_key,unique_key,index}() for this purpose because
* the ALTER TABLE command will fail to add the column without a key
Dries Buytaert
committed
* or index specification. The solution is to use the optional
Dries Buytaert
committed
* $keys_new argument to create the key or index at the same time as
* field.
*
* You could use db_add_{primary_key,unique_key,index}() in all cases
* unless you are converting a field to be type serial. You can use
* the $keys_new argument in all cases.
*
* @param $table
* Name of the table.
* @param $field
* Name of the field to change.
* @param $field_new
* New name for the field (set to the same as $field if you don't want to change the name).
* @param $spec
* The field specification for the new field.
* @param $keys_new
Jennifer Hodgdon
committed
* (optional) Keys and indexes specification to be created on the
Dries Buytaert
committed
* table along with changing the field. The format is the same as a
* table specification but without the 'fields' element.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Dries Buytaert
committed
* If the specified table or source field doesn't exist.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified destination field already exists.
Dries Buytaert
committed
*/
abstract public function changeField($table, $field, $field_new, $spec, $keys_new = []);
Dries Buytaert
committed
/**
* Create a new table from a Drupal table definition.
*
* @param $name
* The name of the table to create.
* @param $table
* A Schema API table definition array.
Dries Buytaert
committed
* @throws \Drupal\Core\Database\SchemaObjectExistsException
Dries Buytaert
committed
* If the specified table already exists.
Dries Buytaert
committed
*/
Dries Buytaert
committed
public function createTable($name, $table) {
Dries Buytaert
committed
if ($this->tableExists($name)) {
throw new SchemaObjectExistsException(t('Table @name already exists.', ['@name' => $name]));
Dries Buytaert
committed
}
Dries Buytaert
committed
$statements = $this->createTableSql($name, $table);
Dries Buytaert
committed
foreach ($statements as $statement) {
Dries Buytaert
committed
$this->connection->query($statement);
Dries Buytaert
committed
}
}
Dries Buytaert
committed
/**
* Return an array of field names from an array of key/index column specifiers.
*
* This is usually an identity function but if a key/index uses a column prefix
* specification, this function extracts just the name.
*
* @param $fields
* An array of key/index column specifiers.
Dries Buytaert
committed
* @return
* An array of field names.
*/
public function fieldNames($fields) {
$return = [];
Dries Buytaert
committed
foreach ($fields as $field) {
if (is_array($field)) {
Dries Buytaert
committed
$return[] = $field[0];
Dries Buytaert
committed
}
else {
Dries Buytaert
committed
$return[] = $field;
Dries Buytaert
committed
}
}
Dries Buytaert
committed
return $return;
Dries Buytaert
committed
}
/**
* Prepare a table or column comment for database query.
* @param $comment
* The comment string to prepare.
* @param $length
* Optional upper limit on the returned string length.
* @return
* The prepared comment.
*/
public function prepareComment($comment, $length = NULL) {
Alex Bronstein
committed
// Remove semicolons to avoid triggering multi-statement check.
$comment = strtr($comment, [';' => '.']);
return $this->connection->quote($comment);
}
/**
* Return an escaped version of its parameter to be used as a default value
* on a column.
*
* @param mixed $value
* The value to be escaped (int, float, null or string).
*
* @return string|int|float
* The escaped value.
*/
protected function escapeDefaultValue($value) {
if (is_null($value)) {
return 'NULL';
}
return is_string($value) ? $this->connection->quote($value) : $value;
}
/**
* Ensures that all the primary key fields are correctly defined.
*
* @param array $primary_key
* An array containing the fields that will form the primary key of a table.
* @param array $fields
* An array containing the field specifications of the table, as per the
* schema data structure format.
*
* @throws \Drupal\Core\Database\SchemaException
* Thrown if any primary key field specification does not exist or if they
* do not define 'not null' as TRUE.
*/
protected function ensureNotNullPrimaryKey(array $primary_key, array $fields) {
foreach (array_intersect($primary_key, array_keys($fields)) as $field_name) {
if (!isset($fields[$field_name]['not null']) || $fields[$field_name]['not null'] !== TRUE) {
throw new SchemaException("The '$field_name' field specification does not define 'not null' as TRUE.");
}
}
}