3 namespace Drupal\KernelTests\Core\Entity;
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Database\Database;
7 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
8 use Drupal\field\Entity\FieldConfig;
9 use Drupal\field\Entity\FieldStorageConfig;
12 * Tests Field SQL Storage .
14 * Field_sql_storage.module implements the default back-end storage plugin
15 * for the Field Storage API.
19 class FieldSqlStorageTest extends EntityKernelTestBase {
26 public static $modules = ['field', 'field_test', 'text', 'entity_test'];
29 * The name of the created field.
38 protected $fieldCardinality;
41 * A field storage to use in this class.
43 * @var \Drupal\field\Entity\FieldStorageConfig
45 protected $fieldStorage;
48 * A field to use in this test class.
50 * @var \Drupal\field\Entity\FieldConfig
55 * Name of the data table of the field.
62 * Name of the revision table of the field.
66 protected $revisionTable;
69 * The table mapping for the tested entity type.
71 * @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping
73 protected $tableMapping;
75 protected function setUp() {
78 $this->installEntitySchema('entity_test_rev');
79 $entity_type = 'entity_test_rev';
81 $this->fieldName = strtolower($this->randomMachineName());
82 $this->fieldCardinality = 4;
83 $this->fieldStorage = FieldStorageConfig::create([
84 'field_name' => $this->fieldName,
85 'entity_type' => $entity_type,
86 'type' => 'test_field',
87 'cardinality' => $this->fieldCardinality,
89 $this->fieldStorage->save();
90 $this->field = FieldConfig::create([
91 'field_storage' => $this->fieldStorage,
92 'bundle' => $entity_type
96 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
97 $table_mapping = \Drupal::entityManager()->getStorage($entity_type)->getTableMapping();
98 $this->tableMapping = $table_mapping;
99 $this->table = $table_mapping->getDedicatedDataTableName($this->fieldStorage);
100 $this->revisionTable = $table_mapping->getDedicatedRevisionTableName($this->fieldStorage);
104 * Tests field loading works correctly by inserting directly in the tables.
106 public function testFieldLoad() {
107 $entity_type = $bundle = 'entity_test_rev';
108 $storage = $this->container->get('entity.manager')->getStorage($entity_type);
110 $columns = ['bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'langcode', $this->tableMapping->getFieldColumnName($this->fieldStorage, 'value')];
112 // Create an entity with four revisions.
114 $entity = $this->container->get('entity_type.manager')
115 ->getStorage($entity_type)
118 $revision_ids[] = $entity->getRevisionId();
119 for ($i = 0; $i < 4; $i++) {
120 $entity->setNewRevision();
122 $revision_ids[] = $entity->getRevisionId();
125 // Generate values and insert them directly in the storage tables.
127 $query = db_insert($this->revisionTable)->fields($columns);
128 foreach ($revision_ids as $revision_id) {
129 // Put one value too many.
130 for ($delta = 0; $delta <= $this->fieldCardinality; $delta++) {
131 $value = mt_rand(1, 127);
132 $values[$revision_id][] = $value;
133 $query->values([$bundle, 0, $entity->id(), $revision_id, $delta, $entity->language()->getId(), $value]);
137 $query = db_insert($this->table)->fields($columns);
138 foreach ($values[$revision_id] as $delta => $value) {
139 $query->values([$bundle, 0, $entity->id(), $revision_id, $delta, $entity->language()->getId(), $value]);
143 // Load every revision and check the values.
144 foreach ($revision_ids as $revision_id) {
145 $entity = $storage->loadRevision($revision_id);
146 foreach ($values[$revision_id] as $delta => $value) {
147 if ($delta < $this->fieldCardinality) {
148 $this->assertEqual($entity->{$this->fieldName}[$delta]->value, $value);
151 $this->assertFalse(array_key_exists($delta, $entity->{$this->fieldName}));
156 // Load the "current revision" and check the values.
157 $entity = $storage->load($entity->id());
158 foreach ($values[$revision_id] as $delta => $value) {
159 if ($delta < $this->fieldCardinality) {
160 $this->assertEqual($entity->{$this->fieldName}[$delta]->value, $value);
163 $this->assertFalse(array_key_exists($delta, $entity->{$this->fieldName}));
167 // Add a translation in an unavailable language code and verify it is not
169 $unavailable_langcode = 'xx';
170 $values = [$bundle, 0, $entity->id(), $entity->getRevisionId(), 0, $unavailable_langcode, mt_rand(1, 127)];
171 db_insert($this->table)->fields($columns)->values($values)->execute();
172 db_insert($this->revisionTable)->fields($columns)->values($values)->execute();
173 $entity = $storage->load($entity->id());
174 $this->assertFalse(array_key_exists($unavailable_langcode, $entity->{$this->fieldName}));
178 * Tests field saving works correctly by reading directly from the tables.
180 public function testFieldWrite() {
181 $entity_type = $bundle = 'entity_test_rev';
182 $entity = $this->container->get('entity_type.manager')
183 ->getStorage($entity_type)
186 $revision_values = [];
188 // Check insert. Add one value too many.
190 for ($delta = 0; $delta <= $this->fieldCardinality; $delta++) {
191 $values[$delta]['value'] = mt_rand(1, 127);
193 $entity->{$this->fieldName} = $values;
196 // Read the tables and check the correct values have been stored.
197 $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
198 $this->assertEqual(count($rows), $this->fieldCardinality);
199 foreach ($rows as $delta => $row) {
203 'entity_id' => $entity->id(),
204 'revision_id' => $entity->getRevisionId(),
205 'langcode' => $entity->language()->getId(),
207 $this->fieldName . '_value' => $values[$delta]['value'],
209 $this->assertEqual($row, $expected, "Row $delta was stored as expected.");
212 // Test update. Add less values and check that the previous values did not
215 for ($delta = 0; $delta <= $this->fieldCardinality - 2; $delta++) {
216 $values[$delta]['value'] = mt_rand(1, 127);
218 $entity->{$this->fieldName} = $values;
220 $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
221 $this->assertEqual(count($rows), count($values));
222 foreach ($rows as $delta => $row) {
226 'entity_id' => $entity->id(),
227 'revision_id' => $entity->getRevisionId(),
228 'langcode' => $entity->language()->getId(),
230 $this->fieldName . '_value' => $values[$delta]['value'],
232 $this->assertEqual($row, $expected, "Row $delta was stored as expected.");
235 // Create a new revision.
236 $revision_values[$entity->getRevisionId()] = $values;
238 for ($delta = 0; $delta < $this->fieldCardinality; $delta++) {
239 $values[$delta]['value'] = mt_rand(1, 127);
241 $entity->{$this->fieldName} = $values;
242 $entity->setNewRevision();
244 $revision_values[$entity->getRevisionId()] = $values;
246 // Check that data for both revisions are in the revision table.
247 foreach ($revision_values as $revision_id => $values) {
248 $rows = db_select($this->revisionTable, 't')->fields('t')->condition('revision_id', $revision_id)->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
249 $this->assertEqual(count($rows), min(count($values), $this->fieldCardinality));
250 foreach ($rows as $delta => $row) {
254 'entity_id' => $entity->id(),
255 'revision_id' => $revision_id,
256 'langcode' => $entity->language()->getId(),
258 $this->fieldName . '_value' => $values[$delta]['value'],
260 $this->assertEqual($row, $expected, "Row $delta was stored as expected.");
264 // Test emptying the field.
265 $entity->{$this->fieldName} = NULL;
267 $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
268 $this->assertEqual(count($rows), 0);
272 * Tests that long entity type and field names do not break.
274 public function testLongNames() {
275 // Use one of the longest entity_type names in core.
276 $entity_type = $bundle = 'entity_test_label_callback';
277 $this->installEntitySchema('entity_test_label_callback');
278 $storage = $this->container->get('entity.manager')->getStorage($entity_type);
280 // Create two fields and generate random values.
281 $name_base = Unicode::strtolower($this->randomMachineName(FieldStorageConfig::NAME_MAX_LENGTH - 1));
284 for ($i = 0; $i < 2; $i++) {
285 $field_names[$i] = $name_base . $i;
286 FieldStorageConfig::create([
287 'field_name' => $field_names[$i],
288 'entity_type' => $entity_type,
289 'type' => 'test_field',
291 FieldConfig::create([
292 'field_name' => $field_names[$i],
293 'entity_type' => $entity_type,
296 $values[$field_names[$i]] = mt_rand(1, 127);
299 // Save an entity with values.
300 $entity = $this->container->get('entity_type.manager')
301 ->getStorage($entity_type)
305 // Load the entity back and check the values.
306 $entity = $storage->load($entity->id());
307 foreach ($field_names as $field_name) {
308 $this->assertEqual($entity->get($field_name)->value, $values[$field_name]);
313 * Test trying to update a field with data.
315 public function testUpdateFieldSchemaWithData() {
316 $entity_type = 'entity_test_rev';
317 // Create a decimal 5.2 field and add some data.
318 $field_storage = FieldStorageConfig::create([
319 'field_name' => 'decimal52',
320 'entity_type' => $entity_type,
322 'settings' => ['precision' => 5, 'scale' => 2],
324 $field_storage->save();
325 $field = FieldConfig::create([
326 'field_storage' => $field_storage,
327 'bundle' => $entity_type,
330 $entity = $this->container->get('entity_type.manager')
331 ->getStorage($entity_type)
336 $entity->decimal52->value = '1.235';
339 // Attempt to update the field in a way that would work without data.
340 $field_storage->setSetting('scale', 3);
342 $field_storage->save();
343 $this->fail(t('Cannot update field schema with data.'));
345 catch (FieldStorageDefinitionUpdateForbiddenException $e) {
346 $this->pass(t('Cannot update field schema with data.'));
351 * Test that failure to create fields is handled gracefully.
353 public function testFieldUpdateFailure() {
354 // Create a text field.
355 $field_storage = FieldStorageConfig::create([
356 'field_name' => 'test_text',
357 'entity_type' => 'entity_test_rev',
359 'settings' => ['max_length' => 255],
361 $field_storage->save();
363 // Attempt to update the field in a way that would break the storage. The
364 // parenthesis suffix is needed because SQLite has *very* relaxed rules for
365 // data types, so we actually need to provide an invalid SQL syntax in order
367 // @see https://www.sqlite.org/datatype3.html
368 $prior_field_storage = $field_storage;
369 $field_storage->setSetting('max_length', '-1)');
371 $field_storage->save();
372 $this->fail(t('Update succeeded.'));
374 catch (\Exception $e) {
375 $this->pass(t('Update properly failed.'));
378 // Ensure that the field tables are still there.
380 $this->tableMapping->getDedicatedDataTableName($prior_field_storage),
381 $this->tableMapping->getDedicatedRevisionTableName($prior_field_storage),
383 foreach ($tables as $table_name) {
384 $this->assertTrue(db_table_exists($table_name), t('Table %table exists.', ['%table' => $table_name]));
389 * Test adding and removing indexes while data is present.
391 public function testFieldUpdateIndexesWithData() {
392 // Create a decimal field.
393 $field_name = 'testfield';
394 $entity_type = 'entity_test_rev';
395 $field_storage = FieldStorageConfig::create([
396 'field_name' => $field_name,
397 'entity_type' => $entity_type,
400 $field_storage->save();
401 $field = FieldConfig::create([
402 'field_storage' => $field_storage,
403 'bundle' => $entity_type,
406 $tables = [$this->tableMapping->getDedicatedDataTableName($field_storage), $this->tableMapping->getDedicatedRevisionTableName($field_storage)];
408 // Verify the indexes we will create do not exist yet.
409 foreach ($tables as $table) {
410 $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value'), t("No index named value exists in @table", ['@table' => $table]));
411 $this->assertFalse(Database::getConnection()->schema()->indexExists($table, 'value_format'), t("No index named value_format exists in @table", ['@table' => $table]));
414 // Add data so the table cannot be dropped.
415 $entity = $this->container->get('entity_type.manager')
416 ->getStorage($entity_type)
421 $entity->$field_name->value = 'field data';
422 $entity->enforceIsNew();
426 $field_storage->setIndexes(['value' => [['value', 255]]]);
427 $field_storage->save();
428 foreach ($tables as $table) {
429 $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), t("Index on value created in @table", ['@table' => $table]));
432 // Add a different index, removing the existing custom one.
433 $field_storage->setIndexes(['value_format' => [['value', 127], ['format', 127]]]);
434 $field_storage->save();
435 foreach ($tables as $table) {
436 $this->assertTrue(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value_format"), t("Index on value_format created in @table", ['@table' => $table]));
437 $this->assertFalse(Database::getConnection()->schema()->indexExists($table, "{$field_name}_value"), t("Index on value removed in @table", ['@table' => $table]));
440 // Verify that the tables were not dropped in the process.
441 $entity = $this->container->get('entity.manager')->getStorage($entity_type)->load(1);
442 $this->assertEqual($entity->$field_name->value, 'field data', t("Index changes performed without dropping the tables"));
446 * Test foreign key support.
448 public function testFieldSqlStorageForeignKeys() {
449 // Create a 'shape' field, with a configurable foreign key (see
450 // field_test_field_schema()).
451 $field_name = 'testfield';
452 $foreign_key_name = 'shape';
453 $field_storage = FieldStorageConfig::create([
454 'field_name' => $field_name,
455 'entity_type' => 'entity_test',
457 'settings' => ['foreign_key_name' => $foreign_key_name],
459 $field_storage->save();
460 // Get the field schema.
461 $schema = $field_storage->getSchema();
463 // Retrieve the field definition and check that the foreign key is in place.
464 $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name preserved through CRUD');
465 $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name preserved through CRUD');
467 // Update the field settings, it should update the foreign key definition too.
468 $foreign_key_name = 'color';
469 $field_storage->setSetting('foreign_key_name', $foreign_key_name);
470 $field_storage->save();
471 // Reload the field schema after the update.
472 $schema = $field_storage->getSchema();
474 // Check that the foreign key is in place.
475 $this->assertEqual($schema['foreign keys'][$foreign_key_name]['table'], $foreign_key_name, 'Foreign key table name modified after update');
476 $this->assertEqual($schema['foreign keys'][$foreign_key_name]['columns'][$foreign_key_name], 'id', 'Foreign key column name modified after update');
480 * Tests table name generation.
482 public function testTableNames() {
483 // Note: we need to test entity types with long names. We therefore use
484 // fields on imaginary entity types (works as long as we don't actually save
485 // them), and just check the generated table names.
487 // Short entity type and field name.
488 $entity_type = 'short_entity_type';
489 $field_name = 'short_field_name';
490 $field_storage = FieldStorageConfig::create([
491 'entity_type' => $entity_type,
492 'field_name' => $field_name,
493 'type' => 'test_field',
495 $expected = 'short_entity_type__short_field_name';
496 $this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
497 $expected = 'short_entity_type_revision__short_field_name';
498 $this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
500 // Short entity type, long field name
501 $entity_type = 'short_entity_type';
502 $field_name = 'long_field_name_abcdefghijklmnopqrstuvwxyz';
503 $field_storage = FieldStorageConfig::create([
504 'entity_type' => $entity_type,
505 'field_name' => $field_name,
506 'type' => 'test_field',
508 $expected = 'short_entity_type__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
509 $this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
510 $expected = 'short_entity_type_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
511 $this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
513 // Long entity type, short field name
514 $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
515 $field_name = 'short_field_name';
516 $field_storage = FieldStorageConfig::create([
517 'entity_type' => $entity_type,
518 'field_name' => $field_name,
519 'type' => 'test_field',
521 $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
522 $this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
523 $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
524 $this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
526 // Long entity type and field name.
527 $entity_type = 'long_entity_type_abcdefghijklmnopqrstuvwxyz';
528 $field_name = 'long_field_name_abcdefghijklmnopqrstuvwxyz';
529 $field_storage = FieldStorageConfig::create([
530 'entity_type' => $entity_type,
531 'field_name' => $field_name,
532 'type' => 'test_field',
534 $expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
535 $this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
536 $expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
537 $this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
538 // Try creating a second field and check there are no clashes.
539 $field_storage2 = FieldStorageConfig::create([
540 'entity_type' => $entity_type,
541 'field_name' => $field_name . '2',
542 'type' => 'test_field',
544 $this->assertNotEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $this->tableMapping->getDedicatedDataTableName($field_storage2));
545 $this->assertNotEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $this->tableMapping->getDedicatedRevisionTableName($field_storage2));
548 $field_storage = FieldStorageConfig::create([
549 'entity_type' => 'some_entity_type',
550 'field_name' => 'some_field_name',
551 'type' => 'test_field',
554 $expected = 'field_deleted_data_' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
555 $this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage, TRUE), $expected);
556 $expected = 'field_deleted_revision_' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
557 $this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage, TRUE), $expected);