Commit 82989783 authored by catch's avatar catch

Issue #2335879 by alexpott, dixon_, Wim Leers, chx, mauzeh, ygerasimov, jeqq:...

Issue #2335879 by alexpott, dixon_, Wim Leers, chx, mauzeh, ygerasimov, jeqq: Change SqlContentEntityStorageSchema::requiresEntityDataMigration() to ask the old storage handler if it has data rather than assuming yes unless NULL storage
parent 77c613e3
......@@ -334,9 +334,9 @@ protected function invokeHook($hook, EntityInterface $entity) {
}
/**
* Implements Drupal\Core\Entity\EntityStorageInterface::getQueryServiceName().
* {@inheritdoc}
*/
public function getQueryServiceName() {
protected function getQueryServiceName() {
return 'entity.query.config';
}
......
......@@ -78,7 +78,7 @@ public function save(EntityInterface $entity) {
/**
* {@inheritdoc}
*/
public function getQueryServiceName() {
protected function getQueryServiceName() {
throw new QueryException('Null implementation can not be queried.');
}
......@@ -138,4 +138,11 @@ public function countFieldData($storage_definition, $as_bool = FALSE) {
return $as_bool ? FALSE : 0;
}
/**
* {@inheritdoc}
*/
public function hasData() {
return FALSE;
}
}
......@@ -42,6 +42,16 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
);
}
/**
* {@inheritdoc}
*/
public function hasData() {
return (bool) $this->getQuery()
->accessCheck(FALSE)
->range(0, 1)
->execute();
}
/**
* {@inheritdoc}
*/
......
......@@ -84,6 +84,14 @@ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batc
*/
public function countFieldData($storage_definition, $as_bool = FALSE);
/**
* Determines if the storage contains any data.
*
* @return bool
* TRUE if the storage contains data, FALSE if not.
*/
public function hasData();
/**
* Performs final cleanup after all data of a field has been purged.
*
......
......@@ -324,23 +324,30 @@ public function getHandler($entity_type, $handler_type) {
if (!$class) {
throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a %s handler.', $entity_type, $handler_type));
}
if (is_subclass_of($class, 'Drupal\Core\Entity\EntityHandlerInterface')) {
$handler = $class::createInstance($this->container, $definition);
}
else {
$handler = new $class($definition);
}
if (method_exists($handler, 'setModuleHandler')) {
$handler->setModuleHandler($this->moduleHandler);
}
if (method_exists($handler, 'setStringTranslation')) {
$handler->setStringTranslation($this->translationManager);
}
$this->handlers[$handler_type][$entity_type] = $handler;
$this->handlers[$handler_type][$entity_type] = $this->createHandlerInstance($class, $definition);
}
return $this->handlers[$handler_type][$entity_type];
}
/**
* {@inheritdoc}
*/
public function createHandlerInstance($class, EntityTypeInterface $definition = null) {
if (is_subclass_of($class, 'Drupal\Core\Entity\EntityHandlerInterface')) {
$handler = $class::createInstance($this->container, $definition);
}
else {
$handler = new $class($definition);
}
if (method_exists($handler, 'setModuleHandler')) {
$handler->setModuleHandler($this->moduleHandler);
}
if (method_exists($handler, 'setStringTranslation')) {
$handler->setStringTranslation($this->translationManager);
}
return $handler;
}
/**
* {@inheritdoc}
*/
......
......@@ -237,20 +237,37 @@ public function getFormObject($entity_type, $operation);
public function hasHandler($entity_type, $handler_type);
/**
* Creates a new handler instance.
* Creates a new handler instance for a entity type and handler type.
*
* @param string $entity_type
* The entity type for this controller.
* @param string $handler_type
* The controller type to create an instance for.
*
* @return mixed
* @return object
* A handler instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function getHandler($entity_type, $handler_type);
/**
* Creates new handler instance.
*
* Usually \Drupal\Core\Entity\EntityManagerInterface::getHandler() is
* preferred since that method has additional checking that the class exists
* and has static caches.
*
* @param mixed $class
* The handler class to instantiate.
* @param \Drupal\Core\Entity\EntityTypeInterface $definition
* The entity type definition.
*
* @return object
* A handler instance.
*/
public function createHandlerInstance($class, EntityTypeInterface $definition = null);
/**
* Get the bundle info of an entity type.
*
......
......@@ -460,7 +460,26 @@ public function loadByProperties(array $values = array()) {
* {@inheritdoc}
*/
public function getQuery($conjunction = 'AND') {
return \Drupal::entityQuery($this->getEntityTypeId(), $conjunction);
// Access the service directly rather than entity.query factory so the
// storage's current entity type is used.
return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction);
}
/**
* {@inheritdoc}
*/
public function getAggregateQuery($conjunction = 'AND') {
// Access the service directly rather than entity.query factory so the
// storage's current entity type is used.
return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction);
}
/**
* Gets the name of the service for the query for this entity storage.
*
* @return string
* The name of the service for the query for this entity storage.
*/
abstract protected function getQueryServiceName();
}
......@@ -147,14 +147,6 @@ public function delete(array $entities);
*/
public function save(EntityInterface $entity);
/**
* Gets the name of the service for the query for this entity storage.
*
* @return string
* The name of the service for the query for this entity storage.
*/
public function getQueryServiceName();
/**
* Returns an entity query instance.
*
......@@ -166,10 +158,25 @@ public function getQueryServiceName();
* @return \Drupal\Core\Entity\Query\QueryInterface
* The query instance.
*
* @see \Drupal\Core\Entity\EntityStorageInterface::getQueryServiceName()
* @see \Drupal\Core\Entity\EntityStorageBase::getQueryServiceName()
*/
public function getQuery($conjunction = 'AND');
/**
* Returns an aggregated query instance.
*
* @param string $conjunction
* (optional) The logical operator for the query, either:
* - AND: all of the conditions on the query need to match.
* - OR: at least one of the conditions on the query need to match.
*
* @return \Drupal\Core\Entity\Query\QueryAggregateInterface
* The aggregated query object that can query the given entity type.
*
* @see \Drupal\Core\Entity\EntityStorageBase::getQueryServiceName()
*/
public function getAggregateQuery($conjunction = 'AND');
/**
* Returns the entity type ID.
*
......
......@@ -202,7 +202,7 @@ protected function has($id, EntityInterface $entity) {
/**
* {@inheritdoc}
*/
public function getQueryServiceName() {
protected function getQueryServiceName() {
return 'entity.query.keyvalue';
}
......
......@@ -13,6 +13,14 @@
/**
* Factory class Creating entity query objects.
*
* Any implementation of this service must call getQuery()/getAggregateQuery()
* of the corresponding entity storage.
*
* @see \Drupal\Core\Entity\EntityStorageBase::getQuery()
*
* @todo https://www.drupal.org/node/2389335 remove entity.query service and
* replace with using the entity storage's getQuery() method.
*/
class QueryFactory implements ContainerAwareInterface {
......@@ -48,8 +56,7 @@ public function __construct(EntityManagerInterface $entity_manager) {
* The query object that can query the given entity type.
*/
public function get($entity_type_id, $conjunction = 'AND') {
$service_name = $this->entityManager->getStorage($entity_type_id)->getQueryServiceName();
return $this->container->get($service_name)->get($this->entityManager->getDefinition($entity_type_id), $conjunction);
return $this->entityManager->getStorage($entity_type_id)->getQuery($conjunction);
}
/**
......@@ -65,8 +72,7 @@ public function get($entity_type_id, $conjunction = 'AND') {
* The aggregated query object that can query the given entity type.
*/
public function getAggregate($entity_type_id, $conjunction = 'AND') {
$service_name = $this->entityManager->getStorage($entity_type_id)->getQueryServiceName();
return $this->container->get($service_name)->getAggregate($this->entityManager->getDefinition($entity_type_id), $conjunction);
return $this->entityManager->getStorage($entity_type_id)->getAggregateQuery($conjunction);
}
}
......@@ -1198,7 +1198,7 @@ protected function saveRevision(EntityInterface $entity) {
/**
* {@inheritdoc}
*/
public function getQueryServiceName() {
protected function getQueryServiceName() {
return 'entity.query.sql';
}
......
......@@ -178,23 +178,15 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac
* {@inheritdoc}
*/
public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
// If we're updating from NULL storage, then there's no stored data that
// requires migration.
// @todo Remove in https://www.drupal.org/node/2335879.
// If the original storage has existing entities, or it is impossible to
// determine if that is the case, require entity data to be migrated.
$original_storage_class = $original->getStorageClass();
$null_storage_class = 'Drupal\Core\Entity\ContentEntityNullStorage';
if ($original_storage_class == $null_storage_class || is_subclass_of($original_storage_class, $null_storage_class)) {
return FALSE;
if (!class_exists($original_storage_class)) {
return TRUE;
}
return
// If the original storage class is different, then there might be
// existing entities in that storage even if the new storage's base
// table is empty.
// @todo Ask the old storage handler rather than assuming:
// https://www.drupal.org/node/2335879.
$entity_type->getStorageClass() != $original_storage_class ||
!$this->isTableEmpty($this->storage->getBaseTable());
// Use the original entity type since the storage has not been updated.
$original_storage = $this->entityManager->createHandlerInstance($original_storage_class, $original);
return $original_storage->hasData();
}
/**
......
......@@ -1056,6 +1056,88 @@ public function testDedicatedTableSchemaForEntityWithStringIdentifier() {
);
}
public function providerTestRequiresEntityDataMigration() {
$updated_entity_type_definition = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface')
->disableOriginalConstructor()
->getMock();
$updated_entity_type_definition->expects($this->any())
->method('getStorageClass')
// A class that exists, *any* class.
->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema');
$original_entity_type_definition = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface')
->disableOriginalConstructor()
->getMock();
$original_entity_type_definition->expects($this->any())
->method('getStorageClass')
// A class that exists, *any* class.
->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema');
$original_entity_type_definition_other_nonexisting = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface')
->disableOriginalConstructor()
->getMock();
$original_entity_type_definition_other_nonexisting->expects($this->any())
->method('getStorageClass')
->willReturn('bar');
$original_entity_type_definition_other_existing = $this->getMockBuilder('\Drupal\Core\Entity\EntityTypeInterface')
->disableOriginalConstructor()
->getMock();
$original_entity_type_definition_other_existing->expects($this->any())
->method('getStorageClass')
// A class that exists, *any* class.
->willReturn('\Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema');
return [
// Case 1: same storage class, ::hasData() === TRUE.
[$updated_entity_type_definition, $original_entity_type_definition, TRUE, TRUE],
// Case 2: same storage class, ::hasData() === FALSE.
[$updated_entity_type_definition, $original_entity_type_definition, FALSE, FALSE],
// Case 3: different storage class, original storage class does not exist.
[$updated_entity_type_definition, $original_entity_type_definition_other_nonexisting, NULL, TRUE],
// Case 4: different storage class, original storage class exists, ::hasData() === TRUE.
[$updated_entity_type_definition, $original_entity_type_definition_other_existing, TRUE, TRUE],
// Case 5: different storage class, original storage class exists, ::hasData() === FALSE.
[$updated_entity_type_definition, $original_entity_type_definition_other_existing, FALSE, FALSE],
];
}
/**
* @covers ::requiresEntityDataMigration
*
* @dataProvider providerTestRequiresEntityDataMigration
*/
public function testRequiresEntityDataMigration($updated_entity_type_definition, $original_entity_type_definition, $original_storage_has_data, $migration_required) {
$this->entityType = new ContentEntityType(array(
'id' => 'entity_test',
'entity_keys' => array('id' => 'id'),
));
$original_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
->disableOriginalConstructor()
->getMock();
$original_storage->expects($this->exactly(is_null($original_storage_has_data) ? 0 : 1))
->method('hasData')
->willReturn($original_storage_has_data);
// Assert hasData() is never called on the new storage definition.
$this->storage->expects($this->never())
->method('hasData');
$connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$this->entityManager->expects($this->any())
->method('createHandlerInstance')
->willReturn($original_storage);
$this->storageSchema = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema')
->setConstructorArgs(array($this->entityManager, $this->entityType, $this->storage, $connection))
->setMethods(array('installedStorageSchema'))
->getMock();
$this->assertEquals($migration_required, $this->storageSchema->requiresEntityDataMigration($updated_entity_type_definition, $original_entity_type_definition));
}
/**
* Sets up the storage schema object to test.
*
......
......@@ -1180,6 +1180,56 @@ public function testLoadMultiplePersistentCacheMiss() {
$this->assertEquals($entity, $entities[$id]);
}
/**
* @covers ::hasData
*/
public function testHasData() {
$query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
$query->expects(($this->once()))
->method('accessCheck')
->with(FALSE)
->willReturn($query);
$query->expects(($this->once()))
->method('range')
->with(0, 1)
->willReturn($query);
$query->expects(($this->once()))
->method('execute')
->willReturn(array(5));
$factory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory')
->disableOriginalConstructor()
->getMock();
$factory->expects($this->once())
->method('get')
->with($this->entityType, 'AND')
->willReturn($query);
$this->container->set('entity.query.sql', $factory);
$database = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$this->entityManager->expects($this->any())
->method('getDefinition')
->will($this->returnValue($this->entityType));
$this->entityManager->expects($this->any())
->method('getFieldStorageDefinitions')
->will($this->returnValue($this->fieldDefinitions));
$this->entityManager->expects($this->any())
->method('getBaseFieldDefinitions')
->will($this->returnValue($this->fieldDefinitions));
$this->entityStorage = new SqlContentEntityStorage($this->entityType, $database, $this->entityManager, $this->cache);
$result = $this->entityStorage->hasData();
$this->assertTrue($result, 'hasData returned TRUE');
}
/**
* Tests entity ID sanitization.
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment