Version 1
[yaffs-website] / web / core / tests / Drupal / KernelTests / Core / Database / SchemaTest.php
1 <?php
2
3 namespace Drupal\KernelTests\Core\Database;
4
5 use Drupal\Core\Database\Database;
6 use Drupal\Core\Database\SchemaException;
7 use Drupal\Core\Database\SchemaObjectDoesNotExistException;
8 use Drupal\Core\Database\SchemaObjectExistsException;
9 use Drupal\KernelTests\KernelTestBase;
10 use Drupal\Component\Utility\Unicode;
11
12 /**
13  * Tests table creation and modification via the schema API.
14  *
15  * @group Database
16  */
17 class SchemaTest extends KernelTestBase {
18
19   /**
20    * A global counter for table and field creation.
21    */
22   protected $counter;
23
24   /**
25    * Tests database interactions.
26    */
27   public function testSchema() {
28     // Try creating a table.
29     $table_specification = [
30       'description' => 'Schema table description may contain "quotes" and could be long—very long indeed.',
31       'fields' => [
32         'id'  => [
33           'type' => 'int',
34           'default' => NULL,
35         ],
36         'test_field'  => [
37           'type' => 'int',
38           'not null' => TRUE,
39           'description' => 'Schema table description may contain "quotes" and could be long—very long indeed. There could be "multiple quoted regions".',
40         ],
41         'test_field_string'  => [
42           'type' => 'varchar',
43           'length' => 20,
44           'not null' => TRUE,
45           'default' => "'\"funky default'\"",
46           'description' => 'Schema column description for string.',
47         ],
48         'test_field_string_ascii'  => [
49           'type' => 'varchar_ascii',
50           'length' => 255,
51           'description' => 'Schema column description for ASCII string.',
52         ],
53       ],
54     ];
55     db_create_table('test_table', $table_specification);
56
57     // Assert that the table exists.
58     $this->assertTrue(db_table_exists('test_table'), 'The table exists.');
59
60     // Assert that the table comment has been set.
61     $this->checkSchemaComment($table_specification['description'], 'test_table');
62
63     // Assert that the column comment has been set.
64     $this->checkSchemaComment($table_specification['fields']['test_field']['description'], 'test_table', 'test_field');
65
66     if (Database::getConnection()->databaseType() == 'mysql') {
67       // Make sure that varchar fields have the correct collation.
68       $columns = db_query('SHOW FULL COLUMNS FROM {test_table}');
69       foreach ($columns as $column) {
70         if ($column->Field == 'test_field_string') {
71           $string_check = ($column->Collation == 'utf8mb4_general_ci');
72         }
73         if ($column->Field == 'test_field_string_ascii') {
74           $string_ascii_check = ($column->Collation == 'ascii_general_ci');
75         }
76       }
77       $this->assertTrue(!empty($string_check), 'string field has the right collation.');
78       $this->assertTrue(!empty($string_ascii_check), 'ASCII string field has the right collation.');
79     }
80
81     // An insert without a value for the column 'test_table' should fail.
82     $this->assertFalse($this->tryInsert(), 'Insert without a default failed.');
83
84     // Add a default value to the column.
85     db_field_set_default('test_table', 'test_field', 0);
86     // The insert should now succeed.
87     $this->assertTrue($this->tryInsert(), 'Insert with a default succeeded.');
88
89     // Remove the default.
90     db_field_set_no_default('test_table', 'test_field');
91     // The insert should fail again.
92     $this->assertFalse($this->tryInsert(), 'Insert without a default failed.');
93
94     // Test for fake index and test for the boolean result of indexExists().
95     $index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
96     $this->assertIdentical($index_exists, FALSE, 'Fake index does not exists');
97     // Add index.
98     db_add_index('test_table', 'test_field', ['test_field'], $table_specification);
99     // Test for created index and test for the boolean result of indexExists().
100     $index_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
101     $this->assertIdentical($index_exists, TRUE, 'Index created.');
102
103     // Rename the table.
104     db_rename_table('test_table', 'test_table2');
105
106     // Index should be renamed.
107     $index_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
108     $this->assertTrue($index_exists, 'Index was renamed.');
109
110     // We need the default so that we can insert after the rename.
111     db_field_set_default('test_table2', 'test_field', 0);
112     $this->assertFalse($this->tryInsert(), 'Insert into the old table failed.');
113     $this->assertTrue($this->tryInsert('test_table2'), 'Insert into the new table succeeded.');
114
115     // We should have successfully inserted exactly two rows.
116     $count = db_query('SELECT COUNT(*) FROM {test_table2}')->fetchField();
117     $this->assertEqual($count, 2, 'Two fields were successfully inserted.');
118
119     // Try to drop the table.
120     db_drop_table('test_table2');
121     $this->assertFalse(db_table_exists('test_table2'), 'The dropped table does not exist.');
122
123     // Recreate the table.
124     db_create_table('test_table', $table_specification);
125     db_field_set_default('test_table', 'test_field', 0);
126     db_add_field('test_table', 'test_serial', ['type' => 'int', 'not null' => TRUE, 'default' => 0, 'description' => 'Added column description.']);
127
128     // Assert that the column comment has been set.
129     $this->checkSchemaComment('Added column description.', 'test_table', 'test_serial');
130
131     // Change the new field to a serial column.
132     db_change_field('test_table', 'test_serial', 'test_serial', ['type' => 'serial', 'not null' => TRUE, 'description' => 'Changed column description.'], ['primary key' => ['test_serial']]);
133
134     // Assert that the column comment has been set.
135     $this->checkSchemaComment('Changed column description.', 'test_table', 'test_serial');
136
137     $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
138     $max1 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
139     $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
140     $max2 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
141     $this->assertTrue($max2 > $max1, 'The serial is monotone.');
142
143     $count = db_query('SELECT COUNT(*) FROM {test_table}')->fetchField();
144     $this->assertEqual($count, 2, 'There were two rows.');
145
146     // Test adding a serial field to an existing table.
147     db_drop_table('test_table');
148     db_create_table('test_table', $table_specification);
149     db_field_set_default('test_table', 'test_field', 0);
150     db_add_field('test_table', 'test_serial', ['type' => 'serial', 'not null' => TRUE], ['primary key' => ['test_serial']]);
151
152     $this->assertPrimaryKeyColumns('test_table', ['test_serial']);
153
154     $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
155     $max1 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
156     $this->assertTrue($this->tryInsert(), 'Insert with a serial succeeded.');
157     $max2 = db_query('SELECT MAX(test_serial) FROM {test_table}')->fetchField();
158     $this->assertTrue($max2 > $max1, 'The serial is monotone.');
159
160     $count = db_query('SELECT COUNT(*) FROM {test_table}')->fetchField();
161     $this->assertEqual($count, 2, 'There were two rows.');
162
163     // Test adding a new column and form a composite primary key with it.
164     db_add_field('test_table', 'test_composite_primary_key', ['type' => 'int', 'not null' => TRUE, 'default' => 0], ['primary key' => ['test_serial', 'test_composite_primary_key']]);
165
166     $this->assertPrimaryKeyColumns('test_table', ['test_serial', 'test_composite_primary_key']);
167
168     // Test renaming of keys and constraints.
169     db_drop_table('test_table');
170     $table_specification = [
171       'fields' => [
172         'id'  => [
173           'type' => 'serial',
174           'not null' => TRUE,
175         ],
176         'test_field'  => [
177           'type' => 'int',
178           'default' => 0,
179         ],
180       ],
181       'primary key' => ['id'],
182       'unique keys' => [
183         'test_field' => ['test_field'],
184       ],
185     ];
186     db_create_table('test_table', $table_specification);
187
188     // Tests for indexes are Database specific.
189     $db_type = Database::getConnection()->databaseType();
190
191     // Test for existing primary and unique keys.
192     switch ($db_type) {
193       case 'pgsql':
194         $primary_key_exists = Database::getConnection()->schema()->constraintExists('test_table', '__pkey');
195         $unique_key_exists = Database::getConnection()->schema()->constraintExists('test_table', 'test_field' . '__key');
196         break;
197       case 'sqlite':
198         // SQLite does not create a standalone index for primary keys.
199         $primary_key_exists = TRUE;
200         $unique_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
201         break;
202       default:
203         $primary_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'PRIMARY');
204         $unique_key_exists = Database::getConnection()->schema()->indexExists('test_table', 'test_field');
205         break;
206     }
207     $this->assertIdentical($primary_key_exists, TRUE, 'Primary key created.');
208     $this->assertIdentical($unique_key_exists, TRUE, 'Unique key created.');
209
210     db_rename_table('test_table', 'test_table2');
211
212     // Test for renamed primary and unique keys.
213     switch ($db_type) {
214       case 'pgsql':
215         $renamed_primary_key_exists = Database::getConnection()->schema()->constraintExists('test_table2', '__pkey');
216         $renamed_unique_key_exists = Database::getConnection()->schema()->constraintExists('test_table2', 'test_field' . '__key');
217         break;
218       case 'sqlite':
219         // SQLite does not create a standalone index for primary keys.
220         $renamed_primary_key_exists = TRUE;
221         $renamed_unique_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
222         break;
223       default:
224         $renamed_primary_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'PRIMARY');
225         $renamed_unique_key_exists = Database::getConnection()->schema()->indexExists('test_table2', 'test_field');
226         break;
227     }
228     $this->assertIdentical($renamed_primary_key_exists, TRUE, 'Primary key was renamed.');
229     $this->assertIdentical($renamed_unique_key_exists, TRUE, 'Unique key was renamed.');
230
231     // For PostgreSQL check in addition that sequence was renamed.
232     if ($db_type == 'pgsql') {
233       // Get information about new table.
234       $info = Database::getConnection()->schema()->queryTableInformation('test_table2');
235       $sequence_name = Database::getConnection()->schema()->prefixNonTable('test_table2', 'id', 'seq');
236       $this->assertEqual($sequence_name, current($info->sequences), 'Sequence was renamed.');
237     }
238
239     // Use database specific data type and ensure that table is created.
240     $table_specification = [
241       'description' => 'Schema table description.',
242       'fields' => [
243         'timestamp'  => [
244           'mysql_type' => 'timestamp',
245           'pgsql_type' => 'timestamp',
246           'sqlite_type' => 'datetime',
247           'not null' => FALSE,
248           'default' => NULL,
249         ],
250       ],
251     ];
252     try {
253       db_create_table('test_timestamp', $table_specification);
254     }
255     catch (\Exception $e) {
256     }
257     $this->assertTrue(db_table_exists('test_timestamp'), 'Table with database specific datatype was created.');
258   }
259
260   /**
261    * Tests that indexes on string fields are limited to 191 characters on MySQL.
262    *
263    * @see \Drupal\Core\Database\Driver\mysql\Schema::getNormalizedIndexes()
264    */
265   public function testIndexLength() {
266     if (Database::getConnection()->databaseType() != 'mysql') {
267       return;
268     }
269     $table_specification = [
270       'fields' => [
271         'id'  => [
272           'type' => 'int',
273           'default' => NULL,
274         ],
275         'test_field_text'  => [
276           'type' => 'text',
277           'not null' => TRUE,
278         ],
279         'test_field_string_long'  => [
280           'type' => 'varchar',
281           'length' => 255,
282           'not null' => TRUE,
283         ],
284         'test_field_string_ascii_long'  => [
285           'type' => 'varchar_ascii',
286           'length' => 255,
287         ],
288         'test_field_string_short'  => [
289           'type' => 'varchar',
290           'length' => 128,
291           'not null' => TRUE,
292         ],
293       ],
294       'indexes' => [
295         'test_regular' => [
296           'test_field_text',
297           'test_field_string_long',
298           'test_field_string_ascii_long',
299           'test_field_string_short',
300         ],
301         'test_length' => [
302           ['test_field_text', 128],
303           ['test_field_string_long', 128],
304           ['test_field_string_ascii_long', 128],
305           ['test_field_string_short', 128],
306         ],
307         'test_mixed' => [
308           ['test_field_text', 200],
309           'test_field_string_long',
310           ['test_field_string_ascii_long', 200],
311           'test_field_string_short',
312         ],
313       ],
314     ];
315     db_create_table('test_table_index_length', $table_specification);
316
317     $schema_object = Database::getConnection()->schema();
318
319     // Ensure expected exception thrown when adding index with missing info.
320     $expected_exception_message = "MySQL needs the 'test_field_text' field specification in order to normalize the 'test_regular' index";
321     $missing_field_spec = $table_specification;
322     unset($missing_field_spec['fields']['test_field_text']);
323     try {
324       $schema_object->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $missing_field_spec);
325       $this->fail('SchemaException not thrown when adding index with missing information.');
326     }
327     catch (SchemaException $e) {
328       $this->assertEqual($expected_exception_message, $e->getMessage());
329     }
330
331     // Add a separate index.
332     $schema_object->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $table_specification);
333     $table_specification_with_new_index = $table_specification;
334     $table_specification_with_new_index['indexes']['test_separate'] = [['test_field_text', 200]];
335
336     // Ensure that the exceptions of addIndex are thrown as expected.
337
338     try {
339       $schema_object->addIndex('test_table_index_length', 'test_separate', [['test_field_text', 200]], $table_specification);
340       $this->fail('\Drupal\Core\Database\SchemaObjectExistsException exception missed.');
341     }
342     catch (SchemaObjectExistsException $e) {
343       $this->pass('\Drupal\Core\Database\SchemaObjectExistsException thrown when index already exists.');
344     }
345
346     try {
347       $schema_object->addIndex('test_table_non_existing', 'test_separate', [['test_field_text', 200]], $table_specification);
348       $this->fail('\Drupal\Core\Database\SchemaObjectDoesNotExistException exception missed.');
349     }
350     catch (SchemaObjectDoesNotExistException $e) {
351       $this->pass('\Drupal\Core\Database\SchemaObjectDoesNotExistException thrown when index already exists.');
352     }
353
354     // Get index information.
355     $results = db_query('SHOW INDEX FROM {test_table_index_length}');
356     $expected_lengths = [
357       'test_regular' => [
358         'test_field_text' => 191,
359         'test_field_string_long' => 191,
360         'test_field_string_ascii_long' => NULL,
361         'test_field_string_short' => NULL,
362       ],
363       'test_length' => [
364         'test_field_text' => 128,
365         'test_field_string_long' => 128,
366         'test_field_string_ascii_long' => 128,
367         'test_field_string_short' => NULL,
368       ],
369       'test_mixed' => [
370         'test_field_text' => 191,
371         'test_field_string_long' => 191,
372         'test_field_string_ascii_long' => 200,
373         'test_field_string_short' => NULL,
374       ],
375       'test_separate' => [
376         'test_field_text' => 191,
377       ],
378     ];
379
380     // Count the number of columns defined in the indexes.
381     $column_count = 0;
382     foreach ($table_specification_with_new_index['indexes'] as $index) {
383       foreach ($index as $field) {
384         $column_count++;
385       }
386     }
387     $test_count = 0;
388     foreach ($results as $result) {
389       $this->assertEqual($result->Sub_part, $expected_lengths[$result->Key_name][$result->Column_name], 'Index length matches expected value.');
390       $test_count++;
391     }
392     $this->assertEqual($test_count, $column_count, 'Number of tests matches expected value.');
393   }
394
395   /**
396    * Tests inserting data into an existing table.
397    *
398    * @param $table
399    *   The database table to insert data into.
400    *
401    * @return
402    *   TRUE if the insert succeeded, FALSE otherwise.
403    */
404   public function tryInsert($table = 'test_table') {
405     try {
406       db_insert($table)
407         ->fields(['id' => mt_rand(10, 20)])
408         ->execute();
409       return TRUE;
410     }
411     catch (\Exception $e) {
412       return FALSE;
413     }
414   }
415
416   /**
417    * Checks that a table or column comment matches a given description.
418    *
419    * @param $description
420    *   The asserted description.
421    * @param $table
422    *   The table to test.
423    * @param $column
424    *   Optional column to test.
425    */
426   public function checkSchemaComment($description, $table, $column = NULL) {
427     if (method_exists(Database::getConnection()->schema(), 'getComment')) {
428       $comment = Database::getConnection()->schema()->getComment($table, $column);
429       // The schema comment truncation for mysql is different.
430       if (Database::getConnection()->databaseType() == 'mysql') {
431         $max_length = $column ? 255 : 60;
432         $description = Unicode::truncate($description, $max_length, TRUE, TRUE);
433       }
434       $this->assertEqual($comment, $description, 'The comment matches the schema description.');
435     }
436   }
437
438   /**
439    * Tests creating unsigned columns and data integrity thereof.
440    */
441   public function testUnsignedColumns() {
442     // First create the table with just a serial column.
443     $table_name = 'unsigned_table';
444     $table_spec = [
445       'fields' => ['serial_column' => ['type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE]],
446       'primary key' => ['serial_column'],
447     ];
448     db_create_table($table_name, $table_spec);
449
450     // Now set up columns for the other types.
451     $types = ['int', 'float', 'numeric'];
452     foreach ($types as $type) {
453       $column_spec = ['type' => $type, 'unsigned' => TRUE];
454       if ($type == 'numeric') {
455         $column_spec += ['precision' => 10, 'scale' => 0];
456       }
457       $column_name = $type . '_column';
458       $table_spec['fields'][$column_name] = $column_spec;
459       db_add_field($table_name, $column_name, $column_spec);
460     }
461
462     // Finally, check each column and try to insert invalid values into them.
463     foreach ($table_spec['fields'] as $column_name => $column_spec) {
464       $this->assertTrue(db_field_exists($table_name, $column_name), format_string('Unsigned @type column was created.', ['@type' => $column_spec['type']]));
465       $this->assertFalse($this->tryUnsignedInsert($table_name, $column_name), format_string('Unsigned @type column rejected a negative value.', ['@type' => $column_spec['type']]));
466     }
467   }
468
469   /**
470    * Tries to insert a negative value into columns defined as unsigned.
471    *
472    * @param $table_name
473    *   The table to insert.
474    * @param $column_name
475    *   The column to insert.
476    *
477    * @return
478    *   TRUE if the insert succeeded, FALSE otherwise.
479    */
480   public function tryUnsignedInsert($table_name, $column_name) {
481     try {
482       db_insert($table_name)
483         ->fields([$column_name => -1])
484         ->execute();
485       return TRUE;
486     }
487     catch (\Exception $e) {
488       return FALSE;
489     }
490   }
491
492   /**
493    * Tests adding columns to an existing table.
494    */
495   public function testSchemaAddField() {
496     // Test varchar types.
497     foreach ([1, 32, 128, 256, 512] as $length) {
498       $base_field_spec = [
499         'type' => 'varchar',
500         'length' => $length,
501       ];
502       $variations = [
503         ['not null' => FALSE],
504         ['not null' => FALSE, 'default' => '7'],
505         ['not null' => FALSE, 'default' => substr('"thing"', 0, $length)],
506         ['not null' => FALSE, 'default' => substr("\"'hing", 0, $length)],
507         ['not null' => TRUE, 'initial' => 'd'],
508         ['not null' => FALSE, 'default' => NULL],
509         ['not null' => TRUE, 'initial' => 'd', 'default' => '7'],
510       ];
511
512       foreach ($variations as $variation) {
513         $field_spec = $variation + $base_field_spec;
514         $this->assertFieldAdditionRemoval($field_spec);
515       }
516     }
517
518     // Test int and float types.
519     foreach (['int', 'float'] as $type) {
520       foreach (['tiny', 'small', 'medium', 'normal', 'big'] as $size) {
521         $base_field_spec = [
522           'type' => $type,
523           'size' => $size,
524         ];
525         $variations = [
526           ['not null' => FALSE],
527           ['not null' => FALSE, 'default' => 7],
528           ['not null' => TRUE, 'initial' => 1],
529           ['not null' => TRUE, 'initial' => 1, 'default' => 7],
530           ['not null' => TRUE, 'initial_from_field' => 'serial_column'],
531         ];
532
533         foreach ($variations as $variation) {
534           $field_spec = $variation + $base_field_spec;
535           $this->assertFieldAdditionRemoval($field_spec);
536         }
537       }
538     }
539
540     // Test numeric types.
541     foreach ([1, 5, 10, 40, 65] as $precision) {
542       foreach ([0, 2, 10, 30] as $scale) {
543         // Skip combinations where precision is smaller than scale.
544         if ($precision <= $scale) {
545           continue;
546         }
547
548         $base_field_spec = [
549           'type' => 'numeric',
550           'scale' => $scale,
551           'precision' => $precision,
552         ];
553         $variations = [
554           ['not null' => FALSE],
555           ['not null' => FALSE, 'default' => 7],
556           ['not null' => TRUE, 'initial' => 1],
557           ['not null' => TRUE, 'initial' => 1, 'default' => 7],
558           ['not null' => TRUE, 'initial_from_field' => 'serial_column'],
559         ];
560
561         foreach ($variations as $variation) {
562           $field_spec = $variation + $base_field_spec;
563           $this->assertFieldAdditionRemoval($field_spec);
564         }
565       }
566     }
567   }
568
569   /**
570    * Asserts that a given field can be added and removed from a table.
571    *
572    * The addition test covers both defining a field of a given specification
573    * when initially creating at table and extending an existing table.
574    *
575    * @param $field_spec
576    *   The schema specification of the field.
577    */
578   protected function assertFieldAdditionRemoval($field_spec) {
579     // Try creating the field on a new table.
580     $table_name = 'test_table_' . ($this->counter++);
581     $table_spec = [
582       'fields' => [
583         'serial_column' => ['type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE],
584         'test_field' => $field_spec,
585       ],
586       'primary key' => ['serial_column'],
587     ];
588     db_create_table($table_name, $table_spec);
589     $this->pass(format_string('Table %table created.', ['%table' => $table_name]));
590
591     // Check the characteristics of the field.
592     $this->assertFieldCharacteristics($table_name, 'test_field', $field_spec);
593
594     // Clean-up.
595     db_drop_table($table_name);
596
597     // Try adding a field to an existing table.
598     $table_name = 'test_table_' . ($this->counter++);
599     $table_spec = [
600       'fields' => [
601         'serial_column' => ['type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE],
602       ],
603       'primary key' => ['serial_column'],
604     ];
605     db_create_table($table_name, $table_spec);
606     $this->pass(format_string('Table %table created.', ['%table' => $table_name]));
607
608     // Insert some rows to the table to test the handling of initial values.
609     for ($i = 0; $i < 3; $i++) {
610       db_insert($table_name)
611         ->useDefaults(['serial_column'])
612         ->execute();
613     }
614
615     db_add_field($table_name, 'test_field', $field_spec);
616     $this->pass(format_string('Column %column created.', ['%column' => 'test_field']));
617
618     // Check the characteristics of the field.
619     $this->assertFieldCharacteristics($table_name, 'test_field', $field_spec);
620
621     // Clean-up.
622     db_drop_field($table_name, 'test_field');
623
624     // Add back the field and then try to delete a field which is also a primary
625     // key.
626     db_add_field($table_name, 'test_field', $field_spec);
627     db_drop_field($table_name, 'serial_column');
628     db_drop_table($table_name);
629   }
630
631   /**
632    * Asserts that a newly added field has the correct characteristics.
633    */
634   protected function assertFieldCharacteristics($table_name, $field_name, $field_spec) {
635     // Check that the initial value has been registered.
636     if (isset($field_spec['initial'])) {
637       // There should be no row with a value different then $field_spec['initial'].
638       $count = db_select($table_name)
639         ->fields($table_name, ['serial_column'])
640         ->condition($field_name, $field_spec['initial'], '<>')
641         ->countQuery()
642         ->execute()
643         ->fetchField();
644       $this->assertEqual($count, 0, 'Initial values filled out.');
645     }
646
647     // Check that the initial value from another field has been registered.
648     if (isset($field_spec['initial_from_field'])) {
649       // There should be no row with a value different than
650       // $field_spec['initial_from_field'].
651       $count = db_select($table_name)
652         ->fields($table_name, ['serial_column'])
653         ->where($table_name . '.' . $field_spec['initial_from_field'] . ' <> ' . $table_name . '.' . $field_name)
654         ->countQuery()
655         ->execute()
656         ->fetchField();
657       $this->assertEqual($count, 0, 'Initial values from another field filled out.');
658     }
659
660     // Check that the default value has been registered.
661     if (isset($field_spec['default'])) {
662       // Try inserting a row, and check the resulting value of the new column.
663       $id = db_insert($table_name)
664         ->useDefaults(['serial_column'])
665         ->execute();
666       $field_value = db_select($table_name)
667         ->fields($table_name, [$field_name])
668         ->condition('serial_column', $id)
669         ->execute()
670         ->fetchField();
671       $this->assertEqual($field_value, $field_spec['default'], 'Default value registered.');
672     }
673   }
674
675   /**
676    * Tests changing columns between types.
677    */
678   public function testSchemaChangeField() {
679     $field_specs = [
680       ['type' => 'int', 'size' => 'normal', 'not null' => FALSE],
681       ['type' => 'int', 'size' => 'normal', 'not null' => TRUE, 'initial' => 1, 'default' => 17],
682       ['type' => 'float', 'size' => 'normal', 'not null' => FALSE],
683       ['type' => 'float', 'size' => 'normal', 'not null' => TRUE, 'initial' => 1, 'default' => 7.3],
684       ['type' => 'numeric', 'scale' => 2, 'precision' => 10, 'not null' => FALSE],
685       ['type' => 'numeric', 'scale' => 2, 'precision' => 10, 'not null' => TRUE, 'initial' => 1, 'default' => 7],
686     ];
687
688     foreach ($field_specs as $i => $old_spec) {
689       foreach ($field_specs as $j => $new_spec) {
690         if ($i === $j) {
691           // Do not change a field into itself.
692           continue;
693         }
694         $this->assertFieldChange($old_spec, $new_spec);
695       }
696     }
697
698     $field_specs = [
699       ['type' => 'varchar_ascii', 'length' => '255'],
700       ['type' => 'varchar', 'length' => '255'],
701       ['type' => 'text'],
702       ['type' => 'blob', 'size' => 'big'],
703     ];
704
705     foreach ($field_specs as $i => $old_spec) {
706       foreach ($field_specs as $j => $new_spec) {
707         if ($i === $j) {
708           // Do not change a field into itself.
709           continue;
710         }
711         // Note if the serialized data contained an object this would fail on
712         // Postgres.
713         // @see https://www.drupal.org/node/1031122
714         $this->assertFieldChange($old_spec, $new_spec, serialize(['string' => "This \n has \\\\ some backslash \"*string action.\\n"]));
715       }
716     }
717
718   }
719
720   /**
721    * Asserts that a field can be changed from one spec to another.
722    *
723    * @param $old_spec
724    *   The beginning field specification.
725    * @param $new_spec
726    *   The ending field specification.
727    */
728   protected function assertFieldChange($old_spec, $new_spec, $test_data = NULL) {
729     $table_name = 'test_table_' . ($this->counter++);
730     $table_spec = [
731       'fields' => [
732         'serial_column' => ['type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE],
733         'test_field' => $old_spec,
734       ],
735       'primary key' => ['serial_column'],
736     ];
737     db_create_table($table_name, $table_spec);
738     $this->pass(format_string('Table %table created.', ['%table' => $table_name]));
739
740     // Check the characteristics of the field.
741     $this->assertFieldCharacteristics($table_name, 'test_field', $old_spec);
742
743     // Remove inserted rows.
744     db_truncate($table_name)->execute();
745
746     if ($test_data) {
747       $id = db_insert($table_name)
748         ->fields(['test_field'], [$test_data])
749         ->execute();
750     }
751
752     // Change the field.
753     db_change_field($table_name, 'test_field', 'test_field', $new_spec);
754
755     if ($test_data) {
756       $field_value = db_select($table_name)
757         ->fields($table_name, ['test_field'])
758         ->condition('serial_column', $id)
759         ->execute()
760         ->fetchField();
761       $this->assertIdentical($field_value, $test_data);
762     }
763
764     // Check the field was changed.
765     $this->assertFieldCharacteristics($table_name, 'test_field', $new_spec);
766
767     // Clean-up.
768     db_drop_table($table_name);
769   }
770
771   /**
772    * Tests the findTables() method.
773    */
774   public function testFindTables() {
775     // We will be testing with three tables, two of them using the default
776     // prefix and the third one with an individually specified prefix.
777
778     // Set up a new connection with different connection info.
779     $connection_info = Database::getConnectionInfo();
780
781     // Add per-table prefix to the second table.
782     $new_connection_info = $connection_info['default'];
783     $new_connection_info['prefix']['test_2_table'] = $new_connection_info['prefix']['default'] . '_shared_';
784     Database::addConnectionInfo('test', 'default', $new_connection_info);
785
786     Database::setActiveConnection('test');
787
788     // Create the tables.
789     $table_specification = [
790       'description' => 'Test table.',
791       'fields' => [
792         'id'  => [
793           'type' => 'int',
794           'default' => NULL,
795         ],
796       ],
797     ];
798     Database::getConnection()->schema()->createTable('test_1_table', $table_specification);
799     Database::getConnection()->schema()->createTable('test_2_table', $table_specification);
800     Database::getConnection()->schema()->createTable('the_third_table', $table_specification);
801
802     // Check the "all tables" syntax.
803     $tables = Database::getConnection()->schema()->findTables('%');
804     sort($tables);
805     $expected = [
806       // The 'config' table is added by
807       // \Drupal\KernelTests\KernelTestBase::containerBuild().
808       'config',
809       'test_1_table',
810       // This table uses a per-table prefix, yet it is returned as un-prefixed.
811       'test_2_table',
812       'the_third_table',
813     ];
814     $this->assertEqual($tables, $expected, 'All tables were found.');
815
816     // Check the restrictive syntax.
817     $tables = Database::getConnection()->schema()->findTables('test_%');
818     sort($tables);
819     $expected = [
820       'test_1_table',
821       'test_2_table',
822     ];
823     $this->assertEqual($tables, $expected, 'Two tables were found.');
824
825     // Go back to the initial connection.
826     Database::setActiveConnection('default');
827   }
828
829   /**
830    * Tests the primary keys of a table.
831    *
832    * @param string $table_name
833    *   The name of the table to check.
834    * @param array $primary_key
835    *   The expected key column specifier for a table's primary key.
836    */
837   protected function assertPrimaryKeyColumns($table_name, array $primary_key = []) {
838     $db_type = Database::getConnection()->databaseType();
839
840     switch ($db_type) {
841       case 'mysql':
842         $result = Database::getConnection()->query("SHOW KEYS FROM {" . $table_name . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
843         $this->assertSame($primary_key, array_keys($result));
844
845         break;
846       case 'pgsql':
847         $result = Database::getConnection()->query("SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
848           FROM pg_index i
849           JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
850           WHERE i.indrelid = '{" . $table_name . "}'::regclass AND i.indisprimary")
851           ->fetchAllAssoc('attname');
852         $this->assertSame($primary_key, array_keys($result));
853
854         break;
855       case 'sqlite':
856         // For SQLite we need access to the protected
857         // \Drupal\Core\Database\Driver\sqlite\Schema::introspectSchema() method
858         // because we have no other way of getting the table prefixes needed for
859         // running a straight PRAGMA query.
860         $schema_object = Database::getConnection()->schema();
861         $reflection = new \ReflectionMethod($schema_object, 'introspectSchema');
862         $reflection->setAccessible(TRUE);
863
864         $table_info = $reflection->invoke($schema_object, $table_name);
865         $this->assertSame($primary_key, $table_info['primary key']);
866
867         break;
868     }
869   }
870
871 }