annotate core/lib/Drupal/Core/Database/Schema.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Database;
Chris@0 4
Chris@0 5 use Drupal\Core\Database\Query\Condition;
Chris@0 6 use Drupal\Core\Database\Query\PlaceholderInterface;
Chris@0 7
Chris@0 8 /**
Chris@0 9 * Provides a base implementation for Database Schema.
Chris@0 10 */
Chris@0 11 abstract class Schema implements PlaceholderInterface {
Chris@0 12
Chris@0 13 /**
Chris@0 14 * The database connection.
Chris@0 15 *
Chris@0 16 * @var \Drupal\Core\Database\Connection
Chris@0 17 */
Chris@0 18 protected $connection;
Chris@0 19
Chris@0 20 /**
Chris@0 21 * The placeholder counter.
Chris@12 22 *
Chris@12 23 * @var int
Chris@0 24 */
Chris@0 25 protected $placeholder = 0;
Chris@0 26
Chris@0 27 /**
Chris@0 28 * Definition of prefixInfo array structure.
Chris@0 29 *
Chris@0 30 * Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
Chris@0 31 * by defining the defaultSchema variable only MySQL has to re-write the
Chris@0 32 * method.
Chris@0 33 *
Chris@0 34 * @see DatabaseSchema::getPrefixInfo()
Chris@12 35 *
Chris@12 36 * @var string
Chris@0 37 */
Chris@0 38 protected $defaultSchema = 'public';
Chris@0 39
Chris@0 40 /**
Chris@0 41 * A unique identifier for this query object.
Chris@0 42 */
Chris@0 43 protected $uniqueIdentifier;
Chris@0 44
Chris@0 45 public function __construct($connection) {
Chris@0 46 $this->uniqueIdentifier = uniqid('', TRUE);
Chris@0 47 $this->connection = $connection;
Chris@0 48 }
Chris@0 49
Chris@0 50 /**
Chris@0 51 * Implements the magic __clone function.
Chris@0 52 */
Chris@0 53 public function __clone() {
Chris@0 54 $this->uniqueIdentifier = uniqid('', TRUE);
Chris@0 55 }
Chris@0 56
Chris@0 57 /**
Chris@0 58 * {@inheritdoc}
Chris@0 59 */
Chris@0 60 public function uniqueIdentifier() {
Chris@0 61 return $this->uniqueIdentifier;
Chris@0 62 }
Chris@0 63
Chris@0 64 /**
Chris@0 65 * {@inheritdoc}
Chris@0 66 */
Chris@0 67 public function nextPlaceholder() {
Chris@0 68 return $this->placeholder++;
Chris@0 69 }
Chris@0 70
Chris@0 71 /**
Chris@0 72 * Get information about the table name and schema from the prefix.
Chris@0 73 *
Chris@0 74 * @param
Chris@0 75 * Name of table to look prefix up for. Defaults to 'default' because that's
Chris@0 76 * default key for prefix.
Chris@0 77 * @param $add_prefix
Chris@0 78 * Boolean that indicates whether the given table name should be prefixed.
Chris@0 79 *
Chris@0 80 * @return
Chris@0 81 * A keyed array with information about the schema, table name and prefix.
Chris@0 82 */
Chris@0 83 protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
Chris@0 84 $info = [
Chris@0 85 'schema' => $this->defaultSchema,
Chris@0 86 'prefix' => $this->connection->tablePrefix($table),
Chris@0 87 ];
Chris@0 88 if ($add_prefix) {
Chris@0 89 $table = $info['prefix'] . $table;
Chris@0 90 }
Chris@0 91 // If the prefix contains a period in it, then that means the prefix also
Chris@0 92 // contains a schema reference in which case we will change the schema key
Chris@0 93 // to the value before the period in the prefix. Everything after the dot
Chris@0 94 // will be prefixed onto the front of the table.
Chris@0 95 if (($pos = strpos($table, '.')) !== FALSE) {
Chris@0 96 // Grab everything before the period.
Chris@0 97 $info['schema'] = substr($table, 0, $pos);
Chris@0 98 // Grab everything after the dot.
Chris@0 99 $info['table'] = substr($table, ++$pos);
Chris@0 100 }
Chris@0 101 else {
Chris@0 102 $info['table'] = $table;
Chris@0 103 }
Chris@0 104 return $info;
Chris@0 105 }
Chris@0 106
Chris@0 107 /**
Chris@0 108 * Create names for indexes, primary keys and constraints.
Chris@0 109 *
Chris@0 110 * This prevents using {} around non-table names like indexes and keys.
Chris@0 111 */
Chris@0 112 public function prefixNonTable($table) {
Chris@0 113 $args = func_get_args();
Chris@0 114 $info = $this->getPrefixInfo($table);
Chris@0 115 $args[0] = $info['table'];
Chris@0 116 return implode('_', $args);
Chris@0 117 }
Chris@0 118
Chris@0 119 /**
Chris@0 120 * Build a condition to match a table name against a standard information_schema.
Chris@0 121 *
Chris@0 122 * The information_schema is a SQL standard that provides information about the
Chris@0 123 * database server and the databases, schemas, tables, columns and users within
Chris@0 124 * it. This makes information_schema a useful tool to use across the drupal
Chris@0 125 * database drivers and is used by a few different functions. The function below
Chris@0 126 * describes the conditions to be meet when querying information_schema.tables
Chris@0 127 * for drupal tables or information associated with drupal tables. Even though
Chris@0 128 * this is the standard method, not all databases follow standards and so this
Chris@0 129 * method should be overwritten by a database driver if the database provider
Chris@0 130 * uses alternate methods. Because information_schema.tables is used in a few
Chris@0 131 * different functions, a database driver will only need to override this function
Chris@0 132 * to make all the others work. For example see
Chris@0 133 * core/includes/databases/mysql/schema.inc.
Chris@0 134 *
Chris@0 135 * @param $table_name
Chris@0 136 * The name of the table in question.
Chris@0 137 * @param $operator
Chris@0 138 * The operator to apply on the 'table' part of the condition.
Chris@0 139 * @param $add_prefix
Chris@0 140 * Boolean to indicate whether the table name needs to be prefixed.
Chris@0 141 *
Chris@0 142 * @return \Drupal\Core\Database\Query\Condition
Chris@0 143 * A Condition object.
Chris@0 144 */
Chris@0 145 protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
Chris@0 146 $info = $this->connection->getConnectionOptions();
Chris@0 147
Chris@0 148 // Retrieve the table name and schema
Chris@0 149 $table_info = $this->getPrefixInfo($table_name, $add_prefix);
Chris@0 150
Chris@0 151 $condition = new Condition('AND');
Chris@0 152 $condition->condition('table_catalog', $info['database']);
Chris@0 153 $condition->condition('table_schema', $table_info['schema']);
Chris@0 154 $condition->condition('table_name', $table_info['table'], $operator);
Chris@0 155 return $condition;
Chris@0 156 }
Chris@0 157
Chris@0 158 /**
Chris@0 159 * Check if a table exists.
Chris@0 160 *
Chris@0 161 * @param $table
Chris@0 162 * The name of the table in drupal (no prefixing).
Chris@0 163 *
Chris@0 164 * @return
Chris@0 165 * TRUE if the given table exists, otherwise FALSE.
Chris@0 166 */
Chris@0 167 public function tableExists($table) {
Chris@0 168 $condition = $this->buildTableNameCondition($table);
Chris@0 169 $condition->compile($this->connection, $this);
Chris@0 170 // Normally, we would heartily discourage the use of string
Chris@0 171 // concatenation for conditionals like this however, we
Chris@0 172 // couldn't use db_select() here because it would prefix
Chris@0 173 // information_schema.tables and the query would fail.
Chris@0 174 // Don't use {} around information_schema.tables table.
Chris@0 175 return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
Chris@0 176 }
Chris@0 177
Chris@0 178 /**
Chris@0 179 * Finds all tables that are like the specified base table name.
Chris@0 180 *
Chris@0 181 * @param string $table_expression
Chris@0 182 * An SQL expression, for example "cache_%" (without the quotes).
Chris@0 183 *
Chris@0 184 * @return array
Chris@0 185 * Both the keys and the values are the matching tables.
Chris@0 186 */
Chris@0 187 public function findTables($table_expression) {
Chris@0 188 // Load all the tables up front in order to take into account per-table
Chris@0 189 // prefixes. The actual matching is done at the bottom of the method.
Chris@0 190 $condition = $this->buildTableNameCondition('%', 'LIKE');
Chris@0 191 $condition->compile($this->connection, $this);
Chris@0 192
Chris@0 193 $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
Chris@0 194 $default_prefix = $this->connection->tablePrefix();
Chris@0 195 $default_prefix_length = strlen($default_prefix);
Chris@0 196 $tables = [];
Chris@0 197 // Normally, we would heartily discourage the use of string
Chris@0 198 // concatenation for conditionals like this however, we
Chris@0 199 // couldn't use db_select() here because it would prefix
Chris@0 200 // information_schema.tables and the query would fail.
Chris@0 201 // Don't use {} around information_schema.tables table.
Chris@17 202 $results = $this->connection->query("SELECT table_name as table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
Chris@0 203 foreach ($results as $table) {
Chris@0 204 // Take into account tables that have an individual prefix.
Chris@0 205 if (isset($individually_prefixed_tables[$table->table_name])) {
Chris@0 206 $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
Chris@0 207 }
Chris@0 208 elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
Chris@0 209 // This table name does not start the default prefix, which means that
Chris@0 210 // it is not managed by Drupal so it should be excluded from the result.
Chris@0 211 continue;
Chris@0 212 }
Chris@0 213 else {
Chris@0 214 $prefix_length = $default_prefix_length;
Chris@0 215 }
Chris@0 216
Chris@0 217 // Remove the prefix from the returned tables.
Chris@0 218 $unprefixed_table_name = substr($table->table_name, $prefix_length);
Chris@0 219
Chris@0 220 // The pattern can match a table which is the same as the prefix. That
Chris@0 221 // will become an empty string when we remove the prefix, which will
Chris@0 222 // probably surprise the caller, besides not being a prefixed table. So
Chris@0 223 // remove it.
Chris@0 224 if (!empty($unprefixed_table_name)) {
Chris@0 225 $tables[$unprefixed_table_name] = $unprefixed_table_name;
Chris@0 226 }
Chris@0 227 }
Chris@0 228
Chris@0 229 // Convert the table expression from its SQL LIKE syntax to a regular
Chris@0 230 // expression and escape the delimiter that will be used for matching.
Chris@0 231 $table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/'));
Chris@0 232 $tables = preg_grep('/^' . $table_expression . '$/i', $tables);
Chris@0 233
Chris@0 234 return $tables;
Chris@0 235 }
Chris@0 236
Chris@0 237 /**
Chris@0 238 * Check if a column exists in the given table.
Chris@0 239 *
Chris@0 240 * @param $table
Chris@0 241 * The name of the table in drupal (no prefixing).
Chris@18 242 * @param $column
Chris@0 243 * The name of the column.
Chris@0 244 *
Chris@0 245 * @return
Chris@0 246 * TRUE if the given column exists, otherwise FALSE.
Chris@0 247 */
Chris@0 248 public function fieldExists($table, $column) {
Chris@0 249 $condition = $this->buildTableNameCondition($table);
Chris@0 250 $condition->condition('column_name', $column);
Chris@0 251 $condition->compile($this->connection, $this);
Chris@0 252 // Normally, we would heartily discourage the use of string
Chris@0 253 // concatenation for conditionals like this however, we
Chris@0 254 // couldn't use db_select() here because it would prefix
Chris@0 255 // information_schema.tables and the query would fail.
Chris@0 256 // Don't use {} around information_schema.columns table.
Chris@0 257 return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
Chris@0 258 }
Chris@0 259
Chris@0 260 /**
Chris@0 261 * Returns a mapping of Drupal schema field names to DB-native field types.
Chris@0 262 *
Chris@0 263 * Because different field types do not map 1:1 between databases, Drupal has
Chris@0 264 * its own normalized field type names. This function returns a driver-specific
Chris@0 265 * mapping table from Drupal names to the native names for each database.
Chris@0 266 *
Chris@0 267 * @return array
Chris@0 268 * An array of Schema API field types to driver-specific field types.
Chris@0 269 */
Chris@0 270 abstract public function getFieldTypeMap();
Chris@0 271
Chris@0 272 /**
Chris@0 273 * Rename a table.
Chris@0 274 *
Chris@0 275 * @param $table
Chris@0 276 * The table to be renamed.
Chris@0 277 * @param $new_name
Chris@0 278 * The new name for the table.
Chris@0 279 *
Chris@0 280 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 281 * If the specified table doesn't exist.
Chris@0 282 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 283 * If a table with the specified new name already exists.
Chris@0 284 */
Chris@0 285 abstract public function renameTable($table, $new_name);
Chris@0 286
Chris@0 287 /**
Chris@0 288 * Drop a table.
Chris@0 289 *
Chris@0 290 * @param $table
Chris@0 291 * The table to be dropped.
Chris@0 292 *
Chris@0 293 * @return
Chris@0 294 * TRUE if the table was successfully dropped, FALSE if there was no table
Chris@0 295 * by that name to begin with.
Chris@0 296 */
Chris@0 297 abstract public function dropTable($table);
Chris@0 298
Chris@0 299 /**
Chris@0 300 * Add a new field to a table.
Chris@0 301 *
Chris@0 302 * @param $table
Chris@0 303 * Name of the table to be altered.
Chris@0 304 * @param $field
Chris@0 305 * Name of the field to be added.
Chris@0 306 * @param $spec
Chris@0 307 * The field specification array, as taken from a schema definition.
Chris@0 308 * The specification may also contain the key 'initial', the newly
Chris@0 309 * created field will be set to the value of the key in all rows.
Chris@0 310 * This is most useful for creating NOT NULL columns with no default
Chris@0 311 * value in existing tables.
Chris@0 312 * Alternatively, the 'initial_form_field' key may be used, which will
Chris@0 313 * auto-populate the new field with values from the specified field.
Chris@0 314 * @param $keys_new
Chris@0 315 * (optional) Keys and indexes specification to be created on the
Chris@0 316 * table along with adding the field. The format is the same as a
Chris@0 317 * table specification but without the 'fields' element. If you are
Chris@0 318 * adding a type 'serial' field, you MUST specify at least one key
Chris@18 319 * or index including it in this array. See ::changeField() for more
Chris@0 320 * explanation why.
Chris@0 321 *
Chris@0 322 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 323 * If the specified table doesn't exist.
Chris@0 324 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 325 * If the specified table already has a field by that name.
Chris@0 326 */
Chris@0 327 abstract public function addField($table, $field, $spec, $keys_new = []);
Chris@0 328
Chris@0 329 /**
Chris@0 330 * Drop a field.
Chris@0 331 *
Chris@0 332 * @param $table
Chris@0 333 * The table to be altered.
Chris@0 334 * @param $field
Chris@0 335 * The field to be dropped.
Chris@0 336 *
Chris@0 337 * @return
Chris@0 338 * TRUE if the field was successfully dropped, FALSE if there was no field
Chris@0 339 * by that name to begin with.
Chris@0 340 */
Chris@0 341 abstract public function dropField($table, $field);
Chris@0 342
Chris@0 343 /**
Chris@0 344 * Set the default value for a field.
Chris@0 345 *
Chris@0 346 * @param $table
Chris@0 347 * The table to be altered.
Chris@0 348 * @param $field
Chris@0 349 * The field to be altered.
Chris@0 350 * @param $default
Chris@0 351 * Default value to be set. NULL for 'default NULL'.
Chris@0 352 *
Chris@0 353 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 354 * If the specified table or field doesn't exist.
Chris@18 355 *
Chris@18 356 * @deprecated as of Drupal 8.7.x, will be removed in Drupal 9.0.0. Instead,
Chris@18 357 * call ::changeField() passing a full field specification.
Chris@18 358 *
Chris@18 359 * @see ::changeField()
Chris@0 360 */
Chris@0 361 abstract public function fieldSetDefault($table, $field, $default);
Chris@0 362
Chris@0 363 /**
Chris@0 364 * Set a field to have no default value.
Chris@0 365 *
Chris@0 366 * @param $table
Chris@0 367 * The table to be altered.
Chris@0 368 * @param $field
Chris@0 369 * The field to be altered.
Chris@0 370 *
Chris@0 371 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 372 * If the specified table or field doesn't exist.
Chris@18 373 *
Chris@18 374 * @deprecated as of Drupal 8.7.x, will be removed in Drupal 9.0.0. Instead,
Chris@18 375 * call ::changeField() passing a full field specification.
Chris@18 376 *
Chris@18 377 * @see ::changeField()
Chris@0 378 */
Chris@0 379 abstract public function fieldSetNoDefault($table, $field);
Chris@0 380
Chris@0 381 /**
Chris@0 382 * Checks if an index exists in the given table.
Chris@0 383 *
Chris@0 384 * @param $table
Chris@0 385 * The name of the table in drupal (no prefixing).
Chris@0 386 * @param $name
Chris@0 387 * The name of the index in drupal (no prefixing).
Chris@0 388 *
Chris@0 389 * @return
Chris@0 390 * TRUE if the given index exists, otherwise FALSE.
Chris@0 391 */
Chris@0 392 abstract public function indexExists($table, $name);
Chris@0 393
Chris@0 394 /**
Chris@0 395 * Add a primary key.
Chris@0 396 *
Chris@0 397 * @param $table
Chris@0 398 * The table to be altered.
Chris@0 399 * @param $fields
Chris@0 400 * Fields for the primary key.
Chris@0 401 *
Chris@0 402 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 403 * If the specified table doesn't exist.
Chris@0 404 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 405 * If the specified table already has a primary key.
Chris@0 406 */
Chris@0 407 abstract public function addPrimaryKey($table, $fields);
Chris@0 408
Chris@0 409 /**
Chris@0 410 * Drop the primary key.
Chris@0 411 *
Chris@0 412 * @param $table
Chris@0 413 * The table to be altered.
Chris@0 414 *
Chris@0 415 * @return
Chris@0 416 * TRUE if the primary key was successfully dropped, FALSE if there was no
Chris@0 417 * primary key on this table to begin with.
Chris@0 418 */
Chris@0 419 abstract public function dropPrimaryKey($table);
Chris@0 420
Chris@0 421 /**
Chris@17 422 * Finds the primary key columns of a table, from the database.
Chris@17 423 *
Chris@17 424 * @param string $table
Chris@17 425 * The name of the table.
Chris@17 426 *
Chris@17 427 * @return string[]|false
Chris@17 428 * A simple array with the names of the columns composing the table's
Chris@17 429 * primary key, or FALSE if the table does not exist.
Chris@17 430 *
Chris@17 431 * @throws \RuntimeException
Chris@17 432 * If the driver does not override this method.
Chris@17 433 */
Chris@17 434 protected function findPrimaryKeyColumns($table) {
Chris@17 435 if (!$this->tableExists($table)) {
Chris@17 436 return FALSE;
Chris@17 437 }
Chris@17 438 throw new \RuntimeException("The '" . $this->connection->driver() . "' database driver does not implement " . __METHOD__);
Chris@17 439 }
Chris@17 440
Chris@17 441 /**
Chris@0 442 * Add a unique key.
Chris@0 443 *
Chris@0 444 * @param $table
Chris@0 445 * The table to be altered.
Chris@0 446 * @param $name
Chris@0 447 * The name of the key.
Chris@0 448 * @param $fields
Chris@0 449 * An array of field names.
Chris@0 450 *
Chris@0 451 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 452 * If the specified table doesn't exist.
Chris@0 453 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 454 * If the specified table already has a key by that name.
Chris@0 455 */
Chris@0 456 abstract public function addUniqueKey($table, $name, $fields);
Chris@0 457
Chris@0 458 /**
Chris@0 459 * Drop a unique key.
Chris@0 460 *
Chris@0 461 * @param $table
Chris@0 462 * The table to be altered.
Chris@0 463 * @param $name
Chris@0 464 * The name of the key.
Chris@0 465 *
Chris@0 466 * @return
Chris@0 467 * TRUE if the key was successfully dropped, FALSE if there was no key by
Chris@0 468 * that name to begin with.
Chris@0 469 */
Chris@0 470 abstract public function dropUniqueKey($table, $name);
Chris@0 471
Chris@0 472 /**
Chris@0 473 * Add an index.
Chris@0 474 *
Chris@0 475 * @param $table
Chris@0 476 * The table to be altered.
Chris@0 477 * @param $name
Chris@0 478 * The name of the index.
Chris@0 479 * @param $fields
Chris@0 480 * An array of field names or field information; if field information is
Chris@0 481 * passed, it's an array whose first element is the field name and whose
Chris@0 482 * second is the maximum length in the index. For example, the following
Chris@0 483 * will use the full length of the `foo` field, but limit the `bar` field to
Chris@0 484 * 4 characters:
Chris@0 485 * @code
Chris@0 486 * $fields = ['foo', ['bar', 4]];
Chris@0 487 * @endcode
Chris@0 488 * @param array $spec
Chris@0 489 * The table specification for the table to be altered. This is used in
Chris@0 490 * order to be able to ensure that the index length is not too long.
Chris@0 491 * This schema definition can usually be obtained through hook_schema(), or
Chris@0 492 * in case the table was created by the Entity API, through the schema
Chris@0 493 * handler listed in the entity class definition. For reference, see
Chris@0 494 * SqlContentEntityStorageSchema::getDedicatedTableSchema() and
Chris@0 495 * SqlContentEntityStorageSchema::getSharedTableFieldSchema().
Chris@0 496 *
Chris@0 497 * In order to prevent human error, it is recommended to pass in the
Chris@0 498 * complete table specification. However, in the edge case of the complete
Chris@0 499 * table specification not being available, we can pass in a partial table
Chris@0 500 * definition containing only the fields that apply to the index:
Chris@0 501 * @code
Chris@0 502 * $spec = [
Chris@0 503 * // Example partial specification for a table:
Chris@0 504 * 'fields' => [
Chris@0 505 * 'example_field' => [
Chris@0 506 * 'description' => 'An example field',
Chris@0 507 * 'type' => 'varchar',
Chris@0 508 * 'length' => 32,
Chris@0 509 * 'not null' => TRUE,
Chris@0 510 * 'default' => '',
Chris@0 511 * ],
Chris@0 512 * ],
Chris@0 513 * 'indexes' => [
Chris@0 514 * 'table_example_field' => ['example_field'],
Chris@0 515 * ],
Chris@0 516 * ];
Chris@0 517 * @endcode
Chris@0 518 * Note that the above is a partial table definition and that we would
Chris@0 519 * usually pass a complete table definition as obtained through
Chris@0 520 * hook_schema() instead.
Chris@0 521 *
Chris@0 522 * @see schemaapi
Chris@0 523 * @see hook_schema()
Chris@0 524 *
Chris@0 525 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 526 * If the specified table doesn't exist.
Chris@0 527 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 528 * If the specified table already has an index by that name.
Chris@0 529 *
Chris@0 530 * @todo remove the $spec argument whenever schema introspection is added.
Chris@0 531 */
Chris@0 532 abstract public function addIndex($table, $name, $fields, array $spec);
Chris@0 533
Chris@0 534 /**
Chris@0 535 * Drop an index.
Chris@0 536 *
Chris@0 537 * @param $table
Chris@0 538 * The table to be altered.
Chris@0 539 * @param $name
Chris@0 540 * The name of the index.
Chris@0 541 *
Chris@0 542 * @return
Chris@0 543 * TRUE if the index was successfully dropped, FALSE if there was no index
Chris@0 544 * by that name to begin with.
Chris@0 545 */
Chris@0 546 abstract public function dropIndex($table, $name);
Chris@0 547
Chris@0 548 /**
Chris@18 549 * Finds the columns for the primary key, unique keys and indexes of a table.
Chris@18 550 *
Chris@18 551 * @param string $table
Chris@18 552 * The name of the table.
Chris@18 553 *
Chris@18 554 * @return array
Chris@18 555 * A schema array with the following keys: 'primary key', 'unique keys' and
Chris@18 556 * 'indexes', and values as arrays of database columns.
Chris@18 557 *
Chris@18 558 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@18 559 * If the specified table doesn't exist.
Chris@18 560 * @throws \RuntimeException
Chris@18 561 * If the driver does not implement this method.
Chris@18 562 */
Chris@18 563 protected function introspectIndexSchema($table) {
Chris@18 564 if (!$this->tableExists($table)) {
Chris@18 565 throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
Chris@18 566 }
Chris@18 567 throw new \RuntimeException("The '{$this->connection->driver()}' database driver does not implement " . __METHOD__);
Chris@18 568 }
Chris@18 569
Chris@18 570 /**
Chris@0 571 * Change a field definition.
Chris@0 572 *
Chris@0 573 * IMPORTANT NOTE: To maintain database portability, you have to explicitly
Chris@0 574 * recreate all indices and primary keys that are using the changed field.
Chris@0 575 *
Chris@0 576 * That means that you have to drop all affected keys and indexes with
Chris@18 577 * Schema::dropPrimaryKey(), Schema::dropUniqueKey(), or Schema::dropIndex()
Chris@18 578 * before calling ::changeField().
Chris@0 579 * To recreate the keys and indices, pass the key definitions as the
Chris@18 580 * optional $keys_new argument directly to ::changeField().
Chris@0 581 *
Chris@0 582 * For example, suppose you have:
Chris@0 583 * @code
Chris@0 584 * $schema['foo'] = array(
Chris@0 585 * 'fields' => array(
Chris@0 586 * 'bar' => array('type' => 'int', 'not null' => TRUE)
Chris@0 587 * ),
Chris@0 588 * 'primary key' => array('bar')
Chris@0 589 * );
Chris@0 590 * @endcode
Chris@0 591 * and you want to change foo.bar to be type serial, leaving it as the
Chris@0 592 * primary key. The correct sequence is:
Chris@0 593 * @code
Chris@18 594 * $injected_database->schema()->dropPrimaryKey('foo');
Chris@18 595 * $injected_database->schema()->changeField('foo', 'bar', 'bar',
Chris@0 596 * array('type' => 'serial', 'not null' => TRUE),
Chris@0 597 * array('primary key' => array('bar')));
Chris@0 598 * @endcode
Chris@0 599 *
Chris@0 600 * The reasons for this are due to the different database engines:
Chris@0 601 *
Chris@0 602 * On PostgreSQL, changing a field definition involves adding a new field
Chris@0 603 * and dropping an old one which* causes any indices, primary keys and
Chris@0 604 * sequences (from serial-type fields) that use the changed field to be dropped.
Chris@0 605 *
Chris@0 606 * On MySQL, all type 'serial' fields must be part of at least one key
Chris@0 607 * or index as soon as they are created. You cannot use
Chris@18 608 * Schema::addPrimaryKey, Schema::addUniqueKey(), or Schema::addIndex()
Chris@18 609 * for this purpose because the ALTER TABLE command will fail to add
Chris@18 610 * the column without a key or index specification.
Chris@18 611 * The solution is to use the optional $keys_new argument to create the key
Chris@18 612 * or index at the same time as field.
Chris@0 613 *
Chris@18 614 * You could use Schema::addPrimaryKey, Schema::addUniqueKey(), or
Chris@18 615 * Schema::addIndex() in all cases unless you are converting a field to
Chris@18 616 * be type serial. You can use the $keys_new argument in all cases.
Chris@0 617 *
Chris@0 618 * @param $table
Chris@0 619 * Name of the table.
Chris@0 620 * @param $field
Chris@0 621 * Name of the field to change.
Chris@0 622 * @param $field_new
Chris@0 623 * New name for the field (set to the same as $field if you don't want to change the name).
Chris@0 624 * @param $spec
Chris@0 625 * The field specification for the new field.
Chris@0 626 * @param $keys_new
Chris@0 627 * (optional) Keys and indexes specification to be created on the
Chris@0 628 * table along with changing the field. The format is the same as a
Chris@0 629 * table specification but without the 'fields' element.
Chris@0 630 *
Chris@0 631 * @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
Chris@0 632 * If the specified table or source field doesn't exist.
Chris@0 633 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 634 * If the specified destination field already exists.
Chris@0 635 */
Chris@0 636 abstract public function changeField($table, $field, $field_new, $spec, $keys_new = []);
Chris@0 637
Chris@0 638 /**
Chris@0 639 * Create a new table from a Drupal table definition.
Chris@0 640 *
Chris@0 641 * @param $name
Chris@0 642 * The name of the table to create.
Chris@0 643 * @param $table
Chris@0 644 * A Schema API table definition array.
Chris@0 645 *
Chris@0 646 * @throws \Drupal\Core\Database\SchemaObjectExistsException
Chris@0 647 * If the specified table already exists.
Chris@0 648 */
Chris@0 649 public function createTable($name, $table) {
Chris@0 650 if ($this->tableExists($name)) {
Chris@0 651 throw new SchemaObjectExistsException(t('Table @name already exists.', ['@name' => $name]));
Chris@0 652 }
Chris@0 653 $statements = $this->createTableSql($name, $table);
Chris@0 654 foreach ($statements as $statement) {
Chris@0 655 $this->connection->query($statement);
Chris@0 656 }
Chris@0 657 }
Chris@0 658
Chris@0 659 /**
Chris@0 660 * Return an array of field names from an array of key/index column specifiers.
Chris@0 661 *
Chris@0 662 * This is usually an identity function but if a key/index uses a column prefix
Chris@0 663 * specification, this function extracts just the name.
Chris@0 664 *
Chris@0 665 * @param $fields
Chris@0 666 * An array of key/index column specifiers.
Chris@0 667 *
Chris@0 668 * @return
Chris@0 669 * An array of field names.
Chris@0 670 */
Chris@0 671 public function fieldNames($fields) {
Chris@0 672 $return = [];
Chris@0 673 foreach ($fields as $field) {
Chris@0 674 if (is_array($field)) {
Chris@0 675 $return[] = $field[0];
Chris@0 676 }
Chris@0 677 else {
Chris@0 678 $return[] = $field;
Chris@0 679 }
Chris@0 680 }
Chris@0 681 return $return;
Chris@0 682 }
Chris@0 683
Chris@0 684 /**
Chris@0 685 * Prepare a table or column comment for database query.
Chris@0 686 *
Chris@0 687 * @param $comment
Chris@0 688 * The comment string to prepare.
Chris@0 689 * @param $length
Chris@0 690 * Optional upper limit on the returned string length.
Chris@0 691 *
Chris@0 692 * @return
Chris@0 693 * The prepared comment.
Chris@0 694 */
Chris@0 695 public function prepareComment($comment, $length = NULL) {
Chris@0 696 // Remove semicolons to avoid triggering multi-statement check.
Chris@0 697 $comment = strtr($comment, [';' => '.']);
Chris@0 698 return $this->connection->quote($comment);
Chris@0 699 }
Chris@0 700
Chris@0 701 /**
Chris@0 702 * Return an escaped version of its parameter to be used as a default value
Chris@0 703 * on a column.
Chris@0 704 *
Chris@0 705 * @param mixed $value
Chris@0 706 * The value to be escaped (int, float, null or string).
Chris@0 707 *
Chris@0 708 * @return string|int|float
Chris@0 709 * The escaped value.
Chris@0 710 */
Chris@0 711 protected function escapeDefaultValue($value) {
Chris@0 712 if (is_null($value)) {
Chris@0 713 return 'NULL';
Chris@0 714 }
Chris@0 715 return is_string($value) ? $this->connection->quote($value) : $value;
Chris@0 716 }
Chris@0 717
Chris@17 718 /**
Chris@17 719 * Ensures that all the primary key fields are correctly defined.
Chris@17 720 *
Chris@17 721 * @param array $primary_key
Chris@17 722 * An array containing the fields that will form the primary key of a table.
Chris@17 723 * @param array $fields
Chris@17 724 * An array containing the field specifications of the table, as per the
Chris@17 725 * schema data structure format.
Chris@17 726 *
Chris@17 727 * @throws \Drupal\Core\Database\SchemaException
Chris@17 728 * Thrown if any primary key field specification does not exist or if they
Chris@17 729 * do not define 'not null' as TRUE.
Chris@17 730 */
Chris@17 731 protected function ensureNotNullPrimaryKey(array $primary_key, array $fields) {
Chris@17 732 foreach (array_intersect($primary_key, array_keys($fields)) as $field_name) {
Chris@17 733 if (!isset($fields[$field_name]['not null']) || $fields[$field_name]['not null'] !== TRUE) {
Chris@17 734 throw new SchemaException("The '$field_name' field specification does not define 'not null' as TRUE.");
Chris@17 735 }
Chris@17 736 }
Chris@17 737 }
Chris@17 738
Chris@0 739 }