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 }
|