Commit c72f39ad authored by catch's avatar catch

Issue #2057401 by plach, socketwench, kfritsche: Fixed Make the node entity...

Issue #2057401 by plach, socketwench, kfritsche: Fixed Make the node entity database schema sensible.
parent f4c5be06
......@@ -525,14 +525,17 @@ function drupal_write_record($table, &$record, $primary_keys = array()) {
* The converted value.
*/
function drupal_schema_get_field_value(array $info, $value) {
if ($info['type'] == 'int' || $info['type'] == 'serial') {
$value = (int) $value;
}
elseif ($info['type'] == 'float') {
$value = (float) $value;
}
else {
$value = (string) $value;
// Preserve legal NULL values.
if (isset($value) || !empty($info['not null'])) {
if ($info['type'] == 'int' || $info['type'] == 'serial') {
$value = (int) $value;
}
elseif ($info['type'] == 'float') {
$value = (float) $value;
}
else {
$value = (string) $value;
}
}
return $value;
}
......
......@@ -318,7 +318,7 @@ protected function buildQuery($ids, $revision_id = FALSE) {
if ($this->revisionKey) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->entityInfo['revision_table']));
// The id field is provided by entity, so remove it.
// The ID field is provided by entity, so remove it.
unset($entity_revision_fields[$this->idKey]);
// Remove all fields from the base table that are also fields by the same
......@@ -331,7 +331,7 @@ protected function buildQuery($ids, $revision_id = FALSE) {
}
$query->fields('revision', $entity_revision_fields);
// Compare revision id of the base and revision table, if equal then this
// Compare revision ID of the base and revision table, if equal then this
// is the default revision.
$query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision');
}
......
......@@ -47,6 +47,13 @@ class DatabaseStorageControllerNG extends DatabaseStorageController {
*/
protected $dataTable;
/**
* The table that stores revision field data if the entity supports revisions.
*
* @var string
*/
protected $revisionDataTable;
/**
* Overrides DatabaseStorageController::__construct().
*/
......@@ -58,6 +65,11 @@ public function __construct($entity_type, array $entity_info, Connection $databa
// Check if the entity type has a dedicated table for properties.
if (!empty($this->entityInfo['data_table'])) {
$this->dataTable = $this->entityInfo['data_table'];
// Entity types having both revision and translation support should always
// define a revision data table.
if ($this->revisionTable && !empty($this->entityInfo['revision_data_table'])) {
$this->revisionDataTable = $this->entityInfo['revision_data_table'];
}
}
// Work-a-round to let load() get stdClass storage records without having to
......@@ -158,55 +170,6 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value
parent::buildPropertyQuery($entity_query, $values);
}
/**
* {@inheritdoc}
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = $this->database->select($this->entityInfo['base_table'], 'base');
$is_revision_query = $this->revisionKey && ($revision_id || !$this->dataTable);
$query->addTag($this->entityType . '_load_multiple');
if ($revision_id) {
$query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id));
}
elseif ($is_revision_query) {
$query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
}
// Add fields from the {entity} table.
$entity_fields = drupal_schema_fields_sql($this->entityInfo['base_table']);
if ($is_revision_query) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->entityInfo['revision_table']));
// The ID field is provided by entity, so remove it.
unset($entity_revision_fields[$this->idKey]);
// Remove all fields from the base table that are also fields by the same
// name in the revision table.
$entity_field_keys = array_flip($entity_fields);
foreach ($entity_revision_fields as $name) {
if (isset($entity_field_keys[$name])) {
unset($entity_fields[$entity_field_keys[$name]]);
}
}
$query->fields('revision', $entity_revision_fields);
// Compare revision ID of the base and revision table, if equal then this
// is the default revision.
$query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision');
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
return $query;
}
/**
* Overrides DatabaseStorageController::attachLoad().
*
......@@ -263,13 +226,13 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
if ($this->dataTable) {
// If a revision table is available, we need all the properties of the
// latest revision. Otherwise we fall back to the data table.
$table = $this->revisionTable ?: $this->dataTable;
$table = $this->revisionDataTable ?: $this->dataTable;
$query = $this->database->select($table, 'data', array('fetch' => \PDO::FETCH_ASSOC))
->fields('data')
->condition($this->idKey, array_keys($entities))
->orderBy('data.' . $this->idKey);
if ($this->revisionTable) {
if ($this->revisionDataTable) {
if ($revision_id) {
$query->condition($this->revisionKey, $revision_id);
}
......@@ -286,8 +249,8 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
$data = $query->execute();
$field_definition = \Drupal::entityManager()->getFieldDefinitions($this->entityType);
$translations = array();
if ($this->revisionTable) {
$data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_table']), drupal_schema_fields_sql($this->entityInfo['base_table'])));
if ($this->revisionDataTable) {
$data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_data_table']), drupal_schema_fields_sql($this->entityInfo['base_table'])));
}
else {
$data_fields = array_flip(drupal_schema_fields_sql($this->entityInfo['data_table']));
......@@ -302,11 +265,7 @@ protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
$translations[$id][$langcode] = TRUE;
foreach ($field_definition as $name => $definition) {
// Set only translatable properties, unless we are dealing with a
// revisable entity, in which case we did not load the untranslatable
// data before.
$translatable = !empty($definition['translatable']);
if (isset($data_fields[$name]) && ($this->revisionTable || $translatable)) {
if (isset($data_fields[$name])) {
$entities[$id][$name][$langcode] = $values[$name];
}
}
......@@ -358,6 +317,9 @@ public function save(EntityInterface $entity) {
if ($this->dataTable) {
$this->savePropertyData($entity);
}
if ($this->revisionDataTable) {
$this->savePropertyData($entity, 'revision_data_table');
}
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
......@@ -368,15 +330,21 @@ public function save(EntityInterface $entity) {
}
}
else {
// Ensure the entity is still seen as new after assigning it an id,
// while storing its data.
$entity->enforceIsNew();
$return = drupal_write_record($this->entityInfo['base_table'], $record);
$entity->{$this->idKey}->value = $record->{$this->idKey};
$entity->{$this->idKey}->value = (string) $record->{$this->idKey};
if ($this->revisionKey) {
$entity->setNewRevision();
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
$entity->{$this->idKey}->value = $record->{$this->idKey};
if ($this->dataTable) {
$this->savePropertyData($entity);
}
if ($this->revisionDataTable) {
$this->savePropertyData($entity, 'revision_data_table');
}
// Reset general caches, but keep caches specific to certain entities.
$this->resetCache(array());
......@@ -411,53 +379,34 @@ public function save(EntityInterface $entity) {
* The revision id.
*/
protected function saveRevision(EntityInterface $entity) {
$return = $entity->id();
$default_langcode = $entity->getUntranslated()->language()->id;
if (!$entity->isNewRevision()) {
// Delete to handle removed values.
$this->database->delete($this->revisionTable)
->condition($this->idKey, $entity->id())
->condition($this->revisionKey, $entity->getRevisionId())
->execute();
}
$record = $this->mapToStorageRecord($entity, 'revision_table');
$languages = $this->dataTable ? $entity->getTranslationLanguages() : array($default_langcode => $entity->language());
foreach ($languages as $langcode => $language) {
$translation = $entity->getTranslation($langcode);
$record = $this->mapToRevisionStorageRecord($translation);
$record->langcode = $langcode;
$record->default_langcode = $langcode == $default_langcode;
// When saving a new revision, set any existing revision ID to NULL so as
// to ensure that a new revision will actually be created.
if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) {
$record->{$this->revisionKey} = NULL;
}
// When saving a new revision, set any existing revision ID to NULL so as
// to ensure that a new revision will actually be created.
if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) {
$record->{$this->revisionKey} = NULL;
}
$entity->preSaveRevision($this, $record);
$entity->preSaveRevision($this, $record);
if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record);
if ($entity->isDefaultRevision()) {
$this->database->update($this->entityInfo['base_table'])
->fields(array($this->revisionKey => $record->{$this->revisionKey}))
->condition($this->idKey, $record->{$this->idKey})
->execute();
}
$entity->setNewRevision(FALSE);
}
else {
// @todo Use multiple insertions to improve performance.
drupal_write_record($this->revisionTable, $record);
if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record);
if ($entity->isDefaultRevision()) {
$this->database->update($this->entityInfo['base_table'])
->fields(array($this->revisionKey => $record->{$this->revisionKey}))
->condition($this->idKey, $record->{$this->idKey})
->execute();
}
// Make sure to update the new revision key for the entity.
$entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
$return = $record->{$this->revisionKey};
$entity->setNewRevision(FALSE);
}
else {
drupal_write_record($this->revisionTable, $record, array($this->revisionKey));
}
return $return;
// Make sure to update the new revision key for the entity.
$entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
return $record->{$this->revisionKey};
}
/**
......@@ -465,17 +414,32 @@ protected function saveRevision(EntityInterface $entity) {
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param string $table_key
* (optional) The entity key identifying the target table. Defaults to
* 'data_table'.
*/
protected function savePropertyData(EntityInterface $entity) {
// Delete and insert to handle removed values.
$this->database->delete($this->dataTable)
->condition($this->idKey, $entity->id())
->execute();
protected function savePropertyData(EntityInterface $entity, $table_key = NULL) {
$revision = TRUE;
if (!isset($table_key)) {
$table_key = 'data_table';
$revision = FALSE;
}
$table_name = $this->entityInfo[$table_key];
if (!$revision || !$entity->isNewRevision()) {
$key = $revision ? $this->revisionKey : $this->idKey;
$value = $revision ? $entity->getRevisionId() : $entity->id();
// Delete and insert to handle removed values.
$this->database->delete($table_name)
->condition($key, $value)
->execute();
}
$query = $this->database->insert($this->dataTable);
$query = $this->database->insert($table_name);
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$record = $this->mapToDataStorageRecord($entity, $langcode);
$translation = $entity->getTranslation($langcode);
$record = $this->mapToDataStorageRecord($translation, $table_key);
$values = (array) $record;
$query
->fields(array_keys($values))
......@@ -486,70 +450,52 @@ protected function savePropertyData(EntityInterface $entity) {
}
/**
* Maps from an entity object to the storage record of the base table.
* Maps from an entity object to the storage record.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param string $table_key
* (optional) The entity key identifying the target table. Defaults to
* 'base_table'.
*
* @return \stdClass
* The record to store.
*/
protected function mapToStorageRecord(EntityInterface $entity) {
$record = new \stdClass();
foreach (drupal_schema_fields_sql($this->entityInfo['base_table']) as $name) {
$record->$name = $entity->$name->value;
}
return $record;
}
/**
* Maps from an entity object to the storage record of the revision table.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return \stdClass
* The record to store.
*/
protected function mapToRevisionStorageRecord(EntityInterface $entity) {
protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'base_table') {
$record = new \stdClass();
$definitions = $entity->getPropertyDefinitions();
foreach (drupal_schema_fields_sql($this->entityInfo['revision_table']) as $name) {
if (isset($definitions[$name]) && isset($entity->$name->value)) {
$record->$name = $entity->$name->value;
$schema = drupal_get_schema($this->entityInfo[$table_key]);
$is_new = $entity->isNew();
foreach (drupal_schema_fields_sql($this->entityInfo[$table_key]) as $name) {
$info = $schema['fields'][$name];
$value = isset($definitions[$name]) && isset($entity->$name->value) ? $entity->$name->value : NULL;
// If we are creating a new entity, we must not populate the record with
// NULL values otherwise defaults would not be applied.
if (isset($value) || !$is_new) {
$record->$name = drupal_schema_get_field_value($info, $value);
}
}
return $record;
}
/**
* Maps from an entity object to the storage record of the data table.
* Maps from an entity object to the storage record of the field data.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param $langcode
* The language code of the translation to get.
* @param string $table_key
* (optional) The entity key identifying the target table. Defaults to
* 'data_table'.
*
* @return \stdClass
* The record to store.
*/
protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
$default_langcode = $entity->getUntranslated()->language()->id;
// Don't use strict mode, this way there's no need to do checks here, as
// non-translatable properties are replicated for each language.
$translation = $entity->getTranslation($langcode);
$definitions = $translation->getPropertyDefinitions();
$schema = drupal_get_schema($this->entityInfo['data_table']);
$record = new \stdClass();
foreach (drupal_schema_fields_sql($this->entityInfo['data_table']) as $name) {
$info = $schema['fields'][$name];
$value = isset($definitions[$name]) && isset($translation->$name->value) ? $translation->$name->value : NULL;
$record->$name = drupal_schema_get_field_value($info, $value);
}
$record->langcode = $langcode;
$record->default_langcode = intval($default_langcode == $langcode);
protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') {
$record = $this->mapToStorageRecord($entity, $table_key);
$record->langcode = $entity->language()->id;
$record->default_langcode = intval($record->langcode == $entity->getUntranslated()->language()->id);
return $record;
}
......@@ -588,6 +534,12 @@ public function delete(array $entities) {
->execute();
}
if ($this->revisionDataTable) {
$this->database->delete($this->revisionDataTable)
->condition($this->idKey, $ids)
->execute();
}
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
......
......@@ -76,6 +76,7 @@ public function testAggregatorItemView() {
// Add a image to ensure that the sanitizing can be tested below.
$values['author'] = $this->randomName() . '<img src="http://example.com/example.png" \>"';
$values['link'] = 'http://drupal.org/node/' . mt_rand(1000, 10000);
$values['guid'] = $this->randomString();
$aggregator_item = $this->itemStorageController->create($values);
$aggregator_item->save();
......
......@@ -31,7 +31,7 @@ public static function getInfo() {
function setUp() {
parent::setUp();
$this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision'));
$this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision', 'node_revision'));
$this->installSchema('file', array('file_managed', 'file_usage'));
// Add text formats.
......
......@@ -71,7 +71,7 @@ function testViewsData() {
$this->assertTrue(isset($data[$revision_table]));
// The node field should join against node.
$this->assertTrue(isset($data[$current_table]['table']['join']['node']));
$this->assertTrue(isset($data[$revision_table]['table']['join']['node_field_revision']));
$this->assertTrue(isset($data[$revision_table]['table']['join']['node_revision']));
$expected_join = array(
'left_field' => 'nid',
......@@ -88,7 +88,7 @@ function testViewsData() {
array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE),
),
);
$this->assertEqual($expected_join, $data[$revision_table]['table']['join']['node_field_revision']);
$this->assertEqual($expected_join, $data[$revision_table]['table']['join']['node_revision']);
}
}
......@@ -422,7 +422,7 @@ display:
provider: views
langcode:
id: langcode
table: node
table: node_revision
field: langcode
operator: in
value: { }
......
......@@ -34,7 +34,8 @@
* },
* base_table = "node",
* data_table = "node_field_data",
* revision_table = "node_field_revision",
* revision_table = "node_revision",
* revision_data_table = "node_field_revision",
* uri_callback = "node_uri",
* fieldable = TRUE,
* translatable = TRUE,
......@@ -396,11 +397,6 @@ public static function baseFieldDefinitions($entity_type) {
'description' => t('A boolean indicating whether the node should be displayed at the top of lists in which it appears.'),
'type' => 'boolean_field',
);
$properties['translate'] = array(
'label' => t('Translate'),
'description' => t('A boolean indicating whether this translation page needs to be updated.'),
'type' => 'boolean_field',
);
$properties['revision_timestamp'] = array(
'label' => t('Revision timestamp'),
'description' => t('The time that the current revision was created.'),
......
......@@ -21,7 +21,7 @@ class UidRevision extends Uid {
public function query($group_by = FALSE) {
$this->ensureMyTable();
$placeholder = $this->placeholder();
$this->query->addWhereExpression(0, "$this->tableAlias.revision_uid = $placeholder OR ((SELECT COUNT(DISTINCT vid) FROM {node_field_revision} nfr WHERE nfr.revision_uid = $placeholder AND nfr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $this->argument));
$this->query->addWhereExpression(0, "$this->tableAlias.revision_uid = $placeholder OR ((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nfr.revision_uid = $placeholder AND nr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $this->argument));
}
}
......@@ -57,7 +57,7 @@ public static function create(ContainerInterface $container, array $configuratio
public function titleQuery() {
$titles = array();
$results = $this->database->query('SELECT npr.vid, npr.nid, npr.title FROM {node_field_revision} npr WHERE npr.vid IN (:vids)', array(':vids' => $this->value))->fetchAllAssoc('vid', PDO::FETCH_ASSOC);
$results = $this->database->query('SELECT nr.vid, nr.nid, npr.title FROM {node_revision} nr WHERE nr.vid IN (:vids)', array(':vids' => $this->value))->fetchAllAssoc('vid', PDO::FETCH_ASSOC);
$nids = array();
foreach ($results as $result) {
$nids[] = $result['nid'];
......
......@@ -28,7 +28,7 @@ class RevisionLink extends Link {
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$this->additional_fields['node_vid'] = array('table' => 'node_field_revision', 'field' => 'vid');
$this->additional_fields['node_vid'] = array('table' => 'node_revision', 'field' => 'vid');
}
public function access() {
......
......@@ -27,7 +27,7 @@ public function query($group_by = FALSE) {
$args = array_values($this->value);
$this->query->addWhereExpression($this->options['group'], "$this->tableAlias.uid IN($placeholder) OR
((SELECT COUNT(DISTINCT vid) FROM {node_field_revision} nfr WHERE nfr.revision_uid IN ($placeholder) AND nfr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $args),
((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nr.revision_uid IN ($placeholder) AND nr.nid = $this->tableAlias.nid) > 0)", array($placeholder => $args),
$args);
}
......
......@@ -20,7 +20,7 @@
*
* @ViewsWizard(
* id = "node_revision",
* base_table = "node_field_revision",
* base_table = "node_revision",
* title = @Translation("Content revisions")
* )
*/
......@@ -36,7 +36,7 @@ class NodeRevision extends WizardPluginBase {
*/
protected $pathField = array(
'id' => 'vid',
'table' => 'node_field_revision',
'table' => 'node_revision',
'field' => 'vid',
'exclude' => TRUE,
'alter' => array(
......@@ -97,7 +97,7 @@ protected function defaultDisplayOptions() {
/* Field: Content revision: Created date */
$display_options['fields']['timestamp']['id'] = 'timestamp';
$display_options['fields']['timestamp']['table'] = 'node_field_revision';
$display_options['fields']['timestamp']['table'] = 'node_revision';
$display_options['fields']['timestamp']['field'] = 'timestamp';
$display_options['fields']['timestamp']['provider'] = 'node';
$display_options['fields']['timestamp']['alter']['alter_text'] = 0;
......
......@@ -26,7 +26,7 @@ public static function getInfo() {
public function setUp() {
parent::setUp();
$this->installSchema('node', array('node', 'node_field_data', 'node_field_revision'));
$this->installSchema('node', array('node', 'node_field_data', 'node_field_revision', 'node_revision'));
}
/**
......
......@@ -32,6 +32,7 @@ public static function getInfo() {
public function setUp() {
parent::setUp();
$this->installSchema('node', 'node');
$this->installSchema('node', 'node_revision');
$this->installSchema('node', 'node_field_data');
$this->installSchema('node', 'node_field_revision');
$this->installSchema('user', array('users'));
......
......@@ -134,14 +134,14 @@ function testRevisions() {
'%title' => $nodes[1]->getTitle(),
)),
'Revision deleted.');
$this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_field_revision} WHERE nid = :nid and vid = :vid',
$this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid',
array(':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()))->fetchField() == 0,
'Revision not found.');
// Set the revision timestamp to an older date to make sure that the
// confirmation message correctly displays the stored revision date.
$old_revision_date = REQUEST_TIME - 86400;
db_update('node_field_revision')
db_update('node_revision')
->condition('vid', $nodes[2]->getRevisionId())
->fields(array(
'revision_timestamp' => $old_revision_date,
......
......@@ -114,12 +114,12 @@ function testRevisions() {
$this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.',
array('%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),