diff --git a/patches/drupal-core-11.1.3.patch b/patches/drupal-core-11.1.3.patch new file mode 100644 index 0000000000000000000000000000000000000000..fe080e2aef9b71dec1baa042048e81a9f3944af2 --- /dev/null +++ b/patches/drupal-core-11.1.3.patch @@ -0,0 +1,16723 @@ +diff --git a/core/lib/Drupal/Component/Datetime/DateTimePlus.php b/core/lib/Drupal/Component/Datetime/DateTimePlus.php +index 35f2e80f0d3354c2f110d2bbf5be2d189d9484e2..7a1f44aa1a2d12123346e01915e4c6313700236f 100644 +--- a/core/lib/Drupal/Component/Datetime/DateTimePlus.php ++++ b/core/lib/Drupal/Component/Datetime/DateTimePlus.php +@@ -3,6 +3,7 @@ + namespace Drupal\Component\Datetime; + + use Drupal\Component\Utility\ToStringTrait; ++use MongoDB\BSON\UTCDateTime; + + /** + * Wraps DateTime(). +@@ -203,6 +204,12 @@ public static function createFromArray(array $date_parts, $timezone = NULL, $set + * If the timestamp is not numeric. + */ + public static function createFromTimestamp($timestamp, $timezone = NULL, $settings = []) { ++ // In MongoDB timestamp are stored as instances of MongoDB\BSON\UTCDateTime. ++ if ($timestamp instanceof UTCDateTime) { ++ $timestamp = (int) $timestamp->__toString(); ++ $timestamp = $timestamp / 1000; ++ $timestamp = (string) $timestamp; ++ } + if (!is_numeric($timestamp)) { + throw new \InvalidArgumentException('The timestamp must be numeric.'); + } +diff --git a/core/lib/Drupal/Core/Batch/BatchStorage.php b/core/lib/Drupal/Core/Batch/BatchStorage.php +index 46f9fb97c0d6359da0cbd84279ba4bdd92b77523..fbc1d8dda75fbd8df70a779b60ab3103816ae634 100644 +--- a/core/lib/Drupal/Core/Batch/BatchStorage.php ++++ b/core/lib/Drupal/Core/Batch/BatchStorage.php +@@ -6,6 +6,7 @@ + use Drupal\Core\Access\CsrfTokenGenerator; + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; ++use MongoDB\BSON\UTCDateTime; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + + class BatchStorage implements BatchStorageInterface { +@@ -15,6 +16,15 @@ class BatchStorage implements BatchStorageInterface { + */ + const TABLE_NAME = 'batch'; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Constructs the database batch storage service. + * +@@ -44,7 +54,7 @@ public function load($id) { + try { + $batch = $this->connection->select('batch', 'b') + ->fields('b', ['batch']) +- ->condition('bid', $id) ++ ->condition('bid', (int) $id) + ->condition('token', $this->csrfToken->get($id)) + ->execute() + ->fetchField(); +@@ -65,7 +75,7 @@ public function load($id) { + public function delete($id) { + try { + $this->connection->delete('batch') +- ->condition('bid', $id) ++ ->condition('bid', (int) $id) + ->execute(); + } + catch (\Exception $e) { +@@ -77,10 +87,16 @@ public function delete($id) { + * {@inheritdoc} + */ + public function update(array $batch) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + try { + $this->connection->update('batch') + ->fields(['batch' => serialize($batch)]) +- ->condition('bid', $batch['id']) ++ ->condition('bid', (int) $batch['id']) + ->execute(); + } + catch (\Exception $e) { +@@ -93,9 +109,14 @@ public function update(array $batch) { + */ + public function cleanup() { + try { ++ $timestamp = $this->time->getRequestTime() - 864000; ++ if ($this->connection->driver() == 'mongodb') { ++ $timestamp = new UTCDateTime($timestamp * 1000); ++ } ++ + // Cleanup the batch table and the queue for failed batches. + $this->connection->delete('batch') +- ->condition('timestamp', $this->time->getRequestTime() - 864000, '<') ++ ->condition('timestamp', $timestamp, '<') + ->execute(); + } + catch (\Exception $e) { +@@ -107,6 +128,12 @@ public function cleanup() { + * {@inheritdoc} + */ + public function create(array $batch) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // Ensure that a session is started before using the CSRF token generator, + // and update the database record. + $this->session->start(); +@@ -115,7 +142,7 @@ public function create(array $batch) { + 'token' => $this->csrfToken->get($batch['id']), + 'batch' => serialize($batch), + ]) +- ->condition('bid', $batch['id']) ++ ->condition('bid', (int) $batch['id']) + ->execute(); + } + +@@ -126,22 +153,39 @@ public function create(array $batch) { + * A batch id. + */ + public function getId(): int { +- $try_again = FALSE; +- try { +- // The batch table might not yet exist. +- return $this->doInsertBatchRecord(); +- } +- catch (\Exception $e) { +- // If there was an exception, try to create the table. +- if (!$try_again = $this->ensureTableExists()) { +- // If the exception happened for other reason than the missing table, +- // propagate the exception. +- throw $e; ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ if (!$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); + } ++ ++ return $this->connection->insert('batch') ++ ->fields([ ++ 'timestamp' => new UTCDateTime($this->time->getRequestTime() * 1000), ++ 'token' => '', ++ 'batch' => NULL, ++ ]) ++ ->execute(); + } +- // Now that the table has been created, try again if necessary. +- if ($try_again) { +- return $this->doInsertBatchRecord(); ++ else { ++ $try_again = FALSE; ++ try { ++ // The batch table might not yet exist. ++ return $this->doInsertBatchRecord(); ++ } ++ catch (\Exception $e) { ++ // If there was an exception, try to create the table. ++ if (!$try_again = $this->ensureTableExists()) { ++ // If the exception happened for other reason than the missing table, ++ // propagate the exception. ++ throw $e; ++ } ++ } ++ // Now that the table has been created, try again if necessary. ++ if ($try_again) { ++ return $this->doInsertBatchRecord(); ++ } + } + } + +@@ -205,7 +249,7 @@ protected function catchException(\Exception $e) { + * @internal + */ + public function schemaDefinition() { +- return [ ++ $schema = [ + 'description' => 'Stores details about batches (processes that run in multiple HTTP requests).', + 'fields' => [ + 'bid' => [ +@@ -237,6 +281,13 @@ public function schemaDefinition() { + 'token' => ['token'], + ], + ]; ++ ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB timestamps are stored as real dates. ++ $schema['fields']['timestamp']['type'] = 'date'; ++ } ++ ++ return $schema; + } + + } +diff --git a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php +index 00bf806c3dee1df743646160e9f5110fdb3034b4..9eb5b0471e67fa197ce2a0acf968c4ad688ee060 100644 +--- a/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php ++++ b/core/lib/Drupal/Core/Cache/CacheTagsChecksumTrait.php +@@ -32,6 +32,15 @@ trait CacheTagsChecksumTrait { + */ + protected $tagCache = []; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Callback to be invoked just after a database transaction gets committed. + * +@@ -51,6 +60,13 @@ public function rootTransactionEndCallback($success) { + * Implements \Drupal\Core\Cache\CacheTagsInvalidatorInterface::invalidateTags() + */ + public function invalidateTags(array $tags) { ++ if (isset($this->connection) && ($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ ++ // Only invalidate tags once per request unless they are written again. + foreach ($tags as $key => $tag) { + if (isset($this->invalidatedTags[$tag])) { + unset($tags[$key]); +@@ -80,6 +96,12 @@ public function invalidateTags(array $tags) { + * Implements \Drupal\Core\Cache\CacheTagsChecksumInterface::getCurrentChecksum() + */ + public function getCurrentChecksum(array $tags) { ++ if (isset($this->connection) && ($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // Any cache writes in this request containing cache tags whose invalidation + // has been delayed due to an in-progress transaction must not be read by + // any other request, so use a nonsensical checksum which will cause any +diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php +index 8b45010914fef5a96c04b1a1b7eacb34c23c5b6b..09b655c0e94efe45d058ede82a592019d6edd028 100644 +--- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php ++++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php +@@ -8,6 +8,9 @@ + use Drupal\Component\Utility\Crypt; + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; ++use MongoDB\BSON\Binary; ++use MongoDB\BSON\Decimal128; + + /** + * Defines a default cache implementation. +@@ -68,6 +71,15 @@ class DatabaseBackend implements CacheBackendInterface { + */ + protected $checksumProvider; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Constructs a DatabaseBackend object. + * +@@ -128,7 +140,23 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { + // ::select() is a much smaller proportion of the request. + $result = []; + try { +- $result = $this->connection->query('SELECT [cid], [data], [created], [expire], [serialized], [tags], [checksum] FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE [cid] IN ( :cids[] ) ORDER BY [cid]', [':cids[]' => array_keys($cid_mapping)]); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->bin; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['cid' => ['$in' => array_keys($cid_mapping)]], ++ [ ++ 'projection' => ['cid' => 1, 'data' => 1, 'created' => 1, 'expire' => 1, 'serialized' => 1, 'tags' => 1, 'checksum' => 1, '_id' => 0], ++ 'sort' => ['cid' => 1], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['cid', 'data', 'created', 'expire', 'serialized', 'tags', 'checksum']); ++ $result = $statement->execute()->fetchAll(); ++ } ++ else { ++ $result = $this->connection->query('SELECT [cid], [data], [created], [expire], [serialized], [tags], [checksum] FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE [cid] IN ( :cids[] ) ORDER BY [cid]', [':cids[]' => array_keys($cid_mapping)]); ++ } + } + catch (\Exception) { + // Nothing to do. +@@ -205,22 +233,33 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { + * {@inheritdoc} + */ + public function setMultiple(array $items) { +- $try_again = FALSE; +- try { +- // The bin might not yet exist. ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB the table need to exists. Otherwise MongoDB creates one ++ // without the correct validation. ++ if (!$this->tableExists) { ++ $this->tableExists = $this->ensureBinExists(); ++ } ++ + $this->doSetMultiple($items); + } +- catch (\Exception $e) { +- // If there was an exception, try to create the bins. +- if (!$try_again = $this->ensureBinExists()) { +- // If the exception happened for other reason than the missing bin +- // table, propagate the exception. +- throw $e; ++ else { ++ $try_again = FALSE; ++ try { ++ // The bin might not yet exist. ++ $this->doSetMultiple($items); ++ } ++ catch (\Exception $e) { ++ // If there was an exception, try to create the bins. ++ if (!$try_again = $this->ensureBinExists()) { ++ // If the exception happened for other reason than the missing bin ++ // table, propagate the exception. ++ throw $e; ++ } ++ } ++ // Now that the bin has been created, try again if necessary. ++ if ($try_again) { ++ $this->doSetMultiple($items); + } +- } +- // Now that the bin has been created, try again if necessary. +- if ($try_again) { +- $this->doSetMultiple($items); + } + } + +@@ -264,14 +303,29 @@ protected function doSetMultiple(array $items) { + continue; + } + +- if (!is_string($item['data'])) { +- $fields['data'] = $this->serializer->encode($item['data']); +- $fields['serialized'] = 1; ++ if ($this->connection->driver() == 'mongodb') { ++ $fields['created'] = new Decimal128($fields['created']); ++ ++ if (!is_string($item['data'])) { ++ $fields['data'] = new Binary(serialize($item['data']), Binary::TYPE_GENERIC); ++ $fields['serialized'] = TRUE; ++ } ++ else { ++ $fields['data'] = new Binary($item['data'], Binary::TYPE_GENERIC); ++ $fields['serialized'] = FALSE; ++ } + } + else { +- $fields['data'] = $item['data']; +- $fields['serialized'] = 0; ++ if (!is_string($item['data'])) { ++ $fields['data'] = $this->serializer->encode($item['data']); ++ $fields['serialized'] = 1; ++ } ++ else { ++ $fields['data'] = $item['data']; ++ $fields['serialized'] = 0; ++ } + } ++ + $values[] = $fields; + } + +@@ -308,6 +362,12 @@ public function delete($cid) { + * {@inheritdoc} + */ + public function deleteMultiple(array $cids) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureBinExists(); ++ } ++ + $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); + try { + // Delete in chunks when a large array is passed. +@@ -331,6 +391,12 @@ public function deleteMultiple(array $cids) { + * {@inheritdoc} + */ + public function deleteAll() { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureBinExists(); ++ } ++ + try { + $this->connection->truncate($this->bin)->execute(); + } +@@ -357,6 +423,15 @@ public function invalidate($cid) { + public function invalidateMultiple(array $cids) { + $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); + try { ++ if ($this->connection->driver() == 'mongodb') { ++ $session = $this->connection->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ + // Update in chunks when a large array is passed. + $requestTime = $this->time->getRequestTime(); + foreach (array_chunk($cids, 1000) as $cids_chunk) { +@@ -365,9 +440,18 @@ public function invalidateMultiple(array $cids) { + ->condition('cid', $cids_chunk, 'IN') + ->execute(); + } ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { +- $this->catchException($e); ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } ++ else { ++ $this->catchException($e); ++ } + } + } + +@@ -394,12 +478,16 @@ public function garbageCollection() { + if ($this->maxRows !== static::MAXIMUM_NONE) { + $first_invalid_create_time = $this->connection->select($this->bin) + ->fields($this->bin, ['created']) +- ->orderBy("{$this->bin}.created", 'DESC') ++ ->orderBy('created', 'DESC') + ->range($this->maxRows, 1) + ->execute() + ->fetchField(); + + if ($first_invalid_create_time) { ++ if ($this->connection->driver() == 'mongodb') { ++ // The created field is saved in MongoDB as Decimal128. ++ $first_invalid_create_time = new Decimal128($first_invalid_create_time); ++ } + $this->connection->delete($this->bin) + ->condition('created', $first_invalid_create_time, '<=') + ->execute(); +@@ -562,6 +650,18 @@ public function schemaDefinition() { + ], + 'primary key' => ['cid'], + ]; ++ ++ if ($this->connection->driver() == 'mongodb') { ++ // The date field cannot be transformed to a real date field, because it can ++ // be set to infinity with the value -1. ++ $schema['fields']['serialized'] = [ ++ 'description' => 'A flag to indicate whether content is serialized (TRUE) or not (FALSE).', ++ 'type' => 'bool', ++ 'not null' => TRUE, ++ 'default' => FALSE, ++ ]; ++ } ++ + return $schema; + } + +diff --git a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php +index aa41952128c240b3d1a4bd00a664dd70834f00fc..f4c99cfb2d824ad6b94a87eee4fb9bf419cc2980 100644 +--- a/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php ++++ b/core/lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php +@@ -4,6 +4,7 @@ + + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; + + /** + * Cache tags invalidations checksum implementation that uses the database. +@@ -35,11 +36,24 @@ public function __construct(Connection $connection) { + protected function doInvalidateTags(array $tags) { + try { + foreach ($tags as $tag) { +- $this->connection->merge('cachetags') +- ->insertFields(['invalidations' => 1]) +- ->expression('invalidations', '[invalidations] + 1') +- ->key('tag', $tag) +- ->execute(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . 'cachetags'; ++ $this->connection->getConnection()->selectCollection($prefixed_table)->updateOne( ++ ['tag' => $tag], ++ ['$inc' => ['invalidations' => 1]], ++ [ ++ 'upsert' => TRUE, ++ 'session' => $this->connection->getMongodbSession(), ++ ], ++ ); ++ } ++ else { ++ $this->connection->merge('cachetags') ++ ->insertFields(['invalidations' => 1]) ++ ->expression('invalidations', '[invalidations] + 1') ++ ->key('tag', $tag) ++ ->execute(); ++ } + } + } + catch (\Exception $e) { +@@ -57,8 +71,22 @@ protected function doInvalidateTags(array $tags) { + */ + protected function getTagInvalidationCounts(array $tags) { + try { +- return $this->connection->query('SELECT [tag], [invalidations] FROM {cachetags} WHERE [tag] IN ( :tags[] )', [':tags[]' => $tags]) +- ->fetchAllKeyed(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . 'cachetags'; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['tag' => ['$in' => array_values($tags)]], ++ [ ++ 'projection' => ['tag' => 1, 'invalidations' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ], ++ ); ++ $statement = new Statement($this->connection, $cursor, ['tag', 'invalidations']); ++ return $statement->execute()->fetchAllKeyed(); ++ } ++ else { ++ return $this->connection->query('SELECT [tag], [invalidations] FROM {cachetags} WHERE [tag] IN ( :tags[] )', [':tags[]' => $tags]) ++ ->fetchAllKeyed(); ++ } + } + catch (\Exception $e) { + // If the table does not exist yet, create. +diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php +index 70933f8fb0eb3006b43603b72e24a9c040714568..a8721eb3d380106995914e60c7aa132df2322183 100644 +--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php ++++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php +@@ -5,6 +5,7 @@ + use Drupal\Component\Utility\Crypt; + use Drupal\Component\Utility\NestedArray; + use Drupal\Core\Config\Entity\ConfigDependencyManager; ++use Drupal\Core\Database\Database; + use Drupal\Core\Extension\ExtensionPathResolver; + use Drupal\Core\Installer\InstallerKernel; + use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +@@ -74,6 +75,13 @@ class ConfigInstaller implements ConfigInstallerInterface { + */ + protected $extensionPathResolver; + ++ /** ++ * The database override directory. ++ * ++ * @var string ++ */ ++ protected $databaseDriverOverrideDirectory; ++ + /** + * Constructs the configuration installer. + * +@@ -100,6 +108,34 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter + $this->eventDispatcher = $event_dispatcher; + $this->installProfile = $install_profile; + $this->extensionPathResolver = $extension_path_resolver; ++ ++ // Init the base database driver override directory. We do this here to do ++ // it only once. ++ $this->initBaseDatabaseDriverOverrideDirectory(); ++ } ++ ++ /** ++ * Initiate the base database driver override directory. ++ */ ++ protected function initBaseDatabaseDriverOverrideDirectory(): void { ++ if (Database::isActiveConnection()) { ++ $database_driver_override_directory = $this->extensionPathResolver->getPath('module', \Drupal::database()->getProvider()) . '/' . InstallStorage::CONFIG_OVERRIDES_DIRECTORY; ++ if (is_dir($database_driver_override_directory)) { ++ // Only set the base database driver when the module providing the ++ // database driver has one. ++ $this->databaseDriverOverrideDirectory = $database_driver_override_directory; ++ } ++ } ++ } ++ ++ /** ++ * Check whether the database driver has a config override directory. ++ * ++ * @return bool ++ * Return TRUE when the database driver has the config override directory. ++ */ ++ protected function hasBaseDatabaseDriverOverrideDirectory(): bool { ++ return (bool) $this->databaseDriverOverrideDirectory; + } + + /** +@@ -128,13 +164,21 @@ public function installDefaultConfig($type, $name) { + $prefix = $name . '.'; + } + ++ $database_driver_override_storage = NULL; ++ if ($this->hasBaseDatabaseDriverOverrideDirectory()) { ++ $database_driver_override_config_directory = $this->databaseDriverOverrideDirectory . '/' . $name . '/install'; ++ if (is_dir($database_driver_override_config_directory)) { ++ $database_driver_override_storage = new FileStorage($database_driver_override_config_directory, StorageInterface::DEFAULT_COLLECTION); ++ } ++ } ++ + // Gets profile storages to search for overrides if necessary. + $profile_storages = $this->getProfileStorages($name); + + // Gather information about all the supported collections. + $collection_info = $this->configManager->getConfigCollectionInfo(); + foreach ($collection_info->getCollectionNames() as $collection) { +- $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages); ++ $config_to_create = $this->getConfigToCreate($storage, $collection, $database_driver_override_storage, $prefix, $profile_storages); + if ($name == $this->drupalGetProfile()) { + // If we're installing a profile ensure simple configuration that + // already exists is excluded as it will have already been written. +@@ -161,7 +205,16 @@ public function installDefaultConfig($type, $name) { + if (is_dir($optional_install_path)) { + // Install any optional config the module provides. + $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION); +- $this->installOptionalConfig($storage, ''); ++ ++ $database_driver_override_storage = NULL; ++ if ($this->hasBaseDatabaseDriverOverrideDirectory()) { ++ $database_driver_override_config_directory = $this->databaseDriverOverrideDirectory . '/' . $name . '/optional'; ++ if (is_dir($database_driver_override_config_directory)) { ++ $database_driver_override_storage = new FileStorage($database_driver_override_config_directory, StorageInterface::DEFAULT_COLLECTION); ++ } ++ } ++ ++ $this->installOptionalConfig($storage, '', $database_driver_override_storage); + } + // Install any optional configuration entities whose dependencies can now + // be met. This searches all the installed modules config/optional +@@ -177,7 +230,7 @@ public function installDefaultConfig($type, $name) { + /** + * {@inheritdoc} + */ +- public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = []) { ++ public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = [], ?StorageInterface $database_driver_override_storage = NULL) { + $profile = $this->drupalGetProfile(); + $enabled_extensions = $this->getEnabledExtensions(); + $existing_config = $this->getActiveStorages()->listAll(); +@@ -221,7 +274,18 @@ public function installOptionalConfig(?StorageInterface $storage = NULL, $depend + + $all_config = array_merge($existing_config, $list); + $all_config = array_combine($all_config, $all_config); +- $config_to_create = $storage->readMultiple($list); ++ ++ // Get the config items from the database driver override directory first. ++ // Get the other config items from the normal storage directory. ++ if ($database_driver_override_storage) { ++ $override_list = $database_driver_override_storage->listAll(); ++ $config_to_create = $database_driver_override_storage->readMultiple($override_list); ++ $list = array_diff($list, $override_list); ++ $config_to_create += $storage->readMultiple($list); ++ } ++ else { ++ $config_to_create = $storage->readMultiple($list); ++ } + // Check to see if the corresponding override storage has any overrides or + // new configuration that can be installed. + if ($profile_storage) { +@@ -268,6 +332,9 @@ public function installOptionalConfig(?StorageInterface $storage = NULL, $depend + * The configuration storage to read configuration from. + * @param string $collection + * The configuration collection to use. ++ * @param StorageInterface|null $database_driver_override_storage ++ * (optional) The database driver override configuration storage to read ++ * configuration from. + * @param string $prefix + * (optional) Limit to configuration starting with the provided string. + * @param \Drupal\Core\Config\StorageInterface[] $profile_storages +@@ -278,11 +345,21 @@ public function installOptionalConfig(?StorageInterface $storage = NULL, $depend + * An array of configuration data read from the source storage keyed by the + * configuration object name. + */ +- protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', array $profile_storages = []) { ++ protected function getConfigToCreate(StorageInterface $storage, $collection, ?StorageInterface $database_driver_override_storage = NULL, $prefix = '', array $profile_storages = []) { + if ($storage->getCollectionName() != $collection) { + $storage = $storage->createCollection($collection); + } +- $data = $storage->readMultiple($storage->listAll($prefix)); ++ ++ // Get the config items from the database driver override directory first. ++ // Get the other config items from the normal storage directory. ++ if ($database_driver_override_storage) { ++ $names = $database_driver_override_storage->listAll($prefix); ++ $data = $database_driver_override_storage->readMultiple($names); ++ $data = array_merge($data, $storage->readMultiple(array_diff($storage->listAll($prefix), $names))); ++ } ++ else { ++ $data = $storage->readMultiple($storage->listAll($prefix)); ++ } + + // Check to see if configuration provided by the install profile has any + // overrides. +@@ -482,13 +559,13 @@ public function isSyncing() { + * Array of configuration object names that already exist keyed by + * collection. + */ +- protected function findPreExistingConfiguration(StorageInterface $storage) { ++ protected function findPreExistingConfiguration(StorageInterface $storage, ?StorageInterface $database_driver_override_storage = NULL) { + $existing_configuration = []; + // Gather information about all the supported collections. + $collection_info = $this->configManager->getConfigCollectionInfo(); + + foreach ($collection_info->getCollectionNames() as $collection) { +- $config_to_create = array_keys($this->getConfigToCreate($storage, $collection)); ++ $config_to_create = array_keys($this->getConfigToCreate($storage, $collection, $database_driver_override_storage)); + $active_storage = $this->getActiveStorages($collection); + foreach ($config_to_create as $config_name) { + if ($active_storage->exists($config_name)) { +@@ -515,6 +592,14 @@ public function checkConfigurationToInstall($type, $name) { + + $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION); + ++ $database_driver_override_storage = NULL; ++ if ($this->hasBaseDatabaseDriverOverrideDirectory()) { ++ $database_driver_override_config_directory = $this->databaseDriverOverrideDirectory . '/' . $name . '/install'; ++ if (is_dir($database_driver_override_config_directory)) { ++ $database_driver_override_storage = new FileStorage($database_driver_override_config_directory, StorageInterface::DEFAULT_COLLECTION); ++ } ++ } ++ + $enabled_extensions = $this->getEnabledExtensions(); + // Add the extension that will be enabled to the list of enabled extensions. + $enabled_extensions[] = $name; +@@ -522,7 +607,7 @@ public function checkConfigurationToInstall($type, $name) { + $profile_storages = $this->getProfileStorages($name); + + // Check the dependencies of configuration provided by the module. +- [$invalid_default_config, $missing_dependencies] = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages); ++ [$invalid_default_config, $missing_dependencies] = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $database_driver_override_storage, $profile_storages); + if (!empty($invalid_default_config)) { + throw UnmetDependenciesException::create($name, array_unique($missing_dependencies, SORT_REGULAR)); + } +@@ -533,7 +618,7 @@ public function checkConfigurationToInstall($type, $name) { + // Throw an exception if the module being installed contains configuration + // that already exists. Additionally, can not continue installing more + // modules because those may depend on the current module being installed. +- $existing_configuration = $this->findPreExistingConfiguration($storage); ++ $existing_configuration = $this->findPreExistingConfiguration($storage, $database_driver_override_storage); + if (!empty($existing_configuration)) { + throw PreExistingConfigException::create($name, $existing_configuration); + } +@@ -547,6 +632,9 @@ public function checkConfigurationToInstall($type, $name) { + * The storage containing the default configuration. + * @param array $enabled_extensions + * A list of all the currently enabled modules and themes. ++ * @param \Drupal\Core\Config\StorageInterface|null $database_driver_override_storage ++ * (optional) The database driver override storage containing the default ++ * configuration. + * @param \Drupal\Core\Config\StorageInterface[] $profile_storages + * An array of storage interfaces containing profile configuration to check + * for overrides. +@@ -557,9 +645,9 @@ public function checkConfigurationToInstall($type, $name) { + * - An array that will be filled with the missing dependency names, keyed + * by the dependents' names. + */ +- protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) { ++ protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, ?StorageInterface $database_driver_override_storage = NULL, array $profile_storages = []) { + $missing_dependencies = []; +- $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages); ++ $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, $database_driver_override_storage, '', $profile_storages); + $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create)); + foreach ($config_to_create as $config_name => $config) { + if ($missing = $this->getMissingDependencies($config_name, $config, $enabled_extensions, $all_config)) { +@@ -691,6 +779,13 @@ protected function getProfileStorages($installing_name = '') { + $profile_path = $this->extensionPathResolver->getPath('module', $profile); + foreach ([InstallStorage::CONFIG_INSTALL_DIRECTORY, InstallStorage::CONFIG_OPTIONAL_DIRECTORY] as $directory) { + if (is_dir($profile_path . '/' . $directory)) { ++ if ($this->hasBaseDatabaseDriverOverrideDirectory()) { ++ $database_driver_override_config_directory = $this->databaseDriverOverrideDirectory . $profile . substr($directory, 6); ++ if (is_dir($database_driver_override_config_directory)) { ++ $profile_storages[] = new FileStorage($database_driver_override_config_directory, StorageInterface::DEFAULT_COLLECTION); ++ } ++ } ++ + $profile_storages[] = new FileStorage($profile_path . '/' . $directory, StorageInterface::DEFAULT_COLLECTION); + } + } +diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php +index c9a25bd19a1e7d62b2969825049cdd94abda5303..20c2de8e916582376f35900fa1f591b2a4f15ab6 100644 +--- a/core/lib/Drupal/Core/Config/DatabaseStorage.php ++++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php +@@ -5,6 +5,7 @@ + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; + use Drupal\Core\DependencyInjection\DependencySerializationTrait; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; + + /** + * Defines the Database storage. +@@ -40,6 +41,15 @@ class DatabaseStorage implements StorageInterface { + */ + protected $collection = StorageInterface::DEFAULT_COLLECTION; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Constructs a new DatabaseStorage. + * +@@ -64,20 +74,41 @@ public function __construct(Connection $connection, $table, array $options = [], + * {@inheritdoc} + */ + public function exists($name) { +- try { +- return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', 0, 1, [ +- ':collection' => $this->collection, +- ':name' => $name, +- ], $this->options)->fetchField(); +- } +- catch (\Exception $e) { +- if ($this->connection->schema()->tableExists($this->table)) { +- throw $e; ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ [ ++ 'collection' => ['$eq' => $this->collection], ++ 'name' => ['$eq' => $name], ++ ], ++ [ ++ 'projection' => ['_id' => 1], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ if ($cursor && !empty($cursor->toArray())) { ++ return TRUE; + } +- // If we attempt a read without actually having the table available, +- // return false so the caller can handle it. ++ + return FALSE; + } ++ else { ++ try { ++ return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', 0, 1, [ ++ ':collection' => $this->collection, ++ ':name' => $name, ++ ], $this->options)->fetchField(); ++ } ++ catch (\Exception $e) { ++ if ($this->connection->schema()->tableExists($this->table)) { ++ throw $e; ++ } ++ // If we attempt a read without actually having the table available, ++ // return false so the caller can handle it. ++ return FALSE; ++ } ++ } + } + + /** +@@ -86,7 +117,22 @@ public function exists($name) { + public function read($name) { + $data = FALSE; + try { +- $raw = $this->connection->query('SELECT [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', [':collection' => $this->collection, ':name' => $name], $this->options)->fetchField(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => $this->collection], 'name' => ['$eq' => $name]], ++ ['projection' => ['data' => 1, '_id' => 0]] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['data']); ++ $raw = $statement->execute()->fetchField(); ++ } ++ else { ++ $raw = $this->connection->query('SELECT [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :name', [ ++ ':collection' => $this->collection, ++ ':name' => $name, ++ ], $this->options)->fetchField(); ++ } + if ($raw !== FALSE) { + $data = $this->decode($raw); + } +@@ -111,7 +157,22 @@ public function readMultiple(array $names) { + + $list = []; + try { +- $list = $this->connection->query('SELECT [name], [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] IN ( :names[] )', [':collection' => $this->collection, ':names[]' => $names], $this->options)->fetchAllKeyed(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => $this->collection], 'name' => ['$in' => $names]], ++ [ ++ 'projection' => ['name' => 1, 'data' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'data']); ++ $list = $statement->execute()->fetchAllKeyed(); ++ } ++ else { ++ $list = $this->connection->query('SELECT [name], [data] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] IN ( :names[] )', [':collection' => $this->collection, ':names[]' => $names], $this->options)->fetchAllKeyed(); ++ } + foreach ($list as &$data) { + $data = $this->decode($data); + } +@@ -131,16 +192,27 @@ public function readMultiple(array $names) { + */ + public function write($name, array $data) { + $data = $this->encode($data); +- try { ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ if (!$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + return $this->doWrite($name, $data); + } +- catch (\Exception $e) { +- // If there was an exception, try to create the table. +- if ($this->ensureTableExists()) { ++ else { ++ try { + return $this->doWrite($name, $data); + } +- // Some other failure that we can not recover from. +- throw new StorageException($e->getMessage(), 0, $e); ++ catch (\Exception $e) { ++ // If there was an exception, try to create the table. ++ if ($this->ensureTableExists()) { ++ return $this->doWrite($name, $data); ++ } ++ // Some other failure that we can not recover from. ++ throw new StorageException($e->getMessage(), 0, $e); ++ } + } + } + +@@ -277,7 +349,12 @@ public function listAll($prefix = '') { + $query->condition('collection', $this->collection, '='); + $query->condition('name', $prefix . '%', 'LIKE'); + $query->orderBy('collection')->orderBy('name'); +- return $query->execute()->fetchCol(); ++ $list = $query->execute()->fetchCol(); ++ if ($this->connection->driver() == 'mongodb') { ++ // MongoDB does not remove duplicate values from the list. ++ return array_unique($list); ++ } ++ return $list; + } + catch (\Exception $e) { + if ($this->connection->schema()->tableExists($this->table)) { +@@ -333,9 +410,24 @@ public function getCollectionName() { + */ + public function getAllCollectionNames() { + try { +- return $this->connection->query('SELECT DISTINCT [collection] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] <> :collection ORDER by [collection]', [ +- ':collection' => StorageInterface::DEFAULT_COLLECTION, +- ])->fetchCol(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $collections = $this->connection->getConnection()->selectCollection($prefixed_table)->distinct( ++ 'collection', ++ ['collection' => ['$ne' => StorageInterface::DEFAULT_COLLECTION]], ++ ['session' => $this->connection->getMongodbSession()] ++ ); ++ ++ // The distinct query does not allow sorting. ++ sort($collections); ++ ++ return $collections; ++ } ++ else { ++ return $this->connection->query('SELECT DISTINCT [collection] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] <> :collection ORDER by [collection]', [ ++ ':collection' => StorageInterface::DEFAULT_COLLECTION, ++ ])->fetchCol(); ++ } + } + catch (\Exception $e) { + if ($this->connection->schema()->tableExists($this->table)) { +diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php +index 136be5d6bd7c54efd5351386a0683b59caaa8055..fffa07ee3c64dc42b595ab9872df1a13db11ea28 100644 +--- a/core/lib/Drupal/Core/Config/InstallStorage.php ++++ b/core/lib/Drupal/Core/Config/InstallStorage.php +@@ -2,6 +2,7 @@ + + namespace Drupal\Core\Config; + ++use Drupal\Core\Database\Database; + use Drupal\Core\Extension\ExtensionDiscovery; + use Drupal\Core\Extension\Extension; + +@@ -33,6 +34,11 @@ class InstallStorage extends FileStorage { + */ + const CONFIG_SCHEMA_DIRECTORY = 'config/schema'; + ++ /** ++ * Extension sub-directory containing configuration database driver overrides. ++ */ ++ const CONFIG_OVERRIDES_DIRECTORY = 'config/overrides'; ++ + /** + * Folder map indexed by configuration name. + * +@@ -47,6 +53,13 @@ class InstallStorage extends FileStorage { + */ + protected $directory; + ++ /** ++ * The database override directory. ++ * ++ * @var string ++ */ ++ protected $databaseDriverOverrideDirectory; ++ + /** + * Constructs an InstallStorage object. + * +@@ -59,6 +72,10 @@ class InstallStorage extends FileStorage { + */ + public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) { + parent::__construct($directory, $collection); ++ ++ // Init the base database driver override directory. We do this here to do ++ // it only once. ++ $this->initBaseDatabaseDriverOverrideDirectory(); + } + + /** +@@ -206,11 +223,84 @@ public function getComponentNames(array $list) { + $folders[basename($file, $extension)] = $directory; + } + } ++ ++ // Let a config item be overridden by a database driver one. ++ if ($this->hasBaseDatabaseDriverOverrideDirectory()) { ++ $database_driver_override_directory = $this->getDatabaseDriverOverrideDirectory($directory, $extension_object); ++ if (is_dir($database_driver_override_directory)) { ++ $database_driver_override_files = scandir($database_driver_override_directory); ++ foreach ($database_driver_override_files as $database_driver_override_file) { ++ if ($database_driver_override_file[0] !== '.' && preg_match($pattern, $database_driver_override_file)) { ++ $folders[basename($database_driver_override_file, $extension)] = $database_driver_override_directory; ++ } ++ } ++ } ++ } + } + } + return $folders; + } + ++ /** ++ * Initiate the base database driver override directory. ++ */ ++ protected function initBaseDatabaseDriverOverrideDirectory(): void { ++ if (Database::isActiveConnection()) { ++ $connection = Database::getConnection(); ++ // Get the module root directory from the autoload directory setting from ++ // the database connection. ++ $database_driver_autoload_directory = $connection->getConnectionOptions()['autoload'] ?? ''; ++ $pos = strpos($database_driver_autoload_directory, 'src/Driver/Database/'); ++ if ($pos !== FALSE) { ++ $database_driver_override_directory = substr($database_driver_autoload_directory, 0, $pos) . self::CONFIG_OVERRIDES_DIRECTORY; ++ if (is_dir($database_driver_override_directory)) { ++ // Only set the base database driver when the module providing the ++ // database driver has one. ++ $this->databaseDriverOverrideDirectory = $database_driver_override_directory; ++ } ++ } ++ } ++ } ++ ++ /** ++ * Check whether the database driver has a config override directory. ++ * ++ * @return bool ++ * Return TRUE when the database driver has the config override directory. ++ */ ++ protected function hasBaseDatabaseDriverOverrideDirectory(): bool { ++ return (bool) $this->databaseDriverOverrideDirectory; ++ } ++ ++ /** ++ * Get the database driver directory for overridden config items. ++ * ++ * @param string $directory ++ * The directory in which to search for config items. ++ * @param \Drupal\Core\Extension\Extension $extension ++ * The extension item from which the config belongs to. ++ * ++ * @return string ++ * The directory to search for by the database driver overridden config ++ * items. ++ */ ++ protected function getDatabaseDriverOverrideDirectory(string $directory, Extension $extension): string { ++ // The overridden config items are in the database drivers override directory ++ $dir = $this->databaseDriverOverrideDirectory . '/' . $extension->getName(); ++ ++ if (str_ends_with($directory, self::CONFIG_INSTALL_DIRECTORY)) { ++ $dir .= '/install'; ++ } ++ elseif (str_ends_with($directory, self::CONFIG_OPTIONAL_DIRECTORY)) { ++ $dir .= '/optional'; ++ } ++ elseif (str_ends_with($directory, self::CONFIG_SCHEMA_DIRECTORY)) { ++ $dir .= '/schema'; ++ } ++ ++ return $dir; ++ } ++ + /** + * Get all configuration names and folders for Drupal core. + * +diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php +index 7a9de46a7d410ebc792743bf136adedbfd5f0b7c..332c9a88125cac8e890a503be18d38802ad35cdc 100644 +--- a/core/lib/Drupal/Core/Database/Connection.php ++++ b/core/lib/Drupal/Core/Database/Connection.php +@@ -1305,6 +1305,9 @@ public function __sleep(): array { + * @param string $root + * The root directory of the Drupal installation. Some database drivers, + * like for example SQLite, need this information. ++ * @param string $hosts ++ * (optional) The host names when there are multiple host names. The host ++ * name in the $url has been replaced with a placeholder. + * + * @return array + * The connection options. +@@ -1319,7 +1322,7 @@ public function __sleep(): array { + * + * @see \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo() + */ +- public static function createConnectionOptionsFromUrl($url, $root) { ++ public static function createConnectionOptionsFromUrl($url, $root, $hosts = '') { + $url_components = parse_url($url); + if (!isset($url_components['scheme'], $url_components['host'], $url_components['path'])) { + throw new \InvalidArgumentException("The database connection URL '$url' is invalid. The minimum requirement is: 'driver://host/database'"); +diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php +index aceb5fd128273f0b95e028cabbb11499f4084bfd..63755c83f4563a7e944242b5d6e353bf73981b04 100644 +--- a/core/lib/Drupal/Core/Database/Database.php ++++ b/core/lib/Drupal/Core/Database/Database.php +@@ -514,23 +514,44 @@ public static function convertDbUrlToConnectionInfo($url, $root, ?bool $include_ + } + $driverName = $matches[1]; + ++ // As MongoDB is a NoSQL database and therefore it works with multiple ++ // servers to create a single logical database. To make maintenance less ++ // complicated MongoDB supports a DNS-constructed seed list. Using DNS to ++ // construct the available servers list allows more flexibility of ++ // deployment and the ability to change the servers in rotation without ++ // reconfiguring clients. ++ if (strpos($driverName, '+') !== FALSE) { ++ $driverNameParts = explode('+', $driverName); ++ $driverName = $driverNameParts[0]; ++ } ++ + // Determine if the database driver is provided by a module. + // @todo https://www.drupal.org/project/drupal/issues/3250999. Refactor when + // all database drivers are provided by modules. + $url_components = parse_url($url); ++ ++ // The function parse_url() can fail for MongoDB. With MongoDB there are ++ // multiple hosts. URLs with multiple hosts are not supported by ++ // parse_url(). Get the host names and replace them with a placeholder ++ // hostname and run parse_url() again. ++ if ($url_components === FALSE) { ++ // The host names are the ones between the character "@" and the character "/". ++ preg_match('/\@(.*)\//', $url, $matches); ++ if (isset($matches[1])) { ++ $hosts = $matches[1]; ++ $url = str_replace($hosts, 'placeholder_host', $url); ++ $url_components = parse_url($url); ++ } ++ } ++ + $url_component_query = $url_components['query'] ?? ''; + parse_str($url_component_query, $query); + +- // Add the module key for core database drivers when the module key is not +- // set. +- if (!isset($query['module']) && in_array($driverName, ['mysql', 'pgsql', 'sqlite'], TRUE)) { +- $query['module'] = $driverName; +- } +- if (!isset($query['module'])) { +- throw new \InvalidArgumentException("Can not convert '$url' to a database connection, the module providing the driver '{$driverName}' is not specified"); +- } ++ // Use the driver name as the module name when the module name is not ++ // provided. ++ $module = $query['module'] ?? $driverName; + +- $driverNamespace = "Drupal\\{$query['module']}\\Driver\\Database\\{$driverName}"; ++ $driverNamespace = "Drupal\\{$module}\\Driver\\Database\\{$driverName}"; + + /** @var \Drupal\Core\Extension\DatabaseDriver $driver */ + $driver = self::getDriverList() +@@ -559,7 +580,7 @@ public static function convertDbUrlToConnectionInfo($url, $root, ?bool $include_ + + $additional_class_loader->register(TRUE); + +- $options = $connection_class::createConnectionOptionsFromUrl($url, $root); ++ $options = $connection_class::createConnectionOptionsFromUrl($url, $root, $hosts ?? ''); + + // Add the necessary information to autoload code. + // @see \Drupal\Core\Site\Settings::initialize() +diff --git a/core/lib/Drupal/Core/Database/Query/ConditionInterface.php b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php +index 01815f2c45a830658c1f74926f1229c53a3f1e66..23a8851ead3423784f1d5542025b8c6932969079 100644 +--- a/core/lib/Drupal/Core/Database/Query/ConditionInterface.php ++++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php +@@ -72,6 +72,28 @@ interface ConditionInterface { + */ + public function condition($field, $value = NULL, $operator = '='); + ++ /** ++ * Compare two database fields with each other. ++ * ++ * This method is used in joins to compare 2 fields from different tables to ++ * each other. ++ * ++ * @param string $field ++ * The name of the field to compare. ++ * @param string $field2 ++ * The name of the other field to compare. ++ * @param string|null $operator ++ * (optional) The operator to use. The supported operators are: =, <>, <, ++ * <=, >, >=, <>. ++ * ++ * @return $this ++ * The called object. ++ * ++ * @throws \Drupal\Core\Database\InvalidQueryException ++ * If passed invalid arguments, such as an empty array as $value. ++ */ ++ public function compare(string $field, string $field2, ?string $operator = '='); ++ + /** + * Adds an arbitrary WHERE clause to the query. + * +diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php +index 06cb31568c2c087555e651ed580c03e05fd3571c..b3d919fde593336c9deaea0c67d45cf5cc04753e 100644 +--- a/core/lib/Drupal/Core/Database/Query/Condition.php ++++ b/core/lib/Drupal/Core/Database/Query/Condition.php +@@ -127,6 +127,51 @@ public function condition($field, $value = NULL, $operator = '=') { + return $this; + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function compare(string $field, string $field2, ?string $operator = '=') { ++ if (!in_array($operator, ['=', '<', '>', '>=', '<=', '<>'], TRUE)) { ++ throw new InvalidQueryException(sprintf("In a query compare '%s %s %s' the operator must be one of the following: '=', '<', '>', '>=', '<=', '<>'.", $field, $operator, $field2)); ++ } ++ ++ $this->conditions[] = [ ++ 'field' => $field, ++ 'field2' => $field2, ++ 'operator' => $operator, ++ ]; ++ ++ $this->changed = TRUE; ++ ++ return $this; ++ } ++ ++ /** ++ * Update the alias placeholder in the condition and its children. ++ * ++ * @param string $placeholder ++ * The value of the placeholder. ++ * @param string $alias ++ * The value to replace the placeholder. ++ * ++ * @internal ++ */ ++ public function updateAliasPlaceholder(string $placeholder, string $alias): void { ++ foreach ($this->conditions as &$condition) { ++ if (isset($condition['field']) && $condition['field'] instanceof ConditionInterface) { ++ $condition['field']->updateAliasPlaceholder($placeholder, $alias); ++ } ++ else { ++ if (isset($condition['field'])) { ++ $condition['field'] = str_replace($placeholder, $alias, $condition['field']); ++ } ++ if (isset($condition['field2'])) { ++ $condition['field2'] = str_replace($placeholder, $alias, $condition['field2']); ++ } ++ } ++ } ++ } ++ + /** + * {@inheritdoc} + */ +@@ -227,6 +272,12 @@ public function compile(Connection $connection, PlaceholderInterface $queryPlace + // condition on its own: ignore the operator and value parts. + $ignore_operator = $condition['operator'] === '=' && $condition['value'] === NULL; + } ++ elseif (isset($condition['field2'])) { ++ // The key field2 is only set when we are comparing 2 fields with each ++ // other. ++ $condition_fragments[] = trim(implode(' ', [$connection->escapeField($condition['field']), $condition['operator'], $connection->escapeField($condition['field2'])])); ++ continue; ++ } + elseif (!isset($condition['operator'])) { + // Left hand part is a literal string added with the + // @see ConditionInterface::where() method. Put brackets around +@@ -361,7 +412,7 @@ public function __clone() { + if ($condition['field'] instanceof ConditionInterface) { + $this->conditions[$key]['field'] = clone($condition['field']); + } +- if ($condition['value'] instanceof SelectInterface) { ++ if (isset($condition['value']) && ($condition['value'] instanceof SelectInterface)) { + $this->conditions[$key]['value'] = clone($condition['value']); + } + } +diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php +index 6f040bd0181433979f190f7c981cdc90e9a500cd..b62e0d423b0196c635ef06bfbebc33848a687b18 100644 +--- a/core/lib/Drupal/Core/Database/Query/Merge.php ++++ b/core/lib/Drupal/Core/Database/Query/Merge.php +@@ -361,7 +361,7 @@ public function execute() { + + $select = $this->connection->select($this->conditionTable) + ->condition($this->condition); +- $select->addExpression('1'); ++ $select->addExpressionConstant('1'); + + if (!$select->execute()->fetchField()) { + try { +diff --git a/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php b/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php +index 3620d9b5d17c9d42b1e368db07be7fc52f9ac1fb..9fbe7098804b3081e8e7cb7a1f95b7fe823820a4 100644 +--- a/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php ++++ b/core/lib/Drupal/Core/Database/Query/PagerSelectExtender.php +@@ -73,7 +73,7 @@ public function execute() { + } + $this->ensureElement(); + +- $total_items = $this->getCountQuery()->execute()->fetchField(); ++ $total_items = (int) $this->getCountQuery()->execute()->fetchField(); + $pager = $this->connection->getPagerManager()->createPager($total_items, $this->limit, $this->element); + $this->range($pager->getCurrentPage() * $this->limit, $this->limit); + +diff --git a/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php b/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php +index 83be429fa581cfa7e4e8360385877c4a72ba6289..8d012c3ab3e5df0fe954e8e1ce205ee704ccd93a 100644 +--- a/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php ++++ b/core/lib/Drupal/Core/Database/Query/QueryConditionTrait.php +@@ -28,6 +28,14 @@ public function condition($field, $value = NULL, $operator = '=') { + return $this; + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function compare($field, $field2, $operator = '=') { ++ $this->condition->compare($field, $field2, $operator); ++ return $this; ++ } ++ + /** + * {@inheritdoc} + */ +diff --git a/core/lib/Drupal/Core/Database/Query/SelectExtender.php b/core/lib/Drupal/Core/Database/Query/SelectExtender.php +index 61d91867a8928081e0e4a4ab612dec50c9cf9dff..d88855918e1457d1940cf504356f95a68044d26c 100644 +--- a/core/lib/Drupal/Core/Database/Query/SelectExtender.php ++++ b/core/lib/Drupal/Core/Database/Query/SelectExtender.php +@@ -123,6 +123,14 @@ public function arguments() { + return $this->query->arguments(); + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function compare(string $field, string $field2, ?string $operator = '=') { ++ $this->query->compare($field, $field2, $operator); ++ return $this; ++ } ++ + /** + * {@inheritdoc} + */ +@@ -359,6 +367,69 @@ public function addExpression($expression, $alias = NULL, $arguments = []) { + return $this->query->addExpression($expression, $alias, $arguments); + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionConstant($constant, $alias = NULL) { ++ return $this->query->addExpressionConstant($constant, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionField($field, $alias = NULL) { ++ return $this->query->addExpressionField($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionMax($field, $alias = NULL) { ++ return $this->query->addExpressionMax($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionMin($field, $alias = NULL) { ++ return $this->query->addExpressionMin($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionSum($field, $alias = NULL) { ++ return $this->query->addExpressionSum($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCount($field, $alias = NULL) { ++ return $this->query->addExpressionCount($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCountAll($alias = NULL) { ++ return $this->query->addExpressionCountAll($alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCountDistinct($field, $alias = NULL) { ++ return $this->query->addExpressionCountDistinct($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCoalesce($fields, $alias = NULL) { ++ return $this->query->addExpressionCoalesce($fields, $alias); ++ } ++ + /** + * {@inheritdoc} + */ +@@ -387,6 +458,13 @@ public function addJoin($type, $table, $alias = NULL, $condition = NULL, $argume + return $this->query->addJoin($type, $table, $alias, $condition, $arguments); + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function joinCondition(string $conjunction = 'AND') { ++ return $this->query->joinCondition($conjunction); ++ } ++ + /** + * {@inheritdoc} + */ +diff --git a/core/lib/Drupal/Core/Database/Query/SelectInterface.php b/core/lib/Drupal/Core/Database/Query/SelectInterface.php +index f3a161af2da6261c8c5a090376495d5ed6746bd6..1c9bd8283f0be78e7e0543687ef12a9047a26f66 100644 +--- a/core/lib/Drupal/Core/Database/Query/SelectInterface.php ++++ b/core/lib/Drupal/Core/Database/Query/SelectInterface.php +@@ -242,6 +242,148 @@ public function fields($table_alias, array $fields = []); + */ + public function addExpression($expression, $alias = NULL, $arguments = []); + ++ /** ++ * Adds a constant as an expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $constant ++ * The field for which to create an expression. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionConstant(string $constant, ?string $alias = NULL); ++ ++ /** ++ * Adds a field expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to create a value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionField(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a maximum field expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to get the maximum value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionMax(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a minimum field expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to get the minimum value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionMin(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a sum field expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to get the sum value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionSum(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a count field expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to get the count value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionCount(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a count all expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionCountAll(?string $alias = NULL); ++ ++ /** ++ * Adds a count distinct expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $field ++ * The field for which to get the count distinct value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionCountDistinct(string $field, ?string $alias = NULL); ++ ++ /** ++ * Adds a coalesce expression to the list of "fields" to be SELECTed. ++ * ++ * @param string $fields ++ * The fields for which to get the coalesce value. ++ * @param string $alias ++ * The alias for this expression. If not specified, one will be generated ++ * automatically in the form "expression_#". The alias will be checked for ++ * uniqueness, so the requested alias may not be the alias that is assigned ++ * in all cases. ++ * ++ * @return string ++ * The unique alias that was assigned for this expression. ++ */ ++ public function addExpressionCoalesce(array $fields, ?string $alias = NULL); ++ + /** + * Default Join against another table in the database. + * +@@ -359,6 +501,17 @@ public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = + */ + public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = []); + ++ /** ++ * Helper method for generation join conditions. ++ * ++ * @param string $conjunction ++ * The operator to use to combine conditions: 'AND' or 'OR'. ++ * ++ * @return \Drupal\Core\Database\Query\ConditionInterface ++ * An object holding a group of conditions. ++ */ ++ public function joinCondition(string $conjunction = 'AND'); ++ + /** + * Orders the result set by a given field. + * +diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php +index 290cbbf487c960815b094874e433043e58430085..6ff10bd3640935691f2868992a6f01c6fad11865 100644 +--- a/core/lib/Drupal/Core/Database/Query/Select.php ++++ b/core/lib/Drupal/Core/Database/Query/Select.php +@@ -601,6 +601,79 @@ public function addExpression($expression, $alias = NULL, $arguments = []) { + return $alias; + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionConstant(string $constant, ?string $alias = NULL) { ++ return $this->addExpression($constant, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionField(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression($field, $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionMax(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression('MAX(' . $field . ')', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionMin(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression('MIN(' . $field . ')', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionSum(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression('SUM(' . $field . ')', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCount(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression('COUNT(' . $field . ')', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCountAll(?string $alias = NULL) { ++ return $this->addExpression('COUNT(*)', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCountDistinct(string $field, ?string $alias = NULL) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ return $this->addExpression('COUNT(DISTINCT(' . $field . '))', $alias); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function addExpressionCoalesce(array $fields, ?string $alias = NULL) { ++ foreach ($fields as &$field) { ++ $field = '[' . str_replace('.', '].[', $field) . ']'; ++ } ++ $expression = 'COALESCE(' . implode(', ', $fields) . ')'; ++ return $this->addExpression($expression, $alias); ++ } ++ + /** + * {@inheritdoc} + */ +@@ -645,6 +718,9 @@ public function addJoin($type, $table, $alias = NULL, $condition = NULL, $argume + if (is_string($condition)) { + $condition = str_replace('%alias', $alias, $condition); + } ++ if ($condition instanceof ConditionInterface) { ++ $condition->updateAliasPlaceholder('%alias', $alias); ++ } + + $this->tables[$alias] = [ + 'join type' => $type, +@@ -657,6 +733,13 @@ public function addJoin($type, $table, $alias = NULL, $condition = NULL, $argume + return $alias; + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function joinCondition(string $conjunction = 'AND') { ++ return $this->connection->condition($conjunction); ++ } ++ + /** + * {@inheritdoc} + */ +@@ -724,7 +807,7 @@ public function countQuery() { + $count = $this->prepareCountQuery(); + + $query = $this->connection->select($count, NULL, $this->queryOptions); +- $query->addExpression('COUNT(*)'); ++ $query->addExpressionCountAll(); + + return $query; + } +@@ -770,7 +853,7 @@ protected function prepareCountQuery() { + + // If we've just removed all fields from the query, make sure there is at + // least one so that the query still runs. +- $count->addExpression('1'); ++ $count->addExpressionConstant('1'); + + // Ordering a count query is a waste of cycles, and breaks on some + // databases anyway. +diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php +index 3724bf94cee3ffbd83c086e062acd70888ef39d0..fd7b8b6a00b5c6e19d6c9340990a627404313fed 100644 +--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php ++++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php +@@ -323,7 +323,7 @@ public function setNewRevision($value = TRUE) { + * {@inheritdoc} + */ + public function getLoadedRevisionId() { +- return $this->loadedRevisionId; ++ return !is_null($this->loadedRevisionId) ? (int) $this->loadedRevisionId : NULL; + } + + /** +diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +index 574d3871db0749a3cd9d2f28bf4f63d3dbbd68aa..f57ae79276e82fd91ff1157bf8681e32c537cbb6 100644 +--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php ++++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +@@ -333,8 +333,8 @@ protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) + } + + $query = $this->getQuery() +- ->condition($this->entityType->getKey('id'), $entity->id()) +- ->condition($this->entityType->getKey('default_langcode'), 0) ++ ->condition($this->entityType->getKey('id'), (int) $entity->id()) ++ ->condition($this->entityType->getKey('default_langcode'), FALSE) + ->accessCheck(FALSE) + ->range(0, 1); + +@@ -483,7 +483,7 @@ public function getLatestRevisionId($entity_id) { + if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) { + $result = $this->getQuery() + ->latestRevision() +- ->condition($this->entityType->getKey('id'), $entity_id) ++ ->condition($this->entityType->getKey('id'), (int) $entity_id) + ->accessCheck(FALSE) + ->execute(); + +@@ -508,8 +508,8 @@ public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) { + if (!isset($this->latestRevisionIds[$entity_id][$langcode])) { + $result = $this->getQuery() + ->allRevisions() +- ->condition($this->entityType->getKey('id'), $entity_id) +- ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode) ++ ->condition($this->entityType->getKey('id'), (int) $entity_id) ++ ->condition($this->entityType->getKey('revision_translation_affected'), TRUE, '=', $langcode) + ->range(0, 1) + ->sort($this->entityType->getKey('revision'), 'DESC') + ->accessCheck(FALSE) +@@ -616,9 +616,14 @@ protected function preLoad(?array &$ids = NULL) { + // If we had to load all the entities ($ids was set to NULL), get an array + // of IDs that still need to be loaded. + else { ++ // For MongoDB all integer values need to be real integer values. ++ $entity_ids = []; ++ foreach (array_keys($entities) as $entity_id) { ++ $entity_ids[] = (int) $entity_id; ++ } + $result = $this->getQuery() + ->accessCheck(FALSE) +- ->condition($this->entityType->getKey('id'), array_keys($entities), 'NOT IN') ++ ->condition($this->entityType->getKey('id'), $entity_ids, 'NOT IN') + ->execute(); + $ids = array_values($result); + } +diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php +index 65de824756c93ff5371d06e9376b758727104ee1..853a448335e836ca861c5765894a6a638c91034a 100644 +--- a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php ++++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php +@@ -134,7 +134,11 @@ protected function prepare() { + // Add a self-join to the base revision table if we're querying only the + // latest revisions. + if ($this->latestRevision && $revision_field) { +- $this->sqlQuery->leftJoin($base_table, 'base_table_2', "[base_table].[$id_field] = [base_table_2].[$id_field] AND [base_table].[$revision_field] < [base_table_2].[$revision_field]"); ++ $this->sqlQuery->leftJoin($base_table, 'base_table_2', ++ $this->sqlQuery->joinCondition() ++ ->compare("base_table.$id_field", "base_table_2.$id_field") ++ ->compare("base_table.$revision_field", "base_table_2.$revision_field", '<') ++ ); + $this->sqlQuery->isNull("base_table_2.$id_field"); + } + +@@ -227,9 +231,12 @@ protected function addSort() { + // Order based on the smallest element of each group if the + // direction is ascending, or on the largest element of each group + // if the direction is descending. +- $function = $direction == 'ASC' ? 'min' : 'max'; +- $expression = "$function($sql_alias)"; +- $expression_alias = $this->sqlQuery->addExpression($expression); ++ if ($direction == 'ASC') { ++ $expression_alias = $this->sqlQuery->addExpressionMin($sql_alias); ++ } ++ else { ++ $expression_alias = $this->sqlQuery->addExpressionMax($sql_alias); ++ } + $this->sqlQuery->orderBy($expression_alias, $direction); + } + } +diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +index a873de2b081b6439af759e82544cee217272d23a..401fe5b3783fc40805af89ea9c06f5688a530e5a 100644 +--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php ++++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +@@ -2,6 +2,7 @@ + + namespace Drupal\Core\Entity\Query\Sql; + ++use Drupal\Core\Database\Query\ConditionInterface; + use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Entity\EntityType; + use Drupal\Core\Entity\Query\QueryException; +@@ -365,7 +366,7 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, + // gets a unique alias. + $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table); + if (!isset($this->entityTables[$key])) { +- $this->entityTables[$key] = $this->addJoin($type, $table, "[%alias].[$id_field] = [$base_table].[$id_field]", $langcode); ++ $this->entityTables[$key] = $this->addJoin($type, $table, $this->sqlQuery->joinCondition()->compare("%alias.$id_field", "$base_table.$id_field"), $langcode); + } + return $this->entityTables[$key]; + } +@@ -411,7 +412,7 @@ protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $b + if ($field->getCardinality() != 1) { + $this->sqlQuery->addMetaData('simple_query', FALSE); + } +- $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "[%alias].[$field_id_field] = [$base_table].[$entity_id_field]", $langcode, $delta); ++ $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, $this->sqlQuery->joinCondition()->compare("%alias.$field_id_field", "$base_table.$entity_id_field"), $langcode, $delta); + } + return $this->fieldTables[$index_prefix . $field_name]; + } +@@ -423,7 +424,7 @@ protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $b + * The join type. + * @param string $table + * The table to join to. +- * @param string $join_condition ++ * @param \Drupal\Core\Database\Query\ConditionInterface|string $join_condition + * The condition on which to join to. + * @param string $langcode + * The langcode used on the join. +@@ -441,14 +442,24 @@ protected function addJoin($type, $table, $join_condition, $langcode, $delta = N + // For a data table, get the entity language key from the entity type. + // A dedicated field table has a hard-coded 'langcode' column. + $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode'; +- $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder(); +- $join_condition .= ' AND [%alias].[' . $langcode_key . '] = ' . $placeholder; +- $arguments[$placeholder] = $langcode; ++ if ($join_condition instanceof ConditionInterface) { ++ $join_condition->condition('%alias.' . $langcode_key, $langcode); ++ } ++ else { ++ $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder(); ++ $join_condition .= ' AND [%alias].[' . $langcode_key . '] = ' . $placeholder; ++ $arguments[$placeholder] = $langcode; ++ } + } + if (isset($delta)) { +- $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder(); +- $join_condition .= ' AND [%alias].[delta] = ' . $placeholder; +- $arguments[$placeholder] = $delta; ++ if ($join_condition instanceof ConditionInterface) { ++ $join_condition->condition('%alias.delta', $delta); ++ } ++ else { ++ $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder(); ++ $join_condition .= ' AND [%alias].[delta] = ' . $placeholder; ++ $arguments[$placeholder] = $delta; ++ } + } + return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments); + } +@@ -499,7 +510,7 @@ protected function getTableMapping($table, $entity_type_id) { + * The alias of the next entity table joined in. + */ + protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) { +- $join_condition = '[%alias].[' . $entity_type->getKey('id') . "] = [$table].[$sql_column]"; ++ $join_condition = $this->sqlQuery->joinCondition()->compare('%alias.' . $entity_type->getKey('id'), "$table.$sql_column"); + return $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition); + } + +diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +index 93398a56fc3bd6d4fba836bfa5805216d890e80e..82658064c1a3b87ab77d07d23e491f9112048108 100644 +--- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php ++++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +@@ -5,6 +5,9 @@ + use Drupal\Core\Entity\ContentEntityTypeInterface; + use Drupal\Core\Entity\EntityTypeInterface; + use Drupal\Core\Field\FieldStorageDefinitionInterface; ++use Drupal\views\ViewsConfigUpdater; ++ ++// cspell:ignore sharded unsharded + + /** + * Defines a default table mapping class. +@@ -62,6 +65,45 @@ class DefaultTableMapping implements TableMappingInterface { + */ + protected $revisionDataTable; + ++ /** ++ * Flag to indicate that we are storing entity data in JSON documents. ++ * ++ * All relational databases (MySQL, MariaDB, PostgreSQL, SQLite, SQL Server ++ * and OracleDB) do not store entity data in JSON documents. Only MongoDB ++ * stores entity data in JSON documents. ++ * ++ * @var bool ++ */ ++ protected bool $jsonStorage; ++ ++ /** ++ * The JSON storage table that stores the all revisions data for the entity. ++ * ++ * @var string ++ */ ++ protected $jsonStorageAllRevisionsTable; ++ ++ /** ++ * The JSON storage table that stores the current revision data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageCurrentRevisionTable; ++ ++ /** ++ * The JSON storage table that stores the latest revision data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageLatestRevisionTable; ++ ++ /** ++ * The JSON storage table that stores the translations data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageTranslationsTable; ++ + /** + * A list of field names per table. + * +@@ -124,23 +166,39 @@ class DefaultTableMapping implements TableMappingInterface { + * @param string $prefix + * (optional) A prefix to be used by all the tables of this mapping. + * Defaults to an empty string. ++ * @param bool $json_storage ++ * (optional) Flag to indicate that we are storing entity data in JSON ++ * documents. Defaults to FALSE. + */ +- public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') { ++ public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '', bool $json_storage = FALSE) { + $this->entityType = $entity_type; + $this->fieldStorageDefinitions = $storage_definitions; + $this->prefix = $prefix; ++ $this->jsonStorage = $json_storage; + + // @todo Remove table names from the entity type definition in + // https://www.drupal.org/node/2232465. + $this->baseTable = $this->prefix . $entity_type->getBaseTable() ?: $entity_type->id(); +- if ($entity_type->isRevisionable()) { +- $this->revisionTable = $this->prefix . $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision'; +- } +- if ($entity_type->isTranslatable()) { +- $this->dataTable = $this->prefix . $entity_type->getDataTable() ?: $entity_type->id() . '_field_data'; ++ if ($this->jsonStorage) { ++ if ($entity_type->isRevisionable()) { ++ $this->jsonStorageAllRevisionsTable = $this->prefix . $entity_type->id() . '_all_revisions'; ++ $this->jsonStorageCurrentRevisionTable = $this->prefix . $entity_type->id() . '_current_revision'; ++ $this->jsonStorageLatestRevisionTable = $this->prefix . $entity_type->id() . '_latest_revision'; ++ } ++ elseif ($entity_type->isTranslatable()) { ++ $this->jsonStorageTranslationsTable = $this->prefix . $entity_type->id() . '_translations'; ++ } + } +- if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) { +- $this->revisionDataTable = $this->prefix . $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision'; ++ else { ++ if ($entity_type->isRevisionable()) { ++ $this->revisionTable = $this->prefix . $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision'; ++ } ++ if ($entity_type->isTranslatable()) { ++ $this->dataTable = $this->prefix . $entity_type->getDataTable() ?: $entity_type->id() . '_field_data'; ++ } ++ if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) { ++ $this->revisionDataTable = $this->prefix . $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision'; ++ } + } + } + +@@ -155,13 +213,16 @@ public function __construct(ContentEntityTypeInterface $entity_type, array $stor + * @param string $prefix + * (optional) A prefix to be used by all the tables of this mapping. + * Defaults to an empty string. ++ * @param bool $json_storage ++ * (optional) Flag to indicate that we are storing entity data in JSON ++ * documents. Defaults to FALSE. + * + * @return static + * + * @internal + */ +- public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') { +- $table_mapping = new static($entity_type, $storage_definitions, $prefix); ++ public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '', bool $json_storage = FALSE) { ++ $table_mapping = new static($entity_type, $storage_definitions, $prefix, $json_storage); + + $revisionable = $entity_type->isRevisionable(); + $translatable = $entity_type->isTranslatable(); +@@ -199,8 +260,10 @@ public static function create(ContentEntityTypeInterface $entity_type, array $st + // denormalized in the base table but also stored in the revision table + // together with the entity ID and the revision ID as identifiers. + $table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields)); +- $revision_key_fields = [$id_key, $revision_key]; +- $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); ++ if (!$json_storage) { ++ $revision_key_fields = [$id_key, $revision_key]; ++ $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); ++ } + } + elseif (!$revisionable && $translatable) { + // Multilingual layouts store key field values in the base table. The +@@ -210,9 +273,10 @@ public static function create(ContentEntityTypeInterface $entity_type, array $st + // performant queries. This means that only the UUID is not stored on + // the data table. Make sure the ID is always in the list, even if the ID + // key and the UUID key point to the same field. +- $table_mapping +- ->setFieldNames($table_mapping->baseTable, $key_fields) +- ->setFieldNames($table_mapping->dataTable, array_values(array_unique(array_merge([$id_key], array_diff($all_fields, [$uuid_key]))))); ++ $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields); ++ if (!$json_storage) { ++ $table_mapping->setFieldNames($table_mapping->dataTable, array_values(array_unique(array_merge([$id_key], array_diff($all_fields, [$uuid_key]))))); ++ } + } + elseif ($revisionable && $translatable) { + // The revisionable multilingual layout stores key field values in the +@@ -224,18 +288,24 @@ public static function create(ContentEntityTypeInterface $entity_type, array $st + // table, as well. + $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields); + +- // Like in the multilingual, non-revisionable case the UUID is not +- // in the data table. Additionally, do not store revision metadata +- // fields in the data table. +- $data_fields = array_values(array_unique(array_merge([$id_key], array_diff($all_fields, [$uuid_key], $revision_metadata_fields)))); +- $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields); +- +- $revision_base_fields = array_merge([$id_key, $revision_key, $langcode_key], $revision_metadata_fields); +- $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields); +- +- $revision_data_key_fields = [$id_key, $revision_key, $langcode_key]; +- $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]); +- $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)); ++ if (!$json_storage) { ++ // Like in the multilingual, non-revisionable case the UUID is not ++ // in the data table. Additionally, do not store revision metadata ++ // fields in the data table. ++ $data_fields = array_values(array_unique(array_merge([$id_key], array_diff($all_fields, [$uuid_key], $revision_metadata_fields)))); ++ $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields); ++ ++ $revision_base_fields = array_merge([ ++ $id_key, ++ $revision_key, ++ $langcode_key, ++ ], $revision_metadata_fields); ++ $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields); ++ ++ $revision_data_key_fields = [$id_key, $revision_key, $langcode_key]; ++ $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]); ++ $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)); ++ } + } + + // Add dedicated tables. +@@ -250,14 +320,52 @@ public static function create(ContentEntityTypeInterface $entity_type, array $st + 'langcode', + 'delta', + ]; +- foreach ($dedicated_table_definitions as $field_name => $definition) { +- $tables = [$table_mapping->getDedicatedDataTableName($definition)]; +- if ($revisionable && $definition->isRevisionable()) { +- $tables[] = $table_mapping->getDedicatedRevisionTableName($definition); ++ ++ if ($json_storage) { ++ // Add all fields to all embedded tables, this makes EntityQuery happy! ++ if ($revisionable) { ++ $table_mapping->setFieldNames($table_mapping->jsonStorageAllRevisionsTable, $all_fields); ++ $table_mapping->setFieldNames($table_mapping->jsonStorageCurrentRevisionTable, $all_fields); ++ $table_mapping->setFieldNames($table_mapping->jsonStorageLatestRevisionTable, $all_fields); + } +- foreach ($tables as $table_name) { +- $table_mapping->setFieldNames($table_name, [$field_name]); +- $table_mapping->setExtraColumns($table_name, $extra_columns); ++ elseif ($translatable) { ++ $table_mapping->setFieldNames($table_mapping->jsonStorageTranslationsTable, $all_fields); ++ } ++ ++ foreach ($dedicated_table_definitions as $field_name => $definition) { ++ $tables = []; ++ if ($table_mapping->jsonStorageCurrentRevisionTable) { ++ $tables[] = $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageCurrentRevisionTable); ++ } ++ if ($table_mapping->jsonStorageTranslationsTable) { ++ $tables[] = $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageTranslationsTable); ++ } ++ if ($table_mapping->jsonStorageAllRevisionsTable) { ++ $tables[] = $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageAllRevisionsTable); ++ } ++ if ($table_mapping->jsonStorageLatestRevisionTable) { ++ $tables[] = $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageLatestRevisionTable); ++ } ++ if (!$definition->isTranslatable() && !$definition->isRevisionable()) { ++ $tables[] = $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->baseTable); ++ } ++ ++ foreach ($tables as $table_name) { ++ $table_mapping->setFieldNames($table_name, [$field_name]); ++ $table_mapping->setExtraColumns($table_name, $extra_columns); ++ } ++ } ++ } ++ else { ++ foreach ($dedicated_table_definitions as $field_name => $definition) { ++ $tables = [$table_mapping->getDedicatedDataTableName($definition)]; ++ if ($revisionable && $definition->isRevisionable()) { ++ $tables[] = $table_mapping->getDedicatedRevisionTableName($definition); ++ } ++ foreach ($tables as $table_name) { ++ $table_mapping->setFieldNames($table_name, [$field_name]); ++ $table_mapping->setExtraColumns($table_name, $extra_columns); ++ } + } + } + +@@ -312,6 +420,54 @@ public function getRevisionDataTable() { + return $this->revisionDataTable; + } + ++ /** ++ * Gets the JSON storage all revisions table name. ++ * ++ * @return string|null ++ * The all revisions table name. ++ * ++ * @internal ++ */ ++ public function getJsonStorageAllRevisionsTable() { ++ return $this->jsonStorageAllRevisionsTable; ++ } ++ ++ /** ++ * Gets the JSON storage current revision table name. ++ * ++ * @return string|null ++ * The current revision table name. ++ * ++ * @internal ++ */ ++ public function getJsonStorageCurrentRevisionTable() { ++ return $this->jsonStorageCurrentRevisionTable; ++ } ++ ++ /** ++ * Gets the JSON storage latest revision table name. ++ * ++ * @return string|null ++ * The latest revision table name. ++ * ++ * @internal ++ */ ++ public function getJsonStorageLatestRevisionTable() { ++ return $this->jsonStorageLatestRevisionTable; ++ } ++ ++ /** ++ * Gets the JSON storage translations table name. ++ * ++ * @return string|null ++ * The translations table name. ++ * ++ * @internal ++ */ ++ public function getJsonStorageTranslationsTable() { ++ return $this->jsonStorageTranslationsTable; ++ } ++ + /** + * {@inheritdoc} + */ +@@ -361,18 +517,57 @@ public function getFieldTableName($field_name) { + $result = NULL; + + if (isset($this->fieldStorageDefinitions[$field_name])) { +- // Since a field may be stored in more than one table, we inspect tables +- // in order of relevance: the data table if present is the main place +- // where field data is stored, otherwise the base table is responsible for +- // storing field data. Revision metadata is an exception as it's stored +- // only in the revision table. + $storage_definition = $this->fieldStorageDefinitions[$field_name]; +- $table_names = array_filter([ +- $this->dataTable, +- $this->baseTable, +- $this->revisionTable, +- $this->getDedicatedDataTableName($storage_definition), +- ]); ++ if ($this->jsonStorage) { ++ $table_names = [ ++ $this->baseTable, ++ ]; ++ ++ if (!$storage_definition->isTranslatable() && !$storage_definition->isRevisionable()) { ++ $table_names[] = $this->getJsonStorageDedicatedTableName($storage_definition, $this->baseTable); ++ } ++ ++ if ($this->jsonStorageTranslationsTable) { ++ $table_names = array_merge($table_names, [ ++ $this->jsonStorageTranslationsTable, ++ $this->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageTranslationsTable), ++ ]); ++ } ++ ++ if ($this->jsonStorageCurrentRevisionTable) { ++ $table_names = array_merge($table_names, [ ++ $this->jsonStorageCurrentRevisionTable, ++ $this->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageCurrentRevisionTable), ++ ]); ++ } ++ ++ if ($this->jsonStorageLatestRevisionTable) { ++ $table_names = array_merge($table_names, [ ++ $this->jsonStorageLatestRevisionTable, ++ $this->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageLatestRevisionTable), ++ ]); ++ } ++ ++ if ($this->jsonStorageAllRevisionsTable) { ++ $table_names = array_merge($table_names, [ ++ $this->jsonStorageAllRevisionsTable, ++ $this->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageAllRevisionsTable), ++ ]); ++ } ++ } ++ else { ++ // Since a field may be stored in more than one table, we inspect tables ++ // in order of relevance: the data table if present is the main place ++ // where field data is stored, otherwise the base table is responsible for ++ // storing field data. Revision metadata is an exception as it's stored ++ // only in the revision table. ++ $table_names = array_filter([ ++ $this->dataTable, ++ $this->baseTable, ++ $this->revisionTable, ++ $this->getDedicatedDataTableName($storage_definition), ++ ]); ++ } + + // Collect field columns. + $field_columns = []; +@@ -395,6 +590,16 @@ public function getFieldTableName($field_name) { + throw new SqlContentEntityStorageException("Table information not available for the '$field_name' field."); + } + ++ // The class Drupal\views\ViewsConfigUpdater needs the entity base table ++ // name instead of the embedded table name. ++ // @todo Need to test if this does not makes the driver slow. ++ if ($this->jsonStorage) { ++ $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT & DEBUG_BACKTRACE_IGNORE_ARGS, 2); ++ if (isset($backtrace[1]['class']) && ($backtrace[1]['class'] == ViewsConfigUpdater::class)) { ++ return $this->entityType->getBaseTable(); ++ } ++ } ++ + return $result; + } + +@@ -537,14 +742,60 @@ public function getDedicatedTableNames() { + $definitions = array_filter($this->fieldStorageDefinitions, function ($definition) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($definition); + }); +- $data_tables = array_map(function ($definition) use ($table_mapping) { +- return $table_mapping->getDedicatedDataTableName($definition); +- }, $definitions); +- $revision_tables = array_map(function ($definition) use ($table_mapping) { +- return $table_mapping->getDedicatedRevisionTableName($definition); +- }, $definitions); +- $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables)); +- return $dedicated_tables; ++ ++ if ($this->jsonStorage) { ++ $dedicated_all_revisions_tables = []; ++ if ($table_mapping->jsonStorageAllRevisionsTable) { ++ $dedicated_all_revisions_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageAllRevisionsTable); ++ }, $definitions); ++ } ++ ++ $dedicated_current_revision_tables = []; ++ if ($table_mapping->jsonStorageCurrentRevisionTable) { ++ $dedicated_current_revision_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageCurrentRevisionTable); ++ }, $definitions); ++ } ++ ++ $dedicated_latest_revision_tables = []; ++ if ($table_mapping->jsonStorageLatestRevisionTable) { ++ $dedicated_latest_revision_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageLatestRevisionTable); ++ }, $definitions); ++ } ++ ++ $dedicated_translations_tables = []; ++ if ($table_mapping->jsonStorageTranslationsTable) { ++ $dedicated_translations_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->jsonStorageTranslationsTable); ++ }, $definitions); ++ } ++ ++ $dedicated_non_revision_non_translation_tables = array_map(function ($definition) use ($table_mapping) { ++ if (!$definition->isTranslatable() && !$definition->isRevisionable()) { ++ return $table_mapping->getJsonStorageDedicatedTableName($definition, $table_mapping->baseTable); ++ } ++ }, $definitions); ++ ++ return array_merge( ++ array_values($dedicated_all_revisions_tables), ++ array_values($dedicated_current_revision_tables), ++ array_values($dedicated_latest_revision_tables), ++ array_values($dedicated_translations_tables), ++ array_values($dedicated_non_revision_non_translation_tables), ++ ); ++ } ++ else { ++ $data_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getDedicatedDataTableName($definition); ++ }, $definitions); ++ $revision_tables = array_map(function ($definition) use ($table_mapping) { ++ return $table_mapping->getDedicatedRevisionTableName($definition); ++ }, $definitions); ++ $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables)); ++ return $dedicated_tables; ++ } + } + + /** +@@ -649,4 +900,42 @@ protected function generateFieldTableName(FieldStorageDefinitionInterface $stora + return $table_name; + } + ++ /** ++ * Generates a table name for a field embedded table. ++ * ++ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition ++ * The field storage definition. ++ * @param string $parent_table_name ++ * The parent table name. ++ * @param bool $is_deleted ++ * (optional) Whether the table name holding the values of a deleted field ++ * should be returned. ++ * ++ * @return string ++ * A string containing the generated name for the database table. ++ */ ++ public function getJsonStorageDedicatedTableName(FieldStorageDefinitionInterface $storage_definition, $parent_table_name, $is_deleted = FALSE) { ++ if ($is_deleted) { ++ // When a field is a deleted, the table is renamed to ++ // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with ++ // table names longer than 64 characters, we hash the unique storage ++ // identifier and return the first 10 characters so we end up with a short ++ // unique ID. ++ return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); ++ } ++ else { ++ $table_name = $parent_table_name . '__' . $storage_definition->getName(); ++ // Limit the string to 220 characters, keeping a 16 characters margin for ++ // db prefixes. ++ // The maximum table name for MongoDB is 255 characters for unsharded ++ // collections and 235 characters for sharded collections. ++ // @see: https://www.mongodb.com/docs/manual/reference/limits/#mongodb-limit-Restriction-on-Collection-Names ++ if (strlen($table_name) > 220) { ++ // Truncate the parent table name and hash the of the field UUID. ++ $table_name = substr($parent_table_name, 0, 208) . '__' . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10); ++ } ++ return $table_name; ++ } ++ } ++ + } +diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +index 93029c399df3cc9bc2dfbf3b3b7ddf8a917df9b7..83905f11487d79d70fd07a834cddb465f72c2eaa 100644 +--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php ++++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +@@ -24,6 +24,7 @@ + use Drupal\Core\Language\LanguageInterface; + use Drupal\Core\Language\LanguageManagerInterface; + use Drupal\Core\Utility\Error; ++use Drupal\mongodb\Driver\Database\mongodb\EmbeddedTableData; + use Symfony\Component\DependencyInjection\ContainerInterface; + + /** +@@ -106,6 +107,41 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt + */ + protected $revisionDataTable; + ++ /** ++ * The JSON storage table that stores the all revisions data for the entity. ++ * ++ * @var string ++ */ ++ protected $jsonStorageAllRevisionsTable; ++ ++ /** ++ * The JSON storage table that stores the current revision data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageCurrentRevisionTable; ++ ++ /** ++ * The JSON storage table that stores the latest revision data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageLatestRevisionTable; ++ ++ /** ++ * The JSON storage table that stores the translations data. ++ * ++ * @var string ++ */ ++ protected $jsonStorageTranslationsTable; ++ ++ /** ++ * The MongoDB sequence service. ++ * ++ * @var \Drupal\mongodb\Driver\Database\mongodb\Sequences ++ */ ++ protected $mongoSequences; ++ + /** + * Active database connection. + * +@@ -200,22 +236,40 @@ protected function initTableLayout() { + $this->dataTable = NULL; + $this->revisionDataTable = NULL; + ++ // The JSON storage embedded tables. ++ $this->jsonStorageAllRevisionsTable = NULL; ++ $this->jsonStorageCurrentRevisionTable = NULL; ++ $this->jsonStorageLatestRevisionTable = NULL; ++ $this->jsonStorageTranslationsTable = NULL; ++ + $table_mapping = $this->getTableMapping(); + $this->baseTable = $table_mapping->getBaseTable(); + $revisionable = $this->entityType->isRevisionable(); + if ($revisionable) { + $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id'; +- $this->revisionTable = $table_mapping->getRevisionTable(); ++ if ($this->database->driver() == 'mongodb') { ++ $this->jsonStorageAllRevisionsTable = $table_mapping->getJsonStorageAllRevisionsTable(); ++ $this->jsonStorageCurrentRevisionTable = $table_mapping->getJsonStorageCurrentRevisionTable(); ++ $this->jsonStorageLatestRevisionTable = $table_mapping->getJsonStorageLatestRevisionTable(); ++ } ++ else { ++ $this->revisionTable = $table_mapping->getRevisionTable(); ++ } + } + $translatable = $this->entityType->isTranslatable(); + if ($translatable) { +- $this->dataTable = $table_mapping->getDataTable(); ++ if ($this->database->driver() != 'mongodb') { ++ $this->dataTable = $table_mapping->getDataTable(); ++ } + $this->langcodeKey = $this->entityType->getKey('langcode'); + $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode'); + } +- if ($revisionable && $translatable) { ++ if ($revisionable && $translatable && ($this->database->driver() != 'mongodb')) { + $this->revisionDataTable = $table_mapping->getRevisionDataTable(); + } ++ if (!$revisionable && $translatable && ($this->database->driver() == 'mongodb')) { ++ $this->jsonStorageTranslationsTable = $table_mapping->getJsonStorageTranslationsTable(); ++ } + } + + /** +@@ -258,6 +312,46 @@ public function getRevisionDataTable() { + return $this->revisionDataTable; + } + ++ /** ++ * Gets the JSON storage all revisions table name. ++ * ++ * @return string|false ++ * The table name or FALSE if it is not available. ++ */ ++ public function getJsonStorageAllRevisionsTable() { ++ return $this->jsonStorageAllRevisionsTable; ++ } ++ ++ /** ++ * Gets the JSON storage current revision table name. ++ * ++ * @return string|false ++ * The table name or FALSE if it is not available. ++ */ ++ public function getJsonStorageCurrentRevisionTable() { ++ return $this->jsonStorageCurrentRevisionTable; ++ } ++ ++ /** ++ * Gets the JSON storage latest revision table name. ++ * ++ * @return string|false ++ * The table name or FALSE if it is not available. ++ */ ++ public function getJsonStorageLatestRevisionTable() { ++ return $this->jsonStorageLatestRevisionTable; ++ } ++ ++ /** ++ * Gets the JSON storage translations table name. ++ * ++ * @return string|false ++ * The table name or FALSE if it is not available. ++ */ ++ public function getJsonStorageTranslationsTable() { ++ return $this->jsonStorageTranslationsTable; ++ } ++ + /** + * Gets the entity type's storage schema object. + * +@@ -347,13 +441,13 @@ public function getTableMapping(?array $storage_definitions = NULL) { + // comparing old and new storage schema, we compute the table mapping + // without caching. + if ($storage_definitions) { +- return $this->getCustomTableMapping($this->entityType, $storage_definitions); ++ return $this->getCustomTableMapping($this->entityType, $storage_definitions, '', ($this->database->driver() == 'mongodb')); + } + + // If we are using our internal storage definitions, which is our main use + // case, we can statically cache the computed table mapping. + if (!isset($this->tableMapping)) { +- $this->tableMapping = $this->getCustomTableMapping($this->entityType, $this->fieldStorageDefinitions); ++ $this->tableMapping = $this->getCustomTableMapping($this->entityType, $this->fieldStorageDefinitions, '', ($this->database->driver() == 'mongodb')); + } + + return $this->tableMapping; +@@ -370,15 +464,18 @@ public function getTableMapping(?array $storage_definitions = NULL) { + * @param string $prefix + * (optional) A prefix to be used by all the tables of this mapping. + * Defaults to an empty string. ++ * @param bool $json_storage ++ * (optional) Flag to indicate that we are storing entity data in JSON ++ * documents. Defaults to FALSE. + * + * @return \Drupal\Core\Entity\Sql\TableMappingInterface + * A table mapping object for the entity's tables. + * + * @internal + */ +- public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') { ++ public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '', bool $json_storage = FALSE) { + $prefix = $prefix ?: ($this->temporary ? 'tmp_' : ''); +- return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix); ++ return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix, $json_storage); + } + + /** +@@ -449,57 +546,115 @@ protected function mapFromStorageRecords(array $records, $load_from_revision = F + return []; + } + +- // Get the names of the fields that are stored in the base table and, if +- // applicable, the revision table. Other entity data will be loaded in +- // loadFromSharedTables() and loadFromDedicatedTables(). +- $field_names = $this->tableMapping->getFieldNames($this->baseTable); +- if ($this->revisionTable) { +- $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable))); +- } +- +- $values = []; +- foreach ($records as $id => $record) { +- $values[$id] = []; +- // Skip the item delta and item value levels (if possible) but let the +- // field assign the value as suiting. This avoids unnecessary array +- // hierarchies and saves memory here. +- foreach ($field_names as $field_name) { +- $field_columns = $this->tableMapping->getColumnNames($field_name); +- // Handle field types that store several properties. +- if (count($field_columns) > 1) { +- $definition_columns = $this->fieldStorageDefinitions[$field_name]->getColumns(); +- foreach ($field_columns as $property_name => $column_name) { ++ if ($this->database->driver() == 'mongodb') { ++ // @todo remove: Get all the embedded table names without the base table. ++ $embedded_table_names = $this->database->tableInformation()->getTableEmbeddedTables($this->baseTable); ++ ++ $values_embedded_tables = []; ++ $values = []; ++ foreach ($records as $id => $record) { ++ $values[$id] = []; ++ // Skip the item delta and item value levels (if possible) but let the ++ // field assign the value as suiting. This avoids unnecessary array ++ // hierarchies and saves memory here. ++ foreach ($record as $name => $value) { ++ // Handle columns named [field_name]__[column_name] (e.g for field types ++ // that store several properties). ++ if (in_array($name, $embedded_table_names, TRUE)) { ++ // Add the embedded table data to the values array. ++ $values_embedded_tables[$id][$name] = $value; ++ } ++ elseif ($field_name = strstr($name, '__', TRUE)) { ++ $property_name = substr($name, strpos($name, '__') + 2); ++ // @todo Test if typecasting is necessary. Maybe special case if ++ // $value is null. ++ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = (is_null($value) ? NULL : (string) $value); ++ } ++ else { ++ // Handle columns named directly after the field (e.g if the field ++ // type only stores one property). ++ // @todo Test if typecasting is necessary. Maybe special case if ++ // $value is null. ++ if (is_null($value)) { ++ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = NULL; ++ } ++ elseif ($value === FALSE) { ++ // Drupal expects boolean values with the value FALSE to ++ // have the string value of zero. ++ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = '0'; ++ } ++ else { ++ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = (string) $value; ++ } ++ } ++ } ++ ++ // @todo Check if we can remove the next if-statement. ++ if ($load_from_revision && ($record->{$this->revisionKey} != $load_from_revision)) { ++ $values[$id][$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT] = (string) $load_from_revision; ++ } ++ } ++ ++ // Initialize translations array. ++ $translations = array_fill_keys(array_keys($values), []); ++ ++ // Load values from shared and dedicated tables. ++ $this->loadFromEmbeddedTables($values, $translations, $values_embedded_tables, $load_from_revision); ++ } ++ else { ++ // Get the names of the fields that are stored in the base table and, if ++ // applicable, the revision table. Other entity data will be loaded in ++ // loadFromSharedTables() and loadFromDedicatedTables(). ++ $field_names = $this->tableMapping->getFieldNames($this->baseTable); ++ if ($this->revisionTable) { ++ $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable))); ++ } ++ ++ $values = []; ++ foreach ($records as $id => $record) { ++ $values[$id] = []; ++ // Skip the item delta and item value levels (if possible) but let the ++ // field assign the value as suiting. This avoids unnecessary array ++ // hierarchies and saves memory here. ++ foreach ($field_names as $field_name) { ++ $field_columns = $this->tableMapping->getColumnNames($field_name); ++ // Handle field types that store several properties. ++ if (count($field_columns) > 1) { ++ $definition_columns = $this->fieldStorageDefinitions[$field_name]->getColumns(); ++ foreach ($field_columns as $property_name => $column_name) { ++ if (property_exists($record, $column_name)) { ++ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = !empty($definition_columns[$property_name]['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name}; ++ unset($record->{$column_name}); ++ } ++ } ++ } ++ // Handle field types that store only one property. ++ else { ++ $column_name = reset($field_columns); + if (property_exists($record, $column_name)) { +- $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = !empty($definition_columns[$property_name]['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name}; ++ $columns = $this->fieldStorageDefinitions[$field_name]->getColumns(); ++ $column = reset($columns); ++ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = !empty($column['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name}; + unset($record->{$column_name}); + } + } + } +- // Handle field types that store only one property. +- else { +- $column_name = reset($field_columns); +- if (property_exists($record, $column_name)) { +- $columns = $this->fieldStorageDefinitions[$field_name]->getColumns(); +- $column = reset($columns); +- $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = !empty($column['serialize']) ? unserialize($record->{$column_name}) : $record->{$column_name}; +- unset($record->{$column_name}); +- } ++ ++ // Handle additional record entries that are not provided by an entity ++ // field, such as 'isDefaultRevision'. ++ foreach ($record as $name => $value) { ++ $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value; + } + } + +- // Handle additional record entries that are not provided by an entity +- // field, such as 'isDefaultRevision'. +- foreach ($record as $name => $value) { +- $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value; +- } +- } ++ // Initialize translations array. ++ $translations = array_fill_keys(array_keys($values), []); + +- // Initialize translations array. +- $translations = array_fill_keys(array_keys($values), []); ++ // Load values from shared and dedicated tables. ++ $this->loadFromSharedTables($values, $translations, $load_from_revision); ++ $this->loadFromDedicatedTables($values, $load_from_revision); + +- // Load values from shared and dedicated tables. +- $this->loadFromSharedTables($values, $translations, $load_from_revision); +- $this->loadFromDedicatedTables($values, $load_from_revision); ++ } + + $entities = []; + foreach ($values as $id => $entity_values) { +@@ -512,6 +667,371 @@ protected function mapFromStorageRecords(array $records, $load_from_revision = F + return $entities; + } + ++ /** ++ * Loads values for fields stored in the embedded tables. ++ * ++ * @param array &$values ++ * Associative array of entities values, keyed on the entity ID. ++ * @param array &$translations ++ * List of translations, keyed on the entity ID. ++ * @param array $values_embedded_tables ++ * The values of the embedded tables. ++ * @param int|bool $load_from_revision_id ++ * Flag to indicate whether revisions should be loaded or not. ++ */ ++ protected function loadFromEmbeddedTables(array &$values, array &$translations, array &$values_embedded_tables, $load_from_revision_id = FALSE) { ++ if ($load_from_revision_id && $this->jsonStorageAllRevisionsTable) { ++ $embedded_table = $this->jsonStorageAllRevisionsTable; ++ $table_mapping = $this->getTableMapping(); ++ ++ // Find revisioned fields that are not entity keys. Exclude the langcode ++ // key as the base table holds only the default language. ++ $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]); ++ ++ $revisioned_fields = array_diff($table_mapping->getFieldNames($this->jsonStorageAllRevisionsTable), [$this->idKey, $this->uuidKey]); ++ ++ // If there are no data fields then only revisioned fields are needed ++ // else both data fields and revisioned fields are needed to map the ++ // entity values. ++ $all_fields = $revisioned_fields; ++ ++ // Get the field name for the default revision field. ++ $revision_default_field = $this->entityType->getRevisionMetadataKey('revision_default'); ++ } ++ elseif ($this->jsonStorageCurrentRevisionTable) { ++ $embedded_table = $this->jsonStorageCurrentRevisionTable; ++ $table_mapping = $this->getTableMapping(); ++ ++ // Find revisioned fields that are not entity keys. Exclude the langcode ++ // key as the base table holds only the default language. ++ $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]); ++ ++ $revisioned_fields = array_diff($table_mapping->getFieldNames($this->jsonStorageCurrentRevisionTable), [$this->idKey, $this->uuidKey]); ++ ++ // If there are no data fields then only revisioned fields are needed ++ // else both data fields and revisioned fields are needed to map the ++ // entity values. ++ $all_fields = $revisioned_fields; ++ ++ // Get the field name for the default revision field. ++ $revision_default_field = $this->entityType->getRevisionMetadataKey('revision_default'); ++ } ++ elseif ($this->jsonStorageTranslationsTable) { ++ $embedded_table = $this->jsonStorageTranslationsTable; ++ $table_mapping = $this->getTableMapping(); ++ ++ // Find revisioned fields that are not entity keys. Exclude the langcode ++ // key as the base table holds only the default language. ++ $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]); ++ ++ $translations_fields = array_diff($table_mapping->getFieldNames($this->jsonStorageTranslationsTable), [$this->idKey, $this->uuidKey]); ++ ++ // If there are no data fields then only revisioned fields are needed ++ // else both data fields and revisioned fields are needed to map the ++ // entity values. ++ $all_fields = $translations_fields; ++ ++ // There is no default revision field to be set. ++ $revision_default_field = NULL; ++ } ++ else { ++ $embedded_table = $this->baseTable; ++ $base_fields = []; ++ $all_fields = []; ++ ++ // There is no default revision field to be set. ++ $revision_default_field = NULL; ++ } ++ ++ // Get the field names for the "created" field types ++ $storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId); ++ $created_fields = array_keys(array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) { ++ return $definition->getType() == 'created'; ++ })); ++ ++ $base_fields += [$this->revisionKey]; ++ $base_fields += [$this->langcodeKey]; ++ $base_fields = array_diff($base_fields, $created_fields); ++ if (isset($revisioned_fields) && is_array($revisioned_fields)) { ++ $base_fields = array_diff($base_fields, $revisioned_fields); ++ } ++ if (isset($translations_fields) && is_array($translations_fields)) { ++ $base_fields = array_diff($base_fields, $translations_fields); ++ } ++ ++ // Get the data table and the data revision table data. ++ foreach ($values_embedded_tables as $id => $embedded_tables) { ++ // Get the embedded table data for one entity. ++ $embedded_table_data = []; ++ foreach ($embedded_tables as $embedded_table_name => $embedded_table_rows) { ++ if (!empty($embedded_table_name) && is_array($embedded_table_rows)) { ++ if ($embedded_table_name == $this->jsonStorageTranslationsTable) { ++ $embedded_table_data[$this->jsonStorageTranslationsTable] = $embedded_table_rows; ++ } ++ elseif (($embedded_table_name == $this->jsonStorageCurrentRevisionTable) && !$load_from_revision_id) { ++ $embedded_table_data[$this->jsonStorageCurrentRevisionTable] = $embedded_table_rows; ++ } ++ elseif (($embedded_table_name == $this->jsonStorageAllRevisionsTable) && $load_from_revision_id) { ++ foreach ($embedded_table_rows as $embedded_table_revision) { ++ if ($load_from_revision_id && isset($embedded_table_revision[$this->revisionKey]) && ($embedded_table_revision[$this->revisionKey] == $load_from_revision_id)) { ++ $embedded_table_data[$this->jsonStorageAllRevisionsTable][] = $embedded_table_revision; ++ } ++ } ++ } ++ elseif (!$this->jsonStorageTranslationsTable && !$this->jsonStorageCurrentRevisionTable && !$this->jsonStorageLatestRevisionTable && !$this->jsonStorageAllRevisionsTable) { ++ $embedded_tables[$this->idKey] = $values[$id][$this->idKey][LanguageInterface::LANGCODE_DEFAULT]; ++ // @todo Maybe there should be some else statement for the next if ++ // statement. ++ if (!empty($values[$id][$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) { ++ $embedded_tables[$this->langcodeKey] = $values[$id][$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]; ++ } ++ $embedded_table_data[$this->baseTable] = [$embedded_tables]; ++ } ++ } ++ } ++ ++ // Get the list of translations from the latest revision. ++ // foreach ($values_embedded_tables as $id => $embedded_tables) { ++ // foreach ($embedded_tables as $embedded_table_name => $embedded_table_rows) { ++ // if (is_array($embedded_table_rows)) { ++ // foreach ($embedded_table_rows as $embedded_table_row) { ++ // if (empty($embedded_table_row[$this->defaultLangcodeKey])) { ++ // $langcode = $embedded_table_row[$this->langcodeKey]; ++ // } ++ // else { ++ // $langcode = LanguageInterface::LANGCODE_DEFAULT; ++ // } ++ // ++ // if ($embedded_table_name == $this->jsonStorageLatestRevisionTable) { ++ // $translations[$id][$langcode] = TRUE; ++ // } ++ // elseif ($embedded_table_name == $this->jsonStorageTranslationsTable) { ++ // $translations[$id][$langcode] = TRUE; ++ // } ++ // } ++ // } ++ // } ++ // } ++ ++ // Use the collected embedded table data to retrieve the entity values. ++ foreach ($embedded_table_data as $table_rows) { ++ if (is_array($table_rows)) { ++ foreach ($table_rows as $table_row) { ++ $id = $table_row[$this->idKey]; ++ ++ // Field values in default language are stored with ++ // LanguageInterface::LANGCODE_DEFAULT as key. ++ if (!empty($this->defaultLangcodeKey) && !empty($this->langcodeKey) && empty($table_row[$this->defaultLangcodeKey]) && !empty($table_row[$this->langcodeKey])) { ++ $langcode = $table_row[$this->langcodeKey]; ++ } ++ else { ++ $langcode = LanguageInterface::LANGCODE_DEFAULT; ++ } ++ ++ $langcode_is_default_langcode = FALSE; ++ if (isset($values[$id][$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]) && ($values[$id][$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT] == $langcode)) { ++ $langcode_is_default_langcode = TRUE; ++ } ++ ++ $translations[$id][$langcode] = TRUE; ++ ++ foreach ($all_fields as $field_name) { ++ if (!in_array($field_name, $base_fields)) { ++ $storage_definition = $storage_definitions[$field_name]; ++ $definition_columns = $storage_definition->getColumns(); ++ $columns = $table_mapping->getColumnNames($field_name); ++ ++ // Do not key single-column fields by property name. ++ if (count($columns) == 1) { ++ if (is_null($table_row[reset($columns)])) { ++ $values[$id][$field_name][$langcode] = NULL; ++ } ++ elseif ($table_row[reset($columns)] === FALSE) { ++ // Drupal expects boolean values with the value FALSE to ++ // have the string value of zero. ++ $values[$id][$field_name][$langcode] = '0'; ++ } ++ else { ++ $column_name = reset($columns); ++ $column_attributes = $definition_columns[key($columns)]; ++ $values[$id][$field_name][$langcode] = (!empty($column_attributes['serialize'])) ? unserialize($table_row[$column_name]) : (string) $table_row[$column_name]; ++ } ++ ++ if ($langcode_is_default_langcode) { ++ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $values[$id][$field_name][$langcode]; ++ } ++ ++ if ($field_name == $revision_default_field) { ++ if ($table_row[reset($columns)] === FALSE) { ++ $values[$id]['isDefaultRevision'][LanguageInterface::LANGCODE_DEFAULT] = '0'; ++ } ++ else { ++ $values[$id]['isDefaultRevision'][LanguageInterface::LANGCODE_DEFAULT] = '1'; ++ } ++ } ++ } ++ else { ++ $item = []; ++ foreach ($storage_definitions[$field_name]->getColumns() as $column => $attributes) { ++ $column_name = $table_mapping->getFieldColumnName($storage_definitions[$field_name], $column); ++ ++ if (is_null($table_row[$column_name])) { ++ $item[$column] = NULL; ++ } ++ elseif ($table_row[$column_name] === FALSE) { ++ // Drupal expects boolean values with the value FALSE to ++ // have the string value of zero. ++ $item[$column] = '0'; ++ } ++ else { ++ $item[$column] = (!empty($attributes['serialize'])) ? unserialize($table_row[$column_name]) : $table_row[$column_name]; ++ } ++ } ++ ++ $values[$id][$field_name][$langcode] = $item; ++ ++ if ($langcode_is_default_langcode) { ++ $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $values[$id][$field_name][$langcode]; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ $this->loadFromEmbeddedDedicatedTables($values, $embedded_table, $embedded_table_data, $load_from_revision_id); ++ } ++ } ++ ++ /** ++ * Loads values of fields stored in dedicated tables for a group of entities. ++ * ++ * @param array &$values ++ * An array of values keyed by entity ID. ++ * @param string $embedded_table_name ++ * The embedded table name. ++ * @param array $embedded_table_data ++ * The embedded table data. ++ * @param bool $load_from_revision_id ++ * (optional) Flag to indicate whether revisions should be loaded or not, ++ * defaults to FALSE. ++ */ ++ protected function loadFromEmbeddedDedicatedTables(array &$values, $embedded_table_name, array $embedded_table_data, $load_from_revision_id) { ++ if (empty($values)) { ++ return; ++ } ++ ++ // Collect entities ids, bundles and languages. ++ $bundles = []; ++ $ids = []; ++ $default_langcodes = []; ++ foreach ($values as $key => $entity_values) { ++ if ($this->bundleKey && !empty($entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT])) { ++ $bundles[$entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT]] = TRUE; ++ } ++ else { ++ $bundles[$this->entityTypeId] = TRUE; ++ } ++ $ids[] = !$load_from_revision_id ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT]; ++ if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) { ++ $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]; ++ } ++ } ++ ++ // Collect impacted fields. ++ $storage_definitions = []; ++ $definitions = []; ++ $table_mapping = $this->getTableMapping(); ++ foreach ($bundles as $bundle => $v) { ++ $definitions[$bundle] = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle); ++ foreach ($definitions[$bundle] as $field_name => $field_definition) { ++ $storage_definition = $field_definition->getFieldStorageDefinition(); ++ if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { ++ $storage_definitions[$field_name] = $storage_definition; ++ } ++ } ++ } ++ ++ // Load field data. ++ $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL)); ++ foreach ($storage_definitions as $field_name => $storage_definition) { ++ $table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $embedded_table_name); ++ ++ if (isset($embedded_table_data[$embedded_table_name])) { ++ $embedded_table_data = $embedded_table_data[$embedded_table_name]; ++ } ++ ++ $rows = []; ++ $deltas = []; ++ foreach ($embedded_table_data as $embedded_table_row) { ++ foreach ($embedded_table_row as $embedded_table_key => $embedded_table_value) { ++ if (($embedded_table_key == $table) && is_array($embedded_table_value)) { ++ foreach ($embedded_table_value as $dedicated_table_row) { ++ if (in_array($dedicated_table_row['langcode'], $langcodes, TRUE)) { ++ if (!isset($dedicated_table_row['deleted']) || !$dedicated_table_row['deleted']) { ++ // Change the table row entity ID to an integer. ++ if (!$load_from_revision_id && in_array(intval($dedicated_table_row['entity_id']), $ids)) { ++ $rows[] = (object) $dedicated_table_row; ++ $deltas[] = $dedicated_table_row['delta']; ++ } ++ // Change the table row revision ID to an integer. ++ elseif ($load_from_revision_id && in_array(intval($dedicated_table_row['revision_id']), $ids)) { ++ $rows[] = (object) $dedicated_table_row; ++ $deltas[] = $dedicated_table_row['delta']; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ // Sort the dedicated rows according to their delta value. ++ array_multisort($deltas, $rows); ++ ++ foreach ($rows as $row) { ++ $bundle = $row->bundle; ++ ++ if (!in_array($row->langcode, $langcodes, TRUE)) { ++ continue; ++ } ++ if (isset($row->deleted) && $row->deleted) { ++ continue; ++ } ++ ++ // Field values in default language are stored with ++ // LanguageInterface::LANGCODE_DEFAULT as key. ++ $langcode = LanguageInterface::LANGCODE_DEFAULT; ++ if ($this->langcodeKey && isset($default_langcodes[$row->entity_id]) && $row->langcode != $default_langcodes[$row->entity_id]) { ++ $langcode = $row->langcode; ++ } ++ ++ if (!isset($values[$row->entity_id][$field_name][$langcode])) { ++ $values[$row->entity_id][$field_name][$langcode] = []; ++ } ++ ++ // Ensure that records for non-translatable fields having invalid ++ // languages are skipped. ++ if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) { ++ if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$row->entity_id][$field_name][$langcode]) < $storage_definition->getCardinality()) { ++ $item = []; ++ // For each column declared by the field, populate the item from the ++ // prefixed database column. ++ foreach ($storage_definition->getColumns() as $column => $attributes) { ++ $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); ++ // Unserialize the value if specified in the column schema. ++ $item[$column] = (!empty($attributes['serialize']) ? unserialize($row->$column_name) : $row->$column_name); ++ } ++ ++ // Add the item to the field values for the entity. ++ $values[$row->entity_id][$field_name][$langcode][] = $item; ++ } ++ } ++ } ++ } ++ } ++ + /** + * Loads values for fields stored in the shared data tables. + * +@@ -551,7 +1071,11 @@ protected function loadFromSharedTables(array &$values, array &$translations, $l + $all_fields = $revisioned_fields; + if ($data_fields) { + $all_fields = array_merge($revisioned_fields, $data_fields); +- $query->leftJoin($this->dataTable, 'data', "([revision].[$this->idKey] = [data].[$this->idKey] AND [revision].[$this->langcodeKey] = [data].[$this->langcodeKey])"); ++ $query->leftJoin($this->dataTable, 'data', ++ $query->joinCondition() ++ ->compare("revision.$this->idKey", "data.$this->idKey") ++ ->compare("revision.$this->langcodeKey", "data.$this->langcodeKey") ++ ); + $column_names = []; + // Some fields can have more then one columns in the data table so + // column names are needed. +@@ -619,13 +1143,31 @@ protected function doLoadMultipleRevisionsFieldItems($revision_ids) { + $revision_ids = $this->cleanIds($revision_ids, 'revision'); + + if (!empty($revision_ids)) { +- // Build and execute the query. +- $query_result = $this->buildQuery(NULL, $revision_ids)->execute(); +- $records = $query_result->fetchAllAssoc($this->revisionKey); ++ if ($this->database->driver() == 'mongodb') { ++ foreach ($revision_ids as $revision_id) { ++ // Build and execute the query. ++ $query_result = $this->buildQuery([], $revision_id)->execute(); ++ $records = $query_result->fetchAllAssoc($this->idKey); ++ ++ if (!empty($records)) { ++ // Convert the raw records to entity objects. ++ $entities = $this->mapFromStorageRecords($records, $revision_id); ++ $revision = reset($entities) ?: NULL; ++ if ($revision) { ++ $revisions[$revision->getRevisionId()] = $revision; ++ } ++ } ++ } ++ } ++ else { ++ // Build and execute the query. ++ $query_result = $this->buildQuery(NULL, $revision_ids)->execute(); ++ $records = $query_result->fetchAllAssoc($this->revisionKey); + +- // Map the loaded records into entity objects and according fields. +- if ($records) { +- $revisions = $this->mapFromStorageRecords($records, TRUE); ++ // Map the loaded records into entity objects and according fields. ++ if ($records) { ++ $revisions = $this->mapFromStorageRecords($records, TRUE); ++ } + } + } + +@@ -636,31 +1178,55 @@ protected function doLoadMultipleRevisionsFieldItems($revision_ids) { + * {@inheritdoc} + */ + protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) { +- $this->database->delete($this->revisionTable) +- ->condition($this->revisionKey, $revision->getRevisionId()) +- ->execute(); ++ if ($this->database->driver() == 'mongodb') { ++ $revision_id = (int) $revision->getRevisionId(); + +- if ($this->revisionDataTable) { +- $this->database->delete($this->revisionDataTable) ++ $field_data = $this->database->tableInformation()->getTableField($this->baseTable, $this->idKey); ++ if (isset($field_data['type']) && in_array($field_data['type'], ['int', 'serial'])) { ++ $entity_id = (int) $revision->id(); ++ } ++ else { ++ $entity_id = (string) $revision->id(); ++ } ++ ++ $prefixed_table = $this->database->getPrefix() . $this->baseTable; ++ $update_operations = []; ++ $update_operations['$pull'] = [$this->jsonStorageAllRevisionsTable => [$this->revisionKey => $revision_id]]; ++ ++ // Perform all update operations on the entity. ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [$this->idKey => $entity_id], ++ $update_operations, ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ else { ++ $this->database->delete($this->revisionTable) + ->condition($this->revisionKey, $revision->getRevisionId()) + ->execute(); +- } + +- $this->deleteRevisionFromDedicatedTables($revision); ++ if ($this->revisionDataTable) { ++ $this->database->delete($this->revisionDataTable) ++ ->condition($this->revisionKey, $revision->getRevisionId()) ++ ->execute(); ++ } ++ ++ $this->deleteRevisionFromDedicatedTables($revision); ++ } + } + + /** + * {@inheritdoc} + */ + protected function buildPropertyQuery(QueryInterface $entity_query, array $values) { +- if ($this->dataTable) { ++ if ($this->entityType->isTranslatable()) { + // @todo We should not be using a condition to specify whether conditions + // apply to the default language. See + // https://www.drupal.org/node/1866330. + // Default to the original entity language if not explicitly specified + // otherwise. + if (!array_key_exists($this->defaultLangcodeKey, $values)) { +- $values[$this->defaultLangcodeKey] = 1; ++ $values[$this->defaultLangcodeKey] = TRUE; + } + // If the 'default_langcode' flag is explicitly not set, we do not care + // whether the queried values are in the original entity language or not. +@@ -696,43 +1262,96 @@ protected function buildQuery($ids, $revision_ids = FALSE) { + + $query->addTag($this->entityTypeId . '_load_multiple'); + +- if ($revision_ids) { +- $query->join($this->revisionTable, 'revision', "[revision].[{$this->idKey}] = [base].[{$this->idKey}] AND [revision].[{$this->revisionKey}] IN (:revisionIds[])", [':revisionIds[]' => $revision_ids]); +- } +- elseif ($this->revisionTable) { +- $query->join($this->revisionTable, 'revision', "[revision].[{$this->revisionKey}] = [base].[{$this->revisionKey}]"); +- } +- +- // Add fields from the {entity} table. +- $table_mapping = $this->getTableMapping(); +- $entity_fields = $table_mapping->getAllColumns($this->baseTable); ++ if ($this->database->driver() == 'mongodb') { ++ // Add fields from the {entity} table. ++ $table_mapping = $this->getTableMapping(); ++ $entity_fields = $table_mapping->getAllColumns($this->baseTable); ++ ++ $query->fields('base', $entity_fields); ++ ++ $table_information = $this->database->tableInformation(); ++ $table_information->load(TRUE); ++ $embedded_table_names = $table_information->getTableEmbeddedTables($this->entityType->getBaseTable()); ++ $query->fields('base', $embedded_table_names); ++ ++ if ($ids) { ++ // MongoDB needs integer values to be real integers. ++ $definition = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)[$this->idKey]; ++ if ($definition->getType() == 'integer') { ++ if (is_array($ids)) { ++ foreach ($ids as &$id) { ++ $id = (int) $id; ++ } ++ } ++ else { ++ $ids = (int) $ids; ++ } ++ } + +- if ($this->revisionTable) { +- // Add all fields from the {entity_revision} table. +- $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable); +- $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields); +- // The ID field is provided by entity, so remove it. +- unset($entity_revision_fields[$this->idKey]); ++ $query->condition("base.{$this->idKey}", $ids, 'IN'); ++ } + +- // 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]]); ++ if ($revision_ids) { ++ // MongoDB needs integer values to be real integers. ++ $definition = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId)[$this->revisionKey]; ++ if ($definition->getType() == 'integer') { ++ if (is_array($revision_ids)) { ++ foreach ($revision_ids as &$revision_id) { ++ $revision_id = (int) $revision_id; ++ } ++ } ++ else { ++ $revision_ids = (int) $revision_ids; ++ } + } +- } +- $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('CASE [base].[' . $this->revisionKey . '] WHEN [revision].[' . $this->revisionKey . '] THEN 1 ELSE 0 END', 'isDefaultRevision'); ++ $all_revisions_table = $this->getJsonStorageAllRevisionsTable(); ++ $query->condition("base.$all_revisions_table.{$this->revisionKey}", $revision_ids, 'IN'); ++ } + } ++ else { ++ if ($revision_ids) { ++ $query->join($this->revisionTable, 'revision', ++ $query->joinCondition() ++ ->compare("revision.{$this->idKey}", "base.{$this->idKey}") ++ ->condition("revision.{$this->revisionKey}", $revision_ids, 'IN') ++ ); ++ } ++ elseif ($this->revisionTable) { ++ $query->join($this->revisionTable, 'revision', $query->joinCondition()->compare("revision.{$this->revisionKey}", "base.{$this->revisionKey}")); ++ } ++ ++ // Add fields from the {entity} table. ++ $table_mapping = $this->getTableMapping(); ++ $entity_fields = $table_mapping->getAllColumns($this->baseTable); + +- $query->fields('base', $entity_fields); ++ if ($this->revisionTable) { ++ // Add all fields from the {entity_revision} table. ++ $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable); ++ $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields); ++ // 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('CASE [base].[' . $this->revisionKey . '] WHEN [revision].[' . $this->revisionKey . '] THEN 1 ELSE 0 END', 'isDefaultRevision'); ++ } + +- if ($ids) { +- $query->condition("base.{$this->idKey}", $ids, 'IN'); ++ $query->fields('base', $entity_fields); ++ ++ if ($ids) { ++ $query->condition("base.{$this->idKey}", $ids, 'IN'); ++ } + } + + return $query; +@@ -748,16 +1367,34 @@ public function delete(array $entities) { + } + + try { +- $transaction = $this->database->startTransaction(); ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } ++ + parent::delete($entities); + + // Ignore replica server temporarily. + \Drupal::service('database.replica_kill_switch')->trigger(); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException(\Drupal::logger($this->entityTypeId), $e); + throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } +@@ -773,26 +1410,28 @@ protected function doDeleteFieldItems($entities) { + ->condition($this->idKey, $ids, 'IN') + ->execute(); + +- if ($this->revisionTable) { +- $this->database->delete($this->revisionTable) +- ->condition($this->idKey, $ids, 'IN') +- ->execute(); +- } ++ if ($this->database->driver() != 'mongodb') { ++ if ($this->revisionTable) { ++ $this->database->delete($this->revisionTable) ++ ->condition($this->idKey, $ids, 'IN') ++ ->execute(); ++ } + +- if ($this->dataTable) { +- $this->database->delete($this->dataTable) +- ->condition($this->idKey, $ids, 'IN') +- ->execute(); +- } ++ if ($this->dataTable) { ++ $this->database->delete($this->dataTable) ++ ->condition($this->idKey, $ids, 'IN') ++ ->execute(); ++ } + +- if ($this->revisionDataTable) { +- $this->database->delete($this->revisionDataTable) +- ->condition($this->idKey, $ids, 'IN') +- ->execute(); +- } ++ if ($this->revisionDataTable) { ++ $this->database->delete($this->revisionDataTable) ++ ->condition($this->idKey, $ids, 'IN') ++ ->execute(); ++ } + +- foreach ($entities as $entity) { +- $this->deleteFromDedicatedTables($entity); ++ foreach ($entities as $entity) { ++ $this->deleteFromDedicatedTables($entity); ++ } + } + } + +@@ -800,20 +1439,50 @@ protected function doDeleteFieldItems($entities) { + * {@inheritdoc} + */ + public function save(EntityInterface $entity) { +- try { +- $transaction = $this->database->startTransaction(); +- $return = parent::save($entity); +- +- // Ignore replica server temporarily. +- \Drupal::service('database.replica_kill_switch')->trigger(); +- return $return; ++ if ($this->database->driver() == 'mongodb') { ++ try { ++ return parent::save($entity); ++ } ++ catch (\Exception $e) { ++ Error::logException(\Drupal::logger($this->entityTypeId), $e); ++ throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); ++ } + } +- catch (\Exception $e) { +- if (isset($transaction)) { +- $transaction->rollBack(); ++ else { ++ try { ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } ++ ++ $return = parent::save($entity); ++ ++ // Ignore replica server temporarily. ++ \Drupal::service('database.replica_kill_switch')->trigger(); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } ++ ++ return $return; ++ } ++ catch (\Exception $e) { ++ if (isset($transaction)) { ++ $transaction->rollBack(); ++ } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } ++ Error::logException(\Drupal::logger($this->entityTypeId), $e); ++ throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } +- Error::logException(\Drupal::logger($this->entityTypeId), $e); +- throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } + } + +@@ -822,7 +1491,18 @@ public function save(EntityInterface $entity) { + */ + public function restore(EntityInterface $entity) { + try { +- $transaction = $this->database->startTransaction(); ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } ++ + // Insert the entity data in the base and data tables only for default + // revisions. + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ +@@ -846,127 +1526,749 @@ public function restore(EntityInterface $entity) { + ->fields((array) $record) + ->execute(); + +- if ($this->revisionDataTable) { +- $this->saveToSharedTables($entity, $this->revisionDataTable); +- } ++ if ($this->revisionDataTable) { ++ $this->saveToSharedTables($entity, $this->revisionDataTable); ++ } ++ } ++ ++ // Insert the entity data in the dedicated tables. ++ $this->saveToDedicatedTables($entity, FALSE, []); ++ ++ // Ignore replica server temporarily. ++ \Drupal::service('database.replica_kill_switch')->trigger(); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } ++ } ++ catch (\Exception $e) { ++ if (isset($transaction)) { ++ $transaction->rollBack(); ++ } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } ++ Error::logException(\Drupal::logger($this->entityTypeId), $e); ++ throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); ++ } ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { ++ $full_save = empty($names); ++ $update = !$full_save || !$entity->isNew(); ++ ++ if ($this->database->driver() == 'mongodb') { ++ // MongoDB does not support auto-increments fields. So we need to add them ++ // ourselves. ++ if ($entity->id() === NULL) { ++ $entity->set($this->idKey, $this->getMongoSequences()->nextEntityId($this->baseTable)); ++ } ++ ++ if ($this->entityType->isRevisionable() && $entity->isNewRevision()) { ++ if ($entity->getRevisionId() === NULL) { ++ $entity->set($this->entityType->getKey('revision'), $this->getMongoSequences()->nextRevisionId($this->baseTable)); ++ } ++ else { ++ // Make sure that the revision_id is not already in use. ++ if ($this->loadRevision($entity->getRevisionId())) { ++ $entity->set($this->entityType->getKey('revision'), $this->getMongoSequences()->nextRevisionId($this->baseTable)); ++ } ++ ++ if ($this->getMongoSequences()->currentRevisionId($this->baseTable) < $entity->getRevisionId()) { ++ $this->getMongoSequences()->setRevisionId($this->baseTable, $entity->getRevisionId()); ++ } ++ } ++ } ++ ++ // Get the current revision ID, so that it can be set correctly in the base ++ // table. ++ if ($this->entityType->isRevisionable() && !$entity->isDefaultRevision()) { ++ $entity_id = $entity->id(); ++ if (is_int($entity_id) || ctype_digit($entity_id)) { ++ $entity_id = (int) $entity_id; ++ } ++ $result = $this->database->select($this->baseTable) ++ ->fields($this->baseTable, [$this->jsonStorageCurrentRevisionTable]) ++ ->condition($this->idKey, $entity_id) ++ ->execute() ++ ->fetchCol(); ++ foreach ($result as $current_revisions) { ++ foreach ($current_revisions as $current_revision) { ++ if (isset($current_revision[$this->revisionKey])) { ++ $current_revision_id = $current_revision[$this->revisionKey]; ++ } ++ } ++ } ++ } ++ ++ if ($this->entityType->isTranslatable() && empty($entity->get($this->langcodeKey)->value)) { ++ $entity->set($this->langcodeKey, $entity->language()->getId()); ++ } ++ ++ $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); ++ $fields = (array) $record; ++ ++ if ($update) { ++ $query = $this->database->update($this->baseTable)->condition($this->idKey, $record->{$this->idKey}); ++ } ++ else { ++ $query = $this->database->insert($this->baseTable); ++ } ++ ++ $embedded_tables = []; ++ if ($this->jsonStorageAllRevisionsTable) { ++ $embedded_tables[] = ['table' => $this->jsonStorageAllRevisionsTable, 'update action' => 'append']; ++ } ++ // Not sure about the change on the next line. It fixes the EntityDuplicateTest. ++ if ($this->jsonStorageCurrentRevisionTable && ($entity->isDefaultRevision() || ($entity->getRevisionId() == $entity->getLoadedRevisionId()))) { ++ $embedded_tables[] = ['table' => $this->jsonStorageCurrentRevisionTable, 'update action' => 'replace']; ++ } ++ if ($this->jsonStorageLatestRevisionTable && ($entity->isNewRevision() || ($entity->getRevisionId() >= $this->getLatestRevisionId($entity->id())))) { ++ $embedded_tables[] = ['table' => $this->jsonStorageLatestRevisionTable, 'update action' => 'replace']; ++ } ++ if ($this->jsonStorageTranslationsTable) { ++ $embedded_tables[] = ['table' => $this->jsonStorageTranslationsTable, 'update action' => 'replace']; ++ } ++ ++ // Get the dedicated table data for the all revisions, current revision, ++ // latest revision and translations tables. ++ $records_allDedicatedTables = $this->getEmbeddedDedicatedTablesRecords($entity, $names); ++ if (empty($embedded_tables)) { ++ // Get the dedicated table data for the base table. ++ $records_dedicatedTables = isset($records_allDedicatedTables[$this->baseTable]) && is_array($records_allDedicatedTables[$this->baseTable]) ? $records_allDedicatedTables[$this->baseTable] : []; ++ ++ $record_baseTable = (array) $record; ++ foreach ($records_dedicatedTables as $dedicated_table_name => $records_dedicatedTable) { ++ $record_baseTable[$dedicated_table_name] = NULL; ++ foreach ($records_dedicatedTable as $record_dedicatedTable) { ++ // The base table idKey does not have to be off the same type as the ++ // dedicated table entity_id (integer vs. string). ++ if (($record_baseTable[$this->idKey] == $record_dedicatedTable['entity_id']) && ++ (empty($this->bundleKey) || ($record_baseTable[$this->bundleKey] === $record_dedicatedTable['bundle'])) && ++ (empty($this->langcodeKey) || ($record_baseTable[$this->langcodeKey] === $record_dedicatedTable['langcode']))) { ++ if (!$record_baseTable[$dedicated_table_name] instanceof EmbeddedTableData) { ++ $record_baseTable[$dedicated_table_name] = $query->embeddedTableData('replace')->fields($record_dedicatedTable); ++ } ++ else { ++ $record_baseTable[$dedicated_table_name]->values($record_dedicatedTable); ++ } ++ } ++ } ++ $fields[$dedicated_table_name] = $record_baseTable[$dedicated_table_name] ?? NULL; ++ } ++ ++ // Dedicated fields with no values set must be set to NULL. ++ $dedicated_table_names = $this->getEmbeddedDedicatedTableNames($entity, $names); ++ if (is_array($dedicated_table_names[$this->baseTable])) { ++ foreach ($dedicated_table_names[$this->baseTable] as $dedicated_table_name) { ++ if (!isset($fields[$dedicated_table_name])) { ++ $fields[$dedicated_table_name] = NULL; ++ } ++ } ++ } ++ } ++ else { ++ foreach ($embedded_tables as $embedded_table) { ++ $embedded_table_name = $embedded_table['table']; ++ ++ // Get the dedicated table data for the embedded table. ++ $records_dedicatedTables = isset($records_allDedicatedTables[$embedded_table_name]) && is_array($records_allDedicatedTables[$embedded_table_name]) ? $records_allDedicatedTables[$embedded_table_name] : []; ++ ++ // Get the embedded table data. ++ $records_embeddedTable = $this->getEmbeddedTableRecords($entity, $embedded_table_name); ++ ++ $data_embeddedTable = NULL; ++ foreach ($records_embeddedTable as $record_embeddedTable) { ++ // Add the dedicated table data to the embedded table row data. ++ foreach ($records_dedicatedTables as $dedicated_table_name => $records_dedicatedTable) { ++ $record_embeddedTable[$dedicated_table_name] = NULL; ++ foreach ($records_dedicatedTable as $record_dedicatedTable) { ++ // The base table idKey does not have to be off the same type as ++ // the dedicated table entity_id (integer vs. string). ++ if ((empty($this->revisionKey) || ($record_embeddedTable[$this->revisionKey] == $record_dedicatedTable['revision_id'])) && ++ (empty($this->bundleKey) || ($record_embeddedTable[$this->bundleKey] === $record_dedicatedTable['bundle'])) && ++ (empty($this->langcodeKey) || ($record_embeddedTable[$this->langcodeKey] === $record_dedicatedTable['langcode']))) { ++ if (!$record_embeddedTable[$dedicated_table_name] instanceof EmbeddedTableData) { ++ $record_embeddedTable[$dedicated_table_name] = $query->embeddedTableData()->fields($record_dedicatedTable); ++ } ++ else { ++ $record_embeddedTable[$dedicated_table_name]->values($record_dedicatedTable); ++ } ++ } ++ } ++ } ++ ++ // Create the embedded table rows. ++ if (!$data_embeddedTable instanceof EmbeddedTableData) { ++ if ($update && $embedded_table['update action'] === 'append') { ++ $action = 'append'; ++ } ++ elseif ($update && $embedded_table['update action'] === 'replace') { ++ $action = 'replace'; ++ } ++ else { ++ $action = ''; ++ } ++ $data_embeddedTable = $query->embeddedTableData($action)->fields($record_embeddedTable); ++ } ++ else { ++ $data_embeddedTable->values($record_embeddedTable); ++ } ++ } ++ $fields[$embedded_table_name] = $data_embeddedTable ?? NULL; ++ } ++ } ++ ++ if ($update) { ++ // Make sure that the revision_id in the base table has the value of the ++ // current revision. ++ if (!empty($this->revisionKey) && !empty($fields[$this->revisionKey]) && !empty($current_revision_id)) { ++ $fields[$this->revisionKey] = (int) $current_revision_id; ++ } ++ ++ $query->fields($fields); ++ $query->execute(); ++ ++ if ($this->entityType->isRevisionable()) { ++ // When updating an entity with revisions and without creating a new ++ // revision creates a problem with MongoDB. The embedded table holding ++ // all the revision data can on update do only one change to the ++ // embedded table data. The new revision data is added to the embedded ++ // table data. In the embedded table holding the all revision data ++ // there are now two sets of revision data for the same revision. When ++ // querying the entity for revision data the query will fail, because ++ // there are two sets of revision data. The older revision data needs ++ // to be removed. ++ $this->cleanupEntityAllRevisionData($entity->id()); ++ } ++ } ++ else { ++ $query->fields($fields); ++ $insert_id = $query->execute(); ++ ++ // Even if this is a new entity the ID key might have been set, in which ++ // case we should not override the provided ID. An ID key that is not set ++ // to any value is interpreted as NULL (or DEFAULT) and thus overridden. ++ if (!isset($record->{$this->idKey})) { ++ $record->{$this->idKey} = $insert_id; ++ } ++ $entity->{$this->idKey} = (string) $record->{$this->idKey}; ++ } ++ } ++ else { ++ if ($full_save) { ++ $shared_table_fields = TRUE; ++ $dedicated_table_fields = TRUE; ++ } ++ else { ++ $table_mapping = $this->getTableMapping(); ++ $shared_table_fields = FALSE; ++ $dedicated_table_fields = []; ++ ++ // Collect the name of fields to be written in dedicated tables and check ++ // whether shared table records need to be updated. ++ foreach ($names as $name) { ++ $storage_definition = $this->fieldStorageDefinitions[$name]; ++ if ($table_mapping->allowsSharedTableStorage($storage_definition)) { ++ $shared_table_fields = TRUE; ++ } ++ elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { ++ $dedicated_table_fields[] = $name; ++ } ++ } ++ } ++ ++ // Update shared table records if necessary. ++ if ($shared_table_fields) { ++ $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); ++ // Create the storage record to be saved. ++ if ($update) { ++ $default_revision = $entity->isDefaultRevision(); ++ if ($default_revision) { ++ $id = $record->{$this->idKey}; ++ // Remove the ID from the record to enable updates on SQL variants ++ // that prevent updating serial columns, for example, mssql. ++ unset($record->{$this->idKey}); ++ $this->database ++ ->update($this->baseTable) ++ ->fields((array) $record) ++ ->condition($this->idKey, $id) ++ ->execute(); ++ } ++ if ($this->revisionTable) { ++ if ($full_save) { ++ $entity->{$this->revisionKey} = $this->saveRevision($entity); ++ } ++ else { ++ $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable); ++ // Remove the revision ID from the record to enable updates on SQL ++ // variants that prevent updating serial columns, for example, ++ // mssql. ++ unset($record->{$this->revisionKey}); ++ $entity->preSaveRevision($this, $record); ++ $this->database ++ ->update($this->revisionTable) ++ ->fields((array) $record) ++ ->condition($this->revisionKey, $entity->getRevisionId()) ++ ->execute(); ++ } ++ } ++ if ($default_revision && $this->dataTable) { ++ $this->saveToSharedTables($entity); ++ } ++ if ($this->revisionDataTable) { ++ $new_revision = $full_save && $entity->isNewRevision(); ++ $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision); ++ } ++ } ++ else { ++ $insert_id = $this->database ++ ->insert($this->baseTable) ++ ->fields((array) $record) ++ ->execute(); ++ // Even if this is a new entity the ID key might have been set, in which ++ // case we should not override the provided ID. An ID key that is not set ++ // to any value is interpreted as NULL (or DEFAULT) and thus overridden. ++ if (!isset($record->{$this->idKey})) { ++ $record->{$this->idKey} = $insert_id; ++ } ++ $entity->{$this->idKey} = (string) $record->{$this->idKey}; ++ if ($this->revisionTable) { ++ $record->{$this->revisionKey} = $this->saveRevision($entity); ++ } ++ if ($this->dataTable) { ++ $this->saveToSharedTables($entity); ++ } ++ if ($this->revisionDataTable) { ++ $this->saveToSharedTables($entity, $this->revisionDataTable); ++ } ++ } ++ } ++ ++ // Update dedicated table records if necessary. ++ if ($dedicated_table_fields) { ++ $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : []; ++ $this->saveToDedicatedTables($entity, $update, $names); ++ } ++ } ++ } ++ ++ /** ++ * Helper method for getting the latest revision ID. ++ */ ++ public function getLatestRevisionId($entity_id) { ++ if (!$this->entityType->isRevisionable()) { ++ return NULL; ++ } ++ ++ if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) { ++ // Create for MongoDB a specific implementation for getting the latest ++ // revision id. MongoDB stores all revision data in a single document/row. ++ // As such there is no need for an aggregate query. ++ $all_revisions = $this->database->select($this->getBaseTable(), 't') ++ ->fields('t', [$this->jsonStorageAllRevisionsTable]) ++ ->condition($this->entityType->getKey('id'), (int) $entity_id) ++ ->execute() ++ ->fetchField(); ++ ++ $latest_revision_id = 0; ++ $revision_key = $this->entityType->getKey('revision'); ++ if (!empty($all_revisions) && is_array($all_revisions)) { ++ foreach ($all_revisions as $revision) { ++ if (isset($revision[$revision_key]) && ($revision[$revision_key] > $latest_revision_id)) { ++ $latest_revision_id = $revision[$revision_key]; ++ } ++ } ++ } ++ ++ $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT] = $latest_revision_id; ++ } ++ ++ return $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT]; ++ } ++ ++ /** ++ * Removes the unneeded revisions from the all_revisions table. ++ * ++ * @param string|int $entity_id ++ * The table name to save to. Defaults to the data table. ++ */ ++ protected function cleanupEntityAllRevisionData($entity_id) { ++ try { ++ // Only do this if the entity is revisionable. ++ if ($this->entityType->isRevisionable()) { ++ $table_mapping = $this->getTableMapping(); ++ // Get the field name for the default revision field. ++ $revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value']; ++ ++ // Make sure that the entity_id is of the correct type (integer or string). ++ $base_table_entity_id_data = $this->database->tableInformation()->getTableField($this->baseTable, $this->idKey); ++ if (isset($base_table_entity_id_data['type']) && in_array($base_table_entity_id_data['type'], ['int', 'serial'])) { ++ $entity_id = (int) $entity_id; ++ } ++ else { ++ $entity_id = (string) $entity_id; ++ } ++ ++ // Get the non-revisionable (translatable and non-translatable) fields. ++ $non_revisionable_translatable_field_names = []; ++ $non_revisionable_non_translatable_field_names = []; ++ foreach ($this->fieldStorageDefinitions as $field_name => $field_definition) { ++ if (!$field_definition->isRevisionable() && !in_array($field_name, [$this->idKey, $this->revisionKey, $this->uuidKey, $this->bundleKey], TRUE)) { ++ if ($field_definition->isTranslatable()) { ++ $non_revisionable_translatable_field_names[] = $field_name; ++ } ++ else { ++ $non_revisionable_non_translatable_field_names[] = $field_name; ++ } ++ } ++ } ++ ++ $prefixed_table = $this->database->getPrefix() . $this->baseTable; ++ $entity_data = $this->database->getConnection()->selectCollection($prefixed_table)->findOne( ++ [$this->idKey => ['$eq' => $entity_id]], ++ [ ++ 'projection' => [$this->jsonStorageAllRevisionsTable => 1, $this->jsonStorageCurrentRevisionTable => 1], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ ++ $non_revisionable_non_translatable_field_data = []; ++ $non_revisionable_translatable_field_data = []; ++ if (isset($entity_data->{$this->jsonStorageCurrentRevisionTable})) { ++ $current_revision_data = (array) $entity_data->{$this->jsonStorageCurrentRevisionTable}; ++ foreach ($current_revision_data as $revision) { ++ // Get the current revision id for setting the default revision field. ++ if (isset($revision->{$this->revisionKey})) { ++ $current_revision_id = $revision->{$this->revisionKey}; ++ } ++ ++ // Get the non-revisionable non-translatable field values from the ++ // current revision. ++ foreach ($non_revisionable_non_translatable_field_names as $non_revisionable_non_translatable_field_name) { ++ if (isset($revision->{$non_revisionable_non_translatable_field_name})) { ++ $non_revisionable_non_translatable_field_data[$non_revisionable_non_translatable_field_name] = $revision->{$non_revisionable_non_translatable_field_name}; ++ } ++ } ++ ++ // Get the non-revisionable translatable field values from the ++ // current revision. ++ foreach ($non_revisionable_translatable_field_names as $non_revisionable_translatable_field_name) { ++ if (isset($revision->{$non_revisionable_translatable_field_name}) && isset($revision->{$this->langcodeKey})) { ++ if (!isset($non_revisionable_translatable_field_data[$non_revisionable_translatable_field_name])) { ++ $non_revisionable_translatable_field_data[$non_revisionable_translatable_field_name] = []; ++ } ++ $non_revisionable_translatable_field_data[$non_revisionable_translatable_field_name][$revision->{$this->langcodeKey}] = $revision->{$non_revisionable_translatable_field_name}; ++ } ++ } ++ } ++ } ++ ++ $revisions_langcodes = []; ++ $new_all_revisions_data = []; ++ if (isset($entity_data->{$this->jsonStorageAllRevisionsTable})) { ++ $all_revisions_data = (array) $entity_data->{$this->jsonStorageAllRevisionsTable}; ++ $all_revisions_data = array_reverse($all_revisions_data); ++ foreach ($all_revisions_data as $revision) { ++ // Update the values of non-revisionable non-translatable fields ++ // for all existing revisions. ++ foreach ($non_revisionable_non_translatable_field_data as $non_revisionable_non_translatable_field_name => $non_revisionable_non_translatable_field_value) { ++ $revision->{$non_revisionable_non_translatable_field_name} = $non_revisionable_non_translatable_field_value; ++ } ++ ++ // @todo We got no testing for this. ++ // Update the values of non-revisionable translatable fields for ++ // all existing revisions. ++ foreach ($non_revisionable_translatable_field_data as $non_revisionable_translatable_field_name => $non_revisionable_translatable_field_values) { ++ if (isset($non_revisionable_translatable_field_values[$revision->{$this->langcodeKey}])) { ++ $revision->{$non_revisionable_translatable_field_name} = $non_revisionable_translatable_field_values[$revision->{$this->langcodeKey}]; ++ } ++ } ++ ++ $exists = FALSE; ++ foreach ($revisions_langcodes as $revision_langcode) { ++ if ($this->entityType->isTranslatable()) { ++ if (($revision_langcode['revision_id'] == $revision->{$this->revisionKey}) && ($revision_langcode['langcode'] == $revision->{$this->langcodeKey})) { ++ $exists = TRUE; ++ } ++ } ++ else { ++ if (($revision_langcode['revision_id'] == $revision->{$this->revisionKey})) { ++ $exists = TRUE; ++ } ++ } ++ if ($current_revision_id && isset($revision->{$this->revisionKey}) && isset($revision->{$revision_default_field})) { ++ if ($revision->{$this->revisionKey} == $current_revision_id) { ++ $revision->{$revision_default_field} = TRUE; ++ } ++ else { ++ // All revisions that are not the current revision should have ++ // set the value of "revision_default" to FALSE. ++ $revision->{$revision_default_field} = FALSE; ++ } ++ } ++ } ++ if (!$exists) { ++ $revisions_langcodes[] = [ ++ 'revision_id' => $revision->{$this->revisionKey}, ++ 'langcode' => $revision->{$this->langcodeKey} ?? 'und', ++ ]; ++ $new_all_revisions_data[] = clone $revision; ++ } ++ } ++ } ++ ++ $new_all_revisions_data = array_reverse($new_all_revisions_data); ++ ++ $set = []; ++ $set[$this->jsonStorageAllRevisionsTable] = $new_all_revisions_data; ++ if (isset($current_revision_id)) { ++ $set[$this->revisionKey] = $current_revision_id; ++ // $this->entityKeys[$this->revisionKey] = $current_revision_id; ++ } ++ ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateOne( ++ [$this->idKey => ['$eq' => $entity_id]], ++ ['$set' => $set], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ } ++ catch (\Exception) { ++ // Throw exception that we could not load the entity. ++ } ++ } ++ ++ /** ++ * Get the fields to be saved from the embedded tables. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityInterface $entity ++ * The entity object. ++ * @param string $table_name ++ * The table name to save to. Defaults to the data table. ++ * ++ * @return array ++ * The records to store for the shared table ++ */ ++ protected function getEmbeddedTableRecords(ContentEntityInterface $entity, $table_name) { ++ $records = []; ++ foreach ($entity->getTranslationLanguages() as $langcode => $language) { ++ $translation = $entity->getTranslation($langcode); ++ $records[] = (array) $this->mapToStorageRecord($translation, $table_name); ++ } ++ ++ return $records; ++ } ++ ++ /** ++ * Get the fields to be saved from the embedded dedicated tables. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityInterface $entity ++ * The entity object. ++ * @param string $names ++ * The table names to save to. Defaults to the data table. ++ * ++ * @return array ++ * The records to store for the shared table ++ */ ++ protected function getEmbeddedDedicatedTableNames(ContentEntityInterface $entity, $names = []) { ++ $bundle = $entity->bundle(); ++ $entity_type = $entity->getEntityTypeId(); ++ $table_mapping = $this->getTableMapping(); ++ $original = !empty($entity->original) ? $entity->original : NULL; ++ ++ // Determine which fields should be actually stored. ++ $definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); ++ if ($names) { ++ $definitions = array_intersect_key($definitions, array_flip($names)); ++ } ++ ++ $dedicated_table_names = []; ++ if ($this->jsonStorageAllRevisionsTable) { ++ $dedicated_table_names[$this->jsonStorageAllRevisionsTable] = []; ++ } ++ if ($this->jsonStorageCurrentRevisionTable) { ++ $dedicated_table_names[$this->jsonStorageCurrentRevisionTable] = []; ++ } ++ if ($this->jsonStorageLatestRevisionTable) { ++ $dedicated_table_names[$this->jsonStorageLatestRevisionTable] = []; ++ } ++ if ($this->jsonStorageTranslationsTable) { ++ $dedicated_table_names[$this->jsonStorageTranslationsTable] = []; ++ } ++ if (!$this->jsonStorageAllRevisionsTable && !$this->jsonStorageCurrentRevisionTable && !$this->jsonStorageLatestRevisionTable && !$this->jsonStorageTranslationsTable) { ++ $dedicated_table_names[$this->baseTable] = []; ++ } ++ ++ foreach ($definitions as $field_definition) { ++ $storage_definition = $field_definition->getFieldStorageDefinition(); ++ if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { ++ continue; ++ } ++ ++ // @todo Test if the code that is below can be deleted. ++ // When updating an existing revision, keep the existing records if the ++ // field values did not change. ++ if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) { ++ continue; ++ } ++ ++ if ($this->jsonStorageAllRevisionsTable) { ++ $dedicated_table_names[$this->jsonStorageAllRevisionsTable][] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageAllRevisionsTable); ++ } ++ ++ if ($this->jsonStorageCurrentRevisionTable) { ++ $dedicated_table_names[$this->jsonStorageCurrentRevisionTable][] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageCurrentRevisionTable); ++ } ++ ++ if ($this->jsonStorageLatestRevisionTable) { ++ $dedicated_table_names[$this->jsonStorageLatestRevisionTable][] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageLatestRevisionTable); + } + +- // Insert the entity data in the dedicated tables. +- $this->saveToDedicatedTables($entity, FALSE, []); ++ if ($this->jsonStorageTranslationsTable) { ++ $dedicated_table_names[$this->jsonStorageTranslationsTable][] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageTranslationsTable); ++ } + +- // Ignore replica server temporarily. +- \Drupal::service('database.replica_kill_switch')->trigger(); +- } +- catch (\Exception $e) { +- if (isset($transaction)) { +- $transaction->rollBack(); ++ if (!$this->jsonStorageAllRevisionsTable && !$this->jsonStorageCurrentRevisionTable && !$this->jsonStorageLatestRevisionTable && !$this->jsonStorageTranslationsTable) { ++ $dedicated_table_names[$this->baseTable][] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->baseTable); + } +- Error::logException(\Drupal::logger($this->entityTypeId), $e); +- throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } ++ ++ return $dedicated_table_names; + } + + /** +- * {@inheritdoc} ++ * Saves values of fields that use embedded dedicated tables. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityInterface $entity ++ * The entity. ++ * @param string[] $names ++ * (optional) The names of the fields to be stored. Defaults to all the ++ * available fields. + */ +- protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { +- $full_save = empty($names); +- $update = !$full_save || !$entity->isNew(); ++ protected function getEmbeddedDedicatedTablesRecords(ContentEntityInterface $entity, $names = []) { ++ $vid = $entity->getRevisionId(); ++ $id = $entity->id(); ++ $bundle = $entity->bundle(); ++ $entity_type = $entity->getEntityTypeId(); ++ $translation_langcodes = array_keys($entity->getTranslationLanguages()); ++ $table_mapping = $this->getTableMapping(); ++ ++ if (!isset($vid)) { ++ $vid = $id; ++ } + +- if ($full_save) { +- $shared_table_fields = TRUE; +- $dedicated_table_fields = TRUE; ++ // Determine which fields should be actually stored. ++ $definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); ++ if ($names) { ++ $definitions = array_intersect_key($definitions, array_flip($names)); + } +- else { +- $table_mapping = $this->getTableMapping(); +- $shared_table_fields = FALSE; +- $dedicated_table_fields = []; + +- // Collect the name of fields to be written in dedicated tables and check +- // whether shared table records need to be updated. +- foreach ($names as $name) { +- $storage_definition = $this->fieldStorageDefinitions[$name]; +- if ($table_mapping->allowsSharedTableStorage($storage_definition)) { +- $shared_table_fields = TRUE; +- } +- elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { +- $dedicated_table_fields[] = $name; +- } +- } ++ $records = []; ++ ++ if ($this->jsonStorageAllRevisionsTable) { ++ $records[$this->jsonStorageAllRevisionsTable] = []; ++ } ++ if ($this->jsonStorageCurrentRevisionTable) { ++ $records[$this->jsonStorageCurrentRevisionTable] = []; ++ } ++ if ($this->jsonStorageLatestRevisionTable) { ++ $records[$this->jsonStorageLatestRevisionTable] = []; ++ } ++ if ($this->jsonStorageTranslationsTable) { ++ $records[$this->jsonStorageTranslationsTable] = []; ++ } ++ if (!$this->jsonStorageAllRevisionsTable && !$this->jsonStorageCurrentRevisionTable && !$this->jsonStorageLatestRevisionTable && !$this->jsonStorageTranslationsTable) { ++ $records[$this->baseTable] = []; + } + +- // Update shared table records if necessary. +- if ($shared_table_fields) { +- $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); +- // Create the storage record to be saved. +- if ($update) { +- $default_revision = $entity->isDefaultRevision(); +- if ($default_revision) { +- $id = $record->{$this->idKey}; +- // Remove the ID from the record to enable updates on SQL variants +- // that prevent updating serial columns, for example, mssql. +- unset($record->{$this->idKey}); +- $this->database +- ->update($this->baseTable) +- ->fields((array) $record) +- ->condition($this->idKey, $id) +- ->execute(); +- } +- if ($this->revisionTable) { +- if ($full_save) { +- $entity->{$this->revisionKey} = $this->saveRevision($entity); ++ foreach ($definitions as $field_name => $field_definition) { ++ $storage_definition = $field_definition->getFieldStorageDefinition(); ++ if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { ++ continue; ++ } ++ ++ $dedicated_all_revisions_table_name = NULL; ++ if ($this->jsonStorageAllRevisionsTable) { ++ $dedicated_all_revisions_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageAllRevisionsTable); ++ } ++ ++ $dedicated_current_revision_table_name = NULL; ++ if ($this->jsonStorageCurrentRevisionTable) { ++ $dedicated_current_revision_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageCurrentRevisionTable); ++ } ++ ++ $dedicated_latest_revision_table_name = NULL; ++ if ($this->jsonStorageLatestRevisionTable) { ++ $dedicated_latest_revision_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageLatestRevisionTable); ++ } ++ ++ $dedicated_translations_table_name = NULL; ++ if ($this->jsonStorageTranslationsTable) { ++ $dedicated_translations_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->jsonStorageTranslationsTable); ++ } ++ ++ $dedicated_base_table_name = NULL; ++ if (!$this->jsonStorageAllRevisionsTable && !$this->jsonStorageCurrentRevisionTable && !$this->jsonStorageLatestRevisionTable && !$this->jsonStorageTranslationsTable) { ++ $dedicated_base_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->baseTable); ++ } ++ ++ // Prepare the multi-insert query. ++ $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode']; ++ foreach ($storage_definition->getColumns() as $column => $attributes) { ++ $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column); ++ } ++ ++ // Save all non-translatable fields for all languages. They belong to ++ // every language. This is also needs for entity filter purposes. ++ foreach ($translation_langcodes as $langcode) { ++ $delta_count = 0; ++ $items = $entity->getTranslation($langcode)->get($field_name); ++ $items->filterEmptyItems(); ++ foreach ($items as $delta => $item) { ++ // We now know we have something to insert. ++ $record = [ ++ 'entity_id' => $id, ++ 'revision_id' => $vid, ++ 'bundle' => $bundle, ++ 'delta' => $delta, ++ 'langcode' => $langcode, ++ ]; ++ foreach ($storage_definition->getColumns() as $column => $attributes) { ++ $column_name = $table_mapping->getFieldColumnName($storage_definition, $column); ++ $value = $item->$column; ++ if (!empty($attributes['serialize'])) { ++ $value = serialize($value); ++ } ++ $record[$column_name] = SqlContentEntityStorageSchema::castValue($attributes, $value); + } +- else { +- $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable); +- // Remove the revision ID from the record to enable updates on SQL +- // variants that prevent updating serial columns, for example, +- // mssql. +- unset($record->{$this->revisionKey}); +- $entity->preSaveRevision($this, $record); +- $this->database +- ->update($this->revisionTable) +- ->fields((array) $record) +- ->condition($this->revisionKey, $entity->getRevisionId()) +- ->execute(); ++ if (isset($records[$this->jsonStorageAllRevisionsTable]) && is_array($records[$this->jsonStorageAllRevisionsTable])) { ++ $records[$this->jsonStorageAllRevisionsTable][$dedicated_all_revisions_table_name][] = $record; ++ } ++ if (isset($records[$this->jsonStorageCurrentRevisionTable]) && is_array($records[$this->jsonStorageCurrentRevisionTable])) { ++ $records[$this->jsonStorageCurrentRevisionTable][$dedicated_current_revision_table_name][] = $record; ++ } ++ if (isset($records[$this->jsonStorageLatestRevisionTable]) && is_array($records[$this->jsonStorageLatestRevisionTable])) { ++ $records[$this->jsonStorageLatestRevisionTable][$dedicated_latest_revision_table_name][] = $record; ++ } ++ if (isset($records[$this->jsonStorageTranslationsTable]) && is_array($records[$this->jsonStorageTranslationsTable])) { ++ $records[$this->jsonStorageTranslationsTable][$dedicated_translations_table_name][] = $record; ++ } ++ if (isset($records[$this->baseTable]) && is_array($records[$this->baseTable])) { ++ $records[$this->baseTable][$dedicated_base_table_name][] = $record; ++ } ++ ++ if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) { ++ break; + } +- } +- if ($default_revision && $this->dataTable) { +- $this->saveToSharedTables($entity); +- } +- if ($this->revisionDataTable) { +- $new_revision = $full_save && $entity->isNewRevision(); +- $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision); +- } +- } +- else { +- $insert_id = $this->database +- ->insert($this->baseTable) +- ->fields((array) $record) +- ->execute(); +- // Even if this is a new entity the ID key might have been set, in which +- // case we should not override the provided ID. An ID key that is not set +- // to any value is interpreted as NULL (or DEFAULT) and thus overridden. +- if (!isset($record->{$this->idKey})) { +- $record->{$this->idKey} = $insert_id; +- } +- $entity->{$this->idKey} = (string) $record->{$this->idKey}; +- if ($this->revisionTable) { +- $record->{$this->revisionKey} = $this->saveRevision($entity); +- } +- if ($this->dataTable) { +- $this->saveToSharedTables($entity); +- } +- if ($this->revisionDataTable) { +- $this->saveToSharedTables($entity, $this->revisionDataTable); + } + } + } + +- // Update dedicated table records if necessary. +- if ($dedicated_table_fields) { +- $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : []; +- $this->saveToDedicatedTables($entity, $update, $names); +- } ++ return $records; + } + + /** +@@ -1556,16 +2858,53 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->getTableMapping(); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { +- // Mark all data associated with the field for deletion. +- $table = $table_mapping->getDedicatedDataTableName($storage_definition); +- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); +- $this->database->update($table) +- ->fields(['deleted' => 1]) +- ->execute(); +- if ($this->entityType->isRevisionable()) { +- $this->database->update($revision_table) ++ if ($this->database->driver() == 'mongodb') { ++ $revisionable = $this->entityType->isRevisionable(); ++ $translatable = $this->entityType->isTranslatable(); ++ ++ $dedicated_tables = []; ++ if ($revisionable) { ++ $dedicated_tables[$this->getJsonStorageAllRevisionsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageAllRevisionsTable()); ++ $dedicated_tables[$this->getJsonStorageCurrentRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageCurrentRevisionTable()); ++ $dedicated_tables[$this->getJsonStorageLatestRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageLatestRevisionTable()); ++ } ++ if (!$revisionable && $translatable) { ++ $dedicated_tables[$this->getJsonStorageTranslationsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageTranslationsTable()); ++ } ++ if (!$revisionable && !$translatable) { ++ $dedicated_tables[$this->getBaseTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getBaseTable()); ++ } ++ ++ foreach ($dedicated_tables as $embedded_to_table => $dedicated_table) { ++ $prefixed_table = $this->database->getPrefix() . $this->getBaseTable(); ++ if ($embedded_to_table == $this->getBaseTable()) { ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ ["$dedicated_table" => ['$exists' => TRUE]], ++ ['$set' => ["$dedicated_table.$[].deleted" => TRUE]], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ else { ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ ["$embedded_to_table.$[].$dedicated_table" => ['$exists' => TRUE]], ++ ['$set' => ["$embedded_to_table.$[].$dedicated_table.$[].deleted" => TRUE]], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ } ++ } ++ else { ++ // Mark all data associated with the field for deletion. ++ $table = $table_mapping->getDedicatedDataTableName($storage_definition); ++ $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); ++ $this->database->update($table) + ->fields(['deleted' => 1]) + ->execute(); ++ if ($this->entityType->isRevisionable()) { ++ $this->database->update($revision_table) ++ ->fields(['deleted' => 1]) ++ ->execute(); ++ } + } + } + +@@ -1609,17 +2948,113 @@ public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definiti + $storage_definition = $field_definition->getFieldStorageDefinition(); + // Mark field data as deleted. + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { +- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); +- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); +- $this->database->update($table_name) +- ->fields(['deleted' => 1]) +- ->condition('bundle', $field_definition->getTargetBundle()) +- ->execute(); +- if ($this->entityType->isRevisionable()) { +- $this->database->update($revision_name) ++ if ($this->database->driver() == 'mongodb') { ++ $prefixed_table = $this->database->getPrefix() . $this->getBaseTable(); ++ ++ if ($this->entityType->isRevisionable()) { ++ $all_revisions_table = $this->getJsonStorageAllRevisionsTable(); ++ $dedicated_all_revisions_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $all_revisions_table); ++ $current_revision_table = $this->getJsonStorageCurrentRevisionTable(); ++ $dedicated_current_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $current_revision_table); ++ $latest_revision_table = $this->getJsonStorageLatestRevisionTable(); ++ $dedicated_latest_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $latest_revision_table); ++ ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ "$current_revision_table.$dedicated_current_revision_table" => ['$exists' => TRUE], ++ ], ++ [ ++ '$set' => [ ++ "$current_revision_table.$[].$dedicated_current_revision_table.$[field].deleted" => TRUE, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [["field.bundle" => $field_definition->getTargetBundle()]], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ "$latest_revision_table.$dedicated_latest_revision_table" => ['$exists' => TRUE], ++ ], ++ [ ++ '$set' => [ ++ "$latest_revision_table.$[].$dedicated_latest_revision_table.$[field].deleted" => TRUE, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [["field.bundle" => $field_definition->getTargetBundle()]], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [], ++ [ ++ '$set' => [ ++ "$all_revisions_table.$[dedicated].$dedicated_all_revisions_table.$[].deleted" => TRUE, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [ ++ ["dedicated.$dedicated_all_revisions_table" => ['$exists' => TRUE]], ++ ], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $translations_table = $this->getJsonStorageTranslationsTable(); ++ $dedicated_translations_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $translations_table); ++ ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ "$translations_table.$dedicated_translations_table" => ['$exists' => TRUE], ++ ], ++ [ ++ '$set' => [ ++ "$translations_table.$[].$dedicated_translations_table.$[field].deleted" => TRUE, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [["field.bundle" => $field_definition->getTargetBundle()]], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ } ++ else { ++ $base_table = $this->getBaseTable(); ++ $dedicated_base_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $base_table); ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ $dedicated_base_table => ['$exists' => TRUE], ++ ], ++ [ ++ '$set' => [ ++ "$dedicated_base_table.$[field].deleted" => TRUE, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [["field.bundle" => $field_definition->getTargetBundle()]], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ } ++ } ++ else { ++ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition); ++ $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition); ++ $this->database->update($table_name) + ->fields(['deleted' => 1]) + ->condition('bundle', $field_definition->getTargetBundle()) + ->execute(); ++ if ($this->entityType->isRevisionable()) { ++ $this->database->update($revision_name) ++ ->fields(['deleted' => 1]) ++ ->condition('bundle', $field_definition->getTargetBundle()) ++ ->execute(); ++ } + } + } + } +@@ -1641,49 +3076,114 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit + // Check whether the whole field storage definition is gone, or just some + // bundle fields. + $storage_definition = $field_definition->getFieldStorageDefinition(); ++ $is_deleted = $storage_definition->isDeleted(); + $table_mapping = $this->getTableMapping(); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted()); + +- // Get the entities which we want to purge first. +- $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]); +- $or = $entity_query->orConditionGroup(); +- foreach ($storage_definition->getColumns() as $column_name => $data) { +- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); +- } +- $entity_query +- ->distinct(TRUE) +- ->fields('t', ['entity_id']) +- ->condition('bundle', $field_definition->getTargetBundle()) +- ->range(0, $batch_size); +- + // Create a map of field data table column names to field column names. + $column_map = []; + foreach ($storage_definition->getColumns() as $column_name => $data) { + $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name; + } + +- $entities = []; +- $items_by_entity = []; +- foreach ($entity_query->execute() as $row) { +- $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]) +- ->fields('t') +- ->condition('entity_id', $row['entity_id']) +- ->condition('deleted', 1) +- ->orderBy('delta'); ++ if ($this->database->driver() == 'mongodb') { ++ $dedicated_tables = []; ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_tables[$this->getJsonStorageAllRevisionsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageAllRevisionsTable(), $is_deleted); ++ $dedicated_tables[$this->getJsonStorageCurrentRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageCurrentRevisionTable(), $is_deleted); ++ $dedicated_tables[$this->getJsonStorageLatestRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageLatestRevisionTable(), $is_deleted); ++ } ++ elseif (!$this->entityType->isRevisionable() && $this->entityType->isTranslatable()) { ++ $dedicated_tables[$this->getJsonStorageTranslationsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageTranslationsTable(), $is_deleted); ++ } ++ elseif (!$this->entityType->isRevisionable() && !$this->entityType->isTranslatable()) { ++ $dedicated_tables[$this->getBaseTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getBaseTable(), $is_deleted); ++ } ++ ++ reset($dedicated_tables); ++ $embedded_to_table = key($dedicated_tables); ++ $dedicated_table = current($dedicated_tables); ++ ++ // Get the entities which we want to purge first. ++ $entity_query = $this->database->select($this->getBaseTable(), 't'); ++ if ($embedded_to_table == $this->getBaseTable()) { ++ $entity_query->isNotNull($dedicated_table); ++ $entity_query->condition("$dedicated_table.bundle", $field_definition->getTargetBundle()); ++ $entity_query->fields('t', ["$dedicated_table"]); ++ } ++ else { ++ $entity_query->isNotNull("$embedded_to_table.$dedicated_table"); ++ $entity_query->condition("$embedded_to_table.$dedicated_table.bundle", $field_definition->getTargetBundle()); ++ } ++ $entity_query->range(0, $batch_size); + +- foreach ($item_query->execute() as $item_row) { +- if (!isset($entities[$item_row['revision_id']])) { +- // Create entity with the right revision id and entity id combination. +- $item_row['entity_type'] = $this->entityTypeId; +- // @todo Replace this by an entity object created via an entity +- // factory. https://www.drupal.org/node/1867228. +- $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row); ++ $entities = []; ++ $items_by_entity = []; ++ foreach ($entity_query->execute() as $row) { ++ if ($embedded_to_table == $this->getBaseTable()) { ++ $dedicated_table_rows = $row->$dedicated_table; ++ } ++ else { ++ $dedicated_table_rows = []; ++ $embedded_to_table_rows = $row->$embedded_to_table; ++ foreach ($embedded_to_table_rows as $embedded_to_table_row) { ++ $dedicated_table_rows += $embedded_to_table_row[$dedicated_table]; ++ } ++ } ++ if (is_array($dedicated_table_rows)) { ++ foreach ($dedicated_table_rows as $dedicated_table_row) { ++ if (!isset($entities[$dedicated_table_row['revision_id']])) { ++ // Create entity with the right revision id and entity id combination. ++ $dedicated_table_row['entity_type'] = $this->entityTypeId; ++ // @todo Replace this by an entity object created via an entity ++ // factory, see https://www.drupal.org/node/1867228. ++ $entities[$dedicated_table_row['revision_id']] = _field_create_entity_from_ids((object) $dedicated_table_row); ++ } ++ $item = []; ++ foreach ($column_map as $db_column => $field_column) { ++ $item[$field_column] = $dedicated_table_row[$db_column]; ++ } ++ $items_by_entity[$dedicated_table_row['revision_id']][] = $item; ++ } + } +- $item = []; +- foreach ($column_map as $db_column => $field_column) { +- $item[$field_column] = $item_row[$db_column]; ++ } ++ } ++ else { ++ // Get the entities which we want to purge first. ++ $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]); ++ $or = $entity_query->orConditionGroup(); ++ foreach ($storage_definition->getColumns() as $column_name => $data) { ++ $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); ++ } ++ $entity_query ++ ->distinct(TRUE) ++ ->fields('t', ['entity_id']) ++ ->condition('bundle', $field_definition->getTargetBundle()) ++ ->range(0, $batch_size); ++ ++ $entities = []; ++ $items_by_entity = []; ++ foreach ($entity_query->execute() as $row) { ++ $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]) ++ ->fields('t') ++ ->condition('entity_id', $row['entity_id']) ++ ->condition('deleted', 1) ++ ->orderBy('delta'); ++ ++ foreach ($item_query->execute() as $item_row) { ++ if (!isset($entities[$item_row['revision_id']])) { ++ // Create entity with the right revision id and entity id combination. ++ $item_row['entity_type'] = $this->entityTypeId; ++ // @todo Replace this by an entity object created via an entity ++ // factory. https://www.drupal.org/node/1867228. ++ $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row); ++ } ++ $item = []; ++ foreach ($column_map as $db_column => $field_column) { ++ $item[$field_column] = $item_row[$db_column]; ++ } ++ $items_by_entity[$item_row['revision_id']][] = $item; + } +- $items_by_entity[$item_row['revision_id']][] = $item; + } + } + +@@ -1702,18 +3202,68 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti + $storage_definition = $field_definition->getFieldStorageDefinition(); + $is_deleted = $storage_definition->isDeleted(); + $table_mapping = $this->getTableMapping(); +- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); +- $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); +- $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); +- $this->database->delete($table_name) +- ->condition('revision_id', $revision_id) +- ->condition('deleted', 1) +- ->execute(); +- if ($this->entityType->isRevisionable()) { +- $this->database->delete($revision_name) ++ ++ if ($this->database->driver() == 'mongodb') { ++ $id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); ++ $id_key = $this->entityType->isRevisionable() ? $this->revisionKey : $this->idKey; ++ ++ $dedicated_tables = []; ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_tables[$this->getJsonStorageAllRevisionsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageAllRevisionsTable(), $is_deleted); ++ $dedicated_tables[$this->getJsonStorageCurrentRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageCurrentRevisionTable(), $is_deleted); ++ $dedicated_tables[$this->getJsonStorageLatestRevisionTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageLatestRevisionTable(), $is_deleted); ++ } ++ elseif (!$this->entityType->isRevisionable() && $this->entityType->isTranslatable()) { ++ $dedicated_tables[$this->getJsonStorageTranslationsTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageTranslationsTable(), $is_deleted); ++ } ++ elseif (!$this->entityType->isRevisionable() && !$this->entityType->isTranslatable()) { ++ $dedicated_tables[$this->getBaseTable()] = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getBaseTable(), $is_deleted); ++ } ++ ++ $field_info = $this->database->tableInformation()->getTableField($this->getBaseTable(), $id_key); ++ if (isset($field_info['type']) && in_array($field_info['type'], ['int', 'serial'], TRUE)) { ++ $id = (int) $id; ++ } ++ ++ $prefixed_table = $this->database->getPrefix() . $this->getBaseTable(); ++ foreach ($dedicated_tables as $embedded_to_table => $dedicated_table) { ++ if ($embedded_to_table == $this->getBaseTable()) { ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ $dedicated_table => ['$exists' => TRUE], ++ $id_key => $id, ++ ], ++ ['$unset' => [$dedicated_table => ""]], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ else { ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ "$embedded_to_table.$dedicated_table" => ['$exists' => TRUE], ++ $id_key => $id, ++ ], ++ ['$unset' => ["$embedded_to_table.$dedicated_table" => ""]], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ } ++ } ++ else { ++ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); ++ $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); ++ $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); ++ ++ $this->database->delete($table_name) + ->condition('revision_id', $revision_id) + ->condition('deleted', 1) + ->execute(); ++ if ($this->entityType->isRevisionable()) { ++ $this->database->delete($revision_name) ++ ->condition('revision_id', $revision_id) ++ ->condition('deleted', 1) ++ ->execute(); ++ } + } + } + +@@ -1735,39 +3285,87 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { + $table_mapping = $this->getTableMapping($storage_definitions); + + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { +- $is_deleted = $storage_definition->isDeleted(); +- if ($this->entityType->isRevisionable()) { +- $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); ++ if ($this->database->driver() == 'mongodb') { ++ $query = $this->database->select($this->getBaseTable(), 't'); ++ $or = $query->orConditionGroup(); ++ ++ $is_deleted = $storage_definition->isDeleted(); ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_all_revisions_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageAllRevisionsTable(), $is_deleted); ++ $or->isNotNull($this->getJsonStorageAllRevisionsTable() . '.' . $dedicated_all_revisions_table_name); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $dedicated_translations_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getJsonStorageTranslationsTable(), $is_deleted); ++ $or->isNotNull($this->getJsonStorageTranslationsTable() . '.' . $dedicated_translations_table_name); ++ } ++ else { ++ $dedicated_base_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->getBaseTable(), $is_deleted); ++ $or->isNotNull($dedicated_base_table_name); ++ } ++ ++ $query ++ ->condition($or) ++ ->fields('t', [$this->idKey]); + } + else { +- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); +- } +- $query = $this->database->select($table_name, 't'); +- $or = $query->orConditionGroup(); +- foreach ($storage_definition->getColumns() as $column_name => $data) { +- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); +- } +- $query->condition($or); +- if (!$as_bool) { +- $query +- ->fields('t', ['entity_id']) +- ->distinct(TRUE); ++ $is_deleted = $storage_definition->isDeleted(); ++ if ($this->entityType->isRevisionable()) { ++ $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); ++ } ++ else { ++ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); ++ } ++ $query = $this->database->select($table_name, 't'); ++ $or = $query->orConditionGroup(); ++ foreach ($storage_definition->getColumns() as $column_name => $data) { ++ $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); ++ } ++ $query->condition($or); ++ if (!$as_bool) { ++ $query ++ ->fields('t', ['entity_id']) ++ ->distinct(TRUE); ++ } + } + } + elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) { +- // Ascertain the table this field is mapped too. +- $field_name = $storage_definition->getName(); +- $table_name = $table_mapping->getFieldTableName($field_name); +- $query = $this->database->select($table_name, 't'); +- $or = $query->orConditionGroup(); +- foreach (array_keys($storage_definition->getColumns()) as $property_name) { +- $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name)); +- } +- $query->condition($or); +- if (!$as_bool) { ++ if ($this->database->driver() == 'mongodb') { ++ $query = $this->database->select($this->getBaseTable(), 't'); ++ $or = $query->orConditionGroup(); ++ ++ foreach (array_keys($storage_definition->getColumns()) as $property_name) { ++ if ($this->entityType->isRevisionable()) { ++ $or->isNotNull($this->getJsonStorageAllRevisionsTable() . '.' . $table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ $or->isNotNull($this->getJsonStorageCurrentRevisionTable() . '.' . $table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ $or->isNotNull($this->getJsonStorageLatestRevisionTable() . '.' . $table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $or->isNotNull($this->getJsonStorageTranslationsTable() . '.' . $table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ } ++ else { ++ $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ } ++ } ++ + $query +- ->fields('t', [$this->idKey]) +- ->distinct(TRUE); ++ ->condition($or) ++ ->fields('t', [$this->idKey]); ++ } ++ else { ++ // Ascertain the table this field is mapped too. ++ $field_name = $storage_definition->getName(); ++ $table_name = $table_mapping->getFieldTableName($field_name); ++ $query = $this->database->select($table_name, 't'); ++ $or = $query->orConditionGroup(); ++ foreach (array_keys($storage_definition->getColumns()) as $property_name) { ++ $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name)); ++ } ++ $query->condition($or); ++ if (!$as_bool) { ++ $query ++ ->fields('t', [$this->idKey]) ++ ->distinct(TRUE); ++ } + } + } + +@@ -1780,7 +3378,7 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { + if ($as_bool) { + $query + ->range(0, 1) +- ->addExpression('1'); ++ ->addExpressionConstant('1'); + } + else { + // Otherwise count the number of rows. +@@ -1791,4 +3389,17 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { + return $as_bool ? (bool) $count : (int) $count; + } + ++ /** ++ * Helper method to get the MongoDB table information service. ++ * ++ * @return \Drupal\mongodb\Driver\Database\mongodb\TableInformation ++ * The MongoDB table information service. ++ */ ++ protected function getMongoSequences() { ++ if (!isset($this->mongoSequences)) { ++ $this->mongoSequences = \Drupal::service('mongodb.sequences'); ++ } ++ return $this->mongoSequences; ++ } ++ + } +diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +index 3b3abae2a04d0646681da85643a71a0c3885be79..41bb19f36dba82d34714f68715586b309f711acf 100644 +--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php ++++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +@@ -3,6 +3,7 @@ + namespace Drupal\Core\Entity\Sql; + + use Drupal\Core\Database\Connection; ++use Drupal\Core\Database\DatabaseExceptionWrapper; + use Drupal\Core\DependencyInjection\DependencySerializationTrait; + use Drupal\Core\Entity\ContentEntityTypeInterface; + use Drupal\Core\Entity\EntityFieldManagerInterface; +@@ -199,7 +200,7 @@ protected function getTableMapping(EntityTypeInterface $entity_type, ?array $sto + $field_storage_definitions = $storage_definitions ?: $this->fieldStorageDefinitions; + } + +- return $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions); ++ return $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, '', ($this->database->driver() == 'mongodb')); + } + + /** +@@ -385,17 +386,39 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); + $schema_handler = $this->database->schema(); + +- // Delete entity and field tables. +- $table_names = $this->getTableNames($entity_type, $this->fieldStorageDefinitions, $this->getTableMapping($entity_type)); +- foreach ($table_names as $table_name) { +- if ($schema_handler->tableExists($table_name)) { +- $schema_handler->dropTable($table_name); ++ if ($this->database->driver() == 'mongodb') { ++ // Delete entity base table. Deleting the base table also deletes all ++ // embedded tables. ++ if ($schema_handler->tableExists($this->storage->getBaseTable())) { ++ $schema_handler->dropTable($this->storage->getBaseTable()); ++ } ++ ++ // Delete dedicated field tables. ++ $table_mapping = $this->getTableMapping($entity_type, $this->fieldStorageDefinitions); ++ foreach ($this->fieldStorageDefinitions as $field_storage_definition) { ++ // If we have a field having dedicated storage we need to drop it, ++ // otherwise we just remove the related schema data. ++ if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { ++ $this->deleteDedicatedTableSchema($field_storage_definition); ++ } ++ elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { ++ $this->deleteFieldSchemaData($field_storage_definition); ++ } + } + } ++ else { ++ // Delete entity and field tables. ++ $table_names = $this->getTableNames($entity_type, $this->fieldStorageDefinitions, $this->getTableMapping($entity_type)); ++ foreach ($table_names as $table_name) { ++ if ($schema_handler->tableExists($table_name)) { ++ $schema_handler->dropTable($table_name); ++ } ++ } + +- // Delete the field schema data. +- foreach ($this->fieldStorageDefinitions as $field_storage_definition) { +- $this->deleteFieldSchemaData($field_storage_definition); ++ // Delete the field schema data. ++ foreach ($this->fieldStorageDefinitions as $field_storage_definition) { ++ $this->deleteFieldSchemaData($field_storage_definition); ++ } + } + + // Delete the entity schema. +@@ -416,22 +439,53 @@ public function onFieldableEntityTypeCreate(EntityTypeInterface $entity_type, ar + + // Create entity tables. + $schema = $this->getEntitySchema($entity_type, TRUE); +- foreach ($schema as $table_name => $table_schema) { +- if (!$schema_handler->tableExists($table_name)) { +- $schema_handler->createTable($table_name, $table_schema); ++ ++ if ($this->database->driver() == 'mongodb') { ++ // Create the base table first. ++ $base_table = $entity_type->getBaseTable(); ++ if (!empty($schema[$base_table]) && !$schema_handler->tableExists($base_table)) { ++ $schema_handler->createTable($base_table, $schema[$base_table]); + } +- } + +- // Create dedicated field tables. +- $table_mapping = $this->getTableMapping($this->entityType); +- foreach ($this->fieldStorageDefinitions as $field_storage_definition) { +- if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { +- $this->createDedicatedTableSchema($field_storage_definition); ++ // Create now all embedded tables. ++ foreach ($schema as $table_name => $table_schema) { ++ if (($base_table != $table_name) && !$schema_handler->tableExists($table_name)) { ++ $schema_handler->createEmbeddedTable($base_table, $table_name, $table_schema); ++ } ++ } ++ ++ // Create dedicated field tables. ++ // $table_mapping = $this->getTableMapping($entity_type, $this->fieldStorageDefinitions); ++ $table_mapping = $this->getTableMapping($entity_type); ++ foreach ($this->fieldStorageDefinitions as $field_storage_definition) { ++ if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { ++ $this->createDedicatedTableSchema($field_storage_definition); ++ } ++ elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { ++ // The shared tables are already fully created, but we need to save the ++ // per-field schema definitions for later use. ++ $this->createSharedTableSchema($field_storage_definition, TRUE); ++ } + } +- elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { +- // The shared tables are already fully created, but we need to save the +- // per-field schema definitions for later use. +- $this->createSharedTableSchema($field_storage_definition, TRUE); ++ } ++ else { ++ foreach ($schema as $table_name => $table_schema) { ++ if (!$schema_handler->tableExists($table_name)) { ++ $schema_handler->createTable($table_name, $table_schema); ++ } ++ } ++ ++ // Create dedicated field tables. ++ $table_mapping = $this->getTableMapping($this->entityType); ++ foreach ($this->fieldStorageDefinitions as $field_storage_definition) { ++ if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { ++ $this->createDedicatedTableSchema($field_storage_definition); ++ } ++ elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { ++ // The shared tables are already fully created, but we need to save the ++ // per-field schema definitions for later use. ++ $this->createSharedTableSchema($field_storage_definition, TRUE); ++ } + } + } + +@@ -451,12 +505,12 @@ public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, En + */ + protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, ?array &$sandbox = NULL) { + $temporary_prefix = static::getTemporaryTableMappingPrefix($entity_type, $field_storage_definitions); +- $sandbox['temporary_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, $temporary_prefix); +- $sandbox['new_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions); +- $sandbox['original_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions); ++ $sandbox['temporary_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, $temporary_prefix, ($this->database->driver() == 'mongodb')); ++ $sandbox['new_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, '', ($this->database->driver() == 'mongodb')); ++ $sandbox['original_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, '', ($this->database->driver() == 'mongodb')); + + $backup_prefix = static::getTemporaryTableMappingPrefix($original, $original_field_storage_definitions, 'old_'); +- $sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, $backup_prefix); ++ $sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, $backup_prefix, ($this->database->driver() == 'mongodb')); + $sandbox['backup_prefix_key'] = substr($backup_prefix, 4); + $sandbox['backup_request_time'] = \Drupal::time()->getRequestTime(); + +@@ -483,8 +537,16 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E + $schema = array_intersect_key($schema, $temporary_table_names); + + // Create entity tables. +- foreach ($schema as $table_name => $table_schema) { +- $this->database->schema()->createTable($temporary_table_names[$table_name], $table_schema); ++ if ($this->database->driver() == 'mongodb') { ++ $base_table = $temporary_table_names[$entity_type->getBaseTable()]; ++ if (!empty($schema[$entity_type->getBaseTable()])) { ++ $this->database->schema()->createTable($base_table, $schema[$entity_type->getBaseTable()]); ++ } ++ } ++ else { ++ foreach ($schema as $table_name => $table_schema) { ++ $this->database->schema()->createTable($temporary_table_names[$table_name], $table_schema); ++ } + } + + // Create dedicated field tables. +@@ -494,8 +556,11 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E + + // Filter out tables which are not part of the table mapping. + $schema = array_intersect_key($schema, $temporary_table_names); +- foreach ($schema as $table_name => $table_schema) { +- $this->database->schema()->createTable($temporary_table_names[$table_name], $table_schema); ++ ++ if ($this->database->driver() != 'mongodb') { ++ foreach ($schema as $table_name => $table_schema) { ++ $this->database->schema()->createTable($temporary_table_names[$table_name], $table_schema); ++ } + } + } + } +@@ -547,7 +612,15 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, + // definitions. + try { + foreach ($sandbox['temporary_table_names'] as $current_table_name => $temp_table_name) { +- $this->database->schema()->renameTable($temp_table_name, $current_table_name); ++ if ($this->database->driver() == 'mongodb') { ++ // For MongoDB all entity data is stored in the base table. ++ if ($current_table_name == $entity_type->getBaseTable()) { ++ $this->database->schema()->renameTable($temp_table_name, $current_table_name); ++ } ++ } ++ else { ++ $this->database->schema()->renameTable($temp_table_name, $current_table_name); ++ } + } + + // Store the updated entity schema. +@@ -707,9 +780,20 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { ++ try { ++ $has_data = $this->storage->countFieldData($storage_definition, TRUE); ++ } ++ catch (DatabaseExceptionWrapper $e) { ++ // This may happen when changing field storage schema, since we are not ++ // able to use a table mapping matching the passed storage definition. ++ // @todo Revisit this once we are able to instantiate the table mapping ++ // properly. See https://www.drupal.org/node/2274017. ++ return; ++ } ++ + // If the field storage does not have any data, we can safely delete its + // schema. +- if (!$this->storage->countFieldData($storage_definition, TRUE)) { ++ if (!$has_data) { + $this->performFieldSchemaOperation('delete', $storage_definition); + return; + } +@@ -720,91 +804,274 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $ + } + + $table_mapping = $this->getTableMapping($this->entityType, [$storage_definition]); +- $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName()); +- + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { +- // Move the table to a unique name while the table contents are being +- // deleted. +- $table = $table_mapping->getDedicatedDataTableName($storage_definition); +- $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); +- $this->database->schema()->renameTable($table, $new_table); +- if ($this->entityType->isRevisionable()) { +- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); +- $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE); +- $this->database->schema()->renameTable($revision_table, $revision_new_table); +- } +- } +- else { +- // Move the field data from the shared table to a dedicated one in order +- // to allow it to be purged like any other field. +- $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName()); ++ if ($this->database->driver() == 'mongodb') { ++ $base_table = $this->storage->getBaseTable(); ++ $prefixed_table = $this->database->getPrefix() . $base_table; ++ $schema = $this->getDedicatedTableSchema($storage_definition); ++ $id_key = $this->entityType->getKey('id'); ++ ++ // Move the table to a unique name while the table contents are being ++ // deleted. ++ if ($this->entityType->isRevisionable()) { ++ // For MongoDB: All embedded table data needs to be renamed. ++ $all_revisions_table = $this->storage->getJsonStorageAllRevisionsTable(); ++ $dedicated_all_revisions_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $all_revisions_table); ++ $dedicated_all_revisions_new_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $all_revisions_table, TRUE); ++ $this->database->schema()->createEmbeddedTable($all_revisions_table, $dedicated_all_revisions_new_table, $schema[$dedicated_all_revisions_table]); ++ ++ $current_revision_table = $this->storage->getJsonStorageCurrentRevisionTable(); ++ $dedicated_current_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $current_revision_table); ++ $dedicated_current_revision_new_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $current_revision_table, TRUE); ++ // Check if there already exists a table with that name. If so, then delete it. ++ if ($this->database->schema()->tableExists($dedicated_current_revision_new_table)) { ++ $this->database->schema()->dropTable($dedicated_current_revision_new_table); ++ } ++ $this->database->schema()->createEmbeddedTable($current_revision_table, $dedicated_current_revision_new_table, $schema[$dedicated_current_revision_table]); ++ ++ $latest_revision_table = $this->storage->getJsonStorageLatestRevisionTable(); ++ $dedicated_latest_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $latest_revision_table); ++ $dedicated_latest_revision_new_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $latest_revision_table, TRUE); ++ // Check if there already exists a table with that name. If so, then delete it. ++ if ($this->database->schema()->tableExists($dedicated_latest_revision_new_table)) { ++ $this->database->schema()->dropTable($dedicated_latest_revision_new_table); ++ } ++ $this->database->schema()->createEmbeddedTable($latest_revision_table, $dedicated_latest_revision_new_table, $schema[$dedicated_latest_revision_table]); ++ ++ $this->database->schema()->dropTable($dedicated_all_revisions_table); ++ $this->database->schema()->dropTable($dedicated_current_revision_table); ++ $this->database->schema()->dropTable($dedicated_latest_revision_table); ++ ++ $cursor = $this->database->getConnection()->selectCollection($prefixed_table)->find( ++ [ ++ "$all_revisions_table.$dedicated_all_revisions_table" => ['$exists' => TRUE], ++ ], ++ [ ++ 'projection' => [ ++ $id_key => 1, ++ $all_revisions_table => 1, ++ $current_revision_table => 1, ++ $latest_revision_table => 1, ++ '_id' => 0, ++ ], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ ++ foreach ($cursor as $entity) { ++ if (isset($entity->{$all_revisions_table})) { ++ foreach ($entity->{$all_revisions_table} as &$revision) { ++ if (isset($revision[$dedicated_all_revisions_table])) { ++ $revision[$dedicated_all_revisions_new_table] = $revision[$dedicated_all_revisions_table]; ++ unset($revision[$dedicated_all_revisions_table]); ++ } ++ } ++ } + +- // Refresh the table mapping to use the deleted storage definition. +- $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()]; +- $table_mapping = $this->getTableMapping($this->entityType, [$deleted_storage_definition]); ++ if (isset($entity->{$current_revision_table})) { ++ foreach ($entity->{$current_revision_table} as &$revision) { ++ if (isset($revision[$dedicated_current_revision_table])) { ++ $revision[$dedicated_current_revision_new_table] = $revision[$dedicated_current_revision_table]; ++ unset($revision[$dedicated_current_revision_table]); ++ } ++ } ++ } + +- $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition); +- $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName()); ++ if (isset($entity->{$latest_revision_table})) { ++ foreach ($entity->{$latest_revision_table} as &$revision) { ++ if (isset($revision[$dedicated_latest_revision_table])) { ++ $revision[$dedicated_latest_revision_new_table] = $revision[$dedicated_latest_revision_table]; ++ unset($revision[$dedicated_latest_revision_table]); ++ } ++ } ++ } + +- $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE); +- $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name; +- if ($this->entityType->isRevisionable()) { +- $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE); +- $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name; +- } ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [$id_key => $entity->{$id_key}], ++ [ ++ '$set' => [ ++ $all_revisions_table => $entity->{$all_revisions_table}, ++ $current_revision_table => $entity->{$current_revision_table}, ++ $latest_revision_table => $entity->{$latest_revision_table}, ++ ], ++ ], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ // For MongoDB: All embedded table data needs to be renamed. ++ $translations_table = $this->storage->getJsonStorageTranslationsTable(); ++ $dedicated_translations_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $translations_table); ++ $dedicated_translations_new_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $translations_table, TRUE); ++ $this->database->schema()->createEmbeddedTable($translations_table, $dedicated_translations_new_table, $schema[$dedicated_translations_table]); ++ $this->database->schema()->dropTable($dedicated_translations_table); ++ ++ $cursor = $this->database->getConnection()->selectCollection($prefixed_table)->find( ++ ["$translations_table.$dedicated_translations_table" => ['$exists' => TRUE]], ++ [ ++ 'projection' => [ ++ $id_key => 1, ++ $translations_table => 1, ++ '_id' => 0, ++ ], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ ++ foreach ($cursor as $entity) { ++ if (isset($entity->{$translations_table})) { ++ foreach ($entity->{$translations_table} as &$revision) { ++ if (isset($revision[$dedicated_translations_table])) { ++ $revision[$dedicated_translations_new_table] = $revision[$dedicated_translations_table]; ++ unset($revision[$dedicated_translations_table]); ++ } ++ } ++ } + +- // Create the dedicated field tables using "deleted" table names. +- foreach ($dedicated_table_field_schema as $name => $table) { +- if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) { +- $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table); ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [$id_key => $entity->{$id_key}], ++ [ ++ '$set' => [ ++ $translations_table => $entity->{$translations_table}, ++ ], ++ ], ++ ['session' => $this->database->getMongodbSession()], ++ ); ++ } + } + else { +- throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.'); ++ // For MongoDB: All embedded table data needs to be renamed. ++ $base_table = $this->storage->getBaseTable(); ++ $dedicated_base_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $base_table); ++ $dedicated_base_new_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $base_table, TRUE); ++ ++ // Delete the old archived table before renaming or the renaming will fail. Two tables cannot have the same name. ++ if ($this->database->schema()->tableExists($dedicated_base_new_table)) { ++ $this->database->schema()->dropTable($dedicated_base_new_table); ++ } ++ $this->database->schema()->renameTable($dedicated_base_table, $dedicated_base_new_table); + } + } +- +- try { +- if ($this->database->supportsTransactionalDDL()) { +- // If the database supports transactional DDL, we can go ahead and rely +- // on it. If not, we will have to rollback manually if something fails. +- $transaction = $this->database->startTransaction(); ++ else { ++ // Move the table to a unique name while the table contents are being ++ // deleted. ++ $table = $table_mapping->getDedicatedDataTableName($storage_definition); ++ $new_table = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); ++ $this->database->schema()->renameTable($table, $new_table); ++ if ($this->entityType->isRevisionable()) { ++ $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); ++ $revision_new_table = $table_mapping->getDedicatedRevisionTableName($storage_definition, TRUE); ++ $this->database->schema()->renameTable($revision_table, $revision_new_table); ++ } ++ } ++ } ++ else { ++ if ($this->database->driver() == 'mongodb') { ++ // Move the field data from the shared table to a dedicated one in order ++ // to allow it to be purged like any other field. ++ $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName()); ++ foreach ($shared_table_field_columns as $shared_table_field_column) { ++ if ($this->entityType->isRevisionable()) { ++ $all_revisions_table = $table_mapping->getJsonStorageAllRevisionsTable(); ++ if ($this->database->schema()->fieldExists($all_revisions_table, $shared_table_field_column)) { ++ $this->database->schema()->dropField($all_revisions_table, $shared_table_field_column); ++ } ++ $current_revision_table = $table_mapping->getJsonStorageCurrentRevisionTable(); ++ if ($this->database->schema()->fieldExists($current_revision_table, $shared_table_field_column)) { ++ $this->database->schema()->dropField($current_revision_table, $shared_table_field_column); ++ } ++ $latest_revision_table = $table_mapping->getJsonStorageLatestRevisionTable(); ++ if ($this->database->schema()->fieldExists($latest_revision_table, $shared_table_field_column)) { ++ $this->database->schema()->dropField($latest_revision_table, $shared_table_field_column); ++ } ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $translations_table = $table_mapping->getJsonStorageTranslationsTable(); ++ if ($this->database->schema()->fieldExists($translations_table, $shared_table_field_column)) { ++ $this->database->schema()->dropField($translations_table, $shared_table_field_column); ++ } ++ } ++ $base_table = $table_mapping->getBaseTable(); ++ if ($this->database->schema()->fieldExists($base_table, $shared_table_field_column)) { ++ $this->database->schema()->dropField($base_table, $shared_table_field_column); ++ } ++ } ++ } ++ else { ++ // Move the field data from the shared table to a dedicated one in order ++ // to allow it to be purged like any other field. ++ $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName()); ++ ++ // Refresh the table mapping to use the deleted storage definition. ++ $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()]; ++ $table_mapping = $this->getTableMapping($this->entityType, [$deleted_storage_definition]); ++ ++ $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition); ++ $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName()); ++ ++ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE); ++ $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name; ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE); ++ $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name; + } + +- // Copy the data from the base table. +- $this->database->insert($dedicated_table_name) +- ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns)) +- ->execute(); +- +- // Copy the data from the revision table. +- if (isset($dedicated_revision_table_name)) { +- if ($this->entityType->isTranslatable()) { +- $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable(); ++ // Create the dedicated field tables using "deleted" table names. ++ foreach ($dedicated_table_field_schema as $name => $table) { ++ if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) { ++ $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table); + } + else { +- $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable(); ++ throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.'); + } +- $this->database->insert($dedicated_revision_table_name) +- ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name)) +- ->execute(); + } +- } +- catch (\Exception $e) { +- if ($this->database->supportsTransactionalDDL()) { +- if (isset($transaction)) { +- $transaction->rollBack(); ++ ++ try { ++ $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName()); ++ ++ if ($this->database->supportsTransactionalDDL()) { ++ // If the database supports transactional DDL, we can go ahead and rely ++ // on it. If not, we will have to rollback manually if something fails. ++ $transaction = $this->database->startTransaction(); ++ } ++ ++ // Copy the data from the base table. ++ $this->database->insert($dedicated_table_name) ++ ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns)) ++ ->execute(); ++ ++ // Copy the data from the revision table. ++ if (isset($dedicated_revision_table_name)) { ++ if ($this->entityType->isTranslatable()) { ++ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable(); ++ } ++ else { ++ $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable(); ++ } ++ $this->database->insert($dedicated_revision_table_name) ++ ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name)) ++ ->execute(); + } + } +- else { +- // Delete the dedicated tables. +- foreach ($dedicated_table_field_schema as $name => $table) { +- $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]); ++ catch (\Exception $e) { ++ if ($this->database->supportsTransactionalDDL()) { ++ if (isset($transaction)) { ++ $transaction->rollBack(); ++ } + } ++ else { ++ // Delete the dedicated tables. ++ foreach ($dedicated_table_field_schema as $name => $table) { ++ $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]); ++ } ++ } ++ throw $e; + } +- throw $e; +- } + +- // Delete the field from the shared tables. +- $this->deleteSharedTableSchema($storage_definition); ++ // Delete the field from the shared tables. ++ $this->deleteSharedTableSchema($storage_definition); ++ } + } + unset($this->fieldStorageDefinitions[$storage_definition->getName()]); + } +@@ -841,13 +1108,13 @@ protected function getSelectQueryForFieldStorageDeletion($table_name, array $sha + // The bundle field is not stored in the revision table, so we need to + // join the data (or base) table and retrieve it from there. + if ($base_table && $base_table !== $table_name) { +- $join_condition = "[entity_table].[{$this->entityType->getKey('id')}] = [%alias].[{$this->entityType->getKey('id')}]"; ++ $join_condition = $select->joinCondition()->compare("entity_table.{$this->entityType->getKey('id')}", "%alias.{$this->entityType->getKey('id')}"); + + // If the entity type is translatable, we also need to add the langcode + // to the join, otherwise we'll get duplicate rows for each language. + if ($this->entityType->isTranslatable()) { + $langcode = $this->entityType->getKey('langcode'); +- $join_condition .= " AND [entity_table].[{$langcode}] = [%alias].[{$langcode}]"; ++ $join_condition->compare("entity_table.{$langcode}", "%alias.{$langcode}"); + } + + $select->join($base_table, 'base_table', $join_condition); +@@ -950,14 +1217,31 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res + + // Initialize the table schema. + $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type); +- if (isset($tables['revision_table'])) { +- $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type); +- } +- if (isset($tables['data_table'])) { +- $schema[$tables['data_table']] = $this->initializeDataTable($entity_type); ++ ++ if ($this->database->driver() == 'mongodb') { ++ if (isset($tables['all_revisions_table'])) { ++ $schema[$tables['all_revisions_table']] = $this->initializeJsonStorageRevisionsTable($entity_type); ++ } ++ if (isset($tables['current_revision_table'])) { ++ $schema[$tables['current_revision_table']] = $this->initializeJsonStorageRevisionsTable($entity_type); ++ } ++ if (isset($tables['latest_revision_table'])) { ++ $schema[$tables['latest_revision_table']] = $this->initializeJsonStorageRevisionsTable($entity_type); ++ } ++ if (isset($tables['translations_table'])) { ++ $schema[$tables['translations_table']] = $this->initializeJsonStorageTranslationsTable($entity_type); ++ } + } +- if (isset($tables['revision_data_table'])) { +- $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type); ++ else { ++ if (isset($tables['revision_table'])) { ++ $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type); ++ } ++ if (isset($tables['data_table'])) { ++ $schema[$tables['data_table']] = $this->initializeDataTable($entity_type); ++ } ++ if (isset($tables['revision_data_table'])) { ++ $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type); ++ } + } + + // We need to act only on shared entity schema tables. +@@ -979,31 +1263,50 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res + } + } + +- // Process tables after having gathered field information. +- if (isset($tables['data_table'])) { +- $this->processDataTable($entity_type, $schema[$tables['data_table']]); +- } +- if (isset($tables['revision_data_table'])) { +- $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]); ++ if ($this->database->driver() == 'mongodb') { ++ // Not sure why the next method has been removed. ++ // $this->processBaseTable($entity_type, $schema[$tables['base_table']]); ++ ++ if (isset($tables['all_revisions_table'])) { ++ $this->processJsonStorageRevisionsTable($entity_type, $schema[$tables['all_revisions_table']]); ++ } ++ if (isset($tables['current_revision_table'])) { ++ $this->processJsonStorageRevisionsTable($entity_type, $schema[$tables['current_revision_table']]); ++ } ++ if (isset($tables['latest_revision_table'])) { ++ $this->processJsonStorageRevisionsTable($entity_type, $schema[$tables['latest_revision_table']]); ++ } ++ if (isset($tables['translations_table'])) { ++ $this->processJsonStorageTranslationsTable($entity_type, $schema[$tables['translations_table']]); ++ } + } ++ else { ++ // Process tables after having gathered field information. ++ if (isset($tables['data_table'])) { ++ $this->processDataTable($entity_type, $schema[$tables['data_table']]); ++ } ++ if (isset($tables['revision_data_table'])) { ++ $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]); ++ } + +- // Add an index for the 'published' entity key. +- if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) { +- $published_key = $entity_type->getKey('published'); +- if ($published_key ++ // Add an index for the 'published' entity key. ++ if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) { ++ $published_key = $entity_type->getKey('published'); ++ if ($published_key + && isset($this->fieldStorageDefinitions[$published_key]) + && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) { +- $published_field_table = $table_mapping->getFieldTableName($published_key); +- $id_key = $entity_type->getKey('id'); +- if ($bundle_key = $entity_type->getKey('bundle')) { +- $key = "{$published_key}_{$bundle_key}"; +- $columns = [$published_key, $bundle_key, $id_key]; +- } +- else { +- $key = $published_key; +- $columns = [$published_key, $id_key]; ++ $published_field_table = $table_mapping->getFieldTableName($published_key); ++ $id_key = $entity_type->getKey('id'); ++ if ($bundle_key = $entity_type->getKey('bundle')) { ++ $key = "{$published_key}_{$bundle_key}"; ++ $columns = [$published_key, $bundle_key, $id_key]; ++ } ++ else { ++ $key = $published_key; ++ $columns = [$published_key, $id_key]; ++ } ++ $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns; + } +- $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns; + } + } + +@@ -1023,13 +1326,25 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res + * A list of entity type tables, keyed by table key. + */ + protected function getEntitySchemaTables(TableMappingInterface $table_mapping) { +- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ +- return array_filter([ +- 'base_table' => $table_mapping->getBaseTable(), +- 'revision_table' => $table_mapping->getRevisionTable(), +- 'data_table' => $table_mapping->getDataTable(), +- 'revision_data_table' => $table_mapping->getRevisionDataTable(), +- ]); ++ if ($this->database->driver() == 'mongodb') { ++ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ ++ return array_filter([ ++ 'base_table' => $table_mapping->getBaseTable(), ++ 'all_revisions_table' => $table_mapping->getJsonStorageAllRevisionsTable(), ++ 'current_revision_table' => $table_mapping->getJsonStorageCurrentRevisionTable(), ++ 'latest_revision_table' => $table_mapping->getJsonStorageLatestRevisionTable(), ++ 'translations_table' => $table_mapping->getJsonStorageTranslationsTable(), ++ ]); ++ } ++ else { ++ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ ++ return array_filter([ ++ 'base_table' => $table_mapping->getBaseTable(), ++ 'revision_table' => $table_mapping->getRevisionTable(), ++ 'data_table' => $table_mapping->getDataTable(), ++ 'revision_data_table' => $table_mapping->getRevisionDataTable(), ++ ]); ++ } + } + + /** +@@ -1433,6 +1748,59 @@ protected function initializeRevisionDataTable(ContentEntityTypeInterface $entit + return $schema; + } + ++ /** ++ * Initializes common information for a JSON storage all revisions table. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type ++ * The entity type. ++ * ++ * @return array ++ * A partial schema array for the all revisions table. ++ */ ++ protected function initializeJsonStorageRevisionsTable(ContentEntityTypeInterface $entity_type) { ++ $entity_type_id = $entity_type->id(); ++ ++ $schema = [ ++ 'description' => "The all revisions table for $entity_type_id entities.", ++ 'indexes' => [], ++ ]; ++ ++ if ($entity_type->isTranslatable()) { ++ $schema['primary key'] = [$entity_type->getKey('revision'), $entity_type->getKey('langcode')]; ++ } ++ else { ++ $schema['primary key'] = [$entity_type->getKey('revision')]; ++ } ++ ++ $this->addTableDefaults($schema); ++ ++ return $schema; ++ } ++ ++ /** ++ * Initializes common information for a JSON storage translations table. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type ++ * The entity type. ++ * ++ * @return array ++ * A partial schema array for the translations table. ++ */ ++ protected function initializeJsonStorageTranslationsTable(ContentEntityTypeInterface $entity_type) { ++ $entity_type_id = $entity_type->id(); ++ ++ $schema = [ ++ 'description' => "The translations table for $entity_type_id entities.", ++ 'indexes' => [], ++ ]; ++ ++ $schema['primary key'] = [$entity_type->getKey('id'), $entity_type->getKey('langcode')]; ++ ++ $this->addTableDefaults($schema); ++ ++ return $schema; ++ } ++ + /** + * Adds defaults to a table schema definition. + * +@@ -1476,6 +1844,33 @@ protected function processRevisionDataTable(ContentEntityTypeInterface $entity_t + $schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE; + } + ++ /** ++ * Processes the gathered schema for a JSON storage all revisions table. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type ++ * The entity type. ++ * @param array &$schema ++ * The table schema, passed by reference. ++ */ ++ protected function processJsonStorageRevisionsTable(ContentEntityTypeInterface $entity_type, array &$schema) { ++ // Change the field "revision_id" from serial to integer. Serial primary key ++ // fields are auto-incremented. This is something we do not want from an ++ // embedded table. ++ if ($entity_type->hasKey('revision')) { ++ $schema['fields'][$entity_type->getKey('revision')]['type'] = 'int'; ++ } ++ } ++ ++ /** ++ * Processes the gathered schema for a JSON storage translations table. ++ * ++ * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type ++ * The entity type. ++ * @param array &$schema ++ * The table schema, passed by reference. ++ */ ++ protected function processJsonStorageTranslationsTable(ContentEntityTypeInterface $entity_type, array &$schema) {} ++ + /** + * Processes the specified entity key. + * +@@ -1545,14 +1940,31 @@ protected function performFieldSchemaOperation($operation, FieldStorageDefinitio + * the dedicated tables. + */ + protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) { ++ $table_mapping = $this->getTableMapping($this->entityType, [$storage_definition]); + $schema = $this->getDedicatedTableSchema($storage_definition); + + if (!$only_save) { +- foreach ($schema as $name => $table) { +- // Check if the table exists because it might already have been +- // created as part of the earlier entity type update event. +- if (!$this->database->schema()->tableExists($name)) { +- $this->database->schema()->createTable($name, $table); ++ if ($this->database->driver() == 'mongodb') { ++ foreach ($this->getEntitySchemaTables($table_mapping) as $table_name) { ++ $dedicated_table_name = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $table_name); ++ if (isset($schema[$dedicated_table_name])) { ++ // Check if the table exists because it might already have been ++ // created as part of the earlier entity type update event. ++ if ($this->database->schema()->tableExists($dedicated_table_name)) { ++ $this->database->schema()->dropTable($dedicated_table_name); ++ } ++ ++ $this->database->schema()->createEmbeddedTable($table_name, $dedicated_table_name, $schema[$dedicated_table_name]); ++ } ++ } ++ } ++ else { ++ foreach ($schema as $name => $table) { ++ // Check if the table exists because it might already have been ++ // created as part of the earlier entity type update event. ++ if (!$this->database->schema()->tableExists($name)) { ++ $this->database->schema()->createTable($name, $table); ++ } + } + } + } +@@ -1645,16 +2057,60 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor + */ + protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->getTableMapping($this->entityType, [$storage_definition]); +- $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted()); +- if ($this->database->schema()->tableExists($table_name)) { +- $this->database->schema()->dropTable($table_name); ++ ++ if ($this->database->driver() == 'mongodb') { ++ // When switching from dedicated to shared field table layout we need need ++ // to delete the field tables with their regular names. When this happens ++ // original definitions will be defined. ++ $table_mapping = $this->getTableMapping($this->entityType, [$storage_definition]); ++ if ($all_revisions_table = $table_mapping->getJsonStorageAllRevisionsTable()) { ++ $dedicated_all_revisions_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $all_revisions_table, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($dedicated_all_revisions_table)) { ++ $this->database->schema()->dropTable($dedicated_all_revisions_table); ++ } ++ } ++ ++ if ($current_revision_table = $table_mapping->getJsonStorageCurrentRevisionTable()) { ++ $dedicated_current_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $current_revision_table, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($dedicated_current_revision_table)) { ++ $this->database->schema()->dropTable($dedicated_current_revision_table); ++ } ++ } ++ ++ if ($latest_revision_table = $table_mapping->getJsonStorageLatestRevisionTable()) { ++ $dedicated_latest_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $latest_revision_table, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($dedicated_latest_revision_table)) { ++ $this->database->schema()->dropTable($dedicated_latest_revision_table); ++ } ++ } ++ ++ if ($translations_table = $table_mapping->getJsonStorageTranslationsTable()) { ++ $dedicated_translations_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $translations_table, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($dedicated_translations_table)) { ++ $this->database->schema()->dropTable($dedicated_translations_table); ++ } ++ } ++ ++ if (!$this->entityType->isRevisionable() && !$this->entityType->isTranslatable()) { ++ $dedicated_base_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $table_mapping->getBaseTable(), $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($dedicated_base_table)) { ++ $this->database->schema()->dropTable($dedicated_base_table); ++ } ++ } + } +- if ($this->entityType->isRevisionable()) { +- $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted()); +- if ($this->database->schema()->tableExists($revision_table_name)) { +- $this->database->schema()->dropTable($revision_table_name); ++ else { ++ $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($table_name)) { ++ $this->database->schema()->dropTable($table_name); ++ } ++ if ($this->entityType->isRevisionable()) { ++ $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted()); ++ if ($this->database->schema()->tableExists($revision_table_name)) { ++ $this->database->schema()->dropTable($revision_table_name); ++ } + } + } ++ + $this->deleteFieldSchemaData($storage_definition); + } + +@@ -1753,8 +2209,23 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s + // indexes and create all the new ones, except for all the priors that + // exist unchanged. + $table_mapping = $this->getTableMapping($this->entityType, [$storage_definition]); +- $table = $table_mapping->getDedicatedDataTableName($original); +- $revision_table = $table_mapping->getDedicatedRevisionTableName($original); ++ if ($this->database->driver() == 'mongodb') { ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_all_revisions_table = $table_mapping->getJsonStorageDedicatedTableName($original, $this->storage->getJsonStorageAllRevisionsTable()); ++ $dedicated_current_revision_table = $table_mapping->getJsonStorageDedicatedTableName($original, $this->storage->getJsonStorageCurrentRevisionTable()); ++ $dedicated_latest_revision_table = $table_mapping->getJsonStorageDedicatedTableName($original, $this->storage->getJsonStorageLatestRevisionTable()); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $dedicated_translations_table = $table_mapping->getJsonStorageDedicatedTableName($original, $this->storage->getJsonStorageTranslationsTable()); ++ } ++ else { ++ $dedicated_base_table = $table_mapping->getJsonStorageDedicatedTableName($original, $this->storage->getBaseTable()); ++ } ++ } ++ else { ++ $table = $table_mapping->getDedicatedDataTableName($original); ++ $revision_table = $table_mapping->getDedicatedRevisionTableName($original); ++ } + + // Get the field schemas. + $schema = $storage_definition->getSchema(); +@@ -1766,12 +2237,43 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s + foreach ($original_schema['indexes'] as $name => $columns) { + if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); +- $this->database->schema()->dropIndex($table, $real_name); +- $this->database->schema()->dropIndex($revision_table, $real_name); ++ if ($this->database->driver() == 'mongodb') { ++ if ($this->entityType->isRevisionable()) { ++ $this->database->schema()->dropIndex($dedicated_all_revisions_table, $real_name); ++ $this->database->schema()->dropIndex($dedicated_current_revision_table, $real_name); ++ $this->database->schema()->dropIndex($dedicated_latest_revision_table, $real_name); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $this->database->schema()->dropIndex($dedicated_translations_table, $real_name); ++ } ++ else { ++ $this->database->schema()->dropIndex($dedicated_base_table, $real_name); ++ } ++ } ++ else { ++ $this->database->schema()->dropIndex($table, $real_name); ++ $this->database->schema()->dropIndex($revision_table, $real_name); ++ } ++ } ++ } ++ ++ if ($this->database->driver() == 'mongodb') { ++ if ($this->entityType->isRevisionable()) { ++ $dedicated_all_revisions_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageAllRevisionsTable()); ++ $dedicated_current_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageCurrentRevisionTable()); ++ $dedicated_latest_revision_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageLatestRevisionTable()); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $dedicated_translations_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageTranslationsTable()); + } ++ else { ++ $dedicated_base_table = $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getBaseTable()); ++ } ++ } ++ else { ++ $table = $table_mapping->getDedicatedDataTableName($storage_definition); ++ $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + } +- $table = $table_mapping->getDedicatedDataTableName($storage_definition); +- $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition); + foreach ($schema['indexes'] as $name => $columns) { + if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) { + $real_name = $this->getFieldIndexName($storage_definition, $name); +@@ -1780,10 +2282,16 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s + // Indexes can be specified as either a column name or an array with + // column name and length. Allow for either case. + if (is_array($column_name)) { +- $real_columns[] = [ +- $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), +- $column_name[1], +- ]; ++ if ($this->database->driver() == 'mongodb') { ++ // MongoDB cannot do anything with the length parameter. ++ $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, (is_array($column_name) ? reset($column_name) : $column_name)); ++ } ++ else { ++ $real_columns[] = [ ++ $table_mapping->getFieldColumnName($storage_definition, $column_name[0]), ++ $column_name[1], ++ ]; ++ } + } + else { + $real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name); +@@ -1791,8 +2299,23 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s + } + // Check if the index exists because it might already have been + // created as part of the earlier entity type update event. +- $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]); +- $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]); ++ if ($this->database->driver() == 'mongodb') { ++ if ($this->entityType->isRevisionable()) { ++ $this->addIndex($dedicated_all_revisions_table, $real_name, $real_columns, $actual_schema[$dedicated_all_revisions_table]); ++ $this->addIndex($dedicated_current_revision_table, $real_name, $real_columns, $actual_schema[$dedicated_current_revision_table]); ++ $this->addIndex($dedicated_latest_revision_table, $real_name, $real_columns, $actual_schema[$dedicated_latest_revision_table]); ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ $this->addIndex($dedicated_translations_table, $real_name, $real_columns, $actual_schema[$dedicated_translations_table]); ++ } ++ else { ++ $this->addIndex($dedicated_base_table, $real_name, $real_columns, $actual_schema[$dedicated_base_table]); ++ } ++ } ++ else { ++ $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]); ++ $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]); ++ } + } + } + $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition)); +@@ -2319,6 +2842,16 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor + ], + ]; + ++ if ($this->database->driver() == 'mongodb') { ++ // MongoDB stores boolean values as a boolean not as an integer. ++ $data_schema['fields']['deleted'] = [ ++ 'type' => 'bool', ++ 'not null' => TRUE, ++ 'default' => FALSE, ++ 'description' => 'A boolean indicating whether this data item has been deleted', ++ ]; ++ } ++ + // Check that the schema does not include forbidden column names. + $schema = $storage_definition->getSchema(); + $properties = $storage_definition->getPropertyDefinitions(); +@@ -2387,19 +2920,69 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor + } + } + +- $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema]; ++ if ($this->database->driver() == 'mongodb') { ++ // For MongoDB all dedicated tables are embedded tables. Therefor they do ++ // not need a primary key index. ++ unset($data_schema['primary key']); ++ // Removing the added indexes. No doing so can result in the error: ++ // "too many indexes". ++ unset($data_schema['unique keys']); ++ unset($data_schema['indexes']); ++ ++ if ($entity_type->isRevisionable()) { ++ // Adding an index for every field can create too many indexes on a single ++ // table. For MongoDB the maximum is 64. ++ // $data_schema['indexes']['primary_key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode']; ++ $data_schema['fields']['revision_id']['not null'] = TRUE; ++ $data_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; ++ ++ $dedicated_all_revisions_schema = $data_schema; ++ $dedicated_all_revisions_schema['description'] = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; ++ ++ $dedicated_current_revision_schema = $data_schema; ++ $dedicated_current_revision_schema['description'] = "Current revision storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; ++ ++ $dedicated_latest_revision_schema = $data_schema; ++ $dedicated_latest_revision_schema['description'] = "Latest revision storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; ++ ++ return [ ++ $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageAllRevisionsTable()) => $dedicated_all_revisions_schema, ++ $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageCurrentRevisionTable()) => $dedicated_current_revision_schema, ++ $table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageLatestRevisionTable()) => $dedicated_latest_revision_schema, ++ ]; ++ } ++ elseif ($entity_type->isTranslatable()) { ++ // Adding an index for every field can create too many indexes on a single ++ // table. For MongoDB the maximum is 64. ++ // $data_schema['indexes']['primary_key'] = ['entity_id', 'deleted', 'delta', 'langcode']; ++ $data_schema['description'] = "Translations storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; ++ ++ return [$table_mapping->getJsonStorageDedicatedTableName($storage_definition, $this->storage->getJsonStorageTranslationsTable()) => $data_schema]; ++ } ++ else { ++ // Adding an index for every field can create too many indexes on a single ++ // table. For MongoDB the maximum is 64. ++ // $data_schema['indexes']['primary_key'] = ['entity_id', 'deleted', 'delta']; ++ $data_schema['description'] = "Storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; + +- // If the entity type is revisionable, construct the revision table. +- if ($entity_type->isRevisionable()) { +- $revision_schema = $data_schema; +- $revision_schema['description'] = $description_revision; +- $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode']; +- $revision_schema['fields']['revision_id']['not null'] = TRUE; +- $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; +- $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema]; ++ return [$table_mapping->getJsonStorageDedicatedTableName($storage_definition, $entity_type->getBaseTable()) => $data_schema]; ++ } + } ++ else { ++ $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema]; ++ ++ // If the entity type is revisionable, construct the revision table. ++ if ($entity_type->isRevisionable()) { ++ $revision_schema = $data_schema; ++ $revision_schema['description'] = $description_revision; ++ $revision_schema['primary key'] = ['entity_id', 'revision_id', 'deleted', 'delta', 'langcode']; ++ $revision_schema['fields']['revision_id']['not null'] = TRUE; ++ $revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to'; ++ $dedicated_table_schema += [$table_mapping->getDedicatedRevisionTableName($storage_definition) => $revision_schema]; ++ } + +- return $dedicated_table_schema; ++ return $dedicated_table_schema; ++ } + } + + /** +diff --git a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php +index 72e49111bac93145c18577db7022aaef7bf2076e..4bf5fff7d4b19931800a0784316317d0e11bb64d 100644 +--- a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php ++++ b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php +@@ -57,16 +57,33 @@ public function onRouterRebuild($event) { + protected function menuLinksRebuild() { + if ($this->lock->acquire(__FUNCTION__)) { + try { +- $transaction = $this->connection->startTransaction(); ++ if ($this->connection->driver() == 'mongodb') { ++ $session = $this->connection->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->connection->startTransaction(); ++ } + // Ensure the menu links are up to date. + $this->menuLinkManager->rebuild(); + // Ignore any database replicas temporarily. + $this->replicaKillSwitch->trigger(); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException($this->logger, $e); + } + +diff --git a/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php +index 81682b8278ea3dab9fc3d5176de147bbeba7ed22..c6144d3e491cb45f0c07903365908bdbf9df1f15 100644 +--- a/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php ++++ b/core/lib/Drupal/Core/Installer/Form/SiteSettingsForm.php +@@ -161,6 +161,34 @@ public function validateForm(array &$form, FormStateInterface $form_state) { + $database['driver'] = $driver; + $database = array_merge($database, $this->databaseDriverList->get($driver)->getAutoloadInfo()); + ++ // For MongoDB there are always multiple hosts. They are in the ++ // settings.php file stored in the hosts array. Change the FORM API values ++ // to the hosts array for the settings.php file. ++ if ($driver == 'Drupal\mongodb\Driver\Database\mongodb') { ++ $hosts = []; ++ foreach ([1, 2, 3] as $i) { ++ if (!empty($database['host' . $i]['host'])) { ++ // Add the port setting when it is given and it is not the default ++ // port. ++ if (isset($database['host' . $i]['port']) && ($database['host' . $i]['port'] != 27017)) { ++ $hosts[] = [ ++ 'host' => $database['host' . $i]['host'], ++ 'port' => $database['host' . $i]['port'], ++ ]; ++ } ++ else { ++ $hosts[] = [ ++ 'host' => $database['host' . $i]['host'], ++ ]; ++ } ++ } ++ unset($database['host' . $i]); ++ } ++ if (!empty($hosts)) { ++ $database['hosts'] = $hosts; ++ } ++ } ++ + $form_state->set('database', $database); + + foreach ($this->getDatabaseErrors($database, $form_state->getValue('settings_file')) as $name => $message) { +diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php +index 34f94c0abffd3e9f1df91e26277b6d03799c12f0..629c66e5283b542f8fbbb06d8186e0c3e6ae81aa 100644 +--- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php ++++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php +@@ -5,6 +5,8 @@ + use Drupal\Component\Datetime\TimeInterface; + use Drupal\Component\Serialization\SerializationInterface; + use Drupal\Core\Database\Connection; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; ++use MongoDB\BSON\UTCDateTime; + + /** + * Defines a default key/value store implementation for expiring items. +@@ -42,17 +44,34 @@ public function __construct( + * {@inheritdoc} + */ + public function has($key) { +- try { +- return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key AND [expire] > :now', [ +- ':collection' => $this->collection, +- ':key' => $key, +- ':now' => $this->time->getRequestTime(), +- ])->fetchField(); +- } +- catch (\Exception $e) { +- $this->catchException($e); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => $this->collection], 'expire' => ['$gt' => new UTCDateTime($this->time->getRequestTime() * 1000)], 'name' => ['$eq' => (string) $key]], ++ [ ++ 'projection' => ['_id' => 1], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ if ($cursor && !empty($cursor->toArray())) { ++ return TRUE; ++ } + return FALSE; + } ++ else { ++ try { ++ return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key AND [expire] > :now', [ ++ ':collection' => $this->collection, ++ ':key' => $key, ++ ':now' => $this->time->getRequestTime(), ++ ])->fetchField(); ++ } ++ catch (\Exception $e) { ++ $this->catchException($e); ++ return FALSE; ++ } ++ } + } + + /** +@@ -60,13 +79,31 @@ public function has($key) { + */ + public function getMultiple(array $keys) { + try { +- $values = $this->connection->query( +- 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [expire] > :now AND [name] IN ( :keys[] ) AND [collection] = :collection', +- [ +- ':now' => $this->time->getRequestTime(), +- ':keys[]' => $keys, +- ':collection' => $this->collection, +- ])->fetchAllKeyed(); ++ if ($this->connection->driver() == 'mongodb') { ++ foreach ($keys as &$key) { ++ $key = (string) $key; ++ } ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => $this->collection], 'expire' => ['$gt' => new UTCDateTime($this->time->getRequestTime() * 1000)], 'name' => ['$in' => $keys]], ++ [ ++ 'projection' => ['name' => 1, 'value' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'value']); ++ $values = $statement->execute()->fetchAllKeyed(); ++ } ++ else { ++ $values = $this->connection->query( ++ 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [expire] > :now AND [name] IN ( :keys[] ) AND [collection] = :collection', ++ [ ++ ':now' => $this->time->getRequestTime(), ++ ':keys[]' => $keys, ++ ':collection' => $this->collection, ++ ])->fetchAllKeyed(); ++ } + return array_map([$this->serializer, 'decode'], $values); + } + catch (\Exception $e) { +@@ -84,12 +121,27 @@ public function getMultiple(array $keys) { + */ + public function getAll() { + try { +- $values = $this->connection->query( +- 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [expire] > :now', +- [ +- ':collection' => $this->collection, +- ':now' => $this->time->getRequestTime(), +- ])->fetchAllKeyed(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => (string) $this->collection], 'expire' => ['$gt' => new UTCDateTime($this->time->getRequestTime() * 1000)]], ++ [ ++ 'projection' => ['name' => 1, 'value' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'value']); ++ $values = $statement->execute()->fetchAllKeyed(); ++ } ++ else { ++ $values = $this->connection->query( ++ 'SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [expire] > :now', ++ [ ++ ':collection' => $this->collection, ++ ':now' => $this->time->getRequestTime(), ++ ])->fetchAllKeyed(); ++ } + return array_map([$this->serializer, 'decode'], $values); + } + catch (\Exception $e) { +@@ -111,9 +163,13 @@ public function getAll() { + * The time to live for items, in seconds. + */ + protected function doSetWithExpire($key, $value, $expire) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $this->connection->merge($this->table) + ->keys([ +- 'name' => $key, ++ 'name' => (string) $key, + 'collection' => $this->collection, + ]) + ->fields([ +@@ -201,8 +257,8 @@ public function deleteMultiple(array $keys) { + /** + * Defines the schema for the key_value_expire table. + */ +- public static function schemaDefinition() { +- return [ ++ public function schemaDefinition() { ++ $schema = [ + 'description' => 'Generic key/value storage table with an expiration.', + 'fields' => [ + 'collection' => [ +@@ -238,6 +294,12 @@ public static function schemaDefinition() { + 'expire' => ['expire'], + ], + ]; ++ ++ if ($this->connection->driver() == 'mongodb') { ++ $schema['fields']['expire']['type'] = 'date'; ++ } ++ ++ return $schema; + } + + } +diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php +index 44d5d9df13fba12f5a31f374c59b1acde6d46f34..b931b7a759e3e06a93c3953e522af9ad3222ac3f 100644 +--- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php ++++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php +@@ -2,11 +2,13 @@ + + namespace Drupal\Core\KeyValueStore; + ++use Drupal\Component\Assertion\Inspector; + use Drupal\Component\Serialization\SerializationInterface; + use Drupal\Core\Database\Query\Merge; + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; + use Drupal\Core\DependencyInjection\DependencySerializationTrait; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; + + /** + * Defines a default key/value store implementation. +@@ -39,6 +41,15 @@ class DatabaseStorage extends StorageBase { + */ + protected $table; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Overrides Drupal\Core\KeyValueStore\StorageBase::__construct(). + * +@@ -62,16 +73,33 @@ public function __construct($collection, SerializationInterface $serializer, Con + * {@inheritdoc} + */ + public function has($key) { +- try { +- return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key', [ +- ':collection' => $this->collection, +- ':key' => $key, +- ])->fetchField(); +- } +- catch (\Exception $e) { +- $this->catchException($e); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => (string) $this->collection], 'name' => ['$eq' => (string) $key]], ++ [ ++ 'projection' => ['_id' => 1], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ if ($cursor && !empty($cursor->toArray())) { ++ return TRUE; ++ } + return FALSE; + } ++ else { ++ try { ++ return (bool) $this->connection->query('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection AND [name] = :key', [ ++ ':collection' => $this->collection, ++ ':key' => $key, ++ ])->fetchField(); ++ } ++ catch (\Exception $e) { ++ $this->catchException($e); ++ return FALSE; ++ } ++ } + } + + /** +@@ -80,7 +108,33 @@ public function has($key) { + public function getMultiple(array $keys) { + $values = []; + try { +- $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [name] IN ( :keys[] ) AND [collection] = :collection', [':keys[]' => $keys, ':collection' => $this->collection])->fetchAllAssoc('name'); ++ if ($this->connection->driver() == 'mongodb') { ++ if (empty($keys)) { ++ return []; ++ } ++ ++ // Check that key values are string values. ++ assert(Inspector::assertAllStrings($keys), 'All keys must be strings.'); ++ ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ [ ++ 'collection' => ['$eq' => (string) $this->collection], ++ 'name' => ['$in' => $keys], ++ ], ++ [ ++ 'projection' => ['name' => 1, 'value' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ], ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'value']); ++ $result = $statement->execute()->fetchAllAssoc('name'); ++ ++ } ++ else { ++ $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [name] IN ( :keys[] ) AND [collection] = :collection', [':keys[]' => $keys, ':collection' => $this->collection])->fetchAllAssoc('name'); ++ } + foreach ($keys as $key) { + if (isset($result[$key])) { + $values[$key] = $this->serializer->decode($result[$key]->value); +@@ -100,7 +154,22 @@ public function getMultiple(array $keys) { + */ + public function getAll() { + try { +- $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection', [':collection' => $this->collection]); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->table; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['collection' => ['$eq' => (string) $this->collection]], ++ [ ++ 'projection' => ['name' => 1, 'value' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'value']); ++ $result = $statement->execute(); ++ } ++ else { ++ $result = $this->connection->query('SELECT [name], [value] FROM {' . $this->connection->escapeTable($this->table) . '} WHERE [collection] = :collection', [':collection' => $this->collection]); ++ } + } + catch (\Exception $e) { + $this->catchException($e); +@@ -140,6 +209,10 @@ protected function doSet($key, $value) { + * {@inheritdoc} + */ + public function set($key, $value) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + try { + $this->doSet($key, $value); + } +@@ -168,6 +241,10 @@ public function set($key, $value) { + * TRUE if the data was set, FALSE if it already existed. + */ + public function doSetIfNotExists($key, $value) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $result = $this->connection->merge($this->table) + ->insertFields([ + 'collection' => $this->collection, +@@ -175,7 +252,7 @@ public function doSetIfNotExists($key, $value) { + 'value' => $this->serializer->encode($value), + ]) + ->condition('collection', $this->collection) +- ->condition('name', $key) ++ ->condition('name', (string) $key) + ->execute(); + return $result == Merge::STATUS_INSERT; + } +@@ -202,11 +279,15 @@ public function setIfNotExists($key, $value) { + * {@inheritdoc} + */ + public function rename($key, $new_key) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + try { + $this->connection->update($this->table) + ->fields(['name' => $new_key]) + ->condition('collection', $this->collection) +- ->condition('name', $key) ++ ->condition('name', (string) $key) + ->execute(); + } + catch (\Exception $e) { +@@ -218,6 +299,14 @@ public function rename($key, $new_key) { + * {@inheritdoc} + */ + public function deleteMultiple(array $keys) { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ ++ foreach ($keys as &$key) { ++ $key = (string) $key; ++ } ++ } ++ + // Delete in chunks when a large array is passed. + while ($keys) { + try { +@@ -236,6 +325,10 @@ public function deleteMultiple(array $keys) { + * {@inheritdoc} + */ + public function deleteAll() { ++ if (($this->connection->driver() == 'mongodb') && !$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + try { + $this->connection->delete($this->table) + ->condition('collection', $this->collection) +@@ -290,8 +383,8 @@ protected function catchException(\Exception $e) { + /** + * Defines the schema for the key_value table. + */ +- public static function schemaDefinition() { +- return [ ++ public function schemaDefinition() { ++ $schema = [ + 'description' => 'Generic key-value storage table. See the state system for an example.', + 'fields' => [ + 'collection' => [ +@@ -317,6 +410,12 @@ public static function schemaDefinition() { + ], + 'primary key' => ['collection', 'name'], + ]; ++ ++ if ($this->connection->driver() == 'mongodb') { ++ $schema['fields']['expire']['type'] = 'date'; ++ } ++ ++ return $schema; + } + + } +diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php +index 4215a9b5d93ece54ddca639f50fd4ae691136bd2..cb835f9577487a2946645f6f211c9d00d6c87e5c 100644 +--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php ++++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php +@@ -5,6 +5,7 @@ + use Drupal\Component\Datetime\TimeInterface; + use Drupal\Component\Serialization\SerializationInterface; + use Drupal\Core\Database\Connection; ++use MongoDB\BSON\UTCDateTime; + + /** + * Defines the key/value store factory for the database backend. +@@ -50,8 +51,12 @@ public function get($collection) { + */ + public function garbageCollection() { + try { ++ $now = $this->time->getRequestTime(); ++ if ($this->connection->driver() == 'mongodb') { ++ $now = new UTCDateTime($now * 1000); ++ } + $this->connection->delete('key_value_expire') +- ->condition('expire', $this->time->getRequestTime(), '<') ++ ->condition('expire', $now, '<') + ->execute(); + } + catch (\Exception $e) { +diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +index b877cf48592d62cb2218e8cb224f1eca1ee6fe2e..485b2f9484d4c88decf8e219e56e039c79b5145d 100644 +--- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php ++++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +@@ -134,7 +134,7 @@ public function checkNodeAccess(array $tree) { + else { + $access_result->addCacheContexts(['user.node_grants:view']); + if (!$this->moduleHandler->hasImplementations('node_grants') && !$this->account->hasPermission('view any unpublished content')) { +- $query->condition('status', NodeInterface::PUBLISHED); ++ $query->condition('status', (bool) NodeInterface::PUBLISHED); + } + } + +diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php +index cc25a39915252927f58a6915f55be7dae94fbb42..d3545d0db2a3d0ab1942a120fc586446ba927cb8 100644 +--- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php ++++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php +@@ -53,6 +53,15 @@ class MenuTreeStorage implements MenuTreeStorageInterface { + */ + protected $table; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Additional database connection options to use in queries. + * +@@ -212,6 +221,12 @@ protected function purgeMultiple(array $ids) { + * failed. + */ + protected function safeExecuteSelect(SelectInterface $query) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + try { + return $query->execute(); + } +@@ -256,6 +271,12 @@ public function save(array $link) { + * depth. + */ + protected function doSave(array $link) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $affected_menus = []; + + // Get the existing definition if it exists. This does not use +@@ -285,7 +306,17 @@ protected function doSave(array $link) { + } + + try { +- $transaction = $this->connection->startTransaction(); ++ if ($this->connection->driver() == 'mongodb') { ++ $session = $this->connection->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->connection->startTransaction(); ++ } + if (!$original) { + // Generate a new mlid. + $link['mlid'] = $this->connection->insert($this->table, $this->options) +@@ -296,18 +327,25 @@ protected function doSave(array $link) { + // We may be moving the link to a new menu. + $affected_menus[$fields['menu_name']] = $fields['menu_name']; + $query = $this->connection->update($this->table, $this->options); +- $query->condition('mlid', $link['mlid']); ++ $query->condition('mlid', (int) $link['mlid']); + $query->fields($fields) + ->execute(); + if ($original) { + $this->updateParentalStatus($original); + } + $this->updateParentalStatus($link); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + throw $e; + } + return $affected_menus; +@@ -426,6 +464,12 @@ public function getSubtreeHeight($id) { + * Returns the relative depth. + */ + protected function doFindChildrenRelativeDepth(array $original) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->addField($this->table, 'depth'); + $query->condition('menu_name', $original['menu_name']); +@@ -433,7 +477,7 @@ protected function doFindChildrenRelativeDepth(array $original) { + $query->range(0, 1); + + for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) { +- $query->condition("p$i", $original["p$i"]); ++ $query->condition("p$i", (int) $original["p$i"]); + } + + $max_depth = $this->safeExecuteSelect($query)->fetchField(); +@@ -502,6 +546,12 @@ protected function setParents(array &$fields, $parent, array $original) { + * The original menu link. + */ + protected function moveChildren($fields, $original) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->update($this->table, $this->options); + + $query->fields(['menu_name' => $fields['menu_name']]); +@@ -586,11 +636,17 @@ protected function findParent($link, $original) { + * The link to get a parent ID from. + */ + protected function updateParentalStatus(array $link) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // If parent is empty, there is nothing to update. + if (!empty($link['parent'])) { + // Check if at least one visible child exists in the table. + $query = $this->connection->select($this->table, NULL, $this->options); +- $query->addExpression('1'); ++ $query->addExpressionConstant('1'); + $query->range(0, 1); + $query + ->condition('menu_name', $link['menu_name']) +@@ -633,6 +689,12 @@ protected function prepareLink(array $link, $intersect = FALSE) { + * {@inheritdoc} + */ + public function loadByProperties(array $properties) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table, $this->definitionFields()); + foreach ($properties as $name => $value) { +@@ -653,6 +715,12 @@ public function loadByProperties(array $properties) { + * {@inheritdoc} + */ + public function loadByRoute($route_name, array $route_parameters = [], $menu_name = NULL) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // Sort the route parameters so that the query string will be the same. + asort($route_parameters); + // Since this will be urlencoded, it's safe to store and match against a +@@ -685,6 +753,12 @@ public function loadMultiple(array $ids) { + $missing_ids = array_diff($ids, array_keys($this->definitions)); + + if ($missing_ids) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table, $this->definitionFields()); + $query->condition('id', $missing_ids, 'IN'); +@@ -731,6 +805,12 @@ protected function loadFull($id) { + * The loaded menu link definitions. + */ + protected function loadFullMultiple(array $ids) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table); + $query->condition('id', $ids, 'IN'); +@@ -749,6 +829,12 @@ protected function loadFullMultiple(array $ids) { + * {@inheritdoc} + */ + public function getRootPathIds($id) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $subquery = $this->connection->select($this->table, NULL, $this->options); + // @todo Consider making this dynamic based on static::MAX_DEPTH or from the + // schema if that is generated using static::MAX_DEPTH. +@@ -773,15 +859,21 @@ public function getRootPathIds($id) { + * {@inheritdoc} + */ + public function getExpanded($menu_name, array $parents) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // @todo Go back to tracking in state or some other way which menus have + // expanded links? https://www.drupal.org/node/2302187 + do { + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table, ['id']); + $query->condition('menu_name', $menu_name); +- $query->condition('expanded', 1); +- $query->condition('has_children', 1); +- $query->condition('enabled', 1); ++ $query->condition('expanded', TRUE); ++ $query->condition('has_children', TRUE); ++ $query->condition('enabled', TRUE); + $query->condition('parent', $parents, 'IN'); + $query->condition('id', $parents, 'NOT IN'); + $result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); +@@ -858,6 +950,12 @@ public function loadTreeData($menu_name, MenuTreeParameters $parameters) { + * depth-first. + */ + protected function loadLinks($menu_name, MenuTreeParameters $parameters) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table); + +@@ -876,7 +974,7 @@ protected function loadLinks($menu_name, MenuTreeParameters $parameters) { + // tree. In other words: we exclude everything unreachable from the + // custom root. + for ($i = 1; $i <= $root['depth']; $i++) { +- $query->condition("p$i", $root["p$i"]); ++ $query->condition("p$i", (int) $root["p$i"]); + } + + // When specifying a custom root, the menu is determined by that root. +@@ -917,10 +1015,10 @@ protected function loadLinks($menu_name, MenuTreeParameters $parameters) { + $query->condition('parent', $parameters->expandedParents, 'IN'); + } + if (isset($parameters->minDepth) && $parameters->minDepth > 1) { +- $query->condition('depth', $parameters->minDepth, '>='); ++ $query->condition('depth', (int) $parameters->minDepth, '>='); + } + if (isset($parameters->maxDepth)) { +- $query->condition('depth', $parameters->maxDepth, '<='); ++ $query->condition('depth', (int) $parameters->maxDepth, '<='); + } + // Add custom query conditions, if any were passed. + if (!empty($parameters->conditions)) { +@@ -1005,6 +1103,12 @@ public function loadSubtreeData($id, $max_relative_depth = NULL) { + * {@inheritdoc} + */ + public function menuNameInUse($menu_name) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->addField($this->table, 'mlid'); + $query->condition('menu_name', $menu_name); +@@ -1016,6 +1120,12 @@ public function menuNameInUse($menu_name) { + * {@inheritdoc} + */ + public function getMenuNames() { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->addField($this->table, 'menu_name'); + $query->distinct(); +@@ -1026,6 +1136,12 @@ public function getMenuNames() { + * {@inheritdoc} + */ + public function countMenuLinks($menu_name = NULL) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + if ($menu_name) { + $query->condition('menu_name', $menu_name); +@@ -1041,11 +1157,18 @@ public function getAllChildIds($id) { + if (!$root) { + return []; + } ++ ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $query = $this->connection->select($this->table, NULL, $this->options); + $query->fields($this->table, ['id']); + $query->condition('menu_name', $root['menu_name']); + for ($i = 1; $i <= $root['depth']; $i++) { +- $query->condition("p$i", $root["p$i"]); ++ $query->condition("p$i", (int) $root["p$i"]); + } + // The next p column should not be empty. This excludes the root link. + $query->condition("p$i", 0, '>'); +@@ -1432,6 +1555,12 @@ protected static function schemaDefinition() { + * A list of menu link IDs that no longer exist. + */ + protected function findNoLongerExistingLinks(array $definitions) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + if ($definitions) { + $query = $this->connection->select($this->table, NULL, $this->options); + $query->addField($this->table, 'id'); +@@ -1455,6 +1584,12 @@ protected function findNoLongerExistingLinks(array $definitions) { + * A list of menu link IDs to be purged. + */ + protected function doDeleteMultiple(array $ids) { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $this->connection->delete($this->table, $this->options) + ->condition('id', $ids, 'IN') + ->execute(); +diff --git a/core/lib/Drupal/Core/Queue/Batch.php b/core/lib/Drupal/Core/Queue/Batch.php +index 1b71959ba20341d843fba1900ff6c4a1bb76a7fd..1c146c41fb667b527e759b9bbdc78d950ef2e126 100644 +--- a/core/lib/Drupal/Core/Queue/Batch.php ++++ b/core/lib/Drupal/Core/Queue/Batch.php +@@ -26,7 +26,14 @@ class Batch extends DatabaseQueue { + */ + public function claimItem($lease_time = 0) { + try { +- $item = $this->connection->queryRange('SELECT [data], [item_id] FROM {queue} q WHERE [name] = :name ORDER BY [item_id] ASC', 0, 1, [':name' => $this->name])->fetchObject(); ++ $item = $this->connection->select('queue', 'q') ++ ->fields('q', ['data', 'item_id']) ++ ->condition('name', $this->name) ++ ->orderBy('item_id', 'ASC') ++ ->range(0, 1) ++ ->execute() ++ ->fetchObject(); ++ + if ($item) { + $item->data = unserialize($item->data); + return $item; +diff --git a/core/lib/Drupal/Core/Queue/DatabaseQueue.php b/core/lib/Drupal/Core/Queue/DatabaseQueue.php +index ca2b8339d5cac8a13fe466ae0a3a4db2cd76c26a..da5c7263ff23f893db2c0f56e16fa14cbd207310 100644 +--- a/core/lib/Drupal/Core/Queue/DatabaseQueue.php ++++ b/core/lib/Drupal/Core/Queue/DatabaseQueue.php +@@ -34,6 +34,15 @@ class DatabaseQueue implements ReliableQueueInterface, QueueGarbageCollectionInt + */ + protected $connection; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Constructs a \Drupal\Core\Queue\DatabaseQueue object. + * +@@ -51,23 +60,34 @@ public function __construct($name, Connection $connection) { + * {@inheritdoc} + */ + public function createItem($data) { +- $try_again = FALSE; +- try { +- $id = $this->doCreateItem($data); +- } +- catch (\Exception $e) { +- // If there was an exception, try to create the table. +- if (!$try_again = $this->ensureTableExists()) { +- // If the exception happened for other reason than the missing table, +- // propagate the exception. +- throw $e; ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB the table needs to exist. Otherwise MongoDB creates one ++ // without the correct validation. ++ if (!$this->tableExists) { ++ $this->tableExists = $this->ensureTableExists(); + } ++ ++ return $this->doCreateItem($data); + } +- // Now that the table has been created, try again if necessary. +- if ($try_again) { +- $id = $this->doCreateItem($data); ++ else { ++ $try_again = FALSE; ++ try { ++ $id = $this->doCreateItem($data); ++ } ++ catch (\Exception $e) { ++ // If there was an exception, try to create the table. ++ if (!$try_again = $this->ensureTableExists()) { ++ // If the exception happened for other reason than the missing table, ++ // propagate the exception. ++ throw $e; ++ } ++ } ++ // Now that the table has been created, try again if necessary. ++ if ($try_again) { ++ $id = $this->doCreateItem($data); ++ } ++ return $id; + } +- return $id; + } + + /** +@@ -101,8 +121,17 @@ protected function doCreateItem($data) { + */ + public function numberOfItems() { + try { +- return (int) $this->connection->query('SELECT COUNT([item_id]) FROM {' . static::TABLE_NAME . '} WHERE [name] = :name', [':name' => $this->name]) +- ->fetchField(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . static::TABLE_NAME; ++ return $this->connection->getConnection()->selectCollection($prefixed_table)->count( ++ ['name' => ['$eq' => $this->name]], ++ ['session' => $this->connection->getMongodbSession()] ++ ); ++ } ++ else { ++ return (int) $this->connection->query('SELECT COUNT([item_id]) FROM {' . static::TABLE_NAME . '} WHERE [name] = :name', [':name' => $this->name]) ++ ->fetchField(); ++ } + } + catch (\Exception $e) { + $this->catchException($e); +@@ -121,7 +150,20 @@ public function claimItem($lease_time = 30) { + // are no unclaimed items left. + while (TRUE) { + try { +- $item = $this->connection->queryRange('SELECT [data], [created], [item_id] FROM {' . static::TABLE_NAME . '} q WHERE [expire] = 0 AND [name] = :name ORDER BY [created], [item_id] ASC', 0, 1, [':name' => $this->name])->fetchObject(); ++ if ($this->connection->driver() == 'mongodb') { ++ $item = $this->connection->select(static::TABLE_NAME, 'b') ++ ->fields('b', ['data', 'created', 'item_id']) ++ ->condition('expire', 0) ++ ->condition('name', $this->name) ++ ->orderBy('created') ++ ->orderBy('item_id') ++ ->range(0, 1) ++ ->execute() ++ ->fetchObject(); ++ } ++ else { ++ $item = $this->connection->queryRange('SELECT [data], [created], [item_id] FROM {' . static::TABLE_NAME . '} q WHERE [expire] = 0 AND [name] = :name ORDER BY [created], [item_id] ASC', 0, 1, [':name' => $this->name])->fetchObject(); ++ } + } + catch (\Exception $e) { + $this->catchException($e); +@@ -143,7 +185,7 @@ public function claimItem($lease_time = 30) { + ->fields([ + 'expire' => \Drupal::time()->getCurrentTime() + $lease_time, + ]) +- ->condition('item_id', $item->item_id) ++ ->condition('item_id', (int) $item->item_id) + ->condition('expire', 0); + // If there are affected rows, this update succeeded. + if ($update->execute()) { +@@ -162,7 +204,7 @@ public function releaseItem($item) { + ->fields([ + 'expire' => 0, + ]) +- ->condition('item_id', $item->item_id); ++ ->condition('item_id', (int) $item->item_id); + return (bool) $update->execute(); + } + catch (\Exception $e) { +@@ -189,7 +231,7 @@ public function delayItem($item, int $delay) { + ->fields([ + 'expire' => $expire, + ]) +- ->condition('item_id', $item->item_id); ++ ->condition('item_id', (int) $item->item_id); + return (bool) $update->execute(); + } + catch (\Exception $e) { +@@ -205,7 +247,7 @@ public function delayItem($item, int $delay) { + public function deleteItem($item) { + try { + $this->connection->delete(static::TABLE_NAME) +- ->condition('item_id', $item->item_id) ++ ->condition('item_id', (int) $item->item_id) + ->execute(); + } + catch (\Exception $e) { +diff --git a/core/lib/Drupal/Core/Recipe/ConfigConfigurator.php b/core/lib/Drupal/Core/Recipe/ConfigConfigurator.php +index ad837d5dc35cebd5e0efae3c3ee449993baa6494..56cc746cc898e9784f47008ef7a7a0629bb48ee7 100644 +--- a/core/lib/Drupal/Core/Recipe/ConfigConfigurator.php ++++ b/core/lib/Drupal/Core/Recipe/ConfigConfigurator.php +@@ -7,6 +7,8 @@ + use Drupal\Core\Config\FileStorage; + use Drupal\Core\Config\NullStorage; + use Drupal\Core\Config\StorageInterface; ++use Drupal\Core\Database\Connection; ++use Drupal\Core\DependencyInjection\DependencySerializationTrait; + + /** + * @internal +@@ -14,10 +16,19 @@ + */ + final class ConfigConfigurator { + ++ use DependencySerializationTrait; ++ + public readonly ?string $recipeConfigDirectory; + + private readonly bool|array $strict; + ++ /** ++ * The database connection. ++ * ++ * @var \Drupal\Core\Database\Connection ++ */ ++ protected Connection $connection; ++ + /** + * @param array $config + * Config options for a recipe. +@@ -25,11 +36,14 @@ final class ConfigConfigurator { + * The path to the recipe. + * @param \Drupal\Core\Config\StorageInterface $active_configuration + * The active configuration storage. ++ * @param \Drupal\Core\Database\Connection $connection ++ * The database connection. + */ +- public function __construct(public readonly array $config, string $recipe_directory, StorageInterface $active_configuration) { ++ public function __construct(public readonly array $config, string $recipe_directory, StorageInterface $active_configuration, Connection $connection) { + $this->recipeConfigDirectory = is_dir($recipe_directory . '/config') ? $recipe_directory . '/config' : NULL; + // @todo Consider defaulting this to FALSE in https://drupal.org/i/3478669. + $this->strict = $config['strict'] ?? TRUE; ++ $this->connection = $connection; + + $recipe_storage = $this->getConfigStorage(); + if ($this->strict === TRUE) { +@@ -96,6 +110,17 @@ public function getConfigStorage(): StorageInterface { + $storages = []; + + if ($this->recipeConfigDirectory) { ++ $directories = explode('/', $this->recipeConfigDirectory); ++ array_pop($directories); ++ $key = array_pop($directories); ++ ++ /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */ ++ $module_list = \Drupal::service('extension.list.module'); ++ $database_override_path = $module_list->getPath($this->connection->getProvider()) . '/config/overrides/recipes/' . $key; ++ if (is_dir($database_override_path)) { ++ $storages[] = new FileStorage($database_override_path); ++ } ++ + // Config provided by the recipe should take priority over config from + // extensions. + $storages[] = new FileStorage($this->recipeConfigDirectory); +@@ -117,10 +142,25 @@ public function getConfigStorage(): StorageInterface { + default => throw new \RuntimeException("$extension is not a theme or module") + }; + +- $storage = new RecipeConfigStorageWrapper( +- new FileStorage($path . '/config/install'), +- new FileStorage($path . '/config/optional'), +- ); ++ // Config item can be overridden by the current database driver. Those ++ // overridden config items are stored in the module of the current ++ // database driver in the "config/override" directory. ++ $database_override_path = $module_list->getPath($this->connection->getProvider()) . '/config/overrides/' . $extension; ++ if (is_dir($database_override_path)) { ++ $storage = new RecipeConfigStorageWrapper( ++ new FileStorage($path . '/config/install'), ++ new FileStorage($path . '/config/optional'), ++ new FileStorage($database_override_path . '/install'), ++ new FileStorage($database_override_path . '/optional'), ++ ); ++ } ++ else { ++ $storage = new RecipeConfigStorageWrapper( ++ new FileStorage($path . '/config/install'), ++ new FileStorage($path . '/config/optional'), ++ ); ++ } ++ + // If we get here, $names is either '*', or a list of config names + // provided by the current extension. In the latter case, we only want + // to import the config that is in the list, so use an +diff --git a/core/lib/Drupal/Core/Recipe/RecipeConfigStorageWrapper.php b/core/lib/Drupal/Core/Recipe/RecipeConfigStorageWrapper.php +index 9af54bfcb733b8e0d472189eefb8814cddcffd40..df7e81bd17a3d543aa0ee9828d25258daffa3210 100644 +--- a/core/lib/Drupal/Core/Recipe/RecipeConfigStorageWrapper.php ++++ b/core/lib/Drupal/Core/Recipe/RecipeConfigStorageWrapper.php +@@ -20,6 +20,10 @@ final class RecipeConfigStorageWrapper implements StorageInterface { + * First config storage to wrap. + * @param \Drupal\Core\Config\StorageInterface $storageB + * Second config storage to wrap. ++ * @param \Drupal\Core\Config\StorageInterface $storageDatabaseOverrideA ++ * First database override config storage to wrap. ++ * @param \Drupal\Core\Config\StorageInterface $storageDatabaseOverrideB ++ * Second database override config storage to wrap. + * @param string $collection + * (optional) The collection to store configuration in. Defaults to the + * default collection. +@@ -27,6 +31,8 @@ final class RecipeConfigStorageWrapper implements StorageInterface { + public function __construct( + protected readonly StorageInterface $storageA, + protected readonly StorageInterface $storageB, ++ protected readonly ?StorageInterface $storageDatabaseOverrideA = NULL, ++ protected readonly ?StorageInterface $storageDatabaseOverrideB = NULL, + protected readonly string $collection = StorageInterface::DEFAULT_COLLECTION, + ) { + } +@@ -66,6 +72,10 @@ public static function createStorageFromArray(array $storages): StorageInterface + * {@inheritdoc} + */ + public function exists($name): bool { ++ if ($this->storageDatabaseOverrideA && $this->storageDatabaseOverrideB) { ++ return $this->storageA->exists($name) || $this->storageB->exists($name) || $this->storageDatabaseOverrideA->exists($name) || $this->storageDatabaseOverrideB->exists($name); ++ } ++ + return $this->storageA->exists($name) || $this->storageB->exists($name); + } + +@@ -73,7 +83,17 @@ public function exists($name): bool { + * {@inheritdoc} + */ + public function read($name): array|bool { +- return $this->storageA->read($name) ?: $this->storageB->read($name); ++ if ($this->storageDatabaseOverrideA && ($data = $this->storageDatabaseOverrideA->read($name))) { ++ return $data; ++ } ++ if ($data = $this->storageA->read($name)) { ++ return $data; ++ } ++ if ($this->storageDatabaseOverrideB && ($data = $this->storageDatabaseOverrideB->read($name))) { ++ return $data; ++ } ++ ++ return $this->storageB->read($name); + } + + /** +@@ -82,6 +102,10 @@ public function read($name): array|bool { + public function readMultiple(array $names): array { + // If both storageA and storageB contain the same configuration, the value + // for storageA takes precedence. ++ if ($this->storageDatabaseOverrideA && $this->storageDatabaseOverrideB) { ++ return array_merge($this->storageB->readMultiple($names), $this->storageDatabaseOverrideB->readMultiple($names), $this->storageA->readMultiple($names), $this->storageDatabaseOverrideA->readMultiple($names)); ++ } ++ + return array_merge($this->storageB->readMultiple($names), $this->storageA->readMultiple($names)); + } + +@@ -124,6 +148,10 @@ public function decode($raw): array { + * {@inheritdoc} + */ + public function listAll($prefix = ''): array { ++ if ($this->storageDatabaseOverrideA && $this->storageDatabaseOverrideB) { ++ return array_unique(array_merge($this->storageA->listAll($prefix), $this->storageB->listAll($prefix), $this->storageDatabaseOverrideA->listAll($prefix), $this->storageDatabaseOverrideB->listAll($prefix))); ++ } ++ + return array_unique(array_merge($this->storageA->listAll($prefix), $this->storageB->listAll($prefix))); + } + +@@ -138,9 +166,21 @@ public function deleteAll($prefix = ''): bool { + * {@inheritdoc} + */ + public function createCollection($collection): static { ++ if ($this->storageDatabaseOverrideA && $this->storageDatabaseOverrideB) { ++ return new static( ++ $this->storageA->createCollection($collection), ++ $this->storageB->createCollection($collection), ++ $this->storageDatabaseOverrideA->createCollection($collection), ++ $this->storageDatabaseOverrideB->createCollection($collection), ++ $collection ++ ); ++ } ++ + return new static( + $this->storageA->createCollection($collection), + $this->storageB->createCollection($collection), ++ NULL, ++ NULL, + $collection + ); + } +@@ -149,6 +189,10 @@ public function createCollection($collection): static { + * {@inheritdoc} + */ + public function getAllCollectionNames(): array { ++ if ($this->storageDatabaseOverrideA && $this->storageDatabaseOverrideB) { ++ return array_unique(array_merge($this->storageA->getAllCollectionNames(), $this->storageB->getAllCollectionNames(), $this->storageDatabaseOverrideA->getAllCollectionNames(), $this->storageDatabaseOverrideB->getAllCollectionNames())); ++ } ++ + return array_unique(array_merge($this->storageA->getAllCollectionNames(), $this->storageB->getAllCollectionNames())); + } + +diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php +index 888f54e4f42cfdea0afd0142c1c011dfa824efa3..770b09b83d870891703c98793c2443061c60538a 100644 +--- a/core/lib/Drupal/Core/Recipe/Recipe.php ++++ b/core/lib/Drupal/Core/Recipe/Recipe.php +@@ -91,7 +91,7 @@ public static function createFromDirectory(string $path): static { + + $recipes = new RecipeConfigurator(is_array($recipe_data['recipes']) ? $recipe_data['recipes'] : [], dirname($path)); + $install = new InstallConfigurator($recipe_data['install'], \Drupal::service('extension.list.module'), \Drupal::service('extension.list.theme')); +- $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage')); ++ $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage'), \Drupal::database()); + $input = new InputConfigurator($recipe_data['input'] ?? [], $recipes, basename($path), \Drupal::typedDataManager()); + $content = new Finder($path . '/content'); + return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $input, $content, $path, $recipe_data['extra'] ?? []); +diff --git a/core/lib/Drupal/Core/Routing/MatcherDumper.php b/core/lib/Drupal/Core/Routing/MatcherDumper.php +index 78619fdd7213fa355420313fa022d6df452e7fa9..20fa7758402bda8a63355255c236af586d91b9b7 100644 +--- a/core/lib/Drupal/Core/Routing/MatcherDumper.php ++++ b/core/lib/Drupal/Core/Routing/MatcherDumper.php +@@ -91,7 +91,17 @@ public function dump(array $options = []): string { + // stale data. The transaction makes it atomic to avoid unstable router + // states due to random failures. + try { +- $transaction = $this->connection->startTransaction(); ++ if ($this->connection->driver() == 'mongodb') { ++ $session = $this->connection->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->connection->startTransaction(); ++ } + // We don't use truncate, because it is not guaranteed to be transaction + // safe. + try { +@@ -142,11 +152,17 @@ public function dump(array $options = []): string { + $insert->execute(); + } + ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException($this->logger, $e); + throw $e; + } +diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php +index 132f89631b0809fcfa36f44507878786a50a3d75..21bb482f7ab4a6e25accd7421a9c9648bafe5b91 100644 +--- a/core/lib/Drupal/Core/Routing/RouteProvider.php ++++ b/core/lib/Drupal/Core/Routing/RouteProvider.php +@@ -10,6 +10,7 @@ + use Drupal\Core\Path\CurrentPathStack; + use Drupal\Core\PathProcessor\InboundPathProcessorInterface; + use Drupal\Core\State\StateInterface; ++use Drupal\mongodb\Driver\Database\mongodb\Statement; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Exception\RouteNotFoundException; +@@ -231,8 +232,23 @@ public function preLoadRoutes($names) { + } + else { + try { +- $result = $this->connection->query('SELECT [name], [route] FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE [name] IN ( :names[] )', [':names[]' => $routes_to_load]); +- $routes = $result->fetchAllKeyed(); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->tableName; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['name' => ['$in' => $routes_to_load]], ++ [ ++ 'projection' => ['name' => 1, 'route' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'route']); ++ $routes = $statement->execute()->fetchAllKeyed(); ++ } ++ else { ++ $result = $this->connection->query('SELECT [name], [route] FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE [name] IN ( :names[] )', [':names[]' => $routes_to_load]); ++ $routes = $result->fetchAllKeyed(); ++ } + + $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']); + } +@@ -367,11 +383,26 @@ protected function getRoutesByPath($path) { + // trailing wildcard parts as long as the pattern matches, since we + // dump the route pattern without those optional parts. + try { +- $routes = $this->connection->query("SELECT [name], [route], [fit] FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE [pattern_outline] IN ( :patterns[] ) AND [number_parts] >= :count_parts", [ +- ':patterns[]' => $ancestors, +- ':count_parts' => count($parts), +- ]) +- ->fetchAll(\PDO::FETCH_ASSOC); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . $this->tableName; ++ $cursor = $this->connection->getConnection()->selectCollection($prefixed_table)->find( ++ ['pattern_outline' => ['$in' => $ancestors], 'number_parts' => ['$gte' => count($parts)]], ++ [ ++ 'projection' => ['name' => 1, 'route' => 1, 'fit' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ] ++ ); ++ ++ $statement = new Statement($this->connection, $cursor, ['name', 'route', 'fit']); ++ $routes = $statement->execute()->fetchAll(\PDO::FETCH_ASSOC); ++ } ++ else { ++ $routes = $this->connection->query("SELECT [name], [route], [fit] FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE [pattern_outline] IN ( :patterns[] ) AND [number_parts] >= :count_parts", [ ++ ':patterns[]' => $ancestors, ++ ':count_parts' => count($parts), ++ ]) ++ ->fetchAll(\PDO::FETCH_ASSOC); ++ } + } + catch (\Exception) { + $routes = []; +diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php +index fe1247158cd143850eca8c024eafa503907fd8dd..e04c84256542ae37e45cffe70d8a63be4b87b6e6 100644 +--- a/core/lib/Drupal/Core/Session/SessionHandler.php ++++ b/core/lib/Drupal/Core/Session/SessionHandler.php +@@ -7,6 +7,8 @@ + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseException; + use Drupal\Core\DependencyInjection\DependencySerializationTrait; ++use MongoDB\BSON\Binary; ++use MongoDB\BSON\UTCDateTime; + use Symfony\Component\HttpFoundation\RequestStack; + use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; + +@@ -17,6 +19,15 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { + + use DependencySerializationTrait; + ++ /** ++ * Indicator for the existence of the database table. ++ * ++ * This variable is only used by the database driver for MongoDB. ++ * ++ * @var bool ++ */ ++ protected $tableExists = FALSE; ++ + /** + * Constructs a new SessionHandler instance. + * +@@ -47,14 +58,31 @@ public function open(string $save_path, string $name): bool { + public function read(#[\SensitiveParameter] string $sid): string|false { + $data = ''; + if (!empty($sid)) { +- try { +- // Read the session data from the database. +- $query = $this->connection +- ->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]); +- $data = (string) $query->fetchField(); ++ // Read the session data from the database. ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . 'sessions'; ++ $result = $this->connection->getConnection()->selectCollection($prefixed_table)->findOne( ++ ['sid' => ['$eq' => Crypt::hashBase64($sid)]], ++ [ ++ 'projection' => ['session' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ], ++ ); ++ ++ // Get the session data. ++ if (isset($result->session) && ($result->session instanceof Binary)) { ++ $data = $result->session->getData(); ++ } + } +- // Swallow the error if the table hasn't been created yet. +- catch (\Exception) { ++ else { ++ try { ++ $query = $this->connection ++ ->queryRange('SELECT [session] FROM {sessions} WHERE [sid] = :sid', 0, 1, [':sid' => Crypt::hashBase64($sid)]); ++ $data = (string) $query->fetchField(); ++ } ++ // Swallow the error if the table hasn't been created yet. ++ catch (\Exception) { ++ } + } + } + return $data; +@@ -64,6 +92,12 @@ public function read(#[\SensitiveParameter] string $sid): string|false { + * {@inheritdoc} + */ + public function write(#[\SensitiveParameter] string $sid, string $value): bool { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table need to exists. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + $try_again = FALSE; + $request = $this->requestStack->getCurrentRequest(); + $fields = [ +@@ -108,6 +142,12 @@ public function close(): bool { + */ + public function destroy(#[\SensitiveParameter] string $sid): bool { + try { ++ if ($this->connection->driver() == 'mongodb' && !$this->tableExists) { ++ // For MongoDB the table need to exists. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ + // Delete session data. + $this->connection->delete('sessions') + ->condition('sid', Crypt::hashBase64($sid)) +@@ -129,9 +169,19 @@ public function gc(int $lifetime): int|false { + // for three weeks before deleting them, you need to set gc_maxlifetime + // to '1814400'. At that value, only after a user doesn't log in after + // three weeks (1814400 seconds) will their session be removed. ++ $timestamp = $this->time->getRequestTime() - $lifetime; ++ if ($this->connection->driver() == 'mongodb') { ++ $timestamp = new UTCDateTime($timestamp * 1000); ++ ++ if (!$this->tableExists) { ++ // For MongoDB the table need to exists. Otherwise MongoDB creates one ++ // without the correct validation. ++ $this->tableExists = $this->ensureTableExists(); ++ } ++ } + try { + return $this->connection->delete('sessions') +- ->condition('timestamp', $this->time->getRequestTime() - $lifetime, '<') ++ ->condition('timestamp', $timestamp, '<') + ->execute(); + } + // Swallow the error if the table hasn't been created yet. +@@ -197,6 +247,10 @@ protected function schemaDefinition(): array { + ], + ]; + ++ if ($this->connection->driver() == 'mongodb') { ++ $schema['fields']['timestamp']['type'] = 'date'; ++ } ++ + return $schema; + } + +diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php +index 1004b1f6621f5e5a67a577ee5f13ce999a67ddff..657c2e2bff0d720409742d56ab85fd12a45a3d62 100644 +--- a/core/lib/Drupal/Core/Session/SessionManager.php ++++ b/core/lib/Drupal/Core/Session/SessionManager.php +@@ -202,7 +202,7 @@ public function delete($uid) { + // The sessions table may not have been created yet. + try { + $this->connection->delete('sessions') +- ->condition('uid', $uid) ++ ->condition('uid', (int) $uid) + ->execute(); + } + catch (\Exception) { +diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php +index b0449e30f2a585b3f4f8cd09b4a79dcc58249197..50308ac070b3bbbae929ddb1d04375eb794a82a3 100644 +--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php ++++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php +@@ -37,7 +37,9 @@ public function validate($value, Constraint $constraint): void { + if ($typed_data instanceof BinaryInterface && !is_resource($value)) { + $valid = FALSE; + } +- if ($typed_data instanceof BooleanInterface && !(is_bool($value) || $value === 0 || $value === '0' || $value === 1 || $value == '1')) { ++ // With MongoDB a boolean with the value FALSE is stored as an empty ++ // string. ++ if ($typed_data instanceof BooleanInterface && !(is_bool($value) || $value === 0 || $value === '0' || $value === 1 || $value == '1' || $value == '')) { + $valid = FALSE; + } + if ($typed_data instanceof FloatInterface && filter_var($value, FILTER_VALIDATE_FLOAT) === FALSE) { +diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php +index 149030512117a95fb680b252e9bd0a8abce67bcc..f74e4cdc3497cad03f6b1955b63cae3dadb0310d 100644 +--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php ++++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php +@@ -53,8 +53,11 @@ public function validate($items, Constraint $constraint): void { + $field_label = $items->getFieldDefinition()->getLabel(); + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); + $property_name = $field_storage_definitions[$field_name]->getMainPropertyName(); ++ $property_schema = $field_storage_definitions[$field_name]->getSchema(); ++ $property_type = $property_schema['columns'][$property_name]['type'] ?? NULL; + + $id_key = $entity_type->getKey('id'); ++ $id_key_type = $field_storage_definitions[$id_key]->getType(); + $is_multiple = $field_storage_definitions[$field_name]->isMultiple(); + $is_new = $entity->isNew(); + $item_values = array_column($items->getValue(), $property_name); +@@ -66,7 +69,12 @@ public function validate($items, Constraint $constraint): void { + ->accessCheck(FALSE) + ->groupBy("$field_name.$property_name"); + if (!$is_new) { +- $entity_id = $entity->id(); ++ if ($id_key_type == 'integer') { ++ $entity_id = (int) $entity->id(); ++ } ++ else { ++ $entity_id = $entity->id(); ++ } + $query->condition($id_key, $entity_id, '<>'); + } + +@@ -76,7 +84,12 @@ public function validate($items, Constraint $constraint): void { + else { + $or_group = $query->orConditionGroup(); + foreach ($item_values as $item_value) { +- $or_group->condition($field_name, \Drupal::database()->escapeLike($item_value), 'LIKE'); ++ if ($property_type === 'int') { ++ $or_group->condition($field_name, $item_value); ++ } ++ else { ++ $or_group->condition($field_name, \Drupal::database()->escapeLike($item_value), 'LIKE'); ++ } + } + $query->condition($or_group); + } +diff --git a/core/modules/block_content/src/BlockContentViewsData.php b/core/modules/block_content/src/BlockContentViewsData.php +index 06c9de77f2286dcb4329bf4633f761903a029fe9..ceb16e4d061777d13532869024291c7c17a0f3a3 100644 +--- a/core/modules/block_content/src/BlockContentViewsData.php ++++ b/core/modules/block_content/src/BlockContentViewsData.php +@@ -16,14 +16,23 @@ public function getViewsData() { + + $data = parent::getViewsData(); + +- $data['block_content_field_data']['id']['field']['id'] = 'field'; ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'block_content'; ++ $revision_table = 'block_content'; ++ } ++ else { ++ $data_table = 'block_content_field_data'; ++ $revision_table = 'block_content_field_revision'; ++ } + +- $data['block_content_field_data']['info']['field']['id'] = 'field'; +- $data['block_content_field_data']['info']['field']['link_to_entity default'] = TRUE; ++ $data[$data_table]['id']['field']['id'] = 'field'; + +- $data['block_content_field_data']['type']['field']['id'] = 'field'; ++ $data[$data_table]['info']['field']['id'] = 'field'; ++ $data[$data_table]['info']['field']['link_to_entity default'] = TRUE; + +- $data['block_content_field_data']['table']['wizard_id'] = 'block_content'; ++ $data[$data_table]['type']['field']['id'] = 'field'; ++ ++ $data[$data_table]['table']['wizard_id'] = 'block_content'; + + $data['block_content']['block_content_listing_empty'] = [ + 'title' => $this->t('Empty block library behavior'), +@@ -33,8 +42,8 @@ public function getViewsData() { + ], + ]; + // Advertise this table as a possible base table. +- $data['block_content_field_revision']['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.'); +- $data['block_content_field_revision']['table']['base']['defaults']['title'] = 'info'; ++ $data[$revision_table]['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.'); ++ $data[$revision_table]['table']['base']['defaults']['title'] = 'info'; + + return $data; + } +diff --git a/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php b/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php +index ce592d27fb34ff77a7eab5f947e6ac7bfa2f9957..5ebd4b61f8ea2057476d2d2d7e050ed8e90f11fa 100644 +--- a/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php ++++ b/core/modules/block_content/src/Plugin/migrate/source/d7/BlockCustomTranslation.php +@@ -49,11 +49,11 @@ public function query() { + + // Add in the property, which is either title or body. Cast the bid to text + // so PostgreSQL can make the join. +- $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', '[i18n].[objectid] = CAST([b].[bid] AS CHAR(255))'); ++ $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', $query->joinCondition()->where('[i18n].[objectid] = CAST([b].[bid] AS CHAR(255))')); + $query->condition('i18n.type', 'block'); + + // Add in the translation for the property. +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + return $query; + } + +diff --git a/core/modules/block/src/Plugin/migrate/source/Block.php b/core/modules/block/src/Plugin/migrate/source/Block.php +index de6060c968d580a7cd102eb9ceb5f51d665d8e10..d33aa619513e122efc9583a45f05a50877ef45a2 100644 +--- a/core/modules/block/src/Plugin/migrate/source/Block.php ++++ b/core/modules/block/src/Plugin/migrate/source/Block.php +@@ -127,7 +127,7 @@ public function prepareRow(Row $row) { + ->fields('br', ['rid']) + ->condition('module', $module) + ->condition('delta', $delta); +- $query->join($this->userRoleTable, 'ur', '[br].[rid] = [ur].[rid]'); ++ $query->join($this->userRoleTable, 'ur', $query->joinCondition()->compare('br.rid', 'ur.rid')); + $roles = $query->execute() + ->fetchCol(); + $row->setSourceProperty('roles', $roles); +diff --git a/core/modules/block/src/Plugin/migrate/source/d6/BlockTranslation.php b/core/modules/block/src/Plugin/migrate/source/d6/BlockTranslation.php +index 6cfa1d6ffcd6041fc67d9159f3dbd5587717344f..7ccc64b80cbb30ee7200da7e4bfe1fcb33fbde3d 100644 +--- a/core/modules/block/src/Plugin/migrate/source/d6/BlockTranslation.php ++++ b/core/modules/block/src/Plugin/migrate/source/d6/BlockTranslation.php +@@ -32,7 +32,11 @@ public function query() { + $query = $this->select('i18n_blocks', 'i18n') + ->fields('i18n') + ->fields('b', ['bid', 'module', 'delta', 'theme', 'title']); +- $query->innerJoin($this->blockTable, 'b', ('[b].[module] = [i18n].[module] AND [b].[delta] = [i18n].[delta]')); ++ $query->innerJoin($this->blockTable, 'b', ++ $query->joinCondition() ++ ->compare('b.module', 'i18n.module') ++ ->compare('b.delta', 'i18n.delta') ++ ); + return $query; + } + +diff --git a/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php b/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php +index a826576642a51aab2621a4360a9655afe4558f1e..f4cfbed988606c8d8f19da50fee9258d6dcc40c2 100644 +--- a/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php ++++ b/core/modules/block/src/Plugin/migrate/source/d7/BlockTranslation.php +@@ -54,8 +54,8 @@ public function query() { + 'plural', + ]) + ->condition('i18n_mode', 1); +- $query->leftJoin($this->blockTable, 'b', ('[b].[delta] = [i18n].[objectid]')); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin($this->blockTable, 'b', $query->joinCondition()->compare('b.delta', 'i18n.objectid')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + + // The i18n_string module adds a status column to locale_target. It was + // originally 'status' in a later revision it was named 'i18n_status'. +diff --git a/core/modules/comment/comment.install b/core/modules/comment/comment.install +index b170eed6d442f89379031975160fdd078b01a639..374b9cf2174312632c099cd9254dd1401adbb63b 100644 +--- a/core/modules/comment/comment.install ++++ b/core/modules/comment/comment.install +@@ -108,6 +108,10 @@ function comment_schema(): array { + ], + ]; + ++ if (\Drupal::database()->driver() == 'mongodb') { ++ $schema['comment_entity_statistics']['fields']['last_comment_timestamp']['type'] = 'date'; ++ } ++ + return $schema; + } + +diff --git a/core/modules/comment/src/CommentManager.php b/core/modules/comment/src/CommentManager.php +index ba26943fe470479fbe91ca424b840ff395889032..e3d70048d61e526fd3ae361f2621cc5ecc4d9f23 100644 +--- a/core/modules/comment/src/CommentManager.php ++++ b/core/modules/comment/src/CommentManager.php +@@ -18,6 +18,7 @@ + use Drupal\field\Entity\FieldConfig; + use Drupal\user\RoleInterface; + use Drupal\user\UserInterface; ++use MongoDB\BSON\UTCDateTime; + + /** + * Comment manager contains common functions to manage comment fields. +@@ -218,14 +219,17 @@ public function getCountNewComments(EntityInterface $entity, $field_name = NULL, + } + } + $timestamp = ($timestamp > HISTORY_READ_LIMIT ? $timestamp : HISTORY_READ_LIMIT); ++ if (\Drupal::database()->databaseType() == 'mongodb') { ++ $timestamp = new UTCDateTime($timestamp * 1000); ++ } + + // Use the timestamp to retrieve the number of new comments. + $query = $this->entityTypeManager->getStorage('comment')->getQuery() + ->accessCheck(TRUE) + ->condition('entity_type', $entity->getEntityTypeId()) +- ->condition('entity_id', $entity->id()) ++ ->condition('entity_id', (int) $entity->id()) + ->condition('created', $timestamp, '>') +- ->condition('status', CommentInterface::PUBLISHED); ++ ->condition('status', (bool) CommentInterface::PUBLISHED); + if ($field_name) { + // Limit to a particular field. + $query->condition('field_name', $field_name); +diff --git a/core/modules/comment/src/CommentStatistics.php b/core/modules/comment/src/CommentStatistics.php +index 785f2b4c807eb4e1b8f4284c9f4e83a6fb2f93d6..3c16501dfc29eaafc9ec174a71e50c69b170e8a6 100644 +--- a/core/modules/comment/src/CommentStatistics.php ++++ b/core/modules/comment/src/CommentStatistics.php +@@ -205,7 +205,7 @@ public function update(CommentInterface $comment) { + } + + $query = $this->database->select('comment_field_data', 'c'); +- $query->addExpression('COUNT([cid])'); ++ $query->addExpressionCount('cid'); + $count = $query->condition('c.entity_id', $comment->getCommentedEntityId()) + ->condition('c.entity_type', $comment->getCommentedEntityTypeId()) + ->condition('c.field_name', $comment->getFieldName()) +diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php +index 4f668eac119964b96a3149a3a5162a30b94935a8..a9d3133b2b10860809673728c0170b1084d9e4b5 100644 +--- a/core/modules/comment/src/CommentStorage.php ++++ b/core/modules/comment/src/CommentStorage.php +@@ -17,6 +17,8 @@ + use Drupal\Core\Language\LanguageManagerInterface; + use Symfony\Component\DependencyInjection\ContainerInterface; + ++// cspell:ignore fieldcompare ++ + /** + * Defines the storage handler class for comments. + * +@@ -80,63 +82,175 @@ public static function createInstance(ContainerInterface $container, EntityTypeI + * {@inheritdoc} + */ + public function getMaxThread(CommentInterface $comment) { +- $query = $this->database->select($this->getDataTable(), 'c') +- ->condition('entity_id', $comment->getCommentedEntityId()) +- ->condition('field_name', $comment->getFieldName()) +- ->condition('entity_type', $comment->getCommentedEntityTypeId()) +- ->condition('default_langcode', 1); +- $query->addExpression('MAX([thread])', 'thread'); +- return $query->execute() +- ->fetchField(); ++ if ($this->database->driver() == 'mongodb') { ++ $result = $this->database->select($this->getBaseTable(), 'c') ++ ->fields('c', ['comment_translations']) ++ ->condition('comment_translations.entity_id', (int) $comment->getCommentedEntityId()) ++ ->condition('comment_translations.field_name', $comment->getFieldName()) ++ ->condition('comment_translations.entity_type', $comment->getCommentedEntityTypeId()) ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->execute() ++ ->fetchAll(); ++ ++ $max_thread = ''; ++ foreach ($result as $row) { ++ foreach ($row->comment_translations as $comment_translation) { ++ if (($comment_translation['entity_id'] == $comment->getCommentedEntityId()) && ++ ($comment_translation['entity_type'] == $comment->getCommentedEntityTypeId()) && ++ ($comment_translation['field_name'] == $comment->getFieldName()) && ++ ($comment_translation['default_langcode'] == CommentInterface::PUBLISHED) ++ ) { ++ if ($comment_translation['thread'] > $max_thread) { ++ $max_thread = $comment_translation['thread']; ++ } ++ } ++ } ++ } ++ return $max_thread; ++ } ++ else { ++ $query = $this->database->select($this->getDataTable(), 'c') ++ ->condition('entity_id', $comment->getCommentedEntityId()) ++ ->condition('field_name', $comment->getFieldName()) ++ ->condition('entity_type', $comment->getCommentedEntityTypeId()) ++ ->condition('default_langcode', 1); ++ $query->addExpressionMax('thread', 'thread'); ++ return $query->execute() ++ ->fetchField(); ++ } + } + + /** + * {@inheritdoc} + */ + public function getMaxThreadPerThread(CommentInterface $comment) { +- $query = $this->database->select($this->getDataTable(), 'c') +- ->condition('entity_id', $comment->getCommentedEntityId()) +- ->condition('field_name', $comment->getFieldName()) +- ->condition('entity_type', $comment->getCommentedEntityTypeId()) +- ->condition('thread', $comment->getParentComment()->getThread() . '.%', 'LIKE') +- ->condition('default_langcode', 1); +- $query->addExpression('MAX([thread])', 'thread'); +- return $query->execute() +- ->fetchField(); ++ if ($this->database->driver() == 'mongodb') { ++ $result = $this->database->select($this->getBaseTable(), 'c') ++ ->fields('c', ['comment_translations']) ++ ->condition('comment_translations.entity_id', (int) $comment->getCommentedEntityId()) ++ ->condition('comment_translations.field_name', $comment->getFieldName()) ++ ->condition('comment_translations.entity_type', $comment->getCommentedEntityTypeId()) ++ ->condition('comment_translations.thread', $comment->getParentComment()->getThread() . '.%', 'LIKE') ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->execute() ++ ->fetchAll(); ++ ++ $max_thread = ''; ++ foreach ($result as $row) { ++ foreach ($row->comment_translations as $comment_translation) { ++ if (($comment_translation['entity_id'] == $comment->getCommentedEntityId()) && ++ ($comment_translation['entity_type'] == $comment->getCommentedEntityTypeId()) && ++ ($comment_translation['field_name'] == $comment->getFieldName()) && ++ ($comment_translation['default_langcode'] == CommentInterface::PUBLISHED) ++ ) { ++ $pattern = '/^' . $comment->getParentComment()->getThread() . '.*/'; ++ if (($comment_translation['thread'] > $max_thread) && preg_match($pattern, $comment_translation['thread'])) { ++ $max_thread = $comment_translation['thread']; ++ } ++ } ++ } ++ } ++ return $max_thread; ++ } ++ else { ++ $query = $this->database->select($this->getDataTable(), 'c') ++ ->condition('entity_id', $comment->getCommentedEntityId()) ++ ->condition('field_name', $comment->getFieldName()) ++ ->condition('entity_type', $comment->getCommentedEntityTypeId()) ++ ->condition('thread', $comment->getParentComment()->getThread() . '.%', 'LIKE') ++ ->condition('default_langcode', 1); ++ $query->addExpressionMax('thread', 'thread'); ++ return $query->execute() ++ ->fetchField(); ++ } + } + + /** + * {@inheritdoc} + */ + public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $divisor = 1) { +- // Count how many comments (c1) are before $comment (c2) in display order. +- // This is the 0-based display ordinal. +- $data_table = $this->getDataTable(); +- $query = $this->database->select($data_table, 'c1'); +- $query->innerJoin($data_table, 'c2', '[c2].[entity_id] = [c1].[entity_id] AND [c2].[entity_type] = [c1].[entity_type] AND [c2].[field_name] = [c1].[field_name]'); +- $query->addExpression('COUNT(*)', 'count'); +- $query->condition('c2.cid', $comment->id()); +- if (!$this->currentUser->hasPermission('administer comments')) { +- $query->condition('c1.status', CommentInterface::PUBLISHED); +- } ++ if ($this->database->driver() == 'mongodb') { ++ // Count how many comments (c1) are before $comment (c2) in display order. ++ // This is the 0-based display ordinal. ++ $query = $this->database->select('comment', 'c1') ++ ->fields('c1', ['cid']); ++ ++ // The comment_translations field must be added in a special way, because ++ // the join operation will overwrite its value. ++ $query->addPreJoinField('c1_comment_translations', 'comment_translations'); ++ ++ $query->addJoin('INNER', 'comment', 'c2', $query->joinCondition() ++ ->compare('c1.comment_translations.entity_id', 'c2.comment_translations.entity_id') ++ ->compare('c1.comment_translations.entity_type', 'c2.comment_translations.entity_type') ++ ->compare('c1.comment_translations.field_name', 'c2.comment_translations.field_name') ++ ); + +- if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) { +- // For rendering flat comments, cid is used for ordering comments due to +- // unpredictable behavior with timestamp, so we make the same assumption +- // here. +- $query->condition('c1.cid', $comment->id(), '<'); ++ $query->condition('c2.comment_translations.cid', (int) $comment->id()); ++ if (!$this->currentUser->hasPermission('administer comments')) { ++ $query->condition('c1_comment_translations.status', (bool) CommentInterface::PUBLISHED); ++ } ++ ++ if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ // For rendering flat comments, cid is used for ordering comments due to ++ // unpredictable behavior with timestamp, so we make the same assumption ++ // here. ++ $query->condition('c1_comment_translations.cid', (int) $comment->id(), '<'); ++ } ++ else { ++ // For threaded comments, the c.thread column is used for ordering. We can ++ // use the sorting code for comparison, but must remove the trailing ++ // slash. ++ $query->addSubstringField('c1_thread', 'c1_comment_translations.thread', 1, -2); ++ ++ // The array "c2.comment_translations" is unwound and yet the MongoDB ++ // throws an exception that it is an array and not a string. For MongoDB ++ // it would be better to store the value thread as a string with a ++ // trailing slash and as an integer value. ++ $query->addSubstringField('c2_thread', 'c2.comment_translations.thread', 1, -2); ++ $query->condition('c2_thread', ['field' => 'c1_thread', 'operator' => '>'], 'FIELDCOMPARE'); ++ } ++ ++ $query->condition('c1_comment_translations.default_langcode', TRUE); ++ $query->condition('c2.comment_translations.default_langcode', TRUE); ++ ++ $result = $query->execute()->fetchAll(); ++ $ordinal = count($result); + } + else { +- // For threaded comments, the c.thread column is used for ordering. We can +- // use the sorting code for comparison, but must remove the trailing +- // slash. +- $query->where('SUBSTRING([c1].[thread], 1, (LENGTH([c1].[thread]) - 1)) < SUBSTRING([c2].[thread], 1, (LENGTH([c2].[thread]) - 1))'); +- } ++ // Count how many comments (c1) are before $comment (c2) in display order. ++ // This is the 0-based display ordinal. ++ $data_table = $this->getDataTable(); ++ $query = $this->database->select($data_table, 'c1'); ++ $query->innerJoin($data_table, 'c2', ++ $query->joinCondition() ++ ->compare('c2.entity_id', 'c1.entity_id') ++ ->compare('c2.entity_type', 'c1.entity_type') ++ ->compare('c2.field_name', 'c1.field_name') ++ ); ++ $query->addExpressionCountAll('count'); ++ $query->condition('c2.cid', $comment->id()); ++ if (!$this->currentUser->hasPermission('administer comments')) { ++ $query->condition('c1.status', CommentInterface::PUBLISHED); ++ } + +- $query->condition('c1.default_langcode', 1); +- $query->condition('c2.default_langcode', 1); ++ if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ // For rendering flat comments, cid is used for ordering comments due to ++ // unpredictable behavior with timestamp, so we make the same assumption ++ // here. ++ $query->condition('c1.cid', $comment->id(), '<'); ++ } ++ else { ++ // For threaded comments, the c.thread column is used for ordering. We can ++ // use the sorting code for comparison, but must remove the trailing ++ // slash. ++ $query->where('SUBSTRING([c1].[thread], 1, (LENGTH([c1].[thread]) - 1)) < SUBSTRING([c2].[thread], 1, (LENGTH([c2].[thread]) - 1))'); ++ } + +- $ordinal = $query->execute()->fetchField(); ++ $query->condition('c1.default_langcode', 1); ++ $query->condition('c2.default_langcode', 1); ++ ++ $ordinal = $query->execute()->fetchField(); ++ } + + return ($divisor > 1) ? floor($ordinal / $divisor) : $ordinal; + } +@@ -147,58 +261,111 @@ public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $div + public function getNewCommentPageNumber($total_comments, $new_comments, FieldableEntityInterface $entity, $field_name) { + $field = $entity->getFieldDefinition($field_name); + $comments_per_page = $field->getSetting('per_page'); +- $data_table = $this->getDataTable(); + +- if ($total_comments <= $comments_per_page) { +- // Only one page of comments. +- $count = 0; +- } +- elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) { +- // Flat comments. +- $count = $total_comments - $new_comments; ++ if ($this->database->driver() == 'mongodb') { ++ $base_table = $this->getBaseTable(); ++ ++ if ($total_comments <= $comments_per_page) { ++ // Only one page of comments. ++ $count = 0; ++ } ++ elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ // Flat comments. ++ $count = $total_comments - $new_comments; ++ } ++ else { ++ // Threaded comments. ++ ++ // 1. Find all the threads with a new comment. ++ $unread_threads = $this->database->select($base_table, 'comment') ++ ->fields('comment', ['thread']) ++ ->condition('comment_translations.entity_id', (int) $entity->id()) ++ ->condition('comment_translations.entity_type', $entity->getEntityTypeId()) ++ ->condition('comment_translations.field_name', $field_name) ++ ->condition('comment_translations.status', (bool) CommentInterface::PUBLISHED) ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->orderBy('comment_translations.created', 'DESC') ++ ->orderBy('comment_translations.cid', 'DESC') ++ ->range(0, $new_comments) ++ ->execute() ++ ->fetchCol(); ++ ++ // 2. Find the first thread. ++ foreach ($unread_threads as &$unread_thread) { ++ $unread_thread = substr($unread_thread, 0, -1); ++ $unread_thread = ltrim($unread_thread, '0'); ++ } ++ natsort($unread_threads); ++ ++ $first_thread = reset($unread_threads); ++ ++ // 3. Find the number of the first comment of the first unread thread. ++ $threads_query = $this->database->select($base_table, 'comment') ++ ->fields('comment', ['cid']) ++ ->condition('comment_translations.entity_id', (int) $entity->id()) ++ ->condition('comment_translations.entity_type', $entity->getEntityTypeId()) ++ ->condition('comment_translations.field_name', $field_name) ++ ->condition('comment_translations.status', (bool) CommentInterface::PUBLISHED); ++ $threads_query->addSubstringField('thread_without_slash', 'thread', 1, -2); ++ $threads_query->condition('thread_without_slash', $first_thread, '<'); ++ $cids = $threads_query->execute()->fetchAll(); ++ $count = count($cids); ++ } + } + else { +- // Threaded comments. +- +- // 1. Find all the threads with a new comment. +- $unread_threads_query = $this->database->select($data_table, 'comment') +- ->fields('comment', ['thread']) +- ->condition('entity_id', $entity->id()) +- ->condition('entity_type', $entity->getEntityTypeId()) +- ->condition('field_name', $field_name) +- ->condition('status', CommentInterface::PUBLISHED) +- ->condition('default_langcode', 1) +- ->orderBy('created', 'DESC') +- ->orderBy('cid', 'DESC') +- ->range(0, $new_comments); +- +- // 2. Find the first thread. +- $first_thread_query = $this->database->select($unread_threads_query, 'thread'); +- $first_thread_query->addExpression('SUBSTRING([thread], 1, (LENGTH([thread]) - 1))', 'torder'); +- $first_thread = $first_thread_query +- ->fields('thread', ['thread']) +- ->orderBy('torder') +- ->range(0, 1) +- ->execute() +- ->fetchField(); ++ $data_table = $this->getDataTable(); + +- // Remove the final '/'. +- $first_thread = substr($first_thread, 0, -1); +- +- // Find the number of the first comment of the first unread thread. +- $count = $this->database->query('SELECT COUNT(*) FROM {' . $data_table . '} WHERE [entity_id] = :entity_id +- AND [entity_type] = :entity_type +- AND [field_name] = :field_name +- AND [status] = :status +- AND SUBSTRING([thread], 1, (LENGTH([thread]) - 1)) < :thread +- AND [default_langcode] = 1', [ +- ':status' => CommentInterface::PUBLISHED, +- ':entity_id' => $entity->id(), +- ':field_name' => $field_name, +- ':entity_type' => $entity->getEntityTypeId(), +- ':thread' => $first_thread, +- ] +- )->fetchField(); ++ if ($total_comments <= $comments_per_page) { ++ // Only one page of comments. ++ $count = 0; ++ } ++ elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ // Flat comments. ++ $count = $total_comments - $new_comments; ++ } ++ else { ++ // Threaded comments. ++ ++ // 1. Find all the threads with a new comment. ++ $unread_threads_query = $this->database->select($data_table, 'comment') ++ ->fields('comment', ['thread']) ++ ->condition('entity_id', $entity->id()) ++ ->condition('entity_type', $entity->getEntityTypeId()) ++ ->condition('field_name', $field_name) ++ ->condition('status', CommentInterface::PUBLISHED) ++ ->condition('default_langcode', 1) ++ ->orderBy('created', 'DESC') ++ ->orderBy('cid', 'DESC') ++ ->range(0, $new_comments); ++ ++ // 2. Find the first thread. ++ $first_thread_query = $this->database->select($unread_threads_query, 'thread'); ++ $first_thread_query->addExpression('SUBSTRING([thread], 1, (LENGTH([thread]) - 1))', 'torder'); ++ $first_thread = $first_thread_query ++ ->fields('thread', ['thread']) ++ ->orderBy('torder') ++ ->range(0, 1) ++ ->execute() ++ ->fetchField(); ++ ++ // Remove the final '/'. ++ $first_thread = substr($first_thread, 0, -1); ++ ++ // Find the number of the first comment of the first unread thread. ++ $count = $this->database->query('SELECT COUNT(*) FROM {' . $data_table . '} WHERE [entity_id] = :entity_id ++ AND [entity_type] = :entity_type ++ AND [field_name] = :field_name ++ AND [status] = :status ++ AND SUBSTRING([thread], 1, (LENGTH([thread]) - 1)) < :thread ++ AND [default_langcode] = 1', [ ++ ':status' => CommentInterface::PUBLISHED, ++ ':entity_id' => $entity->id(), ++ ':field_name' => $field_name, ++ ':entity_type' => $entity->getEntityTypeId(), ++ ':thread' => $first_thread, ++ ] ++ )->fetchField(); ++ } + } + + return $comments_per_page > 0 ? (int) ($count / $comments_per_page) : 0; +@@ -208,12 +375,26 @@ public function getNewCommentPageNumber($total_comments, $new_comments, Fieldabl + * {@inheritdoc} + */ + public function getChildCids(array $comments) { +- return $this->database->select($this->getDataTable(), 'c') +- ->fields('c', ['cid']) +- ->condition('pid', array_keys($comments), 'IN') +- ->condition('default_langcode', 1) +- ->execute() +- ->fetchCol(); ++ if ($this->database->driver() == 'mongodb') { ++ $cids = []; ++ foreach (array_keys($comments) as $cid) { ++ $cids[] = (int) $cid; ++ } ++ return $this->database->select($this->getBaseTable(), 'c') ++ ->fields('c', ['cid']) ++ ->condition('comment_translations.pid', $cids, 'IN') ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->execute() ++ ->fetchCol(); ++ } ++ else { ++ return $this->database->select($this->getDataTable(), 'c') ++ ->fields('c', ['cid']) ++ ->condition('pid', array_keys($comments), 'IN') ++ ->condition('default_langcode', 1) ++ ->execute() ++ ->fetchCol(); ++ } + } + + /** +@@ -274,30 +455,51 @@ public function getChildCids(array $comments) { + * to consider the trailing "/" so we use a substring only. + */ + public function loadThread(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) { +- $data_table = $this->getDataTable(); +- $query = $this->database->select($data_table, 'c'); +- $query->addField('c', 'cid'); +- $query +- ->condition('c.entity_id', $entity->id()) +- ->condition('c.entity_type', $entity->getEntityTypeId()) +- ->condition('c.field_name', $field_name) +- ->condition('c.default_langcode', 1) +- ->addTag('entity_access') +- ->addTag('comment_filter') +- ->addMetaData('base_table', 'comment') +- ->addMetaData('entity', $entity) +- ->addMetaData('field_name', $field_name); +- +- if ($comments_per_page) { +- $query = $query->extend(PagerSelectExtender::class) +- ->limit($comments_per_page); +- if ($pager_id) { +- $query->element($pager_id); ++ if ($this->database->driver() == 'mongodb') { ++ $query = $this->database->select($this->getBaseTable(), 'c'); ++ $query->addField('c', 'cid'); ++ $query ++ ->condition('comment_translations.entity_id', (int) $entity->id()) ++ ->condition('comment_translations.entity_type', $entity->getEntityTypeId()) ++ ->condition('comment_translations.field_name', $field_name) ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->addTag('entity_access') ++ ->addTag('comment_filter') ++ ->addMetaData('base_table', 'comment') ++ ->addMetaData('entity', $entity) ++ ->addMetaData('field_name', $field_name); ++ ++ if ($comments_per_page) { ++ $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender') ++ ->limit($comments_per_page); ++ if ($pager_id) { ++ $query->element($pager_id); ++ } ++ ++ // @todo Start using $query->setCountQuery($count_query); ++ // $query->setCountQueryMethod($this, 'countQueryThread', [$entity, $field_name, $comments_per_page]); + } + +- $count_query = $this->database->select($data_table, 'c'); +- $count_query->addExpression('COUNT(*)'); +- $count_query ++ if (!$this->currentUser->hasPermission('administer comments')) { ++ $query->condition('comment_translations.status', (bool) CommentInterface::PUBLISHED); ++ } ++ if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ $query->orderBy('cid', 'ASC'); ++ } ++ else { ++ // See comment above. Analysis reveals that this doesn't cost too ++ // much. It scales much much better than having the whole comment ++ // structure. ++ $query->addSubstringField('thread_order', 'comment_translations.thread', 1, -2); ++ $query->orderBy('thread_order', 'ASC'); ++ } ++ $cids = $query->execute()->fetchCol(); ++ } ++ else { ++ $data_table = $this->getDataTable(); ++ $query = $this->database->select($data_table, 'c'); ++ $query->addField('c', 'cid'); ++ $query + ->condition('c.entity_id', $entity->id()) + ->condition('c.entity_type', $entity->getEntityTypeId()) + ->condition('c.field_name', $field_name) +@@ -307,26 +509,47 @@ public function loadThread(EntityInterface $entity, $field_name, $mode, $comment + ->addMetaData('base_table', 'comment') + ->addMetaData('entity', $entity) + ->addMetaData('field_name', $field_name); +- $query->setCountQuery($count_query); +- } + +- if (!$this->currentUser->hasPermission('administer comments')) { +- $query->condition('c.status', CommentInterface::PUBLISHED); + if ($comments_per_page) { +- $count_query->condition('c.status', CommentInterface::PUBLISHED); ++ $query = $query->extend(PagerSelectExtender::class) ++ ->limit($comments_per_page); ++ if ($pager_id) { ++ $query->element($pager_id); ++ } ++ ++ $count_query = $this->database->select($data_table, 'c'); ++ $count_query->addExpression('COUNT(*)'); ++ $count_query ++ ->condition('c.entity_id', $entity->id()) ++ ->condition('c.entity_type', $entity->getEntityTypeId()) ++ ->condition('c.field_name', $field_name) ++ ->condition('c.default_langcode', 1) ++ ->addTag('entity_access') ++ ->addTag('comment_filter') ++ ->addMetaData('base_table', 'comment') ++ ->addMetaData('entity', $entity) ++ ->addMetaData('field_name', $field_name); ++ $query->setCountQuery($count_query); ++ } ++ ++ if (!$this->currentUser->hasPermission('administer comments')) { ++ $query->condition('c.status', CommentInterface::PUBLISHED); ++ if ($comments_per_page) { ++ $count_query->condition('c.status', CommentInterface::PUBLISHED); ++ } ++ } ++ if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) { ++ $query->orderBy('c.cid', 'ASC'); ++ } ++ else { ++ // See comment above. Analysis reveals that this doesn't cost too much. It ++ // scales much better than having the whole comment structure. ++ $query->addExpression('SUBSTRING([c].[thread], 1, (LENGTH([c].[thread]) - 1))', 'torder'); ++ $query->orderBy('torder', 'ASC'); + } +- } +- if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) { +- $query->orderBy('c.cid', 'ASC'); +- } +- else { +- // See comment above. Analysis reveals that this doesn't cost too much. It +- // scales much better than having the whole comment structure. +- $query->addExpression('SUBSTRING([c].[thread], 1, (LENGTH([c].[thread]) - 1))', 'torder'); +- $query->orderBy('torder', 'ASC'); +- } + +- $cids = $query->execute()->fetchCol(); ++ $cids = $query->execute()->fetchCol(); ++ } + + $comments = []; + if ($cids) { +@@ -340,12 +563,22 @@ public function loadThread(EntityInterface $entity, $field_name, $mode, $comment + * {@inheritdoc} + */ + public function getUnapprovedCount() { +- return $this->database->select($this->getDataTable(), 'c') +- ->condition('status', CommentInterface::NOT_PUBLISHED, '=') +- ->condition('default_langcode', 1) +- ->countQuery() +- ->execute() +- ->fetchField(); ++ if ($this->database->driver() == 'mongodb') { ++ return $this->database->select($this->getBaseTable(), 'c') ++ ->condition('comment_translations.status', (bool) CommentInterface::NOT_PUBLISHED) ++ ->condition('comment_translations.default_langcode', TRUE) ++ ->countQuery() ++ ->execute() ++ ->fetchField(); ++ } ++ else { ++ return $this->database->select($this->getDataTable(), 'c') ++ ->condition('status', CommentInterface::NOT_PUBLISHED, '=') ++ ->condition('default_langcode', 1) ++ ->countQuery() ++ ->execute() ++ ->fetchField(); ++ } + } + + } +diff --git a/core/modules/comment/src/CommentViewsData.php b/core/modules/comment/src/CommentViewsData.php +index 944827366376f987c3612857763e0a71e6e57d5c..8c7dd64b2283208b2d46c5de13266197169145a8 100644 +--- a/core/modules/comment/src/CommentViewsData.php ++++ b/core/modules/comment/src/CommentViewsData.php +@@ -16,28 +16,35 @@ class CommentViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + +- $data['comment_field_data']['table']['base']['help'] = $this->t('Comments are responses to content.'); +- $data['comment_field_data']['table']['base']['access query tag'] = 'comment_access'; ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'comment'; ++ } ++ else { ++ $data_table = 'comment_field_data'; ++ } ++ ++ $data[$data_table]['table']['base']['help'] = $this->t('Comments are responses to content.'); ++ $data[$data_table]['table']['base']['access query tag'] = 'comment_access'; + +- $data['comment_field_data']['table']['wizard_id'] = 'comment'; ++ $data[$data_table]['table']['wizard_id'] = 'comment'; + +- $data['comment_field_data']['subject']['title'] = $this->t('Title'); +- $data['comment_field_data']['subject']['help'] = $this->t('The title of the comment.'); +- $data['comment_field_data']['subject']['field']['default_formatter'] = 'comment_permalink'; ++ $data[$data_table]['subject']['title'] = $this->t('Title'); ++ $data[$data_table]['subject']['help'] = $this->t('The title of the comment.'); ++ $data[$data_table]['subject']['field']['default_formatter'] = 'comment_permalink'; + +- $data['comment_field_data']['name']['title'] = $this->t('Author'); +- $data['comment_field_data']['name']['help'] = $this->t("The name of the comment's author. Can be rendered as a link to the author's homepage."); +- $data['comment_field_data']['name']['field']['default_formatter'] = 'comment_username'; ++ $data[$data_table]['name']['title'] = $this->t('Author'); ++ $data[$data_table]['name']['help'] = $this->t("The name of the comment's author. Can be rendered as a link to the author's homepage."); ++ $data[$data_table]['name']['field']['default_formatter'] = 'comment_username'; + +- $data['comment_field_data']['homepage']['title'] = $this->t("Author's website"); +- $data['comment_field_data']['homepage']['help'] = $this->t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user."); ++ $data[$data_table]['homepage']['title'] = $this->t("Author's website"); ++ $data[$data_table]['homepage']['help'] = $this->t("The website address of the comment's author. Can be rendered as a link. Will be empty if the author is a registered user."); + +- $data['comment_field_data']['mail']['help'] = $this->t('Email of user that posted the comment. Will be empty if the author is a registered user.'); ++ $data[$data_table]['mail']['help'] = $this->t('Email of user that posted the comment. Will be empty if the author is a registered user.'); + +- $data['comment_field_data']['created']['title'] = $this->t('Post date'); +- $data['comment_field_data']['created']['help'] = $this->t('Date and time of when the comment was created.'); ++ $data[$data_table]['created']['title'] = $this->t('Post date'); ++ $data[$data_table]['created']['help'] = $this->t('Date and time of when the comment was created.'); + +- $data['comment_field_data']['created_fulldata'] = [ ++ $data[$data_table]['created_fulldata'] = [ + 'title' => $this->t('Created date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -46,7 +53,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['created_year_month'] = [ ++ $data[$data_table]['created_year_month'] = [ + 'title' => $this->t('Created year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -55,7 +62,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['created_year'] = [ ++ $data[$data_table]['created_year'] = [ + 'title' => $this->t('Created year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -64,7 +71,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['created_month'] = [ ++ $data[$data_table]['created_month'] = [ + 'title' => $this->t('Created month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -73,7 +80,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['created_day'] = [ ++ $data[$data_table]['created_day'] = [ + 'title' => $this->t('Created day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -82,7 +89,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['created_week'] = [ ++ $data[$data_table]['created_week'] = [ + 'title' => $this->t('Created week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -91,10 +98,10 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed']['title'] = $this->t('Updated date'); +- $data['comment_field_data']['changed']['help'] = $this->t('Date and time of when the comment was last updated.'); ++ $data[$data_table]['changed']['title'] = $this->t('Updated date'); ++ $data[$data_table]['changed']['help'] = $this->t('Date and time of when the comment was last updated.'); + +- $data['comment_field_data']['changed_fulldata'] = [ ++ $data[$data_table]['changed_fulldata'] = [ + 'title' => $this->t('Changed date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -103,7 +110,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed_year_month'] = [ ++ $data[$data_table]['changed_year_month'] = [ + 'title' => $this->t('Changed year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -112,7 +119,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed_year'] = [ ++ $data[$data_table]['changed_year'] = [ + 'title' => $this->t('Changed year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -121,7 +128,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed_month'] = [ ++ $data[$data_table]['changed_month'] = [ + 'title' => $this->t('Changed month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -130,7 +137,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed_day'] = [ ++ $data[$data_table]['changed_day'] = [ + 'title' => $this->t('Changed day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -139,7 +146,7 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['changed_week'] = [ ++ $data[$data_table]['changed_week'] = [ + 'title' => $this->t('Changed week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -148,10 +155,10 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['status']['title'] = $this->t('Approved status'); +- $data['comment_field_data']['status']['help'] = $this->t('Whether the comment is approved (or still in the moderation queue).'); +- $data['comment_field_data']['status']['filter']['label'] = $this->t('Approved comment status'); +- $data['comment_field_data']['status']['filter']['type'] = 'yes-no'; ++ $data[$data_table]['status']['title'] = $this->t('Approved status'); ++ $data[$data_table]['status']['help'] = $this->t('Whether the comment is approved (or still in the moderation queue).'); ++ $data[$data_table]['status']['filter']['label'] = $this->t('Approved comment status'); ++ $data[$data_table]['status']['filter']['type'] = 'yes-no'; + + $data['comment']['approve_comment'] = [ + 'field' => [ +@@ -169,8 +176,8 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['entity_id']['field']['id'] = 'commented_entity'; +- unset($data['comment_field_data']['entity_id']['relationship']); ++ $data[$data_table]['entity_id']['field']['id'] = 'commented_entity'; ++ unset($data[$data_table]['entity_id']['relationship']); + + $data['comment']['comment_bulk_form'] = [ + 'title' => $this->t('Comment operations bulk form'), +@@ -180,18 +187,18 @@ public function getViewsData() { + ], + ]; + +- $data['comment_field_data']['thread']['field'] = [ ++ $data[$data_table]['thread']['field'] = [ + 'title' => $this->t('Depth'), + 'help' => $this->t('Display the depth of the comment if it is threaded.'), + 'id' => 'comment_depth', + ]; +- $data['comment_field_data']['thread']['sort'] = [ ++ $data[$data_table]['thread']['sort'] = [ + 'title' => $this->t('Thread'), + 'help' => $this->t('Sort by the threaded order. This will keep child comments together with their parents.'), + 'id' => 'comment_thread', + ]; +- unset($data['comment_field_data']['thread']['filter']); +- unset($data['comment_field_data']['thread']['argument']); ++ unset($data[$data_table]['thread']['filter']); ++ unset($data[$data_table]['thread']['argument']); + + $entities_types = \Drupal::entityTypeManager()->getDefinitions(); + +@@ -201,20 +208,34 @@ public function getViewsData() { + continue; + } + if (\Drupal::service('comment.manager')->getFields($type)) { +- $data['comment_field_data'][$type] = [ ++ if ($this->connection->driver() == 'mongodb') { ++ $base = $entity_type->getBaseTable(); ++ $relationship_field = 'comment_translations.entity_id'; ++ $left_field = 'comment_translations.entity_type'; ++ } ++ else { ++ $base = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ $relationship_field = 'entity_id'; ++ $left_field = 'entity_type'; ++ } ++ ++ $data[$data_table][$type] = [ + 'relationship' => [ + 'title' => $entity_type->getLabel(), + 'help' => $this->t('The @entity_type to which the comment is a reply to.', ['@entity_type' => $entity_type->getLabel()]), +- 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), ++ 'base' => $base, + 'base field' => $entity_type->getKey('id'), +- 'relationship field' => 'entity_id', ++ 'relationship field' => $relationship_field, + 'id' => 'standard', + 'label' => $entity_type->getLabel(), + 'extra' => [ + [ +- 'field' => 'entity_type', ++ // The left table in this join is comment and ++ // the field entity_type is from that table therefore it Should ++ // be "left_field" and not "field". ++ 'left_field' => $left_field, + 'value' => $type, +- 'table' => 'comment_field_data', ++ 'table' => $data_table, + ], + ], + ], +@@ -222,16 +243,16 @@ public function getViewsData() { + } + } + +- $data['comment_field_data']['uid']['title'] = $this->t('Author uid'); +- $data['comment_field_data']['uid']['help'] = $this->t('If you need more fields than the uid add the comment: author relationship'); +- $data['comment_field_data']['uid']['relationship']['title'] = $this->t('Author'); +- $data['comment_field_data']['uid']['relationship']['help'] = $this->t("The User ID of the comment's author."); +- $data['comment_field_data']['uid']['relationship']['label'] = $this->t('author'); ++ $data[$data_table]['uid']['title'] = $this->t('Author uid'); ++ $data[$data_table]['uid']['help'] = $this->t('If you need more fields than the uid add the comment: author relationship'); ++ $data[$data_table]['uid']['relationship']['title'] = $this->t('Author'); ++ $data[$data_table]['uid']['relationship']['help'] = $this->t("The User ID of the comment's author."); ++ $data[$data_table]['uid']['relationship']['label'] = $this->t('author'); + +- $data['comment_field_data']['pid']['title'] = $this->t('Parent CID'); +- $data['comment_field_data']['pid']['relationship']['title'] = $this->t('Parent comment'); +- $data['comment_field_data']['pid']['relationship']['help'] = $this->t('The parent comment'); +- $data['comment_field_data']['pid']['relationship']['label'] = $this->t('parent'); ++ $data[$data_table]['pid']['title'] = $this->t('Parent CID'); ++ $data[$data_table]['pid']['relationship']['title'] = $this->t('Parent comment'); ++ $data[$data_table]['pid']['relationship']['help'] = $this->t('The parent comment'); ++ $data[$data_table]['pid']['relationship']['label'] = $this->t('parent'); + + // Define the base group of this table. Fields that don't have a group defined + // will go into this field by default. +@@ -242,6 +263,13 @@ public function getViewsData() { + if ($type == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) { + continue; + } ++ if ($this->connection->driver() == 'mongodb') { ++ $entity_type_table = $entity_type->getBaseTable(); ++ } ++ else { ++ $entity_type_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ } ++ + // This relationship does not use the 'field id' column, if the entity has + // multiple comment-fields, then this might introduce duplicates, in which + // case the site-builder should enable aggregation and SUM the comment_count +@@ -249,7 +277,7 @@ public function getViewsData() { + // {comment_entity_statistics} for each field as multiple joins between + // the same two tables is not supported. + if (\Drupal::service('comment.manager')->getFields($type)) { +- $data['comment_entity_statistics']['table']['join'][$entity_type->getDataTable() ?: $entity_type->getBaseTable()] = [ ++ $data['comment_entity_statistics']['table']['join'][$entity_type_table] = [ + 'type' => 'LEFT', + 'left_field' => $entity_type->getKey('id'), + 'field' => 'entity_id', +diff --git a/core/modules/comment/src/Hook/CommentHooks.php b/core/modules/comment/src/Hook/CommentHooks.php +index e104fe978c6c7e10609e02b1b79874fe058977bc..2455dac87e5da35eb53bedb1a82bf7c839d72716 100644 +--- a/core/modules/comment/src/Hook/CommentHooks.php ++++ b/core/modules/comment/src/Hook/CommentHooks.php +@@ -430,7 +430,7 @@ public function nodeSearchResult(EntityInterface $node) { + public function userCancel($edit, UserInterface $account, $method) { + switch ($method) { + case 'user_cancel_block_unpublish': +- $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]); ++ $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => (int) $account->id()]); + foreach ($comments as $comment) { + $comment->setUnpublished(); + $comment->save(); +@@ -439,7 +439,7 @@ public function userCancel($edit, UserInterface $account, $method) { + + case 'user_cancel_reassign': + /** @var \Drupal\comment\CommentInterface[] $comments */ +- $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]); ++ $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => (int) $account->id()]); + foreach ($comments as $comment) { + $langcodes = array_keys($comment->getTranslationLanguages()); + // For efficiency manually save the original comment before applying any +@@ -462,7 +462,7 @@ public function userCancel($edit, UserInterface $account, $method) { + #[Hook('user_predelete')] + public function userPredelete($account) { + $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE); +- $entity_query->condition('uid', $account->id()); ++ $entity_query->condition('uid', (int) $account->id()); + $cids = $entity_query->execute(); + $comment_storage = \Drupal::entityTypeManager()->getStorage('comment'); + $comments = $comment_storage->loadMultiple($cids); +diff --git a/core/modules/comment/src/Hook/CommentViewsHooks.php b/core/modules/comment/src/Hook/CommentViewsHooks.php +index 9ca89e72322ef8344110c27e630e0bc3842f4125..f1216f4031d9536bea5d8a754c0f179ff9fb09bd 100644 +--- a/core/modules/comment/src/Hook/CommentViewsHooks.php ++++ b/core/modules/comment/src/Hook/CommentViewsHooks.php +@@ -25,13 +25,22 @@ public function viewsDataAlter(&$data): void { + 'no group by' => TRUE, + ], + ]; ++ ++ // Get the database driver. ++ $driver = \Drupal::database()->driver(); ++ + // Provides an integration for each entity type except comment. + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) { + continue; + } + $fields = \Drupal::service('comment.manager')->getFields($entity_type_id); +- $base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ if ($entity_type->getDataTable() && ($driver != 'mongodb')) { ++ $base_table = $entity_type->getDataTable(); ++ } ++ else { ++ $base_table = $entity_type->getBaseTable(); ++ } + $args = ['@entity_type' => $entity_type_id]; + if ($fields) { + $data[$base_table]['comments_link'] = [ +@@ -42,7 +51,7 @@ public function viewsDataAlter(&$data): void { + ], + ]; + // Multilingual properties are stored in data table. +- if (!($table = $entity_type->getDataTable())) { ++ if (!($table = $entity_type->getDataTable()) || ($driver == 'mongodb')) { + $table = $entity_type->getBaseTable(); + } + $data[$table]['uid_touch'] = [ +@@ -75,19 +84,19 @@ public function viewsDataAlter(&$data): void { + 'relationship' => [ + 'group' => t('Comment'), + 'label' => t('Comments'), +- 'base' => 'comment_field_data', ++ 'base' => $base_table, + 'base field' => 'entity_id', + 'relationship field' => $entity_type->getKey('id'), + 'id' => 'standard', + 'extra' => [ +- [ +- 'field' => 'entity_type', +- 'value' => $entity_type_id, +- ], +- [ +- 'field' => 'field_name', +- 'value' => $field_name, +- ], ++ [ ++ 'field' => 'entity_type', ++ 'value' => $entity_type_id, ++ ], ++ [ ++ 'field' => 'field_name', ++ 'value' => $field_name, ++ ], + ], + ], + ]; +diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php +index 9cbf62a7f59a8eb333ab6c67cfe0e2cc1f818c26..01c03dae3c7d90070b2c5b9616222cc59f5de2ed 100644 +--- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php ++++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php +@@ -31,7 +31,7 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') + // core requires us to also know about the concept of 'published' and + // 'unpublished'. + if (!$this->currentUser->hasPermission('administer comments')) { +- $query->condition('status', CommentInterface::PUBLISHED); ++ $query->condition('status', (bool) CommentInterface::PUBLISHED); + } + return $query; + } +@@ -75,7 +75,7 @@ public function validateReferenceableEntities(array $ids) { + $query = $this->buildEntityQuery(); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('administer comments')) { +- $query->condition('status', 1); ++ $query->condition('status', TRUE); + } + $result = $query + ->condition($entity_type->getKey('id'), $ids, 'IN') +@@ -90,13 +90,20 @@ public function validateReferenceableEntities(array $ids) { + */ + public function entityQueryAlter(SelectInterface $query) { + parent::entityQueryAlter($query); +- +- $tables = $query->getTables(); +- $data_table = 'comment_field_data'; +- if (!isset($tables['comment_field_data']['alias'])) { +- // If no conditions join against the comment data table, it should be +- // joined manually to allow node access processing. +- $query->innerJoin($data_table, NULL, "[base_table].[cid] = [$data_table].[cid] AND [$data_table].[default_langcode] = 1"); ++ $driver = \Drupal::database()->driver(); ++ ++ if ($driver != 'mongodb') { ++ $tables = $query->getTables(); ++ $data_table = 'comment_field_data'; ++ if (!isset($tables['comment_field_data']['alias'])) { ++ // If no conditions join against the comment data table, it should be ++ // joined manually to allow node access processing. ++ $query->innerJoin($data_table, NULL, ++ $query->joinCondition() ++ ->compare('base_table.cid', "$data_table.cid") ++ ->condition("$data_table.default_langcode", TRUE) ++ ); ++ } + } + + // Historically, comments were always linked to 'node' entities, but that is +@@ -121,7 +128,21 @@ public function entityQueryAlter(SelectInterface $query) { + + // The Comment module doesn't implement per-comment access, so it + // checks instead that the user has access to the host entity. +- $entity_alias = $query->innerJoin($host_entity_field_data_table, 'n', "[%alias].[$id_key] = [$data_table].[entity_id] AND [$data_table].[entity_type] = '$host_entity_type_id'"); ++ if ($driver == 'mongodb') { ++ $entity_alias = $query->innerJoin($host_entity_type->getBaseTable(), 'n', ++ $query->joinCondition() ++ ->compare("%alias.$id_key", 'comment_translations.entity_id') ++ ->condition("%alias.comment_translations.entity_type", $host_entity_type_id) ++ ); ++ } ++ else { ++ $entity_alias = $query->innerJoin($host_entity_field_data_table, 'n', ++ $query->joinCondition() ++ ->compare("%alias.$id_key", "$data_table.entity_id") ++ ->condition("$data_table.entity_type", $host_entity_type_id) ++ ); ++ } ++ + // Pass the query to the entity access control. + $this->reAlterQuery($query, $host_entity_type_id . '_access', $entity_alias); + +@@ -131,7 +152,13 @@ public function entityQueryAlter(SelectInterface $query) { + // insufficient for nodes. + // @see \Drupal\node\Plugin\EntityReferenceSelection\NodeSelection::buildEntityQuery() + if (!$this->currentUser->hasPermission('bypass node access') && !$this->moduleHandler->hasImplementations('node_grants')) { +- $query->condition($entity_alias . '.status', 1); ++ if ($driver == 'mongodb') { ++ $query->addFilterUnwindPath($entity_alias . '.node_current_revision'); ++ $query->condition($entity_alias . '.node_current_revision.status', TRUE); ++ } ++ else { ++ $query->condition($entity_alias . '.status', 1); ++ } + } + } + } +diff --git a/core/modules/comment/src/Plugin/migrate/source/d6/Comment.php b/core/modules/comment/src/Plugin/migrate/source/d6/Comment.php +index 6b1f4651cc4b22b305e7826354a7f3a4d9b603f7..381387f26bd1789ecad1f57fc60280ea01c6a97c 100644 +--- a/core/modules/comment/src/Plugin/migrate/source/d6/Comment.php ++++ b/core/modules/comment/src/Plugin/migrate/source/d6/Comment.php +@@ -31,7 +31,7 @@ public function query() { + 'hostname', 'timestamp', 'status', 'thread', 'name', 'mail', 'homepage', + 'format', + ]); +- $query->innerJoin('node', 'n', '[c].[nid] = [n].[nid]'); ++ $query->innerJoin('node', 'n', $query->joinCondition()->compare('c.nid', 'n.nid')); + $query->fields('n', ['type', 'language']); + $query->orderBy('c.timestamp'); + return $query; +diff --git a/core/modules/comment/src/Plugin/migrate/source/d7/CommentEntityTranslation.php b/core/modules/comment/src/Plugin/migrate/source/d7/CommentEntityTranslation.php +index e14560fe0ce467a316fb07cbc4dd2c4021fba356..d5151dc275023527ec841d562ac055d83a30b237 100644 +--- a/core/modules/comment/src/Plugin/migrate/source/d7/CommentEntityTranslation.php ++++ b/core/modules/comment/src/Plugin/migrate/source/d7/CommentEntityTranslation.php +@@ -33,8 +33,8 @@ public function query() { + ->condition('et.entity_type', 'comment') + ->condition('et.source', '', '<>'); + +- $query->innerJoin('comment', 'c', '[c].[cid] = [et].[entity_id]'); +- $query->innerJoin('node', 'n', '[n].[nid] = [c].[nid]'); ++ $query->innerJoin('comment', 'c', $query->joinCondition()->compare('c.cid', 'et.entity_id')); ++ $query->innerJoin('node', 'n', $query->joinCondition()->compare('n.nid', 'c.nid')); + + $query->addField('n', 'type', 'node_type'); + +diff --git a/core/modules/comment/src/Plugin/migrate/source/d7/Comment.php b/core/modules/comment/src/Plugin/migrate/source/d7/Comment.php +index b06a3d504ba882f440fb76b72aebe2736229296f..e036053ca10f9b31f742ff0898d5f34defffbde3 100644 +--- a/core/modules/comment/src/Plugin/migrate/source/d7/Comment.php ++++ b/core/modules/comment/src/Plugin/migrate/source/d7/Comment.php +@@ -27,7 +27,7 @@ class Comment extends FieldableEntity { + */ + public function query() { + $query = $this->select('comment', 'c')->fields('c'); +- $query->innerJoin('node', 'n', '[c].[nid] = [n].[nid]'); ++ $query->innerJoin('node', 'n', $query->joinCondition()->compare('c.nid', 'n.nid')); + $query->addField('n', 'type', 'node_type'); + $query->orderBy('c.created'); + return $query; +diff --git a/core/modules/comment/src/Plugin/views/wizard/Comment.php b/core/modules/comment/src/Plugin/views/wizard/Comment.php +index 2dbd119beea97bc745f9cd24870e325bda72bc78..8e64c7b06667e0444a700c8231be4e7531142e2f 100644 +--- a/core/modules/comment/src/Plugin/views/wizard/Comment.php ++++ b/core/modules/comment/src/Plugin/views/wizard/Comment.php +@@ -2,9 +2,13 @@ + + namespace Drupal\comment\Plugin\views\wizard; + ++use Drupal\Core\Database\Connection; ++use Drupal\Core\Entity\EntityTypeBundleInfoInterface; ++use Drupal\Core\Menu\MenuParentFormSelectorInterface; + use Drupal\Core\StringTranslation\TranslatableMarkup; + use Drupal\views\Attribute\ViewsWizard; + use Drupal\views\Plugin\views\wizard\WizardPluginBase; ++use Symfony\Component\DependencyInjection\ContainerInterface; + + /** + * @todo replace numbers with constants. +@@ -42,6 +46,32 @@ class Comment extends WizardPluginBase { + ], + ]; + ++ /** ++ * {@inheritdoc} ++ */ ++ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { ++ return new static( ++ $configuration, ++ $plugin_id, ++ $plugin_definition, ++ $container->get('entity_type.bundle.info'), ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') ++ ); ++ } ++ ++ /** ++ * Constructs a WizardPluginBase object. ++ */ ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { ++ parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector, $connection); ++ ++ if ($connection->driver() == 'mongodb') { ++ $this->base_table = 'comment'; ++ $this->filters['status_node']['table'] = 'node'; ++ } ++ } ++ + /** + * {@inheritdoc} + */ +@@ -64,9 +94,19 @@ protected function defaultDisplayOptions() { + + // Add a relationship to nodes. + $display_options['relationships']['node']['id'] = 'node'; +- $display_options['relationships']['node']['table'] = 'comment_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['relationships']['node']['table'] = 'comment'; ++ } ++ else { ++ $display_options['relationships']['node']['table'] = 'comment_field_data'; ++ } + $display_options['relationships']['node']['field'] = 'node'; +- $display_options['relationships']['node']['entity_type'] = 'comment_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['relationships']['node']['entity_type'] = 'comment'; ++ } ++ else { ++ $display_options['relationships']['node']['entity_type'] = 'comment_field_data'; ++ } + $display_options['relationships']['node']['required'] = 1; + $display_options['relationships']['node']['plugin_id'] = 'standard'; + +@@ -75,7 +115,12 @@ protected function defaultDisplayOptions() { + + /* Field: Comment: Title */ + $display_options['fields']['subject']['id'] = 'subject'; +- $display_options['fields']['subject']['table'] = 'comment_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['subject']['table'] = 'comment'; ++ } ++ else { ++ $display_options['fields']['subject']['table'] = 'comment_field_data'; ++ } + $display_options['fields']['subject']['field'] = 'subject'; + $display_options['fields']['subject']['entity_type'] = 'comment'; + $display_options['fields']['subject']['entity_field'] = 'subject'; +diff --git a/core/modules/config_translation/src/Plugin/migrate/source/d6/ProfileFieldTranslation.php b/core/modules/config_translation/src/Plugin/migrate/source/d6/ProfileFieldTranslation.php +index 2c120e2c1aaac19104f66f2b2a2c8f10d3124d7f..17aef147b180d6d5265388b082169db383a5fe3a 100644 +--- a/core/modules/config_translation/src/Plugin/migrate/source/d6/ProfileFieldTranslation.php ++++ b/core/modules/config_translation/src/Plugin/migrate/source/d6/ProfileFieldTranslation.php +@@ -28,8 +28,8 @@ public function query() { + $query = parent::query(); + $query->fields('i18n', ['property']) + ->fields('lt', ['lid', 'translation', 'language']); +- $query->leftJoin('i18n_strings', 'i18n', '[i18n].[objectid] = [pf].[name]'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin('i18n_strings', 'i18n', $query->joinCondition()->compare('i18n.objectid', 'pf.name')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + return $query; + } + +diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php +index 85ef099318566ce8d4e057aac393a10c88dbfa36..a2df544f50562a151fbb935603b95d92ce11ee10 100644 +--- a/core/modules/content_moderation/src/Entity/ContentModerationState.php ++++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php +@@ -10,6 +10,7 @@ + use Drupal\Core\Entity\EntityInterface; + use Drupal\Core\Entity\EntityTypeInterface; + use Drupal\Core\Field\BaseFieldDefinition; ++use Drupal\Core\Language\LanguageInterface; + use Drupal\Core\TypedData\TranslatableInterface; + use Drupal\user\EntityOwnerTrait; + use Drupal\views\EntityViewsData; +@@ -144,12 +145,39 @@ public static function loadFromModeratedEntity(EntityInterface $entity) { + // triggered elsewhere. In this case we have to match on the revision ID + // (instead of the loaded revision ID). + $revision_id = $entity->getLoadedRevisionId() ?: $entity->getRevisionId(); ++ ++ if ((\Drupal::database()->driver() === 'mongodb') && $entity->getLoadedRevisionId() && $entity->getRevisionId() && ($entity->getLoadedRevisionId() != $entity->getRevisionId())) { ++ // Get the langcodes for the entity. ++ $entity_langcodes = array_keys($entity->getTranslationLanguages()); ++ // Load the revision for the loaded revision id. ++ $loaded_revision = $storage->loadRevision($entity->getLoadedRevisionId()); ++ if ($loaded_revision && !empty($entity_langcodes)) { ++ $loaded_revision_langcodes = array_keys($loaded_revision->getTranslationLanguages()); ++ ++ // When the entity langcode is unspecified and the loaded revision ++ // has only a single langcode, then do not switch to the entity ++ // revision ID. ++ $switch_the_revision_id = TRUE; ++ if (($entity_langcodes === [LanguageInterface::LANGCODE_NOT_SPECIFIED]) && (count($loaded_revision_langcodes) === 1)) { ++ $switch_the_revision_id = FALSE; ++ } ++ ++ // When there langcodes missing from the revision from the loaded ++ // revision ID compared to the ones in the entity, should we use the ++ // entity revision ID instead of the loaded revision ID. ++ $missing_langcodes = array_diff($loaded_revision_langcodes, $entity_langcodes); ++ if (!empty($loaded_revision_langcodes) && !empty($missing_langcodes) && $switch_the_revision_id) { ++ $revision_id = $entity->getRevisionId(); ++ } ++ } ++ } ++ + $ids = $storage->getQuery() + ->accessCheck(FALSE) + ->condition('content_entity_type_id', $entity->getEntityTypeId()) +- ->condition('content_entity_id', $entity->id()) ++ ->condition('content_entity_id', (int) $entity->id()) + ->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id()) +- ->condition('content_entity_revision_id', $revision_id) ++ ->condition('content_entity_revision_id', (int) $revision_id) + ->allRevisions() + ->execute(); + +diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php +index 78d0dc6014acfca79622a8ad9349302c6b582b28..8aaafc9ad85fd0a3bbb4566f23e7d594a04b59fe 100644 +--- a/core/modules/content_moderation/src/ModerationInformation.php ++++ b/core/modules/content_moderation/src/ModerationInformation.php +@@ -91,7 +91,7 @@ public function getDefaultRevisionId($entity_type_id, $entity_id) { + if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) { + $result = $storage->getQuery() + ->currentRevision() +- ->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id) ++ ->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), (int) $entity_id) + // No access check is performed here since this is an API function and + // should return the same ID regardless of the current user. + ->accessCheck(FALSE) +diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +index 41174b60490bbf0b8f325c13bd93dac8ad340d59..d8863d8e2d2f14189f50c7ebc153101e7e720e77 100644 +--- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php ++++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +@@ -77,10 +77,10 @@ protected function loadContentModerationStateRevision(ContentEntityInterface $en + $revisions = $content_moderation_storage->getQuery() + ->accessCheck(FALSE) + ->condition('content_entity_type_id', $entity->getEntityTypeId()) +- ->condition('content_entity_id', $entity->id()) ++ ->condition('content_entity_id', (int) $entity->id()) + // Ensure the correct revision is loaded in scenarios where a revision is + // being reverted. +- ->condition('content_entity_revision_id', $entity->isNewRevision() ? $entity->getLoadedRevisionId() : $entity->getRevisionId()) ++ ->condition('content_entity_revision_id', $entity->isNewRevision() ? (int) $entity->getLoadedRevisionId() : (int) $entity->getRevisionId()) + ->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id()) + ->condition('langcode', $entity->language()->getId()) + ->allRevisions() +diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php +index 3fe9b7fa5f2838b4dc4a8906239a02690a098090..dc1e9187849d096f6b563399a90975f477d5beaa 100644 +--- a/core/modules/content_moderation/src/ViewsData.php ++++ b/core/modules/content_moderation/src/ViewsData.php +@@ -55,8 +55,15 @@ public function getViewsData() { + return $this->moderationInformation->isModeratedEntityType($type); + }); + ++ $driver = \Drupal::database()->driver(); ++ + foreach ($entity_types_with_moderation as $entity_type) { +- $table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ if ($driver == 'mongodb') { ++ $table = $entity_type->getBaseTable(); ++ } ++ else { ++ $table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ } + + $data[$table]['moderation_state'] = [ + 'title' => $this->t('Moderation state'), +@@ -69,17 +76,19 @@ public function getViewsData() { + 'sort' => ['id' => 'moderation_state_sort'], + ]; + +- $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); +- $data[$revision_table]['moderation_state'] = [ +- 'title' => $this->t('Moderation state'), +- 'field' => [ +- 'id' => 'moderation_state_field', +- 'default_formatter' => 'content_moderation_state', +- 'field_name' => 'moderation_state', +- ], +- 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE], +- 'sort' => ['id' => 'moderation_state_sort'], +- ]; ++ if ($driver != 'mongodb') { ++ $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); ++ $data[$revision_table]['moderation_state'] = [ ++ 'title' => $this->t('Moderation state'), ++ 'field' => [ ++ 'id' => 'moderation_state_field', ++ 'default_formatter' => 'content_moderation_state', ++ 'field_name' => 'moderation_state', ++ ], ++ 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE], ++ 'sort' => ['id' => 'moderation_state_sort'], ++ ]; ++ } + } + + return $data; +diff --git a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php b/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php +index 29a64920b721d8bacec7bc8ee8393158d4a82e07..62c70cf3dbe04702d6a87667820f58cd6cc91e1b 100644 +--- a/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php ++++ b/core/modules/content_translation/src/Plugin/migrate/source/I18nQueryTrait.php +@@ -73,7 +73,7 @@ protected function getPropertyNotInRowTranslation(Row $row, $property_not_in_row + ->fields('i18n', ['lid']) + ->condition('i18n.property', $property_not_in_row) + ->condition('i18n.objectid', $object_id); +- $query->leftJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->leftJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->condition('lt.language', $language); + $query->addField('lt', 'translation'); + $results = $query->execute()->fetchAssoc(); +diff --git a/core/modules/dblog/dblog.admin.inc b/core/modules/dblog/dblog.admin.inc +index b5eae06ca8ddf9aeb2c58c96e19ce69d96fd3cc5..280ea3f50de3b7f245e0c956b5d0f016a812a155 100644 +--- a/core/modules/dblog/dblog.admin.inc ++++ b/core/modules/dblog/dblog.admin.inc +@@ -27,14 +27,14 @@ function dblog_filters() { + if (!empty($types)) { + $filters['type'] = [ + 'title' => t('Type'), +- 'where' => "w.type = ?", ++ 'field' => "w.type", + 'options' => $types, + ]; + } + + $filters['severity'] = [ + 'title' => t('Severity'), +- 'where' => 'w.severity = ?', ++ 'field' => 'w.severity', + 'options' => RfcLogLevel::getLevels(), + ]; + +diff --git a/core/modules/dblog/dblog.install b/core/modules/dblog/dblog.install +index 78d780ed1e0025c3f9e9c67ecdee40023b6d5b96..9165b3535dedd80f1e0bc1a647ed891df58681ba 100644 +--- a/core/modules/dblog/dblog.install ++++ b/core/modules/dblog/dblog.install +@@ -90,6 +90,10 @@ function dblog_schema(): array { + ], + ]; + ++ if (\Drupal::database()->driver() == 'mongodb') { ++ $schema['watchdog']['fields']['timestamp']['type'] = 'date'; ++ } ++ + return $schema; + } + +diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module +index f2f0eed1772a390282861b974b987915220ca2c7..fef5c01239de6b4799f0a1f836576be5d9b01546 100644 +--- a/core/modules/dblog/dblog.module ++++ b/core/modules/dblog/dblog.module +@@ -11,6 +11,20 @@ + * List of uniquely defined database log message types. + */ + function _dblog_get_message_types() { +- return \Drupal::database()->query('SELECT DISTINCT([type]) FROM {watchdog} ORDER BY [type]') +- ->fetchAllKeyed(0, 0); ++ $connection = \Drupal::database(); ++ if ($connection->driver() == 'mongodb') { ++ $types = $connection->select('watchdog') ++ ->fields('watchdog', ['type']) ++ ->execute() ++ ->fetchCol(); ++ ++ $types = array_unique($types); ++ sort($types); ++ ++ return array_combine($types, $types); ++ } ++ else { ++ return $connection->query('SELECT DISTINCT([type]) FROM {watchdog} ORDER BY [type]') ++ ->fetchAllKeyed(0, 0); ++ } + } +diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php +index 505996b40b262c38b4163418991d2e0d458ae060..0c2c78f83211c0d5e49825fe81255159d4b655b5 100644 +--- a/core/modules/dblog/src/Controller/DbLogController.php ++++ b/core/modules/dblog/src/Controller/DbLogController.php +@@ -10,6 +10,7 @@ + use Drupal\Core\Controller\ControllerBase; + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\Query\PagerSelectExtender; ++use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Database\Query\TableSortExtender; + use Drupal\Core\Datetime\DateFormatterInterface; + use Drupal\Core\Extension\ModuleHandlerInterface; +@@ -104,7 +105,6 @@ public static function getLogLevelClassMap() { + */ + public function overview(Request $request) { + +- $filter = $this->buildFilterQuery($request); + $rows = []; + + $classes = static::getLogLevelClassMap(); +@@ -152,11 +152,10 @@ public function overview(Request $request) { + 'variables', + 'link', + ]); +- $query->leftJoin('users_field_data', 'ufd', '[w].[uid] = [ufd].[uid]'); ++ $query->leftJoin('users', 'ufd', $query->joinCondition()->compare('w.uid', 'ufd.uid')); ++ ++ $this->addFilterToQuery($request, $query); + +- if (!empty($filter['where'])) { +- $query->where($filter['where'], $filter['args']); +- } + $result = $query + ->limit(50) + ->orderByHeader($header) +@@ -229,8 +228,8 @@ public function overview(Request $request) { + public function eventDetails($event_id) { + $query = $this->database->select('watchdog', 'w') + ->fields('w') +- ->condition('w.wid', $event_id); +- $query->leftJoin('users', 'u', '[u].[uid] = [w].[uid]'); ++ ->condition('w.wid', (int) $event_id); ++ $query->leftJoin('users', 'u', $query->joinCondition()->compare('u.uid', 'w.uid')); + $query->addField('u', 'uid', 'uid'); + $dblog = $query->execute()->fetchObject(); + +@@ -304,14 +303,16 @@ public function eventDetails($event_id) { + /** + * Builds a query for database log administration filters based on session. + * ++ * This method retrieves the session-based filters from the request and applies ++ * them to the provided query object. If no filters are present, the query is ++ * left unchanged. ++ * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. +- * +- * @return array|null +- * An associative array with keys 'where' and 'args' or NULL if there were +- * no filters set. ++ * @param \Drupal\Core\Database\Query\SelectInterface $query ++ * The database query. + */ +- protected function buildFilterQuery(Request $request) { ++ protected function addFilterToQuery(Request $request, SelectInterface &$query): void { + $session_filters = $request->getSession()->get('dblog_overview_filter', []); + if (empty($session_filters)) { + return; +@@ -321,24 +322,29 @@ protected function buildFilterQuery(Request $request) { + + $filters = dblog_filters(); + +- // Build query. +- $where = $args = []; ++ // Build the condition. ++ $condition_and = $query->getConnection()->condition('AND'); ++ $condition_and_used = FALSE; + foreach ($session_filters as $key => $filter) { +- $filter_where = []; ++ $condition_or = $query->getConnection()->condition('OR'); ++ $condition_or_used = FALSE; + foreach ($filter as $value) { +- $filter_where[] = $filters[$key]['where']; +- $args[] = $value; ++ if ($key == 'severity') { ++ $value = (int) $value; ++ } ++ if (in_array($value, array_keys($filters[$key]['options']))) { ++ $condition_or->condition($filters[$key]['field'], $value); ++ $condition_or_used = TRUE; ++ } + } +- if (!empty($filter_where)) { +- $where[] = '(' . implode(' OR ', $filter_where) . ')'; ++ if ($condition_or_used) { ++ $condition_and->condition($condition_or); ++ $condition_and_used = TRUE; + } + } +- $where = !empty($where) ? implode(' AND ', $where) : ''; +- +- return [ +- 'where' => $where, +- 'args' => $args, +- ]; ++ if ($condition_and_used) { ++ $query->condition($condition_and); ++ } + } + + /** +@@ -425,13 +431,13 @@ public function topLogMessages($type) { + ]; + + $count_query = $this->database->select('watchdog'); +- $count_query->addExpression('COUNT(DISTINCT([message]))'); ++ $count_query->addExpressionCountDistinct('message'); + $count_query->condition('type', $type); + + $query = $this->database->select('watchdog', 'w') + ->extend(PagerSelectExtender::class) + ->extend(TableSortExtender::class); +- $query->addExpression('COUNT([wid])', 'count'); ++ $query->addExpressionCount('wid', 'count'); + $query = $query + ->fields('w', ['message', 'variables']) + ->condition('w.type', $type) +diff --git a/core/modules/dblog/src/Plugin/rest/resource/DbLogResource.php b/core/modules/dblog/src/Plugin/rest/resource/DbLogResource.php +index d9de3987af980fdcd32118813db2a7b7d0f70b41..b79168ce93ca5191469506c5087918652d5d5056 100644 +--- a/core/modules/dblog/src/Plugin/rest/resource/DbLogResource.php ++++ b/core/modules/dblog/src/Plugin/rest/resource/DbLogResource.php +@@ -7,6 +7,8 @@ + use Drupal\rest\Attribute\RestResource; + use Drupal\rest\Plugin\ResourceBase; + use Drupal\rest\ResourceResponse; ++use MongoDB\BSON\Binary; ++use MongoDB\BSON\UTCDateTime; + use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +@@ -42,10 +44,19 @@ public function get($id = NULL) { + if ($id) { + $record = Database::getConnection()->select('watchdog', 'w') + ->fields('w') +- ->condition('wid', $id) ++ ->condition('wid', (int) $id) + ->execute() + ->fetchAssoc(); + if (!empty($record)) { ++ if (isset($record['timestamp']) && ($record['timestamp'] instanceof UTCDateTime)) { ++ $record['timestamp'] = (int) $record['timestamp']->__toString(); ++ $record['timestamp'] = $record['timestamp'] / 1000; ++ $record['timestamp'] = (string) $record['timestamp']; ++ } ++ if (isset($record['variables']) && ($record['variables'] instanceof Binary)) { ++ $record['variables'] = $record['variables']->getData(); ++ } ++ + return new ResourceResponse($record); + } + +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstanceOptionTranslation.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstanceOptionTranslation.php +index e1aefa69dfc2e5ff9a66b3cdf45fb3272196c7d7..378143a6366ed7d172b5b627ad9e05925928c394 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstanceOptionTranslation.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstanceOptionTranslation.php +@@ -24,7 +24,7 @@ class FieldInstanceOptionTranslation extends FieldOptionTranslation { + */ + public function query() { + $query = parent::query(); +- $query->join('content_node_field_instance', 'cnfi', '[cnfi].[field_name] = [cnf].[field_name]'); ++ $query->join('content_node_field_instance', 'cnfi', $query->joinCondition()->compare('cnfi.field_name', 'cnf.field_name')); + $query->addField('cnfi', 'type_name'); + return $query; + } +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerFormDisplay.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerFormDisplay.php +index 4d2fd561f15cbad51f62ed8cb3b07a8a030735b2..b10f079992dd70a5544de27ce63caeb43dc24d2c 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerFormDisplay.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerFormDisplay.php +@@ -67,7 +67,7 @@ public function query() { + 'type', + 'module', + ]); +- $query->join('content_node_field', 'cnf', '[cnfi].[field_name] = [cnf].[field_name]'); ++ $query->join('content_node_field', 'cnf', $query->joinCondition()->compare('cnfi.field_name', 'cnf.field_name')); + $query->orderBy('cnfi.weight'); + + return $query; +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerViewMode.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerViewMode.php +index 53b8b9452362bffd23c74c5501c29d1715844d61..03ebe7ef58a95902d047cdd1feba6b3da72ba9a5 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerViewMode.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstancePerViewMode.php +@@ -74,7 +74,7 @@ public function query() { + 'type', + 'module', + ]); +- $query->join('content_node_field', 'cnf', '[cnfi].[field_name] = [cnf].[field_name]'); ++ $query->join('content_node_field', 'cnf', $query->joinCondition()->compare('cnfi.field_name', 'cnf.field_name')); + $query->orderBy('cnfi.weight'); + + return $query; +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php +index 76d5ab30b4ac39dde74b59f9195cea6b3ea8b0f4..8269055164477d0721d60f8787def3a82babcd7d 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldInstance.php +@@ -46,7 +46,7 @@ public function query() { + if (isset($this->configuration['node_type'])) { + $query->condition('cnfi.type_name', $this->configuration['node_type']); + } +- $query->join('content_node_field', 'cnf', '[cnf].[field_name] = [cnfi].[field_name]'); ++ $query->join('content_node_field', 'cnf', $query->joinCondition()->compare('cnf.field_name', 'cnfi.field_name')); + $query->fields('cnf'); + $query->orderBy('cnfi.field_name'); + $query->orderBy('cnfi.type_name'); +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldLabelDescriptionTranslation.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldLabelDescriptionTranslation.php +index f39a50c30715b9065df7a6a916cafb9ea4bae99c..7c5956775fef602dab5ca6c4e09b9b40d178f254 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldLabelDescriptionTranslation.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldLabelDescriptionTranslation.php +@@ -34,7 +34,7 @@ public function query() { + ->condition('property', 'widget_label') + ->condition('property', 'widget_description'); + $query->condition($condition); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + + return $query; + } +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/FieldOptionTranslation.php b/core/modules/field/src/Plugin/migrate/source/d6/FieldOptionTranslation.php +index 0be145cf1da61ebc9b0e78ec57ddf46a534206ed..4a0bc71e16d48b10cfd2acdcdd237e1c4037103c 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/FieldOptionTranslation.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/FieldOptionTranslation.php +@@ -34,8 +34,8 @@ public function query() { + ]) + ->condition('i18n.type', 'field') + ->condition('property', 'option\_%', 'LIKE'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); +- $query->leftJoin('content_node_field', 'cnf', '[cnf].[field_name] = [i18n].[objectid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); ++ $query->leftJoin('content_node_field', 'cnf', $query->joinCondition()->compare('cnf.field_name', 'i18n.objectid')); + $query->addField('cnf', 'field_name'); + $query->addField('cnf', 'global_settings'); + // Minimize changes to the d6_field_option_translation.yml, which is copied +diff --git a/core/modules/field/src/Plugin/migrate/source/d6/Field.php b/core/modules/field/src/Plugin/migrate/source/d6/Field.php +index d451269a824795c260055033f3b62e848d59c00d..4962dcd04006986ee91d45ef6437aba6b35da009 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d6/Field.php ++++ b/core/modules/field/src/Plugin/migrate/source/d6/Field.php +@@ -41,7 +41,7 @@ public function query() { + ]) + ->distinct(); + // Only import fields which are actually being used. +- $query->innerJoin('content_node_field_instance', 'cnfi', '[cnfi].[field_name] = [cnf].[field_name]'); ++ $query->innerJoin('content_node_field_instance', 'cnfi', $query->joinCondition()->compare('cnfi.field_name', 'cnf.field_name')); + + return $query; + } +diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php +index 562e3f2cad979418e265ffef951051d7c9f67906..0d49920594ac23ec291e6ddce3328498b64eafe9 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php ++++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php +@@ -66,7 +66,7 @@ public function query() { + ->condition('fc.storage_active', 1) + ->condition('fc.deleted', 0) + ->condition('fci.deleted', 0); +- $query->join('field_config', 'fc', '[fci].[field_id] = [fc].[id]'); ++ $query->join('field_config', 'fc', $query->joinCondition()->compare('fci.field_id', 'fc.id')); + + // Optionally filter by entity type and bundle. + if (isset($this->configuration['entity_type'])) { +diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldLabelDescriptionTranslation.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldLabelDescriptionTranslation.php +index e0968506f38ff518ed32aa8ade07c3dd6949f077..e7a3a9859c1a3babbb31c61d5ae01eeea03919c3 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d7/FieldLabelDescriptionTranslation.php ++++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldLabelDescriptionTranslation.php +@@ -50,9 +50,13 @@ public function query() { + ->condition('textgroup', 'field') + ->condition('objectid', '#allowed_values', '!='); + $query->condition($condition); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + +- $query->leftJoin('field_config_instance', 'fci', '[fci].[bundle] = [i18n].[objectid] AND [fci].[field_name] = [i18n].[type]'); ++ $query->leftJoin('field_config_instance', 'fci', ++ $query->joinCondition() ++ ->compare('fci.bundle', 'i18n.objectid') ++ ->compare('fci.field_name', 'i18n.type') ++ ); + return $query; + } + +diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldOptionTranslation.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldOptionTranslation.php +index 5a3968178e95b58dec28029b595720e438970cfe..406e35bd58db3bc18d35c0e1567a9a1ab9306b15 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d7/FieldOptionTranslation.php ++++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldOptionTranslation.php +@@ -24,8 +24,8 @@ class FieldOptionTranslation extends Field { + */ + public function query() { + $query = parent::query(); +- $query->leftJoin('i18n_string', 'i18n', '[i18n].[type] = [fc].[field_name]'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin('i18n_string', 'i18n', $query->joinCondition()->compare('i18n.type', 'fc.field_name')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + $query->condition('i18n.textgroup', 'field') + ->condition('objectid', '#allowed_values'); + // Add all i18n and locales_target fields. +diff --git a/core/modules/field/src/Plugin/migrate/source/d7/Field.php b/core/modules/field/src/Plugin/migrate/source/d7/Field.php +index 3d123115348d53c08619eb345913254afda4546c..9355122aad823df11b3800e418321f46f2719e99 100644 +--- a/core/modules/field/src/Plugin/migrate/source/d7/Field.php ++++ b/core/modules/field/src/Plugin/migrate/source/d7/Field.php +@@ -36,7 +36,7 @@ public function query() { + ->condition('fc.storage_active', 1) + ->condition('fc.deleted', 0) + ->condition('fci.deleted', 0); +- $query->join('field_config_instance', 'fci', '[fc].[id] = [fci].[field_id]'); ++ $query->join('field_config_instance', 'fci', $query->joinCondition()->compare('fc.id', 'fci.field_id')); + + // The Title module fields are not migrated. + if ($this->moduleExists('title')) { +diff --git a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php +index eab628e0a101a111a0a4c9e6b61ab7bb0ad5b410..473d99bc87f1a8509fab318fb8ff384a9d3058aa 100644 +--- a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php ++++ b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php +@@ -228,7 +228,7 @@ public function validateCardinality(array &$element, FormStateInterface $form_st + // need to be incremented. + $entities_with_higher_delta = \Drupal::entityQuery($this->entity->getTargetEntityTypeId()) + ->accessCheck(FALSE) +- ->condition($this->entity->getName() . '.%delta', $cardinality_number) ++ ->condition($this->entity->getName() . '.%delta', (int) $cardinality_number) + ->count() + ->execute(); + if ($entities_with_higher_delta) { +diff --git a/core/modules/file/src/FileStorage.php b/core/modules/file/src/FileStorage.php +index dedf0851ace65771a6187e01ad0414327318086a..c15e00cae385c46c005fee87602120fbdd0d4139 100644 +--- a/core/modules/file/src/FileStorage.php ++++ b/core/modules/file/src/FileStorage.php +@@ -14,12 +14,27 @@ class FileStorage extends SqlContentEntityStorage implements FileStorageInterfac + */ + public function spaceUsed($uid = NULL, $status = FileInterface::STATUS_PERMANENT) { + $query = $this->database->select($this->entityType->getBaseTable(), 'f') +- ->condition('f.status', $status); +- $query->addExpression('SUM([f].[filesize])', 'filesize'); ++ ->condition('f.status', (bool) $status); + if (isset($uid)) { +- $query->condition('f.uid', $uid); ++ $query->condition('f.uid', (int) $uid); ++ } ++ ++ if ($this->database->driver() == 'mongodb') { ++ $files = $query->execute()->fetchAll(); ++ ++ $size = 0; ++ foreach ($files as $file) { ++ if (isset($file->filesize)) { ++ $size += $file->filesize; ++ } ++ } ++ ++ return $size; ++ } ++ else { ++ $query->addExpressionSum('f.filesize', 'filesize'); ++ return $query->execute()->fetchField(); + } +- return $query->execute()->fetchField(); + } + + } +diff --git a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php +index 8192cd7110a97b595010cd57b8316a0c28d21490..fe22695544c369110953e499d5fc928ab1ce96c8 100644 +--- a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php ++++ b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php +@@ -48,7 +48,7 @@ public function __construct(ConfigFactoryInterface $config_factory, Connection $ + public function add(FileInterface $file, $module, $type, $id, $count = 1) { + $this->connection->merge($this->tableName) + ->keys([ +- 'fid' => $file->id(), ++ 'fid' => (int) $file->id(), + 'module' => $module, + 'type' => $type, + 'id' => $id, +@@ -67,7 +67,7 @@ public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $ + // Delete rows that have an exact or less value to prevent empty rows. + $query = $this->connection->delete($this->tableName) + ->condition('module', $module) +- ->condition('fid', $file->id()); ++ ->condition('fid', (int) $file->id()); + if ($type && $id) { + $query + ->condition('type', $type) +@@ -101,7 +101,7 @@ public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $ + public function listUsage(FileInterface $file) { + $result = $this->connection->select($this->tableName, 'f') + ->fields('f', ['module', 'type', 'id', 'count']) +- ->condition('fid', $file->id()) ++ ->condition('fid', (int) $file->id()) + ->condition('count', 0, '>') + ->execute(); + $references = []; +diff --git a/core/modules/file/src/FileViewsData.php b/core/modules/file/src/FileViewsData.php +index a5f1934c2ac4b07d1d222175224892dc9300ed66..87f542dfab34aecec772a84544d5d0f0ff4c7786 100644 +--- a/core/modules/file/src/FileViewsData.php ++++ b/core/modules/file/src/FileViewsData.php +@@ -15,6 +15,19 @@ class FileViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + ++ if ($this->connection->driver() == 'mongodb') { ++ $node_table = 'node'; ++ $users_table = 'users'; ++ $term_table = 'taxonomy_term_data'; ++ $comment_table = 'comment'; ++ } ++ else { ++ $node_table = 'node_field_data'; ++ $users_table = 'users_field_data'; ++ $term_table = 'taxonomy_term_field_data'; ++ $comment_table = 'comment_field_data'; ++ } ++ + // @todo There is no corresponding information in entity metadata. + $data['file_managed']['table']['base']['help'] = $this->t('Files maintained by Drupal and various modules.'); + $data['file_managed']['table']['base']['defaults']['field'] = 'filename'; +@@ -78,7 +91,7 @@ public function getViewsData() { + ], + // Link ourselves to the {node_field_data} table + // so we can provide node->file relationships. +- 'node_field_data' => [ ++ $node_table => [ + 'join_id' => 'casted_int_field_join', + 'cast' => 'right', + 'field' => 'id', +@@ -87,7 +100,7 @@ public function getViewsData() { + ], + // Link ourselves to the {users_field_data} table + // so we can provide user->file relationships. +- 'users_field_data' => [ ++ $users_table => [ + 'join_id' => 'casted_int_field_join', + 'cast' => 'right', + 'field' => 'id', +@@ -126,7 +139,7 @@ public function getViewsData() { + 'help' => $this->t('Content that is associated with this file, usually because this file is in a field on the content.'), + // Only provide this field/relationship/etc., + // when the 'file_managed' base table is present. +- 'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => [$node_table, 'node_field_revision', $users_table, $comment_table, $term_table], + 'real field' => 'id', + 'relationship' => [ + 'id' => 'standard', +@@ -134,7 +147,7 @@ public function getViewsData() { + 'cast' => 'left', + 'title' => $this->t('Content'), + 'label' => $this->t('Content'), +- 'base' => 'node_field_data', ++ 'base' => $node_table, + 'base field' => 'nid', + 'relationship field' => 'id', + 'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'node']], +@@ -145,7 +158,7 @@ public function getViewsData() { + 'help' => $this->t('A file that is associated with this node, usually because it is in a field on the node.'), + // Only provide this field/relationship/etc., + // when the 'node' base table is present. +- 'skip base' => ['file_managed', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => ['file_managed', $users_table, $comment_table, $term_table], + 'real field' => 'fid', + 'relationship' => [ + 'id' => 'standard', +@@ -163,7 +176,7 @@ public function getViewsData() { + 'help' => $this->t('A user that is associated with this file, usually because this file is in a field on the user.'), + // Only provide this field/relationship/etc., + // when the 'file_managed' base table is present. +- 'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => [$node_table, 'node_field_revision', $users_table, $comment_table, $term_table], + 'real field' => 'id', + 'relationship' => [ + 'id' => 'standard', +@@ -182,7 +195,7 @@ public function getViewsData() { + 'help' => $this->t('A file that is associated with this user, usually because it is in a field on the user.'), + // Only provide this field/relationship/etc., + // when the 'users' base table is present. +- 'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => ['file_managed', $node_table, 'node_field_revision', $comment_table, $term_table], + 'real field' => 'fid', + 'relationship' => [ + 'id' => 'standard', +@@ -202,7 +215,7 @@ public function getViewsData() { + 'help' => $this->t('A comment that is associated with this file, usually because this file is in a field on the comment.'), + // Only provide this field/relationship/etc., + // when the 'file_managed' base table is present. +- 'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => [$node_table, 'node_field_revision', $users_table, $comment_table, $term_table], + 'real field' => 'id', + 'relationship' => [ + 'id' => 'standard', +@@ -210,7 +223,7 @@ public function getViewsData() { + 'cast' => 'left', + 'title' => $this->t('Comment'), + 'label' => $this->t('Comment'), +- 'base' => 'comment_field_data', ++ 'base' => $comment_table, + 'base field' => 'cid', + 'relationship field' => 'id', + 'extra' => [['table' => 'file_usage', 'field' => 'type', 'operator' => '=', 'value' => 'comment']], +@@ -221,7 +234,7 @@ public function getViewsData() { + 'help' => $this->t('A file that is associated with this comment, usually because it is in a field on the comment.'), + // Only provide this field/relationship/etc., + // when the 'comment' base table is present. +- 'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => ['file_managed', $node_table, 'node_field_revision', $users_table, $term_table], + 'real field' => 'fid', + 'relationship' => [ + 'id' => 'standard', +@@ -239,7 +252,7 @@ public function getViewsData() { + 'help' => $this->t('A taxonomy term that is associated with this file, usually because this file is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc., + // when the 'file_managed' base table is present. +- 'skip base' => ['node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data', 'taxonomy_term_field_data'], ++ 'skip base' => [$node_table, 'node_field_revision', $users_table, $comment_table, $term_table], + 'real field' => 'id', + 'relationship' => [ + 'id' => 'standard', +@@ -258,7 +271,7 @@ public function getViewsData() { + 'help' => $this->t('A file that is associated with this taxonomy term, usually because it is in a field on the taxonomy term.'), + // Only provide this field/relationship/etc., + // when the 'taxonomy_term_data' base table is present. +- 'skip base' => ['file_managed', 'node_field_data', 'node_field_revision', 'users_field_data', 'comment_field_data'], ++ 'skip base' => ['file_managed', $node_table, 'node_field_revision', $users_table, $comment_table], + 'real field' => 'fid', + 'relationship' => [ + 'id' => 'standard', +diff --git a/core/modules/file/src/Hook/FileHooks.php b/core/modules/file/src/Hook/FileHooks.php +index c3cb60ca529dcdc984c777cac4e14e02aba5e387..7b67196c65318f844d17d3b493b54da1083d014f 100644 +--- a/core/modules/file/src/Hook/FileHooks.php ++++ b/core/modules/file/src/Hook/FileHooks.php +@@ -3,6 +3,7 @@ + namespace Drupal\file\Hook; + + use Drupal\Core\Form\FormStateInterface; ++use Drupal\Core\Database\Database; + use Drupal\Core\Datetime\Entity\DateFormat; + use Drupal\Core\StringTranslation\ByteSizeMarkup; + use Drupal\Core\Render\BubbleableMetadata; +@@ -12,6 +13,7 @@ + use Drupal\Core\Url; + use Drupal\Core\Routing\RouteMatchInterface; + use Drupal\Core\Hook\Attribute\Hook; ++use MongoDB\BSON\UTCDateTime; + + /** + * Hook implementations for file. +@@ -171,7 +173,12 @@ public function cron(): void { + // Only delete temporary files if older than $age. Note that automatic cleanup + // is disabled if $age set to 0. + if ($age) { +- $fids = \Drupal::entityQuery('file')->accessCheck(FALSE)->condition('status', FileInterface::STATUS_PERMANENT, '<>')->condition('changed', \Drupal::time()->getRequestTime() - $age, '<')->range(0, 100)->execute(); ++ $timestamp = \Drupal::time()->getRequestTime() - $age; ++ if (Database::getConnection()->driver() == 'mongodb') { ++ $timestamp = new UTCDateTime($timestamp * 1000); ++ } ++ ++ $fids = \Drupal::entityQuery('file')->accessCheck(FALSE)->condition('status', FileInterface::STATUS_PERMANENT, '<>')->condition('changed', $timestamp, '<')->range(0, 100)->execute(); + $files = $file_storage->loadMultiple($fids); + foreach ($files as $file) { + $references = \Drupal::service('file.usage')->listUsage($file); +diff --git a/core/modules/file/src/Hook/FileViewsHooks.php b/core/modules/file/src/Hook/FileViewsHooks.php +index 2bc80cdb01f3c34c0fb6e9c117cff51bb63dd9f5..cf117e3885c7d09f4063d5a7cf07f79a042457f8 100644 +--- a/core/modules/file/src/Hook/FileViewsHooks.php ++++ b/core/modules/file/src/Hook/FileViewsHooks.php +@@ -51,6 +51,15 @@ public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInt + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping(); + [$label] = views_entity_field_label($entity_type_id, $field_name); ++ ++ // MongoDB always uses the entity base table as the base. ++ if ((\Drupal::database()->driver() !== 'mongodb') && $entity_type->getDataTable()) { ++ $base = $entity_type->getDataTable(); ++ } ++ else { ++ $base = $entity_type->getBaseTable(); ++ } ++ + $data['file_managed'][$pseudo_field_name]['relationship'] = [ + 'title' => t('@entity using @field', [ + '@entity' => $entity_type->getLabel(), +@@ -65,7 +74,7 @@ public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInt + '@field' => $label, + ]), + 'id' => 'entity_reverse', +- 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), ++ 'base' => $base, + 'entity_type' => $entity_type_id, + 'base field' => $entity_type->getKey('id'), + 'field_name' => $field_name, +@@ -79,6 +88,11 @@ public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInt + ], + ], + ]; ++ ++ // Only set the field table when the database is not MongoDB. ++ if (\Drupal::database()->driver() == 'mongodb') { ++ unset($data['file_managed'][$pseudo_field_name]['relationship']['field table']); ++ } + } + + } +diff --git a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php +index 29359dbb9f742a118c55f36162854c5922d525c9..00a9795e65b3c9a962477fd09682b1f35d75c832 100644 +--- a/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php ++++ b/core/modules/file/src/Plugin/EntityReferenceSelection/FileSelection.php +@@ -30,8 +30,8 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') + // become "permanent" after the containing entity gets validated and + // saved.) + $query->condition($query->orConditionGroup() +- ->condition('status', FileInterface::STATUS_PERMANENT) +- ->condition('uid', $this->currentUser->id())); ++ ->condition('status', (bool) FileInterface::STATUS_PERMANENT) ++ ->condition('uid', (int) $this->currentUser->id())); + return $query; + } + +diff --git a/core/modules/help/src/Plugin/Search/HelpSearch.php b/core/modules/help/src/Plugin/Search/HelpSearch.php +index c3122aa42a8e16fadce15673f4b1e7c72db38a40..3e0ba8ff41410658bdf3cb04acd3d778842faa4a 100644 +--- a/core/modules/help/src/Plugin/Search/HelpSearch.php ++++ b/core/modules/help/src/Plugin/Search/HelpSearch.php +@@ -221,7 +221,11 @@ protected function findResults() { + ->condition('i.langcode', $this->languageManager->getCurrentLanguage()->getId()) + ->extend(SearchQuery::class) + ->extend(PagerSelectExtender::class); +- $query->innerJoin('help_search_items', 'hsi', '[i].[sid] = [hsi].[sid] AND [i].[type] = :type', [':type' => $this->getType()]); ++ $query->innerJoin('help_search_items', 'hsi', ++ $query->joinCondition() ++ ->compare('i.sid', 'hsi.sid') ++ ->condition('i.type', $this->getType()) ++ ); + if ($denied_permissions) { + $query->condition('hsi.permission', $denied_permissions, 'NOT IN'); + } +@@ -317,7 +321,11 @@ public function updateIndex() { + + $query = $this->database->select('help_search_items', 'hsi'); + $query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']); +- $query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]); ++ $query->leftJoin('search_dataset', 'sd', ++ $query->joinCondition() ++ ->compare('sd.sid', 'hsi.sid') ++ ->condition('sd.type', $this->getType()) ++ ); + $query->where('[sd].[sid] IS NULL'); + $query->groupBy('hsi.sid') + ->groupBy('hsi.section_plugin_id') +@@ -330,7 +338,11 @@ public function updateIndex() { + if (count($items) < $limit) { + $query = $this->database->select('help_search_items', 'hsi'); + $query->fields('hsi', ['sid', 'section_plugin_id', 'topic_id']); +- $query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [hsi].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]); ++ $query->leftJoin('search_dataset', 'sd', ++ $query->joinCondition() ++ ->compare('sd.sid', 'hsi.sid') ++ ->condition('sd.type', $this->getType()) ++ ); + $query->condition('sd.reindex', 0, '<>'); + $query->groupBy('hsi.sid') + ->groupBy('hsi.section_plugin_id') +@@ -415,7 +427,7 @@ public function updateTopicList() { + + // Permission has changed, update record. + $this->database->update('help_search_items') +- ->condition('sid', $old_item->sid) ++ ->condition('sid', (int) $old_item->sid) + ->fields(['permission' => $permission]) + ->execute(); + unset($sids_to_remove[$old_item->sid]); +@@ -444,8 +456,12 @@ public function updateTopicList() { + */ + public function updateIndexState() { + $query = $this->database->select('help_search_items', 'hsi'); +- $query->addExpression('COUNT(DISTINCT([hsi].[sid]))'); +- $query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]); ++ $query->addExpressionCountDistinct('hsi.sid'); ++ $query->leftJoin('search_dataset', 'sd', ++ $query->joinCondition() ++ ->compare('hsi.sid', 'sd.sid') ++ ->condition('sd.type', $this->getType()) ++ ); + $query->isNull('sd.sid'); + $never_indexed = $query->execute()->fetchField(); + $this->state->set('help_search_unindexed_count', $never_indexed); +@@ -470,8 +486,12 @@ public function indexStatus() { + ->fetchField(); + + $query = $this->database->select('help_search_items', 'hsi'); +- $query->addExpression('COUNT(DISTINCT([hsi].[sid]))'); +- $query->leftJoin('search_dataset', 'sd', '[hsi].[sid] = [sd].[sid] AND [sd].[type] = :type', [':type' => $this->getType()]); ++ $query->addExpressionCountDistinct('hsi.sid'); ++ $query->leftJoin('search_dataset', 'sd', ++ $query->joinCondition() ++ ->compare('hsi.sid', 'sd.sid') ++ ->condition('sd.type', $this->getType()) ++ ); + $condition = $this->database->condition('OR'); + $condition->condition('sd.reindex', 0, '<>') + ->isNull('sd.sid'); +@@ -496,6 +516,9 @@ protected function removeItemsFromIndex($sids) { + // Remove items from our table in batches of 100, to avoid problems + // with having too many placeholders in database queries. + foreach (array_chunk($sids, 100) as $this_list) { ++ foreach ($this_list as &$item) { ++ $item = (int) $item; ++ } + $this->database->delete('help_search_items') + ->condition('sid', $this_list, 'IN') + ->execute(); +diff --git a/core/modules/history/history.install b/core/modules/history/history.install +index 574c41a8dfa23f3fa5999b1cf3d0226756106d90..5d708ce788ee90e95dc6a301926d4ff1d9809fae 100644 +--- a/core/modules/history/history.install ++++ b/core/modules/history/history.install +@@ -5,6 +5,8 @@ + * Installation functions for History module. + */ + ++use Drupal\Core\Database\Database; ++ + /** + * Implements hook_schema(). + */ +@@ -39,6 +41,10 @@ function history_schema(): array { + ], + ]; + ++ if (Database::getConnection()->driver() == 'mongodb') { ++ $schema['history']['fields']['timestamp']['type'] = 'date'; ++ } ++ + return $schema; + } + +diff --git a/core/modules/history/history.module b/core/modules/history/history.module +index 1d53c536054607004a55757ee3fdc92abec86704..451dcfdc2d3cc11319048d94edabba78daca1163 100644 +--- a/core/modules/history/history.module ++++ b/core/modules/history/history.module +@@ -52,7 +52,7 @@ function history_read_multiple($nids) { + } + else { + // Initialize value if current user has not viewed the node. +- $nodes_to_read[$nid] = 0; ++ $nodes_to_read[(int) $nid] = 0; + } + } + +@@ -62,7 +62,7 @@ function history_read_multiple($nids) { + + $result = \Drupal::database()->select('history', 'h') + ->fields('h', ['nid', 'timestamp']) +- ->condition('uid', \Drupal::currentUser()->id()) ++ ->condition('uid', (int) \Drupal::currentUser()->id()) + ->condition('nid', array_keys($nodes_to_read), 'IN') + ->execute(); + foreach ($result as $row) { +@@ -92,8 +92,8 @@ function history_write($nid, $account = NULL) { + $request_time = \Drupal::time()->getRequestTime(); + \Drupal::database()->merge('history') + ->keys([ +- 'uid' => $account->id(), +- 'nid' => $nid, ++ 'uid' => (int) $account->id(), ++ 'nid' => (int) $nid, + ]) + ->fields(['timestamp' => $request_time]) + ->execute(); +diff --git a/core/modules/history/src/Hook/HistoryHooks.php b/core/modules/history/src/Hook/HistoryHooks.php +index 2b95304dfd2d7a86105f0e67c89344e165b7ba7e..0a3c1e615b0b00cd3e3ccc00f9156c3652c1b9bf 100644 +--- a/core/modules/history/src/Hook/HistoryHooks.php ++++ b/core/modules/history/src/Hook/HistoryHooks.php +@@ -66,7 +66,7 @@ public function nodeViewAlter(array &$build, EntityInterface $node, EntityViewDi + */ + #[Hook('node_delete')] + public function nodeDelete(EntityInterface $node) { +- \Drupal::database()->delete('history')->condition('nid', $node->id())->execute(); ++ \Drupal::database()->delete('history')->condition('nid', (int) $node->id())->execute(); + } + + /** +@@ -76,7 +76,7 @@ public function nodeDelete(EntityInterface $node) { + public function userCancel($edit, UserInterface $account, $method) { + switch ($method) { + case 'user_cancel_reassign': +- \Drupal::database()->delete('history')->condition('uid', $account->id())->execute(); ++ \Drupal::database()->delete('history')->condition('uid', (int) $account->id())->execute(); + break; + } + } +@@ -86,7 +86,7 @@ public function userCancel($edit, UserInterface $account, $method) { + */ + #[Hook('user_delete')] + public function userDelete($account) { +- \Drupal::database()->delete('history')->condition('uid', $account->id())->execute(); ++ \Drupal::database()->delete('history')->condition('uid', (int) $account->id())->execute(); + } + + } +diff --git a/core/modules/history/src/Hook/HistoryViewsHooks.php b/core/modules/history/src/Hook/HistoryViewsHooks.php +index 465988fd5fe32889aef2eb1294f5b0c6e8a846bb..aa2c4c61a2184264ff5b1c273bd94e5802369957 100644 +--- a/core/modules/history/src/Hook/HistoryViewsHooks.php ++++ b/core/modules/history/src/Hook/HistoryViewsHooks.php +@@ -23,19 +23,28 @@ public function viewsData(): array { + // alias it so that we can later add the real table for other purposes if we + // need it. + $data['history']['table']['group'] = t('Content'); ++ ++ // Which table to use as the base table for the entity type "node". ++ if (\Drupal::database()->driver() == 'mongodb') { ++ $node_table = 'node'; ++ } ++ else { ++ $node_table = 'node_field_data'; ++ } ++ + // Explain how this table joins to others. + $data['history']['table']['join'] = [ +- // Directly links to node table. +- 'node_field_data' => [ ++ // Directly links to node table. ++ $node_table => [ + 'table' => 'history', + 'left_field' => 'nid', + 'field' => 'nid', + 'extra' => [ +- [ +- 'field' => 'uid', +- 'value' => '***CURRENT_USER***', +- 'numeric' => TRUE, +- ], ++ [ ++ 'field' => 'uid', ++ 'value' => '***CURRENT_USER***', ++ 'numeric' => TRUE, ++ ], + ], + ], + ]; +diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php +index 25717b33e6edd2cd573c0db9e2f8e137e224ed5e..ccd7b56e9f490adb17a411c82a072588d6c104a0 100644 +--- a/core/modules/layout_builder/src/InlineBlockEntityOperations.php ++++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php +@@ -206,6 +206,9 @@ public function removeUnused($limit = 100) { + */ + protected function getBlockIdsForRevisionIds(array $revision_ids) { + if ($revision_ids) { ++ foreach ($revision_ids as &$revision_id) { ++ $revision_id = (int) $revision_id; ++ } + $query = $this->blockContentStorage->getQuery()->accessCheck(FALSE); + $query->condition('revision_id', $revision_ids, 'IN'); + $block_ids = $query->execute(); +diff --git a/core/modules/layout_builder/src/InlineBlockUsage.php b/core/modules/layout_builder/src/InlineBlockUsage.php +index ab94d4c535bb8f46cc12ae1e9a24382ab45159cb..aaca3f2ce6679502450f76eb781ac9f453203d57 100644 +--- a/core/modules/layout_builder/src/InlineBlockUsage.php ++++ b/core/modules/layout_builder/src/InlineBlockUsage.php +@@ -33,7 +33,7 @@ public function __construct(Connection $database) { + public function addUsage($block_content_id, EntityInterface $entity) { + $this->database->merge('inline_block_usage') + ->keys([ +- 'block_content_id' => $block_content_id, ++ 'block_content_id' => (int) $block_content_id, + 'layout_entity_id' => $entity->id(), + 'layout_entity_type' => $entity->getEntityTypeId(), + ])->execute(); +@@ -69,6 +69,9 @@ public function removeByLayoutEntity(EntityInterface $entity) { + */ + public function deleteUsage(array $block_content_ids) { + if (!empty($block_content_ids)) { ++ foreach ($block_content_ids as &$block_content_id) { ++ $block_content_id = (int) $block_content_id; ++ } + $query = $this->database->delete('inline_block_usage')->condition('block_content_id', $block_content_ids, 'IN'); + $query->execute(); + } +@@ -79,7 +82,7 @@ public function deleteUsage(array $block_content_ids) { + */ + public function getUsage($block_content_id) { + $query = $this->database->select('inline_block_usage'); +- $query->condition('block_content_id', $block_content_id); ++ $query->condition('block_content_id', (int) $block_content_id); + $query->fields('inline_block_usage', ['layout_entity_id', 'layout_entity_type']); + $query->range(0, 1); + return $query->execute()->fetchObject(); +diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc +index e338223e5c1059bf8cbbfc61d484c03ab17eb0c2..87f64d608d28cc0cb60643ba265b139aab06008b 100644 +--- a/core/modules/locale/locale.bulk.inc ++++ b/core/modules/locale/locale.bulk.inc +@@ -57,7 +57,7 @@ function locale_translate_batch_import_files(array $options, $force = FALSE) { + if (!$force) { + $result = \Drupal::database()->select('locale_file', 'lf') + ->fields('lf', ['langcode', 'uri', 'timestamp']) +- ->condition('langcode', $langcodes) ++ ->condition('langcode', $langcodes, 'IN') + ->execute() + ->fetchAllAssoc('uri'); + foreach ($result as $uri => $info) { +diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install +index cf034cc6f36fe48f3eb6c9b313c5a1754b907034..3ba836ad9eb8c359e71b25ef466595b6f6e3e735 100644 +--- a/core/modules/locale/locale.install ++++ b/core/modules/locale/locale.install +@@ -5,6 +5,7 @@ + * Install, update, and uninstall functions for the Locale module. + */ + ++use Drupal\Core\Database\Database; + use Drupal\Core\File\Exception\FileException; + use Drupal\Core\File\FileSystemInterface; + use Drupal\Core\Link; +@@ -249,6 +250,15 @@ function locale_schema(): array { + ], + 'primary key' => ['project', 'langcode'], + ]; ++ ++ if (Database::getConnection()->driver() == 'mongodb') { ++ $schema['locales_target']['fields']['customized']['type'] = 'bool'; ++ $schema['locales_target']['fields']['customized']['default'] = FALSE; ++ ++ $schema['locale_file']['fields']['timestamp']['type'] = 'date'; ++ $schema['locale_file']['fields']['last_checked']['type'] = 'date'; ++ } ++ + return $schema; + } + +diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc +index b0c577c14850bf257fa8f5c04fc15320ed2167ee..1ace50d9b1e1612b0ba6037ebca5abb954ede0a1 100644 +--- a/core/modules/locale/locale.translation.inc ++++ b/core/modules/locale/locale.translation.inc +@@ -5,6 +5,7 @@ + */ + + use Drupal\Core\StreamWrapper\StreamWrapperManager; ++use MongoDB\BSON\UTCDateTime; + + /** + * Comparison result of source files timestamps. +@@ -332,11 +333,14 @@ function locale_cron_fill_queue() { + // Determine which project+language should be updated. + $request_time = \Drupal::time()->getRequestTime(); + $last = $request_time - $config->get('translation.update_interval_days') * 3600 * 24; ++ $connection = \Drupal::database(); ++ if ($connection->driver() == 'mongodb') { ++ $last = new UTCDateTime($last * 1000); ++ } + $projects = \Drupal::service('locale.project')->getAll(); + $projects = array_filter($projects, function ($project) { + return $project['status'] == 1; + }); +- $connection = \Drupal::database(); + $files = $connection->select('locale_file', 'f') + ->condition('f.project', array_keys($projects), 'IN') + ->condition('f.last_checked', $last, '<') +diff --git a/core/modules/locale/src/StringDatabaseStorage.php b/core/modules/locale/src/StringDatabaseStorage.php +index 74e94fca5d160ee85be61e572144ec962f51f38b..f4f63161d92673e55e2760ded54f6f0eed000a51 100644 +--- a/core/modules/locale/src/StringDatabaseStorage.php ++++ b/core/modules/locale/src/StringDatabaseStorage.php +@@ -377,14 +377,16 @@ protected function dbStringSelect(array $conditions, array $options = []) { + if ($join) { + if (isset($conditions['language'])) { + // If we've got a language condition, we use it for the join. +- $query->$join('locales_target', 't', "t.lid = s.lid AND t.language = :langcode", [ +- ':langcode' => $conditions['language'], +- ]); ++ $query->$join('locales_target', 't', ++ $query->joinCondition() ++ ->compare('t.lid', 's.lid') ++ ->condition('t.language', $conditions['language']) ++ ); + unset($conditions['language']); + } + else { + // Since we don't have a language, join with locale id only. +- $query->$join('locales_target', 't', "t.lid = s.lid"); ++ $query->$join('locales_target', 't', $query->joinCondition()->compare('t.lid', 's.lid')); + } + if (!empty($options['translation'])) { + // We cannot just add all fields because 'lid' may get null values. +diff --git a/core/modules/media/src/MediaAccessControlHandler.php b/core/modules/media/src/MediaAccessControlHandler.php +index 4197d07968571573497e45ebf03b0e35d15dc2eb..0c0edf8547c6460330729bf8daede18bd111980c 100644 +--- a/core/modules/media/src/MediaAccessControlHandler.php ++++ b/core/modules/media/src/MediaAccessControlHandler.php +@@ -57,7 +57,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter + } + + $type = $entity->bundle(); +- $is_owner = ($account->id() && $account->id() === $entity->getOwnerId()); ++ $is_owner = ($account->id() && $account->id() == $entity->getOwnerId()); + switch ($operation) { + case 'view': + if ($entity->isPublished()) { +@@ -127,7 +127,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter + $entity_access = $entity->access('view', $account, TRUE); + if (!$entity->isDefaultRevision()) { + $media_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); +- $entity_access->andIf($this->access($media_storage->load($entity->id()), 'view', $account, TRUE)); ++ $entity_access->andIf($this->access($media_storage->load((int) $entity->id()), 'view', $account, TRUE)); + } + + return AccessResult::allowed()->cachePerPermissions()->andIf($entity_access); +diff --git a/core/modules/media/src/MediaViewsData.php b/core/modules/media/src/MediaViewsData.php +index fdfada27281974108b01845326c58ee79fe53357..7d0c21a71a070996b75c6e8f737daf9f930afd42 100644 +--- a/core/modules/media/src/MediaViewsData.php ++++ b/core/modules/media/src/MediaViewsData.php +@@ -15,16 +15,25 @@ class MediaViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + +- $data['media_field_data']['table']['wizard_id'] = 'media'; +- $data['media_field_revision']['table']['wizard_id'] = 'media_revision'; +- +- $data['media_field_data']['user_name']['filter'] = $data['media_field_data']['uid']['filter']; +- $data['media_field_data']['user_name']['filter']['title'] = $this->t('Authored by'); +- $data['media_field_data']['user_name']['filter']['help'] = $this->t('The username of the content author.'); +- $data['media_field_data']['user_name']['filter']['id'] = 'user_name'; +- $data['media_field_data']['user_name']['filter']['real field'] = 'uid'; +- +- $data['media_field_data']['status_extra'] = [ ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'media'; ++ $revision_table = 'media'; ++ } ++ else { ++ $data_table = 'media_field_data'; ++ $revision_table = 'media_field_revision'; ++ } ++ ++ $data[$data_table]['table']['wizard_id'] = 'media'; ++ $data[$revision_table]['table']['wizard_id'] = 'media_revision'; ++ ++ $data[$data_table]['user_name']['filter'] = $data[$data_table]['uid']['filter']; ++ $data[$data_table]['user_name']['filter']['title'] = $this->t('Authored by'); ++ $data[$data_table]['user_name']['filter']['help'] = $this->t('The username of the content author.'); ++ $data[$data_table]['user_name']['filter']['id'] = 'user_name'; ++ $data[$data_table]['user_name']['filter']['real field'] = 'uid'; ++ ++ $data[$data_table]['status_extra'] = [ + 'title' => $this->t('Published status or admin user'), + 'help' => $this->t('Filters out unpublished media if the current user cannot view it.'), + 'filter' => [ +diff --git a/core/modules/media/src/Plugin/EntityReferenceSelection/MediaSelection.php b/core/modules/media/src/Plugin/EntityReferenceSelection/MediaSelection.php +index 01b517c321c6e4ea74d43e15b3ab9863b46e6d81..3f98d839ca58154fb3bcd7efbc0101cce41815c1 100644 +--- a/core/modules/media/src/Plugin/EntityReferenceSelection/MediaSelection.php ++++ b/core/modules/media/src/Plugin/EntityReferenceSelection/MediaSelection.php +@@ -27,7 +27,7 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') + // Ensure that users with insufficient permission cannot see unpublished + // entities. + if (!$this->currentUser->hasPermission('administer media')) { +- $query->condition('status', 1); ++ $query->condition('status', TRUE); + } + return $query; + } +diff --git a/core/modules/media/src/Plugin/views/wizard/Media.php b/core/modules/media/src/Plugin/views/wizard/Media.php +index 322f04fc2885d2a59577bdfce798dc55a8ffdf96..1606a49d50f9bec66303991f2e06c44a80c2f768 100644 +--- a/core/modules/media/src/Plugin/views/wizard/Media.php ++++ b/core/modules/media/src/Plugin/views/wizard/Media.php +@@ -2,9 +2,13 @@ + + namespace Drupal\media\Plugin\views\wizard; + ++use Drupal\Core\Database\Connection; ++use Drupal\Core\Entity\EntityTypeBundleInfoInterface; ++use Drupal\Core\Menu\MenuParentFormSelectorInterface; + use Drupal\Core\StringTranslation\TranslatableMarkup; + use Drupal\views\Attribute\ViewsWizard; + use Drupal\views\Plugin\views\wizard\WizardPluginBase; ++use Symfony\Component\DependencyInjection\ContainerInterface; + + /** + * Provides Views creation wizard for Media. +@@ -23,6 +27,32 @@ class Media extends WizardPluginBase { + */ + protected $createdColumn = 'media_field_data-created'; + ++ /** ++ * {@inheritdoc} ++ */ ++ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { ++ return new static( ++ $configuration, ++ $plugin_id, ++ $plugin_definition, ++ $container->get('entity_type.bundle.info'), ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') ++ ); ++ } ++ ++ /** ++ * Constructs a WizardPluginBase object. ++ */ ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { ++ parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector, $connection); ++ ++ if ($connection->driver() == 'mongodb') { ++ $this->base_table = 'media'; ++ $this->createdColumn = 'media-created'; ++ } ++ } ++ + /** + * {@inheritdoc} + */ +@@ -48,7 +78,12 @@ protected function defaultDisplayOptions() { + // Add the name field, so that the display has content if the user switches + // to a row style that uses fields. + $display_options['fields']['name']['id'] = 'name'; +- $display_options['fields']['name']['table'] = 'media_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['name']['table'] = 'media'; ++ } ++ else { ++ $display_options['fields']['name']['table'] = 'media_field_data'; ++ } + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'media'; + $display_options['fields']['name']['entity_field'] = 'media'; +diff --git a/core/modules/media/src/Plugin/views/wizard/MediaRevision.php b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php +index f7a2aafee23ee8e86aadaa7a5bfe98d3b2d431e6..dbedd54c78067ac80e10c6bc0299155bcf4b82fe 100644 +--- a/core/modules/media/src/Plugin/views/wizard/MediaRevision.php ++++ b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php +@@ -2,9 +2,13 @@ + + namespace Drupal\media\Plugin\views\wizard; + ++use Drupal\Core\Database\Connection; ++use Drupal\Core\Entity\EntityTypeBundleInfoInterface; ++use Drupal\Core\Menu\MenuParentFormSelectorInterface; + use Drupal\Core\StringTranslation\TranslatableMarkup; + use Drupal\views\Attribute\ViewsWizard; + use Drupal\views\Plugin\views\wizard\WizardPluginBase; ++use Symfony\Component\DependencyInjection\ContainerInterface; + + /** + * Provides Views creation wizard for Media revisions. +@@ -23,6 +27,32 @@ class MediaRevision extends WizardPluginBase { + */ + protected $createdColumn = 'media_field_revision-created'; + ++ /** ++ * {@inheritdoc} ++ */ ++ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { ++ return new static( ++ $configuration, ++ $plugin_id, ++ $plugin_definition, ++ $container->get('entity_type.bundle.info'), ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') ++ ); ++ } ++ ++ /** ++ * Constructs a WizardPluginBase object. ++ */ ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { ++ parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector, $connection); ++ ++ if ($connection->driver() == 'mongodb') { ++ $this->base_table = 'media'; ++ $this->createdColumn = 'media-created'; ++ } ++ } ++ + /** + * {@inheritdoc} + */ +@@ -38,7 +68,12 @@ protected function defaultDisplayOptions() { + + // Add the changed field. + $display_options['fields']['changed']['id'] = 'changed'; +- $display_options['fields']['changed']['table'] = 'media_field_revision'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['changed']['table'] = 'media'; ++ } ++ else { ++ $display_options['fields']['changed']['table'] = 'media_field_revision'; ++ } + $display_options['fields']['changed']['field'] = 'changed'; + $display_options['fields']['changed']['entity_type'] = 'media'; + $display_options['fields']['changed']['entity_field'] = 'changed'; +@@ -60,7 +95,12 @@ protected function defaultDisplayOptions() { + + // Add the name field. + $display_options['fields']['name']['id'] = 'name'; +- $display_options['fields']['name']['table'] = 'media_field_revision'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['name']['table'] = 'media'; ++ } ++ else { ++ $display_options['fields']['name']['table'] = 'media_field_revision'; ++ } + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'media'; + $display_options['fields']['name']['entity_field'] = 'name'; +diff --git a/core/modules/menu_link_content/src/MenuLinkContentStorage.php b/core/modules/menu_link_content/src/MenuLinkContentStorage.php +index cce5c93c392d4f640d72774cad55ee74e2e99024..fb1e58637ca91caa676bf51455fda4ebbefd7e79 100644 +--- a/core/modules/menu_link_content/src/MenuLinkContentStorage.php ++++ b/core/modules/menu_link_content/src/MenuLinkContentStorage.php +@@ -20,25 +20,40 @@ public function getMenuLinkIdsWithPendingRevisions() { + $langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value']; + $revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value']; + +- $query = $this->database->select($this->getRevisionDataTable(), 'mlfr'); +- $query->fields('mlfr', [$id_field]); +- $query->addExpression("MAX([mlfr].[$revision_field])", $revision_field); +- +- $query->join($this->getRevisionTable(), 'mlr', "[mlfr].[$revision_field] = [mlr].[$revision_field] AND [mlr].[$revision_default_field] = 0"); +- +- $inner_select = $this->database->select($this->getRevisionDataTable(), 't'); +- $inner_select->condition("t.$rta_field", '1'); +- $inner_select->fields('t', [$id_field, $langcode_field]); +- $inner_select->addExpression("MAX([t].[$revision_field])", $revision_field); +- $inner_select +- ->groupBy("t.$id_field") +- ->groupBy("t.$langcode_field"); +- +- $query->join($inner_select, 'mr', "[mlfr].[$revision_field] = [mr].[$revision_field] AND [mlfr].[$langcode_field] = [mr].[$langcode_field]"); +- +- $query->groupBy("mlfr.$id_field"); +- +- return $query->execute()->fetchAllKeyed(1, 0); ++ if ($this->database->driver() == 'mongodb') { ++ // @todo Fix this query for MongoDB. ++ // See: https://git.drupalcode.org/project/drupal/-/commit/fbdccdc952c53fce12a81ac6640514c52e5fc3af ++ return []; ++ } ++ else { ++ $query = $this->database->select($this->getRevisionDataTable(), 'mlfr'); ++ $query->fields('mlfr', [$id_field]); ++ $query->addExpressionMax("mlfr.$revision_field", $revision_field); ++ ++ $query->join($this->getRevisionTable(), 'mlr', ++ $query->joinCondition() ++ ->compare("mlfr.$revision_field", "mlr.$revision_field") ++ ->condition("mlr.$revision_default_field", 0) ++ ); ++ ++ $inner_select = $this->database->select($this->getRevisionDataTable(), 't'); ++ $inner_select->condition("t.$rta_field", '1'); ++ $inner_select->fields('t', [$id_field, $langcode_field]); ++ $inner_select->addExpressionMax("t.$revision_field", $revision_field); ++ $inner_select ++ ->groupBy("t.$id_field") ++ ->groupBy("t.$langcode_field"); ++ ++ $query->join($inner_select, 'mr', ++ $query->joinCondition() ++ ->compare("mlfr.$revision_field", "mr.$revision_field") ++ ->compare("mlfr.$langcode_field", "mr.$langcode_field") ++ ); ++ ++ $query->groupBy("mlfr.$id_field"); ++ ++ return $query->execute()->fetchAllKeyed(1, 0); ++ } + } + + } +diff --git a/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php b/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php +index 3003310e6d8329305aff694c6d6b900a4dec1870..a7c34a32b2f9f379d14d45fec65fd9819aad7afd 100644 +--- a/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php ++++ b/core/modules/menu_link_content/src/Plugin/migrate/source/d6/MenuLinkTranslation.php +@@ -42,12 +42,12 @@ public function query() { + + // Add in the property, which is either title or description. Cast the mlid + // to text so PostgreSQL can make the join. +- $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', 'CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]'); ++ $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', $query->joinCondition()->where('CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]')); + $query->addField('i18n', 'lid'); + $query->addField('i18n', 'property'); + + // Add in the translation for the property. +- $query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->addField('lt', 'language'); + $query->addField('lt', 'translation'); + return $query; +diff --git a/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php b/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php +index 85a2a4f61505369d178acd447f0cba2da8c1fc15..8cf330a046ce3c5601a6e3a2951fceaeb28e8cac 100644 +--- a/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php ++++ b/core/modules/menu_link_content/src/Plugin/migrate/source/d7/MenuLinkTranslation.php +@@ -28,13 +28,13 @@ public function query() { + + // Add in the property, which is either title or description. Cast the mlid + // to text so PostgreSQL can make the join. +- $query->leftJoin('i18n_string', 'i18n', 'CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]'); ++ $query->leftJoin('i18n_string', 'i18n', $query->joinCondition()->where('CAST([ml].[mlid] AS CHAR(255)) = [i18n].[objectid]')); + $query->fields('i18n', ['lid', 'objectid', 'property', 'textgroup']) + ->condition('i18n.textgroup', 'menu') + ->condition('i18n.type', 'item'); + + // Add in the translation for the property. +- $query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->addField('lt', 'language', 'lt_language'); + $query->fields('lt', ['translation']); + $query->isNotNull('lt.language'); +diff --git a/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php b/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php +index 9af552b56e2ffff0b90eaa3cdc235d1e2f49f9c8..ea515007def2af623a002c33885259e4c95ad87f 100644 +--- a/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php ++++ b/core/modules/menu_link_content/src/Plugin/migrate/source/MenuLink.php +@@ -67,7 +67,7 @@ public function query() { + if (isset($this->configuration['menu_name'])) { + $query->condition('ml.menu_name', (array) $this->configuration['menu_name'], 'IN'); + } +- $query->leftJoin('menu_links', 'pl', '[ml].[plid] = [pl].[mlid]'); ++ $query->leftJoin('menu_links', 'pl', $query->joinCondition()->compare('ml.plid', 'pl.mlid')); + $query->addField('pl', 'link_path', 'parent_link_path'); + $query->orderBy('ml.depth'); + $query->orderby('ml.mlid'); +diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/FieldableEntity.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/FieldableEntity.php +index 2a639219a00eccc1309cb8b06592ff7a55673282..ea8bf908567f90e7f6a718d73f74cdd918da8492 100644 +--- a/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/FieldableEntity.php ++++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d7/FieldableEntity.php +@@ -55,7 +55,7 @@ protected function getFields($entity_type, $bundle = NULL) { + + // Join the 'field_config' table and add the 'translatable' setting to the + // query. +- $query->leftJoin('field_config', 'fc', '[fci].[field_id] = [fc].[id]'); ++ $query->leftJoin('field_config', 'fc', $query->joinCondition()->compare('fci.field_id', 'fc.id')); + $query->addField('fc', 'translatable'); + + $this->fieldInfo[$cid] = $query->execute()->fetchAllAssoc('field_name'); +diff --git a/core/modules/migrate/src/Controller/MigrateMessageController.php b/core/modules/migrate/src/Controller/MigrateMessageController.php +index 385b89f343b8cfa8ee8f94c15a8c8a68480b18e3..77fc28f36eb2b6db2c65921b9cc22b0878441791 100644 +--- a/core/modules/migrate/src/Controller/MigrateMessageController.php ++++ b/core/modules/migrate/src/Controller/MigrateMessageController.php +@@ -6,6 +6,7 @@ + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\DatabaseConnectionRefusedException; + use Drupal\Core\Database\DatabaseNotFoundException; ++use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Form\FormBuilderInterface; + use Drupal\Core\StringTranslation\TranslatableMarkup; + use Drupal\Core\Url; +@@ -186,13 +187,10 @@ public function details(string $migration_id, Request $request): array { + ->extend('\Drupal\Core\Database\Query\PagerSelectExtender') + ->extend('\Drupal\Core\Database\Query\TableSortExtender'); + // Not all messages have a matching row in the map table. +- $query->leftJoin($map_table, 'map', 'msg.source_ids_hash = map.source_ids_hash'); ++ $query->leftJoin($map_table, 'map', $query->joinCondition()->compare('msg.source_ids_hash', 'map.source_ids_hash')); + $query->fields('msg'); + $query->fields('map'); +- $filter = $this->buildFilterQuery($request); +- if (!empty($filter['where'])) { +- $query->where($filter['where'], $filter['args']); +- } ++ $this->addFilterQuery($request, $query); + $result = $query + ->limit(50) + ->orderByHeader($header) +@@ -238,54 +236,57 @@ public function details(string $migration_id, Request $request): array { + } + + /** +- * Builds a query for migrate message administration. ++ * Adds the condition to the query for migrate message administration. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request. +- * +- * @return array|null +- * An associative array with keys 'where' and 'args' or NULL if there were +- * no filters set. ++ * @param \Drupal\Core\Database\Query\SelectInterface $query ++ * The database query. + */ +- protected function buildFilterQuery(Request $request): ?array { ++ protected function addFilterQuery(Request $request, SelectInterface &$query): void { + $session_filters = $request->getSession()->get('migration_messages_overview_filter', []); + if (empty($session_filters)) { +- return NULL; ++ return; + } + +- // Build query. +- $where = $args = []; ++ // Build conditions. ++ $condition_ors = []; + foreach ($session_filters as $filter) { +- $filter_where = []; ++ $condition = $query->orConditionGroup(); ++ $filter_added = FALSE; + + switch ($filter['type']) { + case 'array': + foreach ($filter['value'] as $value) { +- $filter_where[] = $filter['where']; +- $args[] = $value; ++ if ($filter['where'] == 'msg.level') { ++ $value = (int) $value; ++ } ++ $condition->condition($filter['where'], $value); ++ $filter_added = TRUE; + } + break; + + case 'string': +- $filter_where[] = $filter['where']; +- $args[] = '%' . $filter['value'] . '%'; ++ $condition->condition($filter['where'], '%' . $filter['value'] . '%', 'LIKE'); ++ $filter_added = TRUE; + break; + + default: +- $filter_where[] = $filter['where']; +- $args[] = $filter['value']; ++ if ($filter['where'] == 'msg.level') { ++ $filter['value'] = (int) $filter['value']; ++ } ++ $condition->condition($filter['where'], $filter['value']); ++ $filter_added = TRUE; + } + +- if (!empty($filter_where)) { +- $where[] = '(' . implode(' OR ', $filter_where) . ')'; ++ if ($filter_added) { ++ $condition_ors[] = $condition; + } + } +- $where = !empty($where) ? implode(' AND ', $where) : ''; + +- return [ +- 'where' => $where, +- 'args' => $args, +- ]; ++ foreach ($condition_ors as $condition_or) { ++ $query->condition($condition_or); ++ } + } + + /** +diff --git a/core/modules/migrate/src/Form/MessageForm.php b/core/modules/migrate/src/Form/MessageForm.php +index 75522351418d19d88dd99ee46811dabb9ade4c72..4ef16782fc9e7017375ad79adcc26382d30860ad 100644 +--- a/core/modules/migrate/src/Form/MessageForm.php ++++ b/core/modules/migrate/src/Form/MessageForm.php +@@ -82,12 +82,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { + public function submitForm(array &$form, FormStateInterface $form_state) { + $filters['message'] = [ + 'title' => $this->t('message'), +- 'where' => 'msg.message LIKE ?', ++ 'where' => 'msg.message', + 'type' => 'string', + ]; + $filters['severity'] = [ + 'title' => $this->t('Severity'), +- 'where' => 'msg.level = ?', ++ 'where' => 'msg.level', + 'type' => 'array', + ]; + $session_filters = $this->getRequest()->getSession()->get('migration_messages_overview_filter', []); +diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +index 1df1b2f96c7d13e4438f162db238c9bad83a3015..31a3cfa5f8f83ce2ef1bc7bf5c3dd240b5f52b93 100644 +--- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php ++++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +@@ -737,7 +737,7 @@ public function saveMessage(array $source_id_values, $message, $level = Migratio + */ + public function getMessages(array $source_id_values = [], $level = NULL) { + $query = $this->getDatabase()->select($this->messageTableName(), 'msg'); +- $condition = sprintf('[msg].[%s] = [map].[%s]', $this::SOURCE_IDS_HASH, $this::SOURCE_IDS_HASH); ++ $condition = $query->joinCondition()->compare('msg.' . $this::SOURCE_IDS_HASH, 'map.' . $this::SOURCE_IDS_HASH); + $query->addJoin('LEFT', $this->mapTableName(), 'map', $condition); + // Explicitly define the fields we want. The order will be preserved: source + // IDs, destination IDs (if possible), and then the rest. +diff --git a/core/modules/migrate/src/Plugin/migrate/source/DummyQueryTrait.php b/core/modules/migrate/src/Plugin/migrate/source/DummyQueryTrait.php +index 93509af71ce6cb86d55d1dc10166c5caabcda100..5fcced4ef29b1ed820fc03a4d33628d921b2e750 100644 +--- a/core/modules/migrate/src/Plugin/migrate/source/DummyQueryTrait.php ++++ b/core/modules/migrate/src/Plugin/migrate/source/DummyQueryTrait.php +@@ -20,7 +20,7 @@ public function query() { + // anyway. + $query = $this->select(uniqid(), 's') + ->range(0, 1); +- $query->addExpression('1'); ++ $query->addExpressionConstant('1'); + return $query; + } + +diff --git a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php +index 090e93837135aa658873526539337e8a262dfcb2..66720373fde6fcf41945f259ecd68f1912de6013 100644 +--- a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php ++++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php +@@ -279,14 +279,12 @@ protected function initializeIterator() { + // Build the join to the map table. Because the source key could have + // multiple fields, we need to build things up. + $count = 1; +- $map_join = ''; +- $delimiter = ''; ++ $map_join = $this->query->joinCondition(); + foreach ($this->getIds() as $field_name => $field_schema) { + if (isset($field_schema['alias'])) { + $field_name = $field_schema['alias'] . '.' . $this->query->escapeField($field_name); + } +- $map_join .= "$delimiter$field_name = map.sourceid" . $count++; +- $delimiter = ' AND '; ++ $map_join->compare($field_name, "map.sourceid" . $count++); + } + + $alias = $this->query->leftJoin($this->migration->getIdMap() +diff --git a/core/modules/node/node.install b/core/modules/node/node.install +index ddde78929ad540dea0e81f3bbe38a3a629aabff0..9958ea5d3a9083e0b6a5c0f79bef0bf629dc7abb 100644 +--- a/core/modules/node/node.install ++++ b/core/modules/node/node.install +@@ -114,6 +114,28 @@ function node_schema(): array { + ], + ]; + ++ if (Database::getConnection()->driver() == 'mongodb') { ++ $schema['node_access']['fields']['fallback']['type'] = 'bool'; ++ $schema['node_access']['fields']['fallback']['default'] = TRUE; ++ unset($schema['node_access']['fields']['fallback']['unsigned']); ++ unset($schema['node_access']['fields']['fallback']['size']); ++ ++ $schema['node_access']['fields']['grant_view']['type'] = 'bool'; ++ $schema['node_access']['fields']['grant_view']['default'] = FALSE; ++ unset($schema['node_access']['fields']['grant_view']['unsigned']); ++ unset($schema['node_access']['fields']['grant_view']['size']); ++ ++ $schema['node_access']['fields']['grant_update']['type'] = 'bool'; ++ $schema['node_access']['fields']['grant_update']['default'] = FALSE; ++ unset($schema['node_access']['fields']['grant_update']['unsigned']); ++ unset($schema['node_access']['fields']['grant_update']['size']); ++ ++ $schema['node_access']['fields']['grant_delete']['type'] = 'bool'; ++ $schema['node_access']['fields']['grant_delete']['default'] = FALSE; ++ unset($schema['node_access']['fields']['grant_delete']['unsigned']); ++ unset($schema['node_access']['fields']['grant_delete']['size']); ++ } ++ + return $schema; + } + +diff --git a/core/modules/node/node.module b/core/modules/node/node.module +index 917aa8e47f050e9ce60efc430e93d29e3cbbed93..bb88b5370ca2596df265d4d41e1901373fa7c09f 100644 +--- a/core/modules/node/node.module ++++ b/core/modules/node/node.module +@@ -609,7 +609,7 @@ function _node_access_rebuild_batch_operation(&$context) { + // Process the next 20 nodes. + $limit = 20; + $nids = \Drupal::entityQuery('node') +- ->condition('nid', $context['sandbox']['current_node'], '>') ++ ->condition('nid', (int) $context['sandbox']['current_node'], '>') + ->sort('nid', 'ASC') + // Disable access checking since all nodes must be processed even if the + // user does not have access. And unless the current user has the bypass +diff --git a/core/modules/node/src/Hook/NodeHooks1.php b/core/modules/node/src/Hook/NodeHooks1.php +index 663d16fd7cd54b1af93432bab65e9151ed82215c..3571cf1372665d4f28d56ec17a6387f8960176d9 100644 +--- a/core/modules/node/src/Hook/NodeHooks1.php ++++ b/core/modules/node/src/Hook/NodeHooks1.php +@@ -16,6 +16,7 @@ + use Drupal\Core\Url; + use Drupal\Core\Routing\RouteMatchInterface; + use Drupal\Core\Hook\Attribute\Hook; ++use MongoDB\BSON\UTCDateTime; + + /** + * Hook implementations for node. +@@ -231,6 +232,17 @@ public function ranking() { + // Add relevance based on updated date, but only if it the scale values have + // been calculated in node_cron(). + if ($node_min_max = \Drupal::state()->get('node.min_max_update_time')) { ++ if (isset($node_min_max['min_created']) && ($node_min_max['min_created'] instanceof UTCDateTime)) { ++ $node_min_max['min_created'] = (int) $node_min_max['min_created']->__toString(); ++ $node_min_max['min_created'] = $node_min_max['min_created'] / 1000; ++ $node_min_max['min_created'] = (string) $node_min_max['min_created']; ++ } ++ if (isset($node_min_max['max_created']) && ($node_min_max['max_created'] instanceof UTCDateTime)) { ++ $node_min_max['max_created'] = (int) $node_min_max['max_created']->__toString(); ++ $node_min_max['max_created'] = $node_min_max['max_created'] / 1000; ++ $node_min_max['max_created'] = (string) $node_min_max['max_created']; ++ } ++ + $ranking['recent'] = [ + 'title' => t('Recently created'), + // Exponential decay with half life of 14% of the age range of nodes. +@@ -251,7 +263,7 @@ public function ranking() { + public function userPredelete($account) { + // Delete nodes (current revisions). + // @todo Introduce node_mass_delete() or make node_mass_update() more flexible. +- $nids = \Drupal::entityQuery('node')->condition('uid', $account->id())->accessCheck(FALSE)->execute(); ++ $nids = \Drupal::entityQuery('node')->condition('uid', (int) $account->id())->accessCheck(FALSE)->execute(); + // Delete old revisions. + $storage_controller = \Drupal::entityTypeManager()->getStorage('node'); + $nodes = $storage_controller->loadMultiple($nids); +diff --git a/core/modules/node/src/Hook/NodeHooks.php b/core/modules/node/src/Hook/NodeHooks.php +index 38d3fb4e69f156b586c6998aacc7eddc548c0e0b..b0538896345621c4b0cfd42bd32e32ef124f53eb 100644 +--- a/core/modules/node/src/Hook/NodeHooks.php ++++ b/core/modules/node/src/Hook/NodeHooks.php +@@ -47,7 +47,7 @@ public function userCancelBlockUnpublish($edit, UserInterface $account, $method) + if ($method === 'user_cancel_block_unpublish') { + $nids = $this->nodeStorage->getQuery() + ->accessCheck(FALSE) +- ->condition('uid', $account->id()) ++ ->condition('uid', (int) $account->id()) + ->execute(); + $this->moduleHandler->invoke('node', 'mass_update', [$nids, ['status' => 0], NULL, TRUE]); + } +diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php +index eea6cc10012719a9522f2b6b76965c5cafde5743..cbaeffa5c3cd315ed981b93d533cffb1287d9917 100644 +--- a/core/modules/node/src/NodeGrantDatabaseStorage.php ++++ b/core/modules/node/src/NodeGrantDatabaseStorage.php +@@ -83,18 +83,25 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun + + // Check the database for potential access grants. + $query = $this->database->select('node_access'); +- $query->addExpression('1'); +- // Only interested for granting in the current operation. +- $query->condition('grant_' . $operation, 1, '>='); ++ if ($this->database->driver() == 'mongodb') { ++ $query->fields('node_access', ['nid', 'langcode', 'gid', 'realm']); ++ // Only interested for granting in the current operation. ++ $query->condition('grant_' . $operation, TRUE); ++ } ++ else { ++ $query->addExpressionConstant('1'); ++ // Only interested for granting in the current operation. ++ $query->condition('grant_' . $operation, TRUE, '>='); ++ } + // Check for grants for this node and the correct langcode. New translations + // do not yet have a langcode and must check the fallback node record. + $nids = $query->andConditionGroup() +- ->condition('nid', $node->id()); ++ ->condition('nid', (int) $node->id()); + if (!$node->isNewTranslation()) { + $nids->condition('langcode', $node->language()->getId()); + } + else { +- $nids->condition('fallback', 1); ++ $nids->condition('fallback', TRUE); + } + // If the node is published, also take the default grant into account. The + // default is saved with a node ID of 0. +@@ -127,7 +134,15 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun + return $access_result; + }; + +- if ($query->execute()->fetchField()) { ++ if ($this->database->driver() == 'mongodb') { ++ $count = $query->execute()->fetchAll(); ++ $query_result = count($count); ++ } ++ else { ++ $query_result = $query->execute()->fetchField(); ++ } ++ ++ if ($query_result) { + return $set_cacheability(AccessResult::allowed()); + } + else { +@@ -140,17 +155,32 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun + */ + public function checkAll(AccountInterface $account) { + $query = $this->database->select('node_access'); +- $query->addExpression('COUNT(*)'); +- $query +- ->condition('nid', 0) +- ->condition('grant_view', 1, '>='); ++ if ($this->database->driver() == 'mongodb') { ++ $query->fields('node_access', ['nid', 'langcode', 'gid', 'realm']); ++ $query ++ ->condition('nid', 0) ++ ->condition('grant_view', TRUE); ++ } ++ else { ++ $query->addExpressionCountAll(); ++ $query ++ ->condition('nid', 0) ++ ->condition('grant_view', TRUE, '>='); ++ } + + $grants = $this->buildGrantsQueryCondition(node_access_grants('view', $account)); + + if (count($grants) > 0) { + $query->condition($grants); + } +- return $query->execute()->fetchField(); ++ ++ if ($this->database->driver() == 'mongodb') { ++ $count = $query->execute()->fetchAll(); ++ return count($count); ++ } ++ else { ++ return $query->execute()->fetchField(); ++ } + } + + /** +@@ -174,46 +204,71 @@ public function alterQuery($query, array $tables, $operation, AccountInterface $ + foreach ($tables as $table_alias => $tableinfo) { + $table = $tableinfo['table']; + if (!($table instanceof SelectInterface) && $table == $base_table) { +- // Set the subquery. +- $subquery = $this->database->select('node_access', 'na') +- ->fields('na', ['nid']); ++ if ($this->database->driver() == 'mongodb') { ++ // Attach conditions to the sub-query for nodes. ++ if ($grants_exist) { ++ $query->condition($grant_conditions); ++ } ++ ++ $query->condition('grant_' . $operation, TRUE); ++ ++ if ($is_multilingual) { ++ // If no specific langcode to check for is given, use the grant entry ++ // which is set as a fallback. ++ // If a specific langcode is given, use the grant entry for it. ++ if ($langcode === FALSE) { ++ $query->condition('fallback', TRUE); ++ } ++ else { ++ $query->condition('langcode', $langcode); ++ } ++ } + +- // Attach conditions to the sub-query for nodes. +- if ($grants_exist) { +- $subquery->condition($grant_conditions); ++ $query->addJoin('INNER', 'node_access', 'na', $query->joinCondition()->compare('na.nid', "$base_table.nid")); ++ $query->unwindJoinAndAddFields('na', ['nid', 'langcode', 'fallback', 'gid', 'realm', 'grant_' . $operation]); + } +- $subquery->condition('na.grant_' . $operation, 1, '>='); +- +- // Add langcode-based filtering if this is a multilingual site. +- if ($is_multilingual) { +- // If no specific langcode to check for is given, use the grant entry +- // which is set as a fallback. +- // If a specific langcode is given, use the grant entry for it. +- if ($langcode === FALSE) { +- $subquery->condition('na.fallback', 1, '='); ++ else { ++ // Set the subquery. ++ $subquery = $this->database->select('node_access', 'na') ++ ->fields('na', ['nid']); ++ ++ // Attach conditions to the sub-query for nodes. ++ if ($grants_exist) { ++ $subquery->condition($grant_conditions); + } +- else { +- $subquery->condition('na.langcode', $langcode, '='); ++ $subquery->condition('na.grant_' . $operation, TRUE, '>='); ++ ++ // Add langcode-based filtering if this is a multilingual site. ++ if ($is_multilingual) { ++ // If no specific langcode to check for is given, use the grant entry ++ // which is set as a fallback. ++ // If a specific langcode is given, use the grant entry for it. ++ if ($langcode === FALSE) { ++ $subquery->condition('na.fallback', TRUE); ++ } ++ else { ++ $subquery->condition('na.langcode', $langcode); ++ } + } +- } + +- $field = 'nid'; +- // Now handle entities. +- $subquery->where("[$table_alias].[$field] = [na].[nid]"); ++ $field = 'nid'; ++ // Now handle entities. ++ $subquery->where("[$table_alias].[$field] = [na].[nid]"); + +- if (empty($tableinfo['join type'])) { +- $query->exists($subquery); +- } +- else { +- // If this is a join, add the node access check to the join condition. +- // This requires using $query->getTables() to alter the table +- // information. +- $join_cond = $query +- ->andConditionGroup() +- ->exists($subquery); +- $join_cond->where($tableinfo['condition'], $query->getTables()[$table_alias]['arguments']); +- $query->getTables()[$table_alias]['arguments'] = []; +- $query->getTables()[$table_alias]['condition'] = $join_cond; ++ if (empty($tableinfo['join type'])) { ++ $query->exists($subquery); ++ } ++ else { ++ // If this is a join, add the node access check to the join condition. ++ // This requires using $query->getTables() to alter the table ++ // information. ++ $join_cond = $query ++ ->andConditionGroup() ++ ->exists($subquery); ++ $join_cond->where($tableinfo['condition'], $query->getTables()[$table_alias]['arguments']); ++ $query->getTables()[$table_alias]['arguments'] = []; ++ $query->getTables()[$table_alias]['condition'] = $join_cond; ++ } + } + } + } +@@ -224,7 +279,7 @@ public function alterQuery($query, array $tables, $operation, AccountInterface $ + */ + public function write(NodeInterface $node, array $grants, $realm = NULL, $delete = TRUE) { + if ($delete) { +- $query = $this->database->delete('node_access')->condition('nid', $node->id()); ++ $query = $this->database->delete('node_access')->condition('nid', (int) $node->id()); + if ($realm) { + $query->condition('realm', [$realm, 'all'], 'IN'); + } +@@ -293,13 +348,25 @@ public function writeDefault() { + * {@inheritdoc} + */ + public function count() { +- return $this->database->query('SELECT COUNT(*) FROM {node_access}')->fetchField(); ++ if ($this->database->driver() == 'mongodb') { ++ $prefixed_table = $this->database->getPrefix() . 'node_access'; ++ ++ return (string) $this->database->getConnection()->selectCollection($prefixed_table)->count([], ['session' => $this->database->getMongodbSession()]); ++ } ++ else { ++ return $this->database->query('SELECT COUNT(*) FROM {node_access}')->fetchField(); ++ } + } + + /** + * {@inheritdoc} + */ + public function deleteNodeRecords(array $nids) { ++ // Make sure that all $nids have an integer value. ++ foreach ($nids as &$nid) { ++ $nid = (int) $nid; ++ } ++ + $this->database->delete('node_access') + ->condition('nid', $nids, 'IN') + ->execute(); +@@ -320,6 +387,9 @@ protected function buildGrantsQueryCondition(array $node_access_grants) { + $grants = $this->database->condition('OR'); + foreach ($node_access_grants as $realm => $gids) { + if (!empty($gids)) { ++ foreach ($gids as &$gid) { ++ $gid = (int) $gid; ++ } + $and = $this->database->condition('AND'); + $grants->condition($and + ->condition('gid', $gids, 'IN') +diff --git a/core/modules/node/src/NodeViewsData.php b/core/modules/node/src/NodeViewsData.php +index 2f3a837277506eb538ed02c87b127a55cfab2bb7..a04bae8a59ac9f59f0af440942e151ec2cf027ae 100644 +--- a/core/modules/node/src/NodeViewsData.php ++++ b/core/modules/node/src/NodeViewsData.php +@@ -15,28 +15,37 @@ class NodeViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + +- $data['node_field_data']['table']['base']['weight'] = -10; +- $data['node_field_data']['table']['base']['access query tag'] = 'node_access'; +- $data['node_field_data']['table']['wizard_id'] = 'node'; ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'node'; ++ $revision_table = 'node'; ++ } ++ else { ++ $data_table = 'node_field_data'; ++ $revision_table = 'node_field_revision'; ++ } ++ ++ $data[$data_table]['table']['base']['weight'] = -10; ++ $data[$data_table]['table']['base']['access query tag'] = 'node_access'; ++ $data[$data_table]['table']['wizard_id'] = 'node'; + +- $data['node_field_data']['nid']['argument'] = [ ++ $data[$data_table]['nid']['argument'] = [ + 'id' => 'node_nid', + 'name field' => 'title', + 'numeric' => TRUE, + 'validate type' => 'nid', + ]; + +- $data['node_field_data']['title']['field']['default_formatter_settings'] = ['link_to_entity' => TRUE]; +- $data['node_field_data']['title']['field']['link_to_node default'] = TRUE; ++ $data[$data_table]['title']['field']['default_formatter_settings'] = ['link_to_entity' => TRUE]; ++ $data[$data_table]['title']['field']['link_to_node default'] = TRUE; + +- $data['node_field_data']['type']['argument']['id'] = 'node_type'; ++ $data[$data_table]['type']['argument']['id'] = 'node_type'; + +- $data['node_field_data']['status']['filter']['label'] = $this->t('Published status'); +- $data['node_field_data']['status']['filter']['type'] = 'yes-no'; ++ $data[$data_table]['status']['filter']['label'] = $this->t('Published status'); ++ $data[$data_table]['status']['filter']['type'] = 'yes-no'; + // Use status = 1 instead of status <> 0 in WHERE statement. +- $data['node_field_data']['status']['filter']['use_equal'] = TRUE; ++ $data[$data_table]['status']['filter']['use_equal'] = TRUE; + +- $data['node_field_data']['status_extra'] = [ ++ $data[$data_table]['status_extra'] = [ + 'title' => $this->t('Published status or admin user'), + 'help' => $this->t('Filters out unpublished content if the current user cannot view it.'), + 'filter' => [ +@@ -46,14 +55,14 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.'); +- $data['node_field_data']['promote']['filter']['label'] = $this->t('Promoted to front page status'); +- $data['node_field_data']['promote']['filter']['type'] = 'yes-no'; ++ $data[$data_table]['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.'); ++ $data[$data_table]['promote']['filter']['label'] = $this->t('Promoted to front page status'); ++ $data[$data_table]['promote']['filter']['type'] = 'yes-no'; + +- $data['node_field_data']['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.'); +- $data['node_field_data']['sticky']['filter']['label'] = $this->t('Sticky status'); +- $data['node_field_data']['sticky']['filter']['type'] = 'yes-no'; +- $data['node_field_data']['sticky']['sort']['help'] = $this->t('Whether or not the content is sticky. To list sticky content first, set this to descending.'); ++ $data[$data_table]['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.'); ++ $data[$data_table]['sticky']['filter']['label'] = $this->t('Sticky status'); ++ $data[$data_table]['sticky']['filter']['type'] = 'yes-no'; ++ $data[$data_table]['sticky']['sort']['help'] = $this->t('Whether or not the content is sticky. To list sticky content first, set this to descending.'); + + $data['node']['node_bulk_form'] = [ + 'title' => $this->t('Node operations bulk form'), +@@ -67,7 +76,7 @@ public function getViewsData() { + + // @todo Add similar support to any date field + // @see https://www.drupal.org/node/2337507 +- $data['node_field_data']['created_fulldate'] = [ ++ $data[$data_table]['created_fulldate'] = [ + 'title' => $this->t('Created date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -76,7 +85,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['created_year_month'] = [ ++ $data[$data_table]['created_year_month'] = [ + 'title' => $this->t('Created year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -85,7 +94,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['created_year'] = [ ++ $data[$data_table]['created_year'] = [ + 'title' => $this->t('Created year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -94,7 +103,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['created_month'] = [ ++ $data[$data_table]['created_month'] = [ + 'title' => $this->t('Created month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -103,7 +112,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['created_day'] = [ ++ $data[$data_table]['created_day'] = [ + 'title' => $this->t('Created day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -112,7 +121,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['created_week'] = [ ++ $data[$data_table]['created_week'] = [ + 'title' => $this->t('Created week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -121,7 +130,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_fulldate'] = [ ++ $data[$data_table]['changed_fulldate'] = [ + 'title' => $this->t('Updated date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -130,7 +139,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_year_month'] = [ ++ $data[$data_table]['changed_year_month'] = [ + 'title' => $this->t('Updated year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -139,7 +148,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_year'] = [ ++ $data[$data_table]['changed_year'] = [ + 'title' => $this->t('Updated year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -148,7 +157,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_month'] = [ ++ $data[$data_table]['changed_month'] = [ + 'title' => $this->t('Updated month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -157,7 +166,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_day'] = [ ++ $data[$data_table]['changed_day'] = [ + 'title' => $this->t('Updated day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -166,7 +175,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['changed_week'] = [ ++ $data[$data_table]['changed_week'] = [ + 'title' => $this->t('Updated week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -183,47 +192,65 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_data']['uid_revision']['title'] = $this->t('User has a revision'); +- $data['node_field_data']['uid_revision']['help'] = $this->t('All nodes where a certain user has a revision'); +- $data['node_field_data']['uid_revision']['real field'] = 'nid'; +- $data['node_field_data']['uid_revision']['filter']['id'] = 'node_uid_revision'; +- $data['node_field_data']['uid_revision']['argument']['id'] = 'node_uid_revision'; ++ if ($this->connection->driver() == 'mongodb') { ++ // @todo Find out if this is still needed. ++ $data['node']['uid']['help'] = t('The user authoring the content. If you need more fields than the uid add the content: author relationship'); ++ $data['node']['uid']['filter']['id'] = 'user_name'; ++ $data['node']['uid']['relationship']['title'] = t('Content author'); ++ $data['node']['uid']['relationship']['help'] = t('Relate content to the user who created it.'); ++ $data['node']['uid']['relationship']['label'] = t('author'); ++ $data['node']['uid']['relationship']['base'] = 'users'; ++ } + +- $data['node_field_revision']['table']['wizard_id'] = 'node_revision'; ++ $data[$data_table]['uid_revision']['title'] = $this->t('User has a revision'); ++ $data[$data_table]['uid_revision']['help'] = $this->t('All nodes where a certain user has a revision'); ++ $data[$data_table]['uid_revision']['real field'] = 'nid'; ++ $data[$data_table]['uid_revision']['filter']['id'] = 'node_uid_revision'; ++ $data[$data_table]['uid_revision']['argument']['id'] = 'node_uid_revision'; ++ ++ if ($this->connection->driver() == 'mongodb') { ++ // @todo Find out if this is still needed. ++ $data['node']['revision_uid']['help'] = t('The user who created the revision.'); ++ $data['node']['revision_uid']['relationship']['label'] = t('revision user'); ++ $data['node']['revision_uid']['filter']['id'] = 'user_name'; ++ } ++ else { ++ $data['node_field_revision']['table']['wizard_id'] = 'node_revision'; + +- // Advertise this table as a possible base table. +- $data['node_field_revision']['table']['base']['help'] = $this->t('Content revision is a history of changes to content.'); +- $data['node_field_revision']['table']['base']['defaults']['title'] = 'title'; ++ // Advertise this table as a possible base table. ++ $data['node_field_revision']['table']['base']['help'] = $this->t('Content revision is a history of changes to content.'); ++ $data['node_field_revision']['table']['base']['defaults']['title'] = 'title'; + +- $data['node_field_revision']['nid']['argument'] = [ +- 'id' => 'node_nid', +- 'numeric' => TRUE, +- ]; +- // @todo the NID field needs different behavior on revision/non-revision +- // tables. It would be neat if this could be encoded in the base field +- // definition. +- $data['node_field_revision']['vid'] = [ +- 'argument' => [ +- 'id' => 'node_vid', ++ $data['node_field_revision']['nid']['argument'] = [ ++ 'id' => 'node_nid', + 'numeric' => TRUE, +- ], +- ] + $data['node_field_revision']['vid']; ++ ]; ++ // @todo the NID field needs different behavior on revision/non-revision ++ // tables. It would be neat if this could be encoded in the base field ++ // definition. ++ $data['node_field_revision']['vid'] = [ ++ 'argument' => [ ++ 'id' => 'node_vid', ++ 'numeric' => TRUE, ++ ], ++ ] + $data['node_field_revision']['vid']; + +- $data['node_field_revision']['langcode']['help'] = $this->t('The language the original content is in.'); ++ $data['node_field_revision']['langcode']['help'] = $this->t('The language the original content is in.'); + +- $data['node_field_revision']['table']['wizard_id'] = 'node_field_revision'; ++ $data['node_field_revision']['table']['wizard_id'] = 'node_field_revision'; + +- $data['node_field_revision']['status']['filter']['label'] = $this->t('Published'); +- $data['node_field_revision']['status']['filter']['type'] = 'yes-no'; +- $data['node_field_revision']['status']['filter']['use_equal'] = TRUE; ++ $data['node_field_revision']['status']['filter']['label'] = $this->t('Published'); ++ $data['node_field_revision']['status']['filter']['type'] = 'yes-no'; ++ $data['node_field_revision']['status']['filter']['use_equal'] = TRUE; + +- $data['node_field_revision']['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.'); ++ $data['node_field_revision']['promote']['help'] = $this->t('A boolean indicating whether the node is visible on the front page.'); + +- $data['node_field_revision']['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.'); ++ $data['node_field_revision']['sticky']['help'] = $this->t('A boolean indicating whether the node should sort to the top of content lists.'); + +- $data['node_field_revision']['langcode']['help'] = $this->t('The language of the content or translation.'); ++ $data['node_field_revision']['langcode']['help'] = $this->t('The language of the content or translation.'); ++ } + +- $data['node_field_revision']['link_to_revision'] = [ ++ $data[$revision_table]['link_to_revision'] = [ + 'field' => [ + 'title' => $this->t('Link to revision'), + 'help' => $this->t('Provide a simple link to the revision.'), +@@ -232,7 +259,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_revision']['revert_revision'] = [ ++ $data[$revision_table]['revert_revision'] = [ + 'field' => [ + 'title' => $this->t('Link to revert revision'), + 'help' => $this->t('Provide a simple link to revert to the revision.'), +@@ -241,7 +268,7 @@ public function getViewsData() { + ], + ]; + +- $data['node_field_revision']['delete_revision'] = [ ++ $data[$revision_table]['delete_revision'] = [ + 'field' => [ + 'title' => $this->t('Link to delete revision'), + 'help' => $this->t('Provide a simple link to delete the content revision.'), +@@ -256,7 +283,7 @@ public function getViewsData() { + + // For other base tables, explain how we join. + $data['node_access']['table']['join'] = [ +- 'node_field_data' => [ ++ $data_table => [ + 'left_field' => 'nid', + 'field' => 'nid', + ], +@@ -289,11 +316,21 @@ public function getViewsData() { + // Use a Views table alias to allow other modules to use this table too, + // if they use the search index. + $data['node_search_index']['table']['join'] = [ +- 'node_field_data' => [ ++ $data_table => [ + 'left_field' => 'nid', + 'field' => 'sid', + 'table' => 'search_index', +- 'extra' => "node_search_index.type = 'node_search' AND node_search_index.langcode = node_field_data.langcode", ++ 'extra' => [ ++ [ ++ 'field' => 'type', ++ 'value' => 'node_search', ++ 'operator' => '=', ++ ], ++ [ ++ 'field' => 'langcode', ++ 'field2' => ($this->connection->driver() == 'mongodb' ? 'node_current_revision.langcode' : 'langcode'), ++ ], ++ ], + ], + ]; + +@@ -305,12 +342,23 @@ public function getViewsData() { + ]; + + $data['node_search_dataset']['table']['join'] = [ +- 'node_field_data' => [ ++ $data_table => [ + 'left_field' => 'sid', + 'left_table' => 'node_search_index', + 'field' => 'sid', + 'table' => 'search_dataset', +- 'extra' => 'node_search_index.type = node_search_dataset.type AND node_search_index.langcode = node_search_dataset.langcode', ++ 'extra' => [ ++ [ ++ 'field' => 'type', ++ 'field2' => 'type', ++ 'operator' => '=', ++ ], ++ [ ++ 'field' => 'langcode', ++ 'field2' => 'langcode', ++ 'operator' => '=', ++ ], ++ ], + 'type' => 'INNER', + ], + ]; +diff --git a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php +index a89e94f92e3e5b03224145134dc0c9a451756ed3..0af19721efbcd331d257d3b0f7bfd3e7b975dc2f 100644 +--- a/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php ++++ b/core/modules/node/src/Plugin/EntityReferenceSelection/NodeSelection.php +@@ -30,7 +30,7 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') + // modules in use on the site. As long as one access control module is there, + // it is supposed to handle this check. + if (!$this->currentUser->hasPermission('bypass node access') && !$this->moduleHandler->hasImplementations('node_grants')) { +- $query->condition('status', NodeInterface::PUBLISHED); ++ $query->condition('status', (bool) NodeInterface::PUBLISHED); + } + return $query; + } +diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php +index c7019932bd3f8527c63a086b2739b213df898a4f..9899462b5c240fb041cfa3c7aa107007cb547663 100644 +--- a/core/modules/node/src/Plugin/Search/NodeSearch.php ++++ b/core/modules/node/src/Plugin/Search/NodeSearch.php +@@ -265,7 +265,11 @@ protected function findResults() { + ->select('search_index', 'i') + ->extend(SearchQuery::class) + ->extend(PagerSelectExtender::class); +- $query->join('node_field_data', 'n', '[n].[nid] = [i].[sid] AND [n].[langcode] = [i].[langcode]'); ++ $query->join('node_field_data', 'n', ++ $query->joinCondition() ++ ->compare('n.nid', 'i.sid') ++ ->compare('n.langcode', 'i.langcode') ++ ); + $query->condition('n.status', 1) + ->addTag('node_access') + ->searchExpression($keys, $this->getPluginId()); +@@ -302,7 +306,7 @@ protected function findResults() { + } + $query->condition($where); + if (!empty($info['join'])) { +- $query->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']); ++ $query->join($info['join']['table'], $info['join']['alias'], $query->joinCondition()->where($info['join']['condition'])); + } + } + } +@@ -445,7 +449,7 @@ protected function addNodeRankings(SelectExtender $query) { + $node_rank = $this->configuration['rankings'][$rank]; + // If the table defined in the ranking isn't already joined, then add it. + if (isset($values['join']) && !isset($tables[$values['join']['alias']])) { +- $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']); ++ $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $query->joinCondition()->where($values['join']['on'])); + } + $arguments = $values['arguments'] ?? []; + $query->addScore($values['score'], $arguments, $node_rank); +@@ -464,9 +468,13 @@ public function updateIndex() { + + $query = $this->databaseReplica->select('node', 'n'); + $query->addField('n', 'nid'); +- $query->leftJoin('search_dataset', 'sd', '[sd].[sid] = [n].[nid] AND [sd].[type] = :type', [':type' => $this->getPluginId()]); ++ $query->leftJoin('search_dataset', 'sd', ++ $query->joinCondition() ++ ->compare('sd.sid', 'n.nid') ++ ->condition('sd.type', $this->getPluginId()) ++ ); + $query->addExpression('CASE MAX([sd].[reindex]) WHEN NULL THEN 0 ELSE 1 END', 'ex'); +- $query->addExpression('MAX([sd].[reindex])', 'ex2'); ++ $query->addExpressionMax('sd.reindex', 'ex2'); + $query->condition( + $query->orConditionGroup() + ->where('[sd].[sid] IS NULL') +diff --git a/core/modules/node/src/Plugin/views/wizard/Node.php b/core/modules/node/src/Plugin/views/wizard/Node.php +index b29396fcbba4b1cf91cf751b072cc56bc0c09d43..7c0dd79014b4d9fff534f31d7ded424c6da817db 100644 +--- a/core/modules/node/src/Plugin/views/wizard/Node.php ++++ b/core/modules/node/src/Plugin/views/wizard/Node.php +@@ -2,6 +2,7 @@ + + namespace Drupal\node\Plugin\views\wizard; + ++use Drupal\Core\Database\Connection; + use Drupal\Core\Entity\EntityDisplayRepositoryInterface; + use Drupal\Core\Entity\EntityFieldManagerInterface; + use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +@@ -65,12 +66,19 @@ class Node extends WizardPluginBase { + * The entity field manager. + * @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $parent_form_selector + * The parent form selector service. ++ * @param \Drupal\Core\Database\Connection $connection ++ * The database connection. + */ +- public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, EntityDisplayRepositoryInterface $entity_display_repository, EntityFieldManagerInterface $entity_field_manager, MenuParentFormSelectorInterface $parent_form_selector) { +- parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector); ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, EntityDisplayRepositoryInterface $entity_display_repository, EntityFieldManagerInterface $entity_field_manager, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { ++ parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector, $connection); + + $this->entityDisplayRepository = $entity_display_repository; + $this->entityFieldManager = $entity_field_manager; ++ ++ if ($connection->driver() == 'mongodb') { ++ $this->base_table = 'node'; ++ $this->createdColumn = 'node-created'; ++ } + } + + /** +@@ -84,7 +92,8 @@ public static function create(ContainerInterface $container, array $configuratio + $container->get('entity_type.bundle.info'), + $container->get('entity_display.repository'), + $container->get('entity_field.manager'), +- $container->get('menu.parent_form_selector') ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') + ); + } + +@@ -97,9 +106,16 @@ public static function create(ContainerInterface $container, array $configuratio + */ + public function getAvailableSorts() { + // You can't execute functions in properties, so override the method +- return [ +- 'node_field_data-title:ASC' => $this->t('Title'), +- ]; ++ if ($this->connection->driver() == 'mongodb') { ++ return [ ++ 'node-title:ASC' => $this->t('Title'), ++ ]; ++ } ++ else { ++ return [ ++ 'node_field_data-title:ASC' => $this->t('Title'), ++ ]; ++ } + } + + /** +@@ -132,7 +148,12 @@ protected function defaultDisplayOptions() { + // to a row style that uses fields. + /* Field: Content: Title */ + $display_options['fields']['title']['id'] = 'title'; +- $display_options['fields']['title']['table'] = 'node_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['title']['table'] = 'node'; ++ } ++ else { ++ $display_options['fields']['title']['table'] = 'node_field_data'; ++ } + $display_options['fields']['title']['field'] = 'title'; + $display_options['fields']['title']['entity_type'] = 'node'; + $display_options['fields']['title']['entity_field'] = 'title'; +@@ -229,7 +250,12 @@ protected function display_options_row(&$display_options, $row_plugin, $row_opti + case 'titles': + $display_options['row']['type'] = 'fields'; + $display_options['fields']['title']['id'] = 'title'; +- $display_options['fields']['title']['table'] = 'node_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['title']['table'] = 'node'; ++ } ++ else { ++ $display_options['fields']['title']['table'] = 'node_field_data'; ++ } + $display_options['fields']['title']['field'] = 'title'; + $display_options['fields']['title']['settings']['link_to_entity'] = $row_plugin === 'titles_linked'; + $display_options['fields']['title']['plugin_id'] = 'field'; +diff --git a/core/modules/path_alias/src/AliasRepository.php b/core/modules/path_alias/src/AliasRepository.php +index 21eb3daef0d65bc2a85a14fd9285cea81c827c29..01fd8390cecd82e48f8241bcb59e403fb0cc4433 100644 +--- a/core/modules/path_alias/src/AliasRepository.php ++++ b/core/modules/path_alias/src/AliasRepository.php +@@ -99,7 +99,7 @@ public function lookupByAlias($alias, $langcode) { + */ + public function pathHasMatchingAlias($initial_substring) { + $query = $this->getBaseQuery(); +- $query->addExpression(1); ++ $query->addExpressionConstant(1); + + return (bool) $query + ->condition('base_table.path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE') +@@ -116,7 +116,7 @@ public function pathHasMatchingAlias($initial_substring) { + */ + protected function getBaseQuery() { + $query = $this->connection->select('path_alias', 'base_table'); +- $query->condition('base_table.status', 1); ++ $query->condition('base_table.status', TRUE); + + return $query; + } +diff --git a/core/modules/path_alias/src/PathAliasStorageSchema.php b/core/modules/path_alias/src/PathAliasStorageSchema.php +index d2bc87de1672251354dd523295ebe34f9e1d5de9..7b6ea485c8e5ff8173e461c5acb9ce13eb5ca258 100644 +--- a/core/modules/path_alias/src/PathAliasStorageSchema.php ++++ b/core/modules/path_alias/src/PathAliasStorageSchema.php +@@ -22,10 +22,12 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res + 'path_alias__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'], + 'path_alias__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'], + ]; +- $schema[$revision_table]['indexes'] += [ +- 'path_alias_revision__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'], +- 'path_alias_revision__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'], +- ]; ++ if ($revision_table) { ++ $schema[$revision_table]['indexes'] += [ ++ 'path_alias_revision__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'], ++ 'path_alias_revision__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'], ++ ]; ++ } + + // Unset the path_alias__status index as it is slower than the above + // indexes and MySQL 5.7 chooses to use it even though it is suboptimal. +diff --git a/core/modules/search/src/SearchIndex.php b/core/modules/search/src/SearchIndex.php +index 1a0bdbdd14772237b7d539d1cd0ac8cbe71aa238..2b8434e0117a6bf12192e407c8c2659dc1f0b62d 100644 +--- a/core/modules/search/src/SearchIndex.php ++++ b/core/modules/search/src/SearchIndex.php +@@ -204,8 +204,8 @@ public function clear($type = NULL, $sid = NULL, $langcode = NULL) { + $query_index->condition('type', $type); + $query_dataset->condition('type', $type); + if ($sid) { +- $query_index->condition('sid', $sid); +- $query_dataset->condition('sid', $sid); ++ $query_index->condition('sid', (int) $sid); ++ $query_dataset->condition('sid', (int) $sid); + if ($langcode) { + $query_index->condition('langcode', $langcode); + $query_dataset->condition('langcode', $langcode); +@@ -242,7 +242,7 @@ public function markForReindex($type = NULL, $sid = NULL, $langcode = NULL) { + if ($type) { + $query->condition('type', $type); + if ($sid) { +- $query->condition('sid', $sid); ++ $query->condition('sid', (int) $sid); + if ($langcode) { + $query->condition('langcode', $langcode); + } +diff --git a/core/modules/search/src/SearchQuery.php b/core/modules/search/src/SearchQuery.php +index 78c0f9461d84c85da55f8e1824d10ece3566b3df..68c23dfe2889317507b0883d031276afbe9bad18 100644 +--- a/core/modules/search/src/SearchQuery.php ++++ b/core/modules/search/src/SearchQuery.php +@@ -410,7 +410,7 @@ public function prepareAndNormalize() { + $this->condition($or); + + // Add keyword normalization information to the query. +- $this->join('search_total', 't', '[i].[word] = [t].[word]'); ++ $this->join('search_total', 't', $this->joinCondition()->compare('i.word', 't.word')); + $this + ->condition('i.type', $this->type) + ->groupBy('i.type') +@@ -430,7 +430,12 @@ public function prepareAndNormalize() { + // For complex search queries, add the LIKE conditions; if the query is + // simple, we do not need them for normalization. + if (!$this->simple) { +- $normalize_query->join('search_dataset', 'd', '[i].[sid] = [d].[sid] AND [i].[type] = [d].[type] AND [i].[langcode] = [d].[langcode]'); ++ $normalize_query->join('search_dataset', 'd', ++ $normalize_query->joinCondition() ++ ->compare('i.sid', 'd.sid') ++ ->compare('i.type', 'd.type') ++ ->compare('i.langcode', 'd.langcode') ++ ); + if (count($this->conditions)) { + $normalize_query->condition($this->conditions); + } +@@ -552,7 +557,12 @@ public function execute() { + } + + // Add conditions to the query. +- $this->join('search_dataset', 'd', '[i].[sid] = [d].[sid] AND [i].[type] = [d].[type] AND [i].[langcode] = [d].[langcode]'); ++ $this->join('search_dataset', 'd', ++ $this->joinCondition() ++ ->compare('i.sid', 'd.sid') ++ ->compare('i.type', 'd.type') ++ ->compare('i.langcode', 'd.langcode') ++ ); + if (count($this->conditions)) { + $this->condition($this->conditions); + } +@@ -610,7 +620,11 @@ public function countQuery() { + $inner = clone $this->query; + + // Add conditions to query. +- $inner->join('search_dataset', 'd', '[i].[sid] = [d].[sid] AND [i].[type] = [d].[type]'); ++ $inner->join('search_dataset', 'd', ++ $inner->joinCondition() ++ ->compare('i.sid', 'd.sid') ++ ->compare('i.type', 'd.type') ++ ); + if (count($this->conditions)) { + $inner->condition($this->conditions); + } +@@ -626,7 +640,7 @@ public function countQuery() { + $count = $this->connection->select($inner->fields('i', ['sid']), NULL); + + // Add the COUNT() expression. +- $count->addExpression('COUNT(*)'); ++ $count->addExpressionCountAll(); + + return $count; + } +diff --git a/core/modules/shortcut/src/ShortcutSetStorage.php b/core/modules/shortcut/src/ShortcutSetStorage.php +index c2bccb9387c72c0d3707147453ca96d7cf58f23b..fcb26b4e8de9b9239e29ba2ae0eb5bd49023f644 100644 +--- a/core/modules/shortcut/src/ShortcutSetStorage.php ++++ b/core/modules/shortcut/src/ShortcutSetStorage.php +@@ -91,7 +91,7 @@ public function deleteAssignedShortcutSets(ShortcutSetInterface $entity) { + public function assignUser(ShortcutSetInterface $shortcut_set, $account) { + $current_shortcut_set = $this->getDisplayedToUser($account); + $this->connection->merge('shortcut_set_users') +- ->key('uid', $account->id()) ++ ->key('uid', (int) $account->id()) + ->fields(['set_name' => $shortcut_set->id()]) + ->execute(); + if ($current_shortcut_set instanceof ShortcutSetInterface) { +@@ -105,7 +105,7 @@ public function assignUser(ShortcutSetInterface $shortcut_set, $account) { + public function unassignUser($account) { + $current_shortcut_set = $this->getDisplayedToUser($account); + $deleted = $this->connection->delete('shortcut_set_users') +- ->condition('uid', $account->id()) ++ ->condition('uid', (int) $account->id()) + ->execute(); + if ($current_shortcut_set instanceof ShortcutSetInterface) { + Cache::invalidateTags($current_shortcut_set->getCacheTagsToInvalidate()); +@@ -119,7 +119,7 @@ public function unassignUser($account) { + public function getAssignedToUser($account) { + $query = $this->connection->select('shortcut_set_users', 'ssu'); + $query->fields('ssu', ['set_name']); +- $query->condition('ssu.uid', $account->id()); ++ $query->condition('ssu.uid', (int) $account->id()); + return $query->execute()->fetchField(); + } + +diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php +index bd35a7af852e8afdc2f33f9b27ad52aa75dec706..b2c9bcbdbb6612ed930fe69473f3d4fa5083f958 100644 +--- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php ++++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php +@@ -424,8 +424,8 @@ public function getFullQualifiedTableName($table) { + /** + * {@inheritdoc} + */ +- public static function createConnectionOptionsFromUrl($url, $root) { +- $database = parent::createConnectionOptionsFromUrl($url, $root); ++ public static function createConnectionOptionsFromUrl($url, $root, $hosts = '') { ++ $database = parent::createConnectionOptionsFromUrl($url, $root, $hosts); + + // A SQLite database path with two leading slashes indicates a system path. + // Otherwise the path is relative to the Drupal root. +diff --git a/core/modules/system/src/Plugin/migrate/source/d7/MenuTranslation.php b/core/modules/system/src/Plugin/migrate/source/d7/MenuTranslation.php +index 9553e7ec1ce3a425b4c56638582036522174b014..863a8bb3ad22f366ea82450ba990d1786137c076 100644 +--- a/core/modules/system/src/Plugin/migrate/source/d7/MenuTranslation.php ++++ b/core/modules/system/src/Plugin/migrate/source/d7/MenuTranslation.php +@@ -49,8 +49,8 @@ public function query() { + ->isNotNull('lt.lid'); + + $query->addField('m', 'language', 'm_language'); +- $query->leftJoin('i18n_string', 'i18n', '[i18n].[objectid] = [m].[menu_name]'); +- $query->leftJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin('i18n_string', 'i18n', $query->joinCondition()->compare('i18n.objectid', 'm.menu_name')); ++ $query->leftJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + + return $query; + } +diff --git a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyHooks.php +index 00477f824d9a5bba36b84e40c7c774702ae86b93..596b029028c9fa4146b8af4801daf6537eeb6e8b 100644 +--- a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php ++++ b/core/modules/taxonomy/src/Hook/TaxonomyHooks.php +@@ -172,7 +172,7 @@ public function nodePredelete(EntityInterface $node) { + public function taxonomyTermDelete(Term $term) { + if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { + // Clean up the {taxonomy_index} table when terms are deleted. +- \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute(); ++ \Drupal::database()->delete('taxonomy_index')->condition('tid', (int) $term->id())->execute(); + } + } + +diff --git a/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php +index 07f5e6b9f9c6349b632a50e74e26da8afb95c41b..7b1fe2b8802c17d3c8d55abb3d8f5a6b43ad1f27 100644 +--- a/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php ++++ b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php +@@ -124,7 +124,7 @@ public function tokens($type, $tokens, array $data, array $options, BubbleableMe + + case 'node-count': + $query = \Drupal::database()->select('taxonomy_index'); +- $query->condition('tid', $term->id()); ++ $query->condition('tid', (int) $term->id()); + $query->addTag('term_node_count'); + $count = $query->countQuery()->execute()->fetchField(); + $replacements[$original] = $count; +diff --git a/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php +index bdc7b16a5ef196a0cf879463a583c9d2acd01b1a..7959e1450fcc78593435d6f4de175d96e6fc9fe3 100644 +--- a/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php ++++ b/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php +@@ -15,13 +15,22 @@ class TaxonomyViewsHooks { + */ + #[Hook('views_data_alter')] + public function viewsDataAlter(&$data): void { +- $data['node_field_data']['term_node_tid'] = [ ++ if (\Drupal::database()->driver() == 'mongodb') { ++ $node_table = 'node'; ++ $taxonomy_term_table = 'taxonomy_term_data'; ++ } ++ else { ++ $node_table = 'node_field_data'; ++ $taxonomy_term_table = 'taxonomy_term_field_data'; ++ } ++ ++ $data[$node_table]['term_node_tid'] = [ + 'title' => t('Taxonomy terms on node'), + 'help' => t('Relate nodes to taxonomy terms, specifying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'), + 'relationship' => [ + 'id' => 'node_term_data', + 'label' => t('term'), +- 'base' => 'taxonomy_term_field_data', ++ 'base' => $taxonomy_term_table, + ], + 'field' => [ + 'title' => t('All taxonomy terms'), +@@ -31,7 +40,7 @@ public function viewsDataAlter(&$data): void { + 'click sortable' => FALSE, + ], + ]; +- $data['node_field_data']['term_node_tid_depth'] = [ ++ $data[$node_table]['term_node_tid_depth'] = [ + 'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'), + 'real field' => 'nid', + 'argument' => [ +@@ -44,7 +53,7 @@ public function viewsDataAlter(&$data): void { + 'id' => 'taxonomy_index_tid_depth', + ], + ]; +- $data['node_field_data']['term_node_tid_depth_modifier'] = [ ++ $data[$node_table]['term_node_tid_depth_modifier'] = [ + 'title' => t('Has taxonomy term ID depth modifier'), + 'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'), + 'argument' => [ +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php +index 63dfc9fa2ae0ee83fa597e49f76011766c06a9c4..22d1baa2b894e29bf462104a58033fb30ef7daa0 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d6/TermLocalizedTranslation.php +@@ -39,13 +39,13 @@ public function query() { + + // Add in the property, which is either name or description. + // Cast td.tid as char for PostgreSQL compatibility. +- $query->leftJoin('i18n_strings', 'i18n', 'CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]'); ++ $query->leftJoin('i18n_strings', 'i18n', $query->joinCondition()->where('CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]')); + $query->condition('i18n.type', 'term'); + $query->addField('i18n', 'lid'); + $query->addField('i18n', 'property'); + + // Add in the translation for the property. +- $query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->addField('lt', 'language', 'lt.language'); + $query->addField('lt', 'translation'); + return $query; +@@ -76,7 +76,7 @@ public function prepareRow(Row $row) { + ->condition('i18n.type', 'term') + ->condition('i18n.property', $other_property) + ->condition('i18n.objectid', $tid); +- $query->leftJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->leftJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->condition('lt.language', $language); + $query->addField('lt', 'translation'); + $results = $query->execute()->fetchAssoc(); +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyPerType.php b/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyPerType.php +index 667d9f3cbab512c92edd675a0eeb81b38a943dc2..4176c4a3fd1c9bd543313021c0e44598d70c9046 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyPerType.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyPerType.php +@@ -26,7 +26,7 @@ class VocabularyPerType extends Vocabulary { + */ + public function query() { + $query = parent::query(); +- $query->join('vocabulary_node_types', 'nt', '[v].[vid] = [nt].[vid]'); ++ $query->join('vocabulary_node_types', 'nt', $query->joinCondition()->compare('v.vid', 'nt.vid')); + $query->fields('nt', ['type']); + return $query; + } +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyTranslation.php +index daf0731fe8a56f3839fcd5698c84d7da9020f826..c5580220f0f469074758737ac48311e3e199e033 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyTranslation.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d6/VocabularyTranslation.php +@@ -36,8 +36,8 @@ public function query() { + // and objectindex. The objectid column is a text field. Therefore, for the + // join to work in PostgreSQL, use the objectindex field as this is numeric + // like the vid field. +- $query->join('i18n_strings', 'i18n', '[v].[vid] = [i18n].[objectindex]'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->join('i18n_strings', 'i18n', $query->joinCondition()->compare('v.vid', 'i18n.objectindex')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + + return $query; + } +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermEntityTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermEntityTranslation.php +index 88cb561b8537249c9cd8a0e8cbd19674f7ffb294..96803dc1cba91161e750c04e01c54b5a63b3f753 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermEntityTranslation.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermEntityTranslation.php +@@ -62,8 +62,8 @@ public function query() { + ->condition('et.entity_type', 'taxonomy_term') + ->condition('et.source', '', '<>'); + +- $query->innerJoin('taxonomy_term_data', 'td', '[td].[tid] = [et].[entity_id]'); +- $query->innerJoin('taxonomy_vocabulary', 'tv', '[td].[vid] = [tv].[vid]'); ++ $query->innerJoin('taxonomy_term_data', 'td', $query->joinCondition()->compare('td.tid', 'et.entity_id')); ++ $query->innerJoin('taxonomy_vocabulary', 'tv', $query->joinCondition()->compare('td.vid', 'tv.vid')); + + if (isset($this->configuration['bundle'])) { + $query->condition('tv.machine_name', (array) $this->configuration['bundle'], 'IN'); +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php +index 5ca0dfe7d503c3a6ec90d09426f3361ddcdb194b..cb7781aa9a2ec4f7abacb9095a419c317e32b689 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d7/TermLocalizedTranslation.php +@@ -42,13 +42,13 @@ public function query() { + + // Add in the property, which is either name or description. + // Cast td.tid as char for PostgreSQL compatibility. +- $query->leftJoin('i18n_string', 'i18n', 'CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]'); ++ $query->leftJoin('i18n_string', 'i18n', $query->joinCondition()->where('CAST([td].[tid] AS CHAR(255)) = [i18n].[objectid]')); + $query->condition('i18n.type', 'term'); + $query->addField('i18n', 'lid'); + $query->addField('i18n', 'property'); + + // Add in the translation for the property. +- $query->innerJoin('locales_target', 'lt', '[i18n].[lid] = [lt].[lid]'); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('i18n.lid', 'lt.lid')); + $query->addField('lt', 'language', 'lt.language'); + $query->addField('lt', 'translation'); + return $query; +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d7/Term.php b/core/modules/taxonomy/src/Plugin/migrate/source/d7/Term.php +index edb74619b4e67fe5636a696f9c465599dd1ce853..f9ae9568c28992e7f409337d5b86c8a7562d4396 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d7/Term.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d7/Term.php +@@ -55,7 +55,7 @@ public function query() { + ->fields('td') + ->distinct() + ->orderBy('tid'); +- $query->leftJoin('taxonomy_vocabulary', 'tv', '[td].[vid] = [tv].[vid]'); ++ $query->leftJoin('taxonomy_vocabulary', 'tv', $query->joinCondition()->compare('td.vid', 'tv.vid')); + $query->addField('tv', 'machine_name'); + + if ($this->getDatabase() +diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d7/VocabularyTranslation.php b/core/modules/taxonomy/src/Plugin/migrate/source/d7/VocabularyTranslation.php +index f189f2d41a0b59443234fd46abf575afbb59f3d8..8ff9720eee666954f27d23de6d69d61595de4e8b 100644 +--- a/core/modules/taxonomy/src/Plugin/migrate/source/d7/VocabularyTranslation.php ++++ b/core/modules/taxonomy/src/Plugin/migrate/source/d7/VocabularyTranslation.php +@@ -24,8 +24,8 @@ class VocabularyTranslation extends Vocabulary { + */ + public function query() { + $query = parent::query(); +- $query->leftJoin('i18n_string', 'i18n', 'CAST ([v].[vid] AS CHAR(222)) = [i18n].[objectid]'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin('i18n_string', 'i18n', $query->joinCondition()->where('CAST ([v].[vid] AS CHAR(222)) = [i18n].[objectid]')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + $query + ->condition('type', 'vocabulary') + ->fields('lt') +diff --git a/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php +index 5dd75072810cfec1aaa4af0dfb32c4240239a5ff..3be6d54e71b2a4f4450fc4514b6c08d1f0152eb5 100644 +--- a/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php ++++ b/core/modules/taxonomy/src/Plugin/views/field/TaxonomyIndexTid.php +@@ -62,10 +62,22 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ + + // @todo Wouldn't it be possible to use $this->base_table and no if here? + if ($view->storage->get('base_table') == 'node_field_revision') { +- $this->additional_fields['nid'] = ['table' => 'node_field_revision', 'field' => 'nid']; ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $table = 'node'; ++ } ++ else { ++ $table = 'node_field_revision'; ++ } ++ $this->additional_fields['nid'] = ['table' => $table, 'field' => 'nid']; + } + else { +- $this->additional_fields['nid'] = ['table' => 'node_field_data', 'field' => 'nid']; ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $table = 'node'; ++ } ++ else { ++ $table = 'node_field_data'; ++ } ++ $this->additional_fields['nid'] = ['table' => $table, 'field' => 'nid']; + } + } + +diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php +index 4cad71a81d16460675e3516009d168b4378c7fb5..fb29854fd20b2efdb10cc6df949dd5ac6e28f670 100644 +--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php ++++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php +@@ -401,6 +401,71 @@ public function adminSummary() { + return parent::adminSummary(); + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function query() { ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if ($this->table == $this->view->storage->get('base_table')) { ++ $this->mongodbField = $this->realField; ++ } ++ elseif (!empty($this->relationship)) { ++ $this->mongodbField = "$this->relationship.$this->realField"; ++ } ++ else { ++ // Throw an exception. ++ $this->mongodbField = $this->realField; ++ } ++ ++ $info = $this->operators(); ++ if (!empty($info[$this->operator]['method'])) { ++ $this->{$info[$this->operator]['method']}(); ++ } ++ } ++ else { ++ parent::query(); ++ } ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ protected function opSimple() { ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if (empty($this->value)) { ++ return; ++ } ++ $this->ensureMyTable(); ++ ++ // We use array_values() because the checkboxes keep keys and that can cause ++ // array addition problems. ++ $this->query->addCondition($this->options['group'], $this->mongodbField, array_values($this->value), $this->operator); ++ } ++ else { ++ parent::opSimple(); ++ } ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ protected function opEmpty() { ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $this->ensureMyTable(); ++ if ($this->operator == 'empty') { ++ $operator = "IS NULL"; ++ } ++ else { ++ $operator = "IS NOT NULL"; ++ } ++ ++ $this->query->addCondition($this->options['group'], $this->mongodbField, NULL, $operator); ++ } ++ else { ++ parent::opSimple(); ++ } ++ } ++ + /** + * {@inheritdoc} + */ +diff --git a/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php b/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php +index edb8619e4ee8a2c197498cb27b67f592263de082..182d14c363f57e0e9a8b455b216be4ae8ddb42ee 100644 +--- a/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php ++++ b/core/modules/taxonomy/src/Plugin/views/relationship/NodeTermData.php +@@ -111,7 +111,7 @@ public function query() { + $def['adjusted'] = TRUE; + + $query = Database::getConnection()->select('taxonomy_term_field_data', 'td'); +- $query->addJoin($def['type'], 'taxonomy_index', 'tn', '[tn].[tid] = [td].[tid]'); ++ $query->addJoin($def['type'], 'taxonomy_index', 'tn', $query->joinCondition()->compare('tn.tid', 'td.tid')); + $query->condition('td.vid', array_filter($this->options['vids']), 'IN'); + if (empty($this->query->options['disable_sql_rewrite'])) { + $query->addTag('taxonomy_term_access'); +diff --git a/core/modules/taxonomy/src/Plugin/views/wizard/TaxonomyTerm.php b/core/modules/taxonomy/src/Plugin/views/wizard/TaxonomyTerm.php +index 751f365c493e1acf153a4e3de265339356fd75ee..bdb330aa1f6e23cf11316025bc46245458f8f83e 100644 +--- a/core/modules/taxonomy/src/Plugin/views/wizard/TaxonomyTerm.php ++++ b/core/modules/taxonomy/src/Plugin/views/wizard/TaxonomyTerm.php +@@ -31,7 +31,12 @@ protected function defaultDisplayOptions() { + + /* Field: Taxonomy: Term */ + $display_options['fields']['name']['id'] = 'name'; +- $display_options['fields']['name']['table'] = 'taxonomy_term_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['name']['table'] = 'taxonomy_term_data'; ++ } ++ else { ++ $display_options['fields']['name']['table'] = 'taxonomy_term_field_data'; ++ } + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'taxonomy_term'; + $display_options['fields']['name']['entity_field'] = 'name'; +diff --git a/core/modules/taxonomy/src/TaxonomyIndexDepthQueryTrait.php b/core/modules/taxonomy/src/TaxonomyIndexDepthQueryTrait.php +index f1f47ff96c4e3ba926ff61916d6df86bf3bc00e9..04c946ef58f17c1bac1d96ad52683178544bec4e 100644 +--- a/core/modules/taxonomy/src/TaxonomyIndexDepthQueryTrait.php ++++ b/core/modules/taxonomy/src/TaxonomyIndexDepthQueryTrait.php +@@ -66,11 +66,11 @@ protected function addSubQueryJoin($tids): void { + $union_query->addField('tn', 'nid'); + $left_join = "[tn].[tid]"; + if ($this->options['depth'] > 0) { +- $union_query->join('taxonomy_term__parent', "th", "$left_join = [th].[entity_id]"); ++ $union_query->join('taxonomy_term__parent', "th", $union_query->joinCondition()->compare($left_join, 'th.entity_id')); + $left_join = "[th].[$left_field]"; + } + foreach (range(1, $count) as $inner_count) { +- $union_query->join('taxonomy_term__parent', "th$inner_count", "$left_join = [th$inner_count].[$right_field]"); ++ $union_query->join('taxonomy_term__parent', "th$inner_count", $union_query->joinCondition()->compare($left_join, "th$inner_count.$right_field")); + $left_join = "[th$inner_count].[$left_field]"; + } + $union_query->condition("th$inner_count.entity_id", $tids, $operator); +diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php +index 5248840121c4ae80e50b36592d31c190d82b7a42..afc70795fd8cb6bbe0dc4288b7bc2b541d7382ff 100644 +--- a/core/modules/taxonomy/src/TermStorage.php ++++ b/core/modules/taxonomy/src/TermStorage.php +@@ -198,7 +198,7 @@ public function loadChildren($tid, $vid = NULL) { + public function getChildren(TermInterface $term) { + $query = \Drupal::entityQuery('taxonomy_term') + ->accessCheck(TRUE) +- ->condition('parent', $term->id()); ++ ->condition('parent', (int) $term->id()); + return static::loadMultiple($query->execute()); + } + +@@ -214,21 +214,61 @@ public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = + $this->treeChildren[$vid] = []; + $this->treeParents[$vid] = []; + $this->treeTerms[$vid] = []; +- $query = $this->database->select($this->getDataTable(), 't'); +- $query->join('taxonomy_term__parent', 'p', '[t].[tid] = [p].[entity_id]'); +- $query->addExpression('[parent_target_id]', 'parent'); +- $result = $query +- ->addTag('taxonomy_term_access') +- ->fields('t') +- ->condition('t.vid', $vid) +- ->condition('t.default_langcode', 1) +- ->orderBy('t.weight') +- ->orderBy('t.name') +- ->execute(); +- foreach ($result as $term) { +- $this->treeChildren[$vid][$term->parent][] = $term->tid; +- $this->treeParents[$vid][$term->tid][] = $term->parent; +- $this->treeTerms[$vid][$term->tid] = $term; ++ ++ if ($this->database->driver() == 'mongodb') { ++ $query = $this->database->select($this->getBaseTable(), 't') ++ ->fields('t', ['tid', 'taxonomy_term_current_revision']) ++ ->addTag('taxonomy_term_access') ++ ->condition('taxonomy_term_current_revision.vid', $vid) ++ ->condition('taxonomy_term_current_revision.default_langcode', TRUE) ++ ->orderBy('taxonomy_term_current_revision.weight') ++ ->orderBy('taxonomy_term_current_revision.name'); ++ ++ $result = $query->execute()->fetchAll(); ++ foreach ($result as $row) { ++ foreach ($row->taxonomy_term_current_revision as $current_revision) { ++ $term = new \stdClass(); ++ $term->name = $current_revision['name'] ?? ''; ++ $term->depth = 0; ++ $term->tid = $current_revision['tid']; ++ $term->vid = $current_revision['vid']; ++ $term->weight = $current_revision['weight']; ++ ++ if (is_array($current_revision['taxonomy_term_current_revision__parent'])) { ++ foreach ($current_revision['taxonomy_term_current_revision__parent'] as $current_revision_parent) { ++ $term->parent = NULL; ++ if (isset($current_revision_parent['parent_target_id'])) { ++ $term->parent = $current_revision_parent['parent_target_id']; ++ } ++ if (!is_null($term->parent)) { ++ $this->treeChildren[$vid][$term->parent][] = $term->tid; ++ $this->treeParents[$vid][$term->tid][] = $term->parent; ++ $this->treeTerms[$vid][$term->tid] = $term; ++ } ++ } ++ } ++ } ++ unset($term->taxonomy_term_current_revision); ++ } ++ } ++ else { ++ $query = $this->database->select($this->getDataTable(), 't'); ++ $query->join('taxonomy_term__parent', 'p', $query->joinCondition() ++ ->compare('t.tid', 'p.entity_id')); ++ $query->addExpressionField('parent_target_id', 'parent'); ++ $result = $query ++ ->addTag('taxonomy_term_access') ++ ->fields('t') ++ ->condition('t.vid', $vid) ++ ->condition('t.default_langcode', 1) ++ ->orderBy('t.weight') ++ ->orderBy('t.name') ++ ->execute(); ++ foreach ($result as $term) { ++ $this->treeChildren[$vid][$term->parent][] = $term->tid; ++ $this->treeParents[$vid][$term->tid][] = $term->parent; ++ $this->treeTerms[$vid][$term->tid] = $term; ++ } + } + } + +@@ -306,48 +346,118 @@ public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = + * {@inheritdoc} + */ + public function nodeCount($vid) { +- $query = $this->database->select('taxonomy_index', 'ti'); +- $query->addExpression('COUNT(DISTINCT [ti].[nid])'); +- $query->leftJoin($this->getBaseTable(), 'td', '[ti].[tid] = [td].[tid]'); +- $query->condition('td.vid', $vid); +- $query->addTag('vocabulary_node_count'); +- return $query->execute()->fetchField(); ++ if ($this->database->driver() == 'mongodb') { ++ // @todo There is too little testing for this. Why is there a join in this ++ // query. ++ // @see \Drupal\Tests\taxonomy\Functional\TokenReplaceTest. ++ $query = $this->database->select('taxonomy_index', 'ti'); ++ $query->addJoin('LEFT', 'taxonomy_term_data', 'td', $query->joinCondition()->compare('ti.tid', 'td.tid')->condition('taxonomy_term_current_revision.vid', $vid)); ++ $query->addTag('vocabulary_node_count'); ++ $results = $query->execute()->fetchAll(); ++ $nids = []; ++ foreach ($results as $result) { ++ if (isset($result->nid) && !in_array($result->nid, $nids)) { ++ $nids[] = $result->nid; ++ } ++ } ++ return count($nids); ++ } ++ else { ++ $query = $this->database->select('taxonomy_index', 'ti'); ++ $query->addExpressionCountDistinct('ti.nid'); ++ $query->leftJoin($this->getBaseTable(), 'td', $query->joinCondition()->compare('ti.tid', 'td.tid')); ++ $query->condition('td.vid', $vid); ++ $query->addTag('vocabulary_node_count'); ++ return $query->execute()->fetchField(); ++ } + } + + /** + * {@inheritdoc} + */ + public function resetWeights($vid) { +- $this->database->update($this->getDataTable()) +- ->fields(['weight' => 0]) +- ->condition('vid', $vid) +- ->execute(); ++ if ($this->database->driver() == 'mongodb') { ++ $prefixed_table = $this->database->getPrefix() . 'taxonomy_term_data'; ++ $this->database->getConnection()->selectCollection($prefixed_table)->updateMany( ++ [ ++ 'vid' => $vid, ++ ], ++ [ ++ '$set' => [ ++ 'weight' => 0, ++ "taxonomy_term_current_revision.$[translation].weight" => 0, ++ ], ++ ], ++ [ ++ 'arrayFilters' => [ ++ ["translation.vid" => $vid], ++ ], ++ 'session' => $this->database->getMongodbSession(), ++ ], ++ ); ++ } ++ else { ++ $this->database->update($this->getDataTable()) ++ ->fields(['weight' => 0]) ++ ->condition('vid', $vid) ++ ->execute(); ++ } + } + + /** + * {@inheritdoc} + */ + public function getNodeTerms(array $nids, array $vids = [], $langcode = NULL) { +- $query = $this->database->select($this->getDataTable(), 'td'); +- $query->innerJoin('taxonomy_index', 'tn', '[td].[tid] = [tn].[tid]'); +- $query->fields('td', ['tid']); +- $query->addField('tn', 'nid', 'node_nid'); +- $query->orderby('td.weight'); +- $query->orderby('td.name'); +- $query->condition('tn.nid', $nids, 'IN'); +- $query->addTag('taxonomy_term_access'); +- if (!empty($vids)) { +- $query->condition('td.vid', $vids, 'IN'); +- } +- if (!empty($langcode)) { +- $query->condition('td.langcode', $langcode); ++ if ($this->database->driver() == 'mongodb') { ++ $query = $this->database->select('taxonomy_term_data', 'td'); ++ foreach ($nids as &$nid) { ++ $nid = (int) $nid; ++ } ++ $query->addJoin('INNER', 'taxonomy_index', 'tn', $query->joinCondition()->compare('tn.tid', 'td.tid')); ++ $query->fields('td', ['tid']); ++ $query->addField('tn', 'nid', 'node_nid'); ++ $query->condition('tn.nid', $nids, 'IN'); ++ $query->orderby('taxonomy_term_current_revision.weight'); ++ $query->orderby('taxonomy_term_current_revision.name'); ++ $query->addTag('taxonomy_term_access'); ++ if (!empty($vids)) { ++ $query->condition('taxonomy_term_current_revision.vid', $vids, 'IN'); ++ } ++ if (!empty($langcode)) { ++ $query->condition('taxonomy_term_current_revision.langcode', $langcode); ++ } ++ ++ $results = []; ++ $all_tids = []; ++ foreach ($query->execute() as $term_record) { ++ if (isset($term_record->tid) && isset($term_record->node_nid)) { ++ $results[$term_record->node_nid][] = $term_record->tid; ++ $all_tids[] = $term_record->tid; ++ } ++ } + } ++ else { ++ $query = $this->database->select($this->getDataTable(), 'td'); ++ $query->innerJoin('taxonomy_index', 'tn', $query->joinCondition()->compare('td.tid', 'tn.tid')); ++ $query->fields('td', ['tid']); ++ $query->addField('tn', 'nid', 'node_nid'); ++ $query->orderby('td.weight'); ++ $query->orderby('td.name'); ++ $query->condition('tn.nid', $nids, 'IN'); ++ $query->addTag('taxonomy_term_access'); ++ if (!empty($vids)) { ++ $query->condition('td.vid', $vids, 'IN'); ++ } ++ if (!empty($langcode)) { ++ $query->condition('td.langcode', $langcode); ++ } + +- $results = []; +- $all_tids = []; +- foreach ($query->execute() as $term_record) { +- $results[$term_record->node_nid][] = $term_record->tid; +- $all_tids[] = $term_record->tid; ++ $results = []; ++ $all_tids = []; ++ foreach ($query->execute() as $term_record) { ++ $results[$term_record->node_nid][] = $term_record->tid; ++ $all_tids[] = $term_record->tid; ++ } + } + + $all_terms = $this->loadMultiple($all_tids); +@@ -371,25 +481,59 @@ public function getTermIdsWithPendingRevisions() { + $langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value']; + $revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value']; + +- $query = $this->database->select($this->getRevisionDataTable(), 'tfr'); +- $query->fields('tfr', [$id_field]); +- $query->addExpression("MAX([tfr].[$revision_field])", $revision_field); +- +- $query->join($this->getRevisionTable(), 'tr', "[tfr].[$revision_field] = [tr].[$revision_field] AND [tr].[$revision_default_field] = 0"); +- +- $inner_select = $this->database->select($this->getRevisionDataTable(), 't'); +- $inner_select->condition("t.$rta_field", '1'); +- $inner_select->fields('t', [$id_field, $langcode_field]); +- $inner_select->addExpression("MAX([t].[$revision_field])", $revision_field); +- $inner_select +- ->groupBy("t.$id_field") +- ->groupBy("t.$langcode_field"); +- +- $query->join($inner_select, 'mr', "[tfr].[$revision_field] = [mr].[$revision_field] AND [tfr].[$langcode_field] = [mr].[$langcode_field]"); +- +- $query->groupBy("tfr.$id_field"); ++ if ($this->database->driver() == 'mongodb') { ++ $latest_revision_table = $this->getJsonStorageLatestRevisionTable(); ++ ++ $results = $this->database->select($this->getBaseTable(), 't') ++ ->fields('t', [$id_field, $latest_revision_table]) ++ ->execute() ++ ->fetchAll(); ++ ++ $term_ids_with_pending_revisions = []; ++ foreach ($results as $result) { ++ $latest_revision = $result->{$latest_revision_table}; ++ $revision_id = NULL; ++ foreach ($latest_revision as $latest_revision_language) { ++ if (($latest_revision_language[$rta_field] === TRUE) && ($latest_revision_language[$revision_default_field] === FALSE)) { ++ $revision_id = $latest_revision_language[$revision_field]; ++ } ++ } ++ if (!is_null($revision_id)) { ++ $term_ids_with_pending_revisions[$result->{$id_field}] = $revision_id; ++ } ++ } + +- return $query->execute()->fetchAllKeyed(1, 0); ++ return $term_ids_with_pending_revisions; ++ } ++ else { ++ $query = $this->database->select($this->getRevisionDataTable(), 'tfr'); ++ $query->fields('tfr', [$id_field]); ++ $query->addExpressionMax("tfr.$revision_field", $revision_field); ++ ++ $query->join($this->getRevisionTable(), 'tr', ++ $query->joinCondition() ++ ->compare("tfr.$revision_field", "tr.$revision_field") ++ ->condition("tr.$revision_default_field", 0) ++ ); ++ ++ $inner_select = $this->database->select($this->getRevisionDataTable(), 't'); ++ $inner_select->condition("t.$rta_field", '1'); ++ $inner_select->fields('t', [$id_field, $langcode_field]); ++ $inner_select->addExpressionMax("t.$revision_field", $revision_field); ++ $inner_select ++ ->groupBy("t.$id_field") ++ ->groupBy("t.$langcode_field"); ++ ++ $query->join($inner_select, 'mr', ++ $query->joinCondition() ++ ->compare("tfr.$revision_field", "mr.$revision_field") ++ ->compare("tfr.$langcode_field", "mr.$langcode_field") ++ ); ++ ++ $query->groupBy("tfr.$id_field"); ++ ++ return $query->execute()->fetchAllKeyed(1, 0); ++ } + } + + /** +@@ -407,12 +551,53 @@ public function getVocabularyHierarchyType($vid) { + $target_id_column = $table_mapping->getFieldColumnName($parent_field_storage, 'target_id'); + $delta_column = $table_mapping->getFieldColumnName($parent_field_storage, TableMappingInterface::DELTA); + +- $query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p'); +- $query->addExpression("MAX([$target_id_column])", 'max_parent_id'); +- $query->addExpression("MAX([$delta_column])", 'max_delta'); +- $query->condition('bundle', $vid); ++ if ($this->database->driver() == 'mongodb') { ++ $all_revisions_table = $table_mapping->getJsonStorageAllRevisionsTable(0); ++ $parent_table = $table_mapping->getJsonStorageDedicatedTableName($parent_field_storage, $all_revisions_table); ++ ++ $rows = $this->database->select($this->getBaseTable()) ++ ->fields($this->getBaseTable(), [$all_revisions_table]) ++ ->condition("$all_revisions_table.vid", $vid) ++ ->execute() ++ ->fetchAll(); ++ ++ $max_parent_id = 0; ++ $max_delta = 0; ++ foreach ($rows as $row) { ++ if (isset($row->{$all_revisions_table})) { ++ foreach ($row->{$all_revisions_table} as $taxonomy_term_revision) { ++ if (isset($taxonomy_term_revision[$parent_table])) { ++ foreach ($taxonomy_term_revision[$parent_table] as $parent_table_row) { ++ $parent_id = (int) $parent_table_row['parent_target_id']; ++ if ($parent_id > $max_parent_id) { ++ $max_parent_id = (int) $parent_id; ++ } ++ $delta = (int) $parent_table_row['delta']; ++ if ($delta > $max_delta) { ++ $max_delta = (int) $delta; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ // Create the result as it is created for a relational database. ++ $result = [ ++ 0 => (object) [ ++ 'max_parent_id' => $max_parent_id, ++ 'max_delta' => $max_delta, ++ ], ++ ]; ++ } ++ else { ++ $query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p'); ++ $query->addExpressionMax("$target_id_column", 'max_parent_id'); ++ $query->addExpressionMax("$delta_column", 'max_delta'); ++ $query->condition('bundle', $vid); + +- $result = $query->execute()->fetchAll(); ++ $result = $query->execute()->fetchAll(); ++ } + + // If all the terms have the same parent, the parent can only be root (0). + if ((int) $result[0]->max_parent_id === 0) { +diff --git a/core/modules/taxonomy/src/TermStorageSchema.php b/core/modules/taxonomy/src/TermStorageSchema.php +index d9e1bbf7aa85807ddae4fdfe385b201fdb00f076..3f20d5c2768c9b2bfd60af22f4e42903895e160c 100644 +--- a/core/modules/taxonomy/src/TermStorageSchema.php ++++ b/core/modules/taxonomy/src/TermStorageSchema.php +@@ -17,13 +17,6 @@ class TermStorageSchema extends SqlContentEntityStorageSchema { + protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { + $schema = parent::getEntitySchema($entity_type, $reset); + +- if ($data_table = $this->storage->getDataTable()) { +- $schema[$data_table]['indexes'] += [ +- 'taxonomy_term__tree' => ['vid', 'weight', 'name'], +- 'taxonomy_term__vid_name' => ['vid', 'name'], +- ]; +- } +- + $schema['taxonomy_index'] = [ + 'description' => 'Maintains denormalized information about node/term relationships.', + 'fields' => [ +@@ -77,6 +70,21 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res + ], + ]; + ++ if ($this->database->driver() == 'mongodb') { ++ // Boolean fields in MongoDB are stored as a boolean value. ++ $schema['taxonomy_index']['fields']['status']['type'] = 'bool'; ++ $schema['taxonomy_index']['fields']['sticky']['type'] = 'bool'; ++ ++ // Date fields in MongoDB are stored as a date value. ++ $schema['taxonomy_index']['fields']['created']['type'] = 'date'; ++ } ++ elseif ($data_table = $this->storage->getDataTable()) { ++ $schema[$data_table]['indexes'] += [ ++ 'taxonomy_term__tree' => ['vid', 'weight', 'name'], ++ 'taxonomy_term__vid_name' => ['vid', 'name'], ++ ]; ++ } ++ + return $schema; + } + +@@ -120,14 +128,16 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor + if ($storage_definition->getName() === 'parent') { + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->storage->getTableMapping(); +- $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + +- unset($dedicated_table_schema[$dedicated_table_name]['indexes']['bundle']); +- $dedicated_table_schema[$dedicated_table_name]['indexes']['bundle_delta_target_id'] = [ +- 'bundle', +- 'delta', +- $table_mapping->getFieldColumnName($storage_definition, 'target_id'), +- ]; ++ if ($this->database->driver() != 'mongodb') { ++ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition); ++ unset($dedicated_table_schema[$dedicated_table_name]['indexes']['bundle']); ++ $dedicated_table_schema[$dedicated_table_name]['indexes']['bundle_delta_target_id'] = [ ++ 'bundle', ++ 'delta', ++ $table_mapping->getFieldColumnName($storage_definition, 'target_id'), ++ ]; ++ } + } + + return $dedicated_table_schema; +diff --git a/core/modules/taxonomy/src/TermViewsData.php b/core/modules/taxonomy/src/TermViewsData.php +index b997aecc3b3e5661a2a4f445001bf702bef0b989..38f94eb7577bf9ec6a9c8bcd25eb06a3bae7220c 100644 +--- a/core/modules/taxonomy/src/TermViewsData.php ++++ b/core/modules/taxonomy/src/TermViewsData.php +@@ -15,11 +15,22 @@ class TermViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + +- $data['taxonomy_term_field_data']['table']['base']['help'] = $this->t('Taxonomy terms are attached to nodes.'); +- $data['taxonomy_term_field_data']['table']['base']['access query tag'] = 'taxonomy_term_access'; +- $data['taxonomy_term_field_data']['table']['wizard_id'] = 'taxonomy_term'; +- +- $data['taxonomy_term_field_data']['table']['join'] = [ ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'taxonomy_term_data'; ++ $parent_table = 'taxonomy_term_data'; ++ $node_table = 'node'; ++ } ++ else { ++ $data_table = 'taxonomy_term_field_data'; ++ $parent_table = 'taxonomy_term__parent'; ++ $node_table = 'node_field_data'; ++ } ++ ++ $data[$data_table]['table']['base']['help'] = $this->t('Taxonomy terms are attached to nodes.'); ++ $data[$data_table]['table']['base']['access query tag'] = 'taxonomy_term_access'; ++ $data[$data_table]['table']['wizard_id'] = 'taxonomy_term'; ++ ++ $data[$data_table]['table']['join'] = [ + // This is provided for the many_to_one argument. + 'taxonomy_index' => [ + 'field' => 'tid', +@@ -27,19 +38,19 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['tid']['help'] = $this->t('The tid of a taxonomy term.'); ++ $data[$data_table]['tid']['help'] = $this->t('The tid of a taxonomy term.'); + +- $data['taxonomy_term_field_data']['tid']['argument']['id'] = 'taxonomy'; +- $data['taxonomy_term_field_data']['tid']['argument']['name field'] = 'name'; +- $data['taxonomy_term_field_data']['tid']['argument']['zero is null'] = TRUE; ++ $data[$data_table]['tid']['argument']['id'] = 'taxonomy'; ++ $data[$data_table]['tid']['argument']['name field'] = 'name'; ++ $data[$data_table]['tid']['argument']['zero is null'] = TRUE; + +- $data['taxonomy_term_field_data']['tid']['filter']['id'] = 'taxonomy_index_tid'; +- $data['taxonomy_term_field_data']['tid']['filter']['title'] = $this->t('Term'); +- $data['taxonomy_term_field_data']['tid']['filter']['help'] = $this->t('Taxonomy term chosen from autocomplete or select widget.'); +- $data['taxonomy_term_field_data']['tid']['filter']['hierarchy table'] = 'taxonomy_term__parent'; +- $data['taxonomy_term_field_data']['tid']['filter']['numeric'] = TRUE; ++ $data[$data_table]['tid']['filter']['id'] = 'taxonomy_index_tid'; ++ $data[$data_table]['tid']['filter']['title'] = $this->t('Term'); ++ $data[$data_table]['tid']['filter']['help'] = $this->t('Taxonomy term chosen from autocomplete or select widget.'); ++ $data[$data_table]['tid']['filter']['hierarchy table'] = $parent_table; ++ $data[$data_table]['tid']['filter']['numeric'] = TRUE; + +- $data['taxonomy_term_field_data']['tid_raw'] = [ ++ $data[$data_table]['tid_raw'] = [ + 'title' => $this->t('Term ID'), + 'help' => $this->t('The tid of a taxonomy term.'), + 'real field' => 'tid', +@@ -49,7 +60,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['tid_representative'] = [ ++ $data[$data_table]['tid_representative'] = [ + 'relationship' => [ + 'title' => $this->t('Representative node'), + 'label' => $this->t('Representative node'), +@@ -57,31 +68,31 @@ public function getViewsData() { + 'id' => 'groupwise_max', + 'relationship field' => 'tid', + 'outer field' => 'taxonomy_term_field_data.tid', +- 'argument table' => 'taxonomy_term_field_data', ++ 'argument table' => $data_table, + 'argument field' => 'tid', +- 'base' => 'node_field_data', ++ 'base' => $node_table, + 'field' => 'nid', +- 'relationship' => 'node_field_data:term_node_tid', ++ 'relationship' => "$node_table:term_node_tid", + ], + ]; + +- $data['taxonomy_term_field_data']['vid']['help'] = $this->t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'); +- $data['taxonomy_term_field_data']['vid']['field']['help'] = t('The vocabulary name.'); +- $data['taxonomy_term_field_data']['vid']['argument']['id'] = 'vocabulary_vid'; ++ $data[$data_table]['vid']['help'] = $this->t('Filter the results of "Taxonomy: Term" to a particular vocabulary.'); ++ $data[$data_table]['vid']['field']['help'] = t('The vocabulary name.'); ++ $data[$data_table]['vid']['argument']['id'] = 'vocabulary_vid'; + +- $data['taxonomy_term_field_data']['vid']['sort']['title'] = t('Vocabulary ID'); +- $data['taxonomy_term_field_data']['vid']['sort']['help'] = t('The raw vocabulary ID.'); ++ $data[$data_table]['vid']['sort']['title'] = t('Vocabulary ID'); ++ $data[$data_table]['vid']['sort']['help'] = t('The raw vocabulary ID.'); + +- $data['taxonomy_term_field_data']['name']['field']['id'] = 'term_name'; +- $data['taxonomy_term_field_data']['name']['argument']['many to one'] = TRUE; +- $data['taxonomy_term_field_data']['name']['argument']['empty field name'] = $this->t('Uncategorized'); ++ $data[$data_table]['name']['field']['id'] = 'term_name'; ++ $data[$data_table]['name']['argument']['many to one'] = TRUE; ++ $data[$data_table]['name']['argument']['empty field name'] = $this->t('Uncategorized'); + +- $data['taxonomy_term_field_data']['description__value']['field']['click sortable'] = FALSE; ++ $data[$data_table]['description__value']['field']['click sortable'] = FALSE; + +- $data['taxonomy_term_field_data']['changed']['title'] = $this->t('Updated date'); +- $data['taxonomy_term_field_data']['changed']['help'] = $this->t('The date the term was last updated.'); ++ $data[$data_table]['changed']['title'] = $this->t('Updated date'); ++ $data[$data_table]['changed']['help'] = $this->t('The date the term was last updated.'); + +- $data['taxonomy_term_field_data']['changed_fulldate'] = [ ++ $data[$data_table]['changed_fulldate'] = [ + 'title' => $this->t('Updated date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -90,7 +101,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['changed_year_month'] = [ ++ $data[$data_table]['changed_year_month'] = [ + 'title' => $this->t('Updated year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -99,7 +110,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['changed_year'] = [ ++ $data[$data_table]['changed_year'] = [ + 'title' => $this->t('Updated year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -108,7 +119,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['changed_month'] = [ ++ $data[$data_table]['changed_month'] = [ + 'title' => $this->t('Updated month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -117,7 +128,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['changed_day'] = [ ++ $data[$data_table]['changed_day'] = [ + 'title' => $this->t('Updated day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -126,7 +137,7 @@ public function getViewsData() { + ], + ]; + +- $data['taxonomy_term_field_data']['changed_week'] = [ ++ $data[$data_table]['changed_week'] = [ + 'title' => $this->t('Updated week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -138,31 +149,34 @@ public function getViewsData() { + $data['taxonomy_index']['table']['group'] = $this->t('Taxonomy term'); + + $data['taxonomy_index']['table']['join'] = [ +- 'taxonomy_term_field_data' => [ ++ $data_table => [ + // Links directly to taxonomy_term_field_data via tid + 'left_field' => 'tid', + 'field' => 'tid', + ], +- 'node_field_data' => [ ++ $node_table => [ + // Links directly to node via nid + 'left_field' => 'nid', + 'field' => 'nid', + ], +- 'taxonomy_term__parent' => [ ++ ]; ++ ++ if ($this->connection->driver() != 'mongodb') { ++ $data['taxonomy_index']['table']['join']['taxonomy_term__parent'] = [ + 'left_field' => 'entity_id', + 'field' => 'tid', +- ], +- ]; ++ ]; ++ } + + $data['taxonomy_index']['nid'] = [ + 'title' => $this->t('Content with term'), + 'help' => $this->t('Relate all content tagged with a term.'), + 'relationship' => [ + 'id' => 'standard', +- 'base' => 'node_field_data', ++ 'base' => $node_table, + 'base field' => 'nid', + 'label' => $this->t('node'), +- 'skip base' => 'node_field_data', ++ 'skip base' => $node_table, + ], + ]; + +@@ -174,18 +188,18 @@ public function getViewsData() { + 'help' => $this->t('Display content if it has the selected taxonomy terms.'), + 'argument' => [ + 'id' => 'taxonomy_index_tid', +- 'name table' => 'taxonomy_term_field_data', ++ 'name table' => $data_table, + 'name field' => 'name', + 'empty field name' => $this->t('Uncategorized'), + 'numeric' => TRUE, +- 'skip base' => 'taxonomy_term_field_data', ++ 'skip base' => $data_table, + ], + 'filter' => [ + 'title' => $this->t('Has taxonomy term'), + 'id' => 'taxonomy_index_tid', +- 'hierarchy table' => 'taxonomy_term__parent', ++ 'hierarchy table' => $parent_table, + 'numeric' => TRUE, +- 'skip base' => 'taxonomy_term_field_data', ++ 'skip base' => $data_table, + 'allow empty' => TRUE, + ], + ]; +@@ -225,15 +239,17 @@ public function getViewsData() { + ], + ]; + +- // Link to self through left.parent = right.tid (going down in depth). +- $data['taxonomy_term__parent']['table']['join']['taxonomy_term__parent'] = [ +- 'left_field' => 'entity_id', +- 'field' => 'parent_target_id', +- ]; ++ if ($this->connection->driver() != 'mongodb') { ++ // Link to self through left.parent = right.tid (going down in depth). ++ $data['taxonomy_term__parent']['table']['join']['taxonomy_term__parent'] = [ ++ 'left_field' => 'entity_id', ++ 'field' => 'parent_target_id', ++ ]; + +- $data['taxonomy_term__parent']['parent_target_id']['help'] = $this->t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.'); +- $data['taxonomy_term__parent']['parent_target_id']['relationship']['label'] = $this->t('Parent'); +- $data['taxonomy_term__parent']['parent_target_id']['argument']['id'] = 'taxonomy'; ++ $data['taxonomy_term__parent']['parent_target_id']['help'] = $this->t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.'); ++ $data['taxonomy_term__parent']['parent_target_id']['relationship']['label'] = $this->t('Parent'); ++ $data['taxonomy_term__parent']['parent_target_id']['argument']['id'] = 'taxonomy'; ++ } + + return $data; + } +diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module +index d00cf3f80502039b46696c3679bb7433916380ba..26f78ed3488c16bb84e65e03e8f0c3f3457c8965 100644 +--- a/core/modules/taxonomy/taxonomy.module ++++ b/core/modules/taxonomy/taxonomy.module +@@ -131,7 +131,7 @@ function taxonomy_build_node_index($node) { + $connection = \Drupal::database(); + foreach ($tid_all as $tid) { + $connection->merge('taxonomy_index') +- ->keys(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()]) ++ ->keys(['nid' => (int) $node->id(), 'tid' => (int) $tid, 'status' => (bool) $node->isPublished()]) + ->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()]) + ->execute(); + } +@@ -147,7 +147,7 @@ function taxonomy_build_node_index($node) { + */ + function taxonomy_delete_node_index(EntityInterface $node) { + if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) { +- \Drupal::database()->delete('taxonomy_index')->condition('nid', $node->id())->execute(); ++ \Drupal::database()->delete('taxonomy_index')->condition('nid', (int) $node->id())->execute(); + } + } + +diff --git a/core/modules/user/src/Authentication/Provider/Cookie.php b/core/modules/user/src/Authentication/Provider/Cookie.php +index fb2c9cd5845585db4107daf9258f6287e82d4d72..64b33fd6645292711d9b23d03d9fce3b43784eef 100644 +--- a/core/modules/user/src/Authentication/Provider/Cookie.php ++++ b/core/modules/user/src/Authentication/Provider/Cookie.php +@@ -93,19 +93,78 @@ public function authenticate(Request $request) { + */ + protected function getUserFromSession(SessionInterface $session) { + if ($uid = $session->get('uid')) { +- // @todo Load the User entity in SessionHandler so we don't need queries. +- // @see https://www.drupal.org/node/2345611 +- $values = $this->connection +- ->query('SELECT * FROM {users_field_data} [u] WHERE [u].[uid] = :uid AND [u].[default_langcode] = 1', [':uid' => $uid]) +- ->fetchAssoc(); +- +- // Check if the user data was found and the user is active. +- if (!empty($values) && $values['status'] == 1) { +- // Add the user's roles. +- $rids = $this->connection +- ->query('SELECT [roles_target_id] FROM {user__roles} WHERE [entity_id] = :uid', [':uid' => $values['uid']]) +- ->fetchCol(); +- $values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids); ++ if ($this->connection->driver() == 'mongodb') { ++ $prefixed_table = $this->connection->getPrefix() . 'users'; ++ $result = $this->connection->getConnection()->selectCollection($prefixed_table)->findOne( ++ ['uid' => ['$eq' => (int) $uid]], ++ [ ++ 'projection' => ['user_translations' => 1, '_id' => 0], ++ 'session' => $this->connection->getMongodbSession(), ++ ], ++ ); ++ ++ $values = []; ++ if (isset($result->user_translations)) { ++ $user_translations = (array) $result->user_translations; ++ foreach ($user_translations as $user_translation) { ++ if (isset($user_translation->default_langcode) && ($user_translation->default_langcode === TRUE)) { ++ if (isset($user_translation->uid)) { ++ $values['uid'] = (string) $user_translation->uid; ++ } ++ if (isset($user_translation->access)) { ++ $values['access'] = (int) $user_translation->access->__toString(); ++ $values['access'] = $values['access'] / 1000; ++ $values['access'] = (string) $values['access']; ++ } ++ if (isset($user_translation->name)) { ++ $values['name'] = $user_translation->name; ++ } ++ if (isset($user_translation->preferred_langcode)) { ++ $values['preferred_langcode'] = $user_translation->preferred_langcode; ++ } ++ if (isset($user_translation->preferred_admin_langcode)) { ++ $values['preferred_admin_langcode'] = $user_translation->preferred_admin_langcode; ++ } ++ if (isset($user_translation->mail)) { ++ $values['mail'] = $user_translation->mail; ++ } ++ if (isset($user_translation->timezone)) { ++ $values['timezone'] = $user_translation->timezone; ++ } ++ ++ // Add the user role authenticated. ++ $values['roles'] = [AccountInterface::AUTHENTICATED_ROLE]; ++ if (isset($user_translation->user_translations__roles)) { ++ $user_translations__roles = (array) $user_translation->user_translations__roles; ++ foreach ($user_translations__roles as $user_translations__role) { ++ if (isset($user_translations__role->roles_target_id)) { ++ $values['roles'][] = $user_translations__role->roles_target_id; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ if (!empty($values)) { ++ return new UserSession($values); ++ } ++ } ++ else { ++ // @todo Load the User entity in SessionHandler so we don't need queries. ++ // @see https://www.drupal.org/node/2345611 ++ $values = $this->connection ++ ->query('SELECT * FROM {users_field_data} [u] WHERE [u].[uid] = :uid AND [u].[default_langcode] = 1', [':uid' => $uid]) ++ ->fetchAssoc(); ++ ++ // Check if the user data was found and the user is active. ++ if (!empty($values) && $values['status'] == 1) { ++ // Add the user's roles. ++ $rids = $this->connection ++ ->query('SELECT [roles_target_id] FROM {user__roles} WHERE [entity_id] = :uid', [':uid' => $values['uid']]) ++ ->fetchCol(); ++ $values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids); ++ } + + return new UserSession($values); + } +diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php +index af31a878ddcc957bbd2581bf257ab38703c0fbd4..4e4cd7ea27a7d170aa709f7737dbc2a68b69cf24 100644 +--- a/core/modules/user/src/Controller/UserAuthenticationController.php ++++ b/core/modules/user/src/Controller/UserAuthenticationController.php +@@ -420,7 +420,7 @@ protected function floodControl(Request $request, $username) { + */ + protected function getLoginFloodIdentifier(Request $request, $username) { + $flood_config = $this->config('user.flood'); +- $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]); ++ $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => TRUE]); + if ($account = reset($accounts)) { + if ($flood_config->get('uid_only')) { + // Register flood events based on the uid only, so they apply for any +diff --git a/core/modules/user/src/Hook/UserViewsExecutionHooks.php b/core/modules/user/src/Hook/UserViewsExecutionHooks.php +index 0acca99b3ea2d471757d43484a25eee4417424d5..03ac68152ed0d8005d58eceebe7d6383978dafcf 100644 +--- a/core/modules/user/src/Hook/UserViewsExecutionHooks.php ++++ b/core/modules/user/src/Hook/UserViewsExecutionHooks.php +@@ -17,7 +17,7 @@ class UserViewsExecutionHooks { + */ + #[Hook('views_query_substitutions')] + public function viewsQuerySubstitutions(ViewExecutable $view) { +- return ['***CURRENT_USER***' => \Drupal::currentUser()->id()]; ++ return ['***CURRENT_USER***' => (int) \Drupal::currentUser()->id()]; + } + + } +diff --git a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php +index 719538201c4ddbfcd32db68d4ba6646ebd72fdfe..2e4e06ce7e83ba3b2a69aae4ba279b2681a2cb77 100644 +--- a/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php ++++ b/core/modules/user/src/Plugin/EntityReferenceSelection/UserSelection.php +@@ -2,6 +2,7 @@ + + namespace Drupal\user\Plugin\EntityReferenceSelection; + ++use Daffie\SqlLikeToRegularExpression; + use Drupal\Core\Database\Connection; + use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Entity\Attribute\EntityReferenceSelection; +@@ -178,7 +179,7 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') + // Adding the permission check is sadly insufficient for users: core + // requires us to also know about the concept of 'blocked' and 'active'. + if (!$this->currentUser->hasPermission('administer users')) { +- $query->condition('status', 1); ++ $query->condition('status', TRUE); + } + return $query; + } +@@ -236,7 +237,7 @@ public function entityQueryAlter(SelectInterface $query) { + // database. + $conditions = &$query->conditions(); + foreach ($conditions as $key => $condition) { +- if ($key !== '#conjunction' && is_string($condition['field']) && $condition['field'] === 'users_field_data.name') { ++ if ($key !== '#conjunction' && is_string($condition['field']) && (($condition['field'] === 'users_field_data.name') || ($condition['field'] === 'user_translations.name'))) { + // Remove the condition. + unset($conditions[$key]); + +@@ -245,18 +246,31 @@ public function entityQueryAlter(SelectInterface $query) { + // WHERE (name LIKE :name) OR (:anonymous_name LIKE :name AND uid = 0) + $or = $this->connection->condition('OR'); + $or->condition($condition['field'], $condition['value'], $condition['operator']); +- // Sadly, the Database layer doesn't allow us to build a condition +- // in the form ':placeholder = :placeholder2', because the 'field' +- // part of a condition is always escaped. +- // As a (cheap) workaround, we separately build a condition with no +- // field, and concatenate the field and the condition separately. +- $value_part = $this->connection->condition('AND'); +- $value_part->condition('anonymous_name', $condition['value'], $condition['operator']); +- $value_part->compile($this->connection, $query); +- $or->condition(($this->connection->condition('AND')) +- ->where(str_replace($query->escapeField('anonymous_name'), ':anonymous_name', (string) $value_part), $value_part->arguments() + [':anonymous_name' => \Drupal::config('user.settings')->get('anonymous')]) +- ->condition('base_table.uid', 0) +- ); ++ ++ if ($condition['field'] === 'user_translations.name') { ++ $pattern = SqlLikeToRegularExpression::convert($condition['value']); ++ preg_match('/' . $pattern . '/i', \Drupal::config('user.settings')->get('anonymous'), $matches); ++ if ($matches) { ++ $or->condition('uid', 0); ++ } ++ } ++ else { ++ // Sadly, the Database layer doesn't allow us to build a condition ++ // in the form ':placeholder = :placeholder2', because the 'field' ++ // part of a condition is always escaped. ++ // As a (cheap) workaround, we separately build a condition with no ++ // field, and concatenate the field and the condition separately. ++ $value_part = $this->connection->condition('AND'); ++ $value_part->condition('anonymous_name', $condition['value'], $condition['operator']); ++ $value_part->compile($this->connection, $query); ++ $or->condition(($this->connection->condition('AND')) ++ ->where(str_replace($query->escapeField('anonymous_name'), ':anonymous_name', (string) $value_part), $value_part->arguments() + [ ++ ':anonymous_name' => \Drupal::config('user.settings')->get('anonymous'), ++ ]) ++ ->condition('base_table.uid', 0) ++ ); ++ } ++ + $query->condition($or); + } + } +diff --git a/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldOptionTranslation.php b/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldOptionTranslation.php +index ddaf3093f0df30f717ac379ad2101cd1ed676e01..46f91d0cdc0961bf8d4d136498cb433f4a525988 100644 +--- a/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldOptionTranslation.php ++++ b/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldOptionTranslation.php +@@ -31,8 +31,8 @@ public function query() { + ->fields('lt', ['translation', 'language']) + ->condition('i18n.type', 'field') + ->condition('property', 'options'); +- $query->leftJoin('i18n_strings', 'i18n', '[pf].[name] = [i18n].[objectid]'); +- $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); ++ $query->leftJoin('i18n_strings', 'i18n', $query->joinCondition()->compare('pf.name', 'i18n.objectid')); ++ $query->innerJoin('locales_target', 'lt', $query->joinCondition()->compare('lt.lid', 'i18n.lid')); + + return $query; + } +diff --git a/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldValues.php b/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldValues.php +index 8445be5cc3f8ab898335790aefad3a857c345618..5a958786a6c694ef341c69c010cb0523c3f6adac 100644 +--- a/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldValues.php ++++ b/core/modules/user/src/Plugin/migrate/source/d6/ProfileFieldValues.php +@@ -38,7 +38,7 @@ public function prepareRow(Row $row) { + // Find profile values for this row. + $query = $this->select('profile_values', 'pv') + ->fields('pv', ['fid', 'value']); +- $query->leftJoin('profile_fields', 'pf', '[pf].[fid] = [pv].[fid]'); ++ $query->leftJoin('profile_fields', 'pf', $query->joinCondition()->compare('pf.fid', 'pv.fid')); + $query->fields('pf', ['name', 'type']); + $query->condition('uid', $row->getSourceProperty('uid')); + $results = $query->execute(); +@@ -74,7 +74,7 @@ public function fields() { + + $query = $this->select('profile_values', 'pv') + ->fields('pv', ['fid', 'value']); +- $query->leftJoin('profile_fields', 'pf', '[pf].[fid] = [pv].[fid]'); ++ $query->leftJoin('profile_fields', 'pf', $query->joinCondition()->compare('pf.fid', 'pv.fid')); + $query->fields('pf', ['name', 'title']); + $results = $query->execute(); + foreach ($results as $profile) { +diff --git a/core/modules/user/src/Plugin/migrate/source/d7/User.php b/core/modules/user/src/Plugin/migrate/source/d7/User.php +index affcf58b2076d40fd8d2c4b477ed9418c0013453..e5ac71a2931255a5bddf9c561e9135c2d865bbf4 100644 +--- a/core/modules/user/src/Plugin/migrate/source/d7/User.php ++++ b/core/modules/user/src/Plugin/migrate/source/d7/User.php +@@ -100,7 +100,7 @@ public function prepareRow(Row $row) { + if ($this->getDatabase()->schema()->tableExists('profile_value')) { + $query = $this->select('profile_value', 'pv') + ->fields('pv', ['fid', 'value']); +- $query->leftJoin('profile_field', 'pf', '[pf].[fid] = [pv].[fid]'); ++ $query->leftJoin('profile_field', 'pf', $query->joinCondition()->compare('pf.fid', 'pv.fid')); + $query->fields('pf', ['name', 'type']); + $query->condition('uid', $row->getSourceProperty('uid')); + $results = $query->execute(); +diff --git a/core/modules/user/src/Plugin/views/wizard/Users.php b/core/modules/user/src/Plugin/views/wizard/Users.php +index 963f5c16b5922abd75a3cb2e4b4c2ad09c2e0ebc..9d4eeb284044806c15ff1c59e8ad28287844e89c 100644 +--- a/core/modules/user/src/Plugin/views/wizard/Users.php ++++ b/core/modules/user/src/Plugin/views/wizard/Users.php +@@ -2,9 +2,13 @@ + + namespace Drupal\user\Plugin\views\wizard; + ++use Drupal\Core\Database\Connection; ++use Drupal\Core\Entity\EntityTypeBundleInfoInterface; ++use Drupal\Core\Menu\MenuParentFormSelectorInterface; + use Drupal\Core\StringTranslation\TranslatableMarkup; + use Drupal\views\Attribute\ViewsWizard; + use Drupal\views\Plugin\views\wizard\WizardPluginBase; ++use Symfony\Component\DependencyInjection\ContainerInterface; + + /** + * @todo Replace numbers with constants. +@@ -43,6 +47,32 @@ class Users extends WizardPluginBase { + ], + ]; + ++ /** ++ * {@inheritdoc} ++ */ ++ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { ++ return new static( ++ $configuration, ++ $plugin_id, ++ $plugin_definition, ++ $container->get('entity_type.bundle.info'), ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') ++ ); ++ } ++ ++ /** ++ * Constructs a WizardPluginBase object. ++ */ ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { ++ parent::__construct($configuration, $plugin_id, $plugin_definition, $bundle_info_service, $parent_form_selector, $connection); ++ ++ if ($connection->driver() == 'mongodb') { ++ $this->base_table = 'users'; ++ $this->filters['status']['table'] = 'users'; ++ } ++ } ++ + /** + * {@inheritdoc} + */ +@@ -58,7 +88,12 @@ protected function defaultDisplayOptions() { + + /* Field: User: Name */ + $display_options['fields']['name']['id'] = 'name'; +- $display_options['fields']['name']['table'] = 'users_field_data'; ++ if ($this->connection->driver() == 'mongodb') { ++ $display_options['fields']['name']['table'] = 'users'; ++ } ++ else { ++ $display_options['fields']['name']['table'] = 'users_field_data'; ++ } + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'user'; + $display_options['fields']['name']['entity_field'] = 'name'; +diff --git a/core/modules/user/src/UserData.php b/core/modules/user/src/UserData.php +index 8056f33ce13d103fadc5366281be9feb164a28db..129105dedbbba641c90b3f1b551df22e570f907f 100644 +--- a/core/modules/user/src/UserData.php ++++ b/core/modules/user/src/UserData.php +@@ -34,7 +34,7 @@ public function get($module, $uid = NULL, $name = NULL) { + ->fields('ud') + ->condition('module', $module); + if (isset($uid)) { +- $query->condition('uid', $uid); ++ $query->condition('uid', (int) $uid); + } + if (isset($name)) { + $query->condition('name', $name); +diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php +index 9d24d4c1b4d728bf7025a7707989045849822e82..d66049525af1ba133aee3d0434cec65afc0ab4cb 100644 +--- a/core/modules/user/src/UserStorage.php ++++ b/core/modules/user/src/UserStorage.php +@@ -58,7 +58,7 @@ public function updateLastAccessTimestamp(AccountInterface $account, $timestamp) + ->fields([ + 'access' => $timestamp, + ]) +- ->condition('uid', $account->id()) ++ ->condition('uid', (int) $account->id()) + ->execute(); + // Ensure that the entity cache is cleared. + $this->resetCache([$account->id()]); +diff --git a/core/modules/user/src/UserViewsData.php b/core/modules/user/src/UserViewsData.php +index 7581d67b67cbb2544651d809814067a1a2749d41..87a04bd291329c6dc66068efd4563571ba473b2d 100644 +--- a/core/modules/user/src/UserViewsData.php ++++ b/core/modules/user/src/UserViewsData.php +@@ -15,31 +15,40 @@ class UserViewsData extends EntityViewsData { + public function getViewsData() { + $data = parent::getViewsData(); + +- $data['users_field_data']['table']['base']['help'] = $this->t('Users who have created accounts on your site.'); +- $data['users_field_data']['table']['base']['access query tag'] = 'user_access'; +- +- $data['users_field_data']['table']['wizard_id'] = 'user'; +- +- $data['users_field_data']['uid']['argument']['id'] = 'user_uid'; +- $data['users_field_data']['uid']['argument'] += [ +- 'name table' => 'users_field_data', ++ if ($this->connection->driver() == 'mongodb') { ++ $data_table = 'users'; ++ $roles_table = 'users'; ++ } ++ else { ++ $data_table = 'users_field_data'; ++ $roles_table = 'user__roles'; ++ } ++ ++ $data[$data_table]['table']['base']['help'] = $this->t('Users who have created accounts on your site.'); ++ $data[$data_table]['table']['base']['access query tag'] = 'user_access'; ++ ++ $data[$data_table]['table']['wizard_id'] = 'user'; ++ ++ $data[$data_table]['uid']['argument']['id'] = 'user_uid'; ++ $data[$data_table]['uid']['argument'] += [ ++ 'name table' => $data_table, + 'name field' => 'name', + 'empty field name' => \Drupal::config('user.settings')->get('anonymous'), + ]; +- $data['users_field_data']['uid']['filter']['id'] = 'user_name'; +- $data['users_field_data']['uid']['filter']['title'] = $this->t('Name (autocomplete)'); +- $data['users_field_data']['uid']['filter']['help'] = $this->t('The user or author name. Uses an autocomplete widget to find a user name, the actual filter uses the resulting user ID.'); +- $data['users_field_data']['uid']['relationship'] = [ ++ $data[$data_table]['uid']['filter']['id'] = 'user_name'; ++ $data[$data_table]['uid']['filter']['title'] = $this->t('Name (autocomplete)'); ++ $data[$data_table]['uid']['filter']['help'] = $this->t('The user or author name. Uses an autocomplete widget to find a user name, the actual filter uses the resulting user ID.'); ++ $data[$data_table]['uid']['relationship'] = [ + 'title' => $this->t('Content authored'), + 'help' => $this->t('Relate content to the user who created it. This relationship will create one record for each content item created by the user.'), + 'id' => 'standard', +- 'base' => 'node_field_data', ++ 'base' => ($this->connection->driver() == 'mongodb' ? 'node' : 'node_field_data'), + 'base field' => 'uid', + 'field' => 'uid', + 'label' => $this->t('nodes'), + ]; + +- $data['users_field_data']['uid_raw'] = [ ++ $data[$data_table]['uid_raw'] = [ + 'help' => $this->t('The raw numeric user ID.'), + 'real field' => 'uid', + 'filter' => [ +@@ -48,19 +57,19 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['uid_representative'] = [ ++ $data[$data_table]['uid_representative'] = [ + 'relationship' => [ + 'title' => $this->t('Representative node'), + 'label' => $this->t('Representative node'), + 'help' => $this->t('Obtains a single representative node for each user, according to a chosen sort criterion.'), + 'id' => 'groupwise_max', + 'relationship field' => 'uid', +- 'outer field' => 'users_field_data.uid', +- 'argument table' => 'users_field_data', ++ 'outer field' => "$data_table.uid", ++ 'argument table' => $data_table, + 'argument field' => 'uid', +- 'base' => 'node_field_data', ++ 'base' => ($this->connection->driver() == 'mongodb' ? 'node' : 'node_field_data'), + 'field' => 'nid', +- 'relationship' => 'node_field_data:uid', ++ 'relationship' => ($this->connection->driver() == 'mongodb' ? 'node:uid' : 'node_field_data:uid'), + ], + ]; + +@@ -74,22 +83,22 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['name']['help'] = $this->t('The user or author name.'); +- $data['users_field_data']['name']['field']['default_formatter'] = 'user_name'; +- $data['users_field_data']['name']['filter']['title'] = $this->t('Name (raw)'); +- $data['users_field_data']['name']['filter']['help'] = $this->t('The user or author name. This filter does not check if the user exists and allows partial matching. Does not use autocomplete.'); ++ $data[$data_table]['name']['help'] = $this->t('The user or author name.'); ++ $data[$data_table]['name']['field']['default_formatter'] = 'user_name'; ++ $data[$data_table]['name']['filter']['title'] = $this->t('Name (raw)'); ++ $data[$data_table]['name']['filter']['help'] = $this->t('The user or author name. This filter does not check if the user exists and allows partial matching. Does not use autocomplete.'); + + // Note that this field implements field level access control. +- $data['users_field_data']['mail']['help'] = $this->t('Email address for a given user. This field is normally not shown to users, so be cautious when using it.'); ++ $data[$data_table]['mail']['help'] = $this->t('Email address for a given user. This field is normally not shown to users, so be cautious when using it.'); + +- $data['users_field_data']['langcode']['help'] = $this->t('Language of the translation of user information'); ++ $data[$data_table]['langcode']['help'] = $this->t('Language of the translation of user information'); + +- $data['users_field_data']['preferred_langcode']['title'] = $this->t('Preferred language'); +- $data['users_field_data']['preferred_langcode']['help'] = $this->t('Preferred language of the user'); +- $data['users_field_data']['preferred_admin_langcode']['title'] = $this->t('Preferred admin language'); +- $data['users_field_data']['preferred_admin_langcode']['help'] = $this->t('Preferred administrative language of the user'); ++ $data[$data_table]['preferred_langcode']['title'] = $this->t('Preferred language'); ++ $data[$data_table]['preferred_langcode']['help'] = $this->t('Preferred language of the user'); ++ $data[$data_table]['preferred_admin_langcode']['title'] = $this->t('Preferred admin language'); ++ $data[$data_table]['preferred_admin_langcode']['help'] = $this->t('Preferred administrative language of the user'); + +- $data['users_field_data']['created_fulldate'] = [ ++ $data[$data_table]['created_fulldate'] = [ + 'title' => $this->t('Created date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -98,7 +107,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['created_year_month'] = [ ++ $data[$data_table]['created_year_month'] = [ + 'title' => $this->t('Created year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -107,7 +116,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['created_year'] = [ ++ $data[$data_table]['created_year'] = [ + 'title' => $this->t('Created year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -116,7 +125,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['created_month'] = [ ++ $data[$data_table]['created_month'] = [ + 'title' => $this->t('Created month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -125,7 +134,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['created_day'] = [ ++ $data[$data_table]['created_day'] = [ + 'title' => $this->t('Created day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -134,7 +143,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['created_week'] = [ ++ $data[$data_table]['created_week'] = [ + 'title' => $this->t('Created week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -143,12 +152,12 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['status']['filter']['label'] = $this->t('Active'); +- $data['users_field_data']['status']['filter']['type'] = 'yes-no'; ++ $data[$data_table]['status']['filter']['label'] = $this->t('Active'); ++ $data[$data_table]['status']['filter']['type'] = 'yes-no'; + +- $data['users_field_data']['changed']['title'] = $this->t('Updated date'); ++ $data[$data_table]['changed']['title'] = $this->t('Updated date'); + +- $data['users_field_data']['changed_fulldate'] = [ ++ $data[$data_table]['changed_fulldate'] = [ + 'title' => $this->t('Updated date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ +@@ -157,7 +166,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['changed_year_month'] = [ ++ $data[$data_table]['changed_year_month'] = [ + 'title' => $this->t('Updated year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ +@@ -166,7 +175,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['changed_year'] = [ ++ $data[$data_table]['changed_year'] = [ + 'title' => $this->t('Updated year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ +@@ -175,7 +184,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['changed_month'] = [ ++ $data[$data_table]['changed_month'] = [ + 'title' => $this->t('Updated month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ +@@ -184,7 +193,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['changed_day'] = [ ++ $data[$data_table]['changed_day'] = [ + 'title' => $this->t('Updated day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ +@@ -193,7 +202,7 @@ public function getViewsData() { + ], + ]; + +- $data['users_field_data']['changed_week'] = [ ++ $data[$data_table]['changed_week'] = [ + 'title' => $this->t('Updated week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ +@@ -219,14 +228,20 @@ public function getViewsData() { + ], + ]; + ++ // Not sure if this is needed anymore. ++ if ($this->connection->driver() == 'mongodb') { ++ $data[$roles_table]['roles_target_id']['title'] = $this->t('Roles'); ++ $data[$roles_table]['roles_target_id']['help'] = $this->t('Roles that a user belongs to.'); ++ } ++ + // Alter the user roles target_id column. +- $data['user__roles']['roles_target_id']['field']['id'] = 'user_roles'; +- $data['user__roles']['roles_target_id']['field']['no group by'] = TRUE; ++ $data[$roles_table]['roles_target_id']['field']['id'] = 'user_roles'; ++ $data[$roles_table]['roles_target_id']['field']['no group by'] = TRUE; + +- $data['user__roles']['roles_target_id']['filter']['id'] = 'user_roles'; +- $data['user__roles']['roles_target_id']['filter']['allow empty'] = TRUE; ++ $data[$roles_table]['roles_target_id']['filter']['id'] = 'user_roles'; ++ $data[$roles_table]['roles_target_id']['filter']['allow empty'] = TRUE; + +- $data['user__roles']['roles_target_id']['argument'] = [ ++ $data[$roles_table]['roles_target_id']['argument'] = [ + 'id' => 'user__roles_rid', + 'name table' => 'role', + 'name field' => 'name', +@@ -235,7 +250,7 @@ public function getViewsData() { + 'numeric' => FALSE, + ]; + +- $data['user__roles']['permission'] = [ ++ $data[$roles_table]['permission'] = [ + 'title' => $this->t('Permission'), + 'help' => $this->t('The user permissions.'), + 'field' => [ +@@ -250,7 +265,7 @@ public function getViewsData() { + + // Unset the "pass" field because the access control handler for the user + // entity type allows editing the password, but not viewing it. +- unset($data['users_field_data']['pass']); ++ unset($data[$data_table]['pass']); + + return $data; + } +diff --git a/core/modules/user/user.install b/core/modules/user/user.install +index 99ede9e59b764db9271cb034132102277b2d4a89..bc9199b5e64ee5b9dba8c832147ee65abf60db7e 100644 +--- a/core/modules/user/user.install ++++ b/core/modules/user/user.install +@@ -5,6 +5,8 @@ + * Install, update and uninstall functions for the user module. + */ + ++use Drupal\mongodb\Driver\Database\mongodb\Statement; ++ + /** + * Implements hook_schema(). + */ +@@ -62,6 +64,14 @@ function user_schema(): array { + ], + ]; + ++ if (\Drupal::database()->driver() == 'mongodb') { ++ $schema['users_data']['fields']['serialized'] = [ ++ 'description' => 'Whether value is serialized.', ++ 'type' => 'bool', ++ 'default' => FALSE, ++ ]; ++ } ++ + return $schema; + } + +@@ -116,12 +126,58 @@ function user_requirements($phase): array { + ]; + } + +- $query = \Drupal::database()->select('users_field_data'); +- $query->addExpression('LOWER(mail)', 'lower_mail'); +- $query->isNotNull('mail'); +- $query->groupBy('lower_mail'); +- $query->having('COUNT(uid) > :matches', [':matches' => 1]); +- $conflicts = $query->countQuery()->execute()->fetchField(); ++ $connection = \Drupal::database(); ++ if ($connection->driver() === 'mongodb') { ++ $prefixed_table = $connection->getPrefix() . 'users'; ++ $cursor = $connection->getConnection()->selectCollection($prefixed_table)->aggregate( ++ [ ++ [ ++ '$unwind' => ['path' => '$user_translations'], ++ ], ++ [ ++ '$replaceRoot' => [ ++ 'newRoot' => [ ++ '$mergeObjects' => [ ++ '$$ROOT', ++ '$user_translations', ++ ], ++ ], ++ ], ++ ], ++ [ ++ '$match' => [ ++ 'mail' => ['$ne' => NULL], ++ ], ++ ], ++ [ ++ '$addFields' => [ ++ 'lower_mail' => ['$toLower' => '$mail'], ++ ], ++ ], ++ [ ++ '$group' => [ ++ '_id' => '$lower_mail', ++ 'mail_count' => ['$sum' => 1], ++ ], ++ ], ++ [ ++ '$match' => [ ++ 'mail_count' => ['$gt' => 1], ++ ], ++ ], ++ ], ++ ); ++ $statement = new Statement($connection, $cursor, ['mail_count']); ++ $conflicts = $statement->execute()->fetchField(); ++ } ++ else { ++ $query = \Drupal::database()->select('users_field_data'); ++ $query->addExpression('LOWER(mail)', 'lower_mail'); ++ $query->isNotNull('mail'); ++ $query->groupBy('lower_mail'); ++ $query->having('COUNT(uid) > :matches', [':matches' => 1]); ++ $conflicts = $query->countQuery()->execute()->fetchField(); ++ } + + if ($conflicts > 0) { + $return['conflicting emails'] = [ +diff --git a/core/modules/user/user.module b/core/modules/user/user.module +index 907fa44f56c92999a4485aaff462de1349cf9d01..5c3ebf38ad9795c9d44eb8daf6b7a1d802c5e792 100644 +--- a/core/modules/user/user.module ++++ b/core/modules/user/user.module +@@ -108,7 +108,7 @@ function user_is_blocked($name) { + return (bool) \Drupal::entityQuery('user') + ->accessCheck(FALSE) + ->condition('name', $name) +- ->condition('status', 0) ++ ->condition('status', FALSE) + ->execute(); + } + +diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php +index b93be8c7e0bcc85d7dd0fb79bb102d0bac7b4ac0..e06d44c17bb00985836c9c5331d911347f99a0f4 100644 +--- a/core/modules/views/src/Entity/View.php ++++ b/core/modules/views/src/Entity/View.php +@@ -114,6 +114,13 @@ class View extends ConfigEntityBase implements ViewEntityInterface { + */ + protected $module = 'views'; + ++ /** ++ * The MongoDB base table. ++ * ++ * @var string ++ */ ++ public $mongodb_base_table; ++ + /** + * {@inheritdoc} + */ +@@ -448,7 +455,7 @@ public function mergeDefaultDisplaysOptions() { + * {@inheritdoc} + */ + public function isInstallable() { +- $table_definition = \Drupal::service('views.views_data')->get($this->base_table); ++ $table_definition = \Drupal::service('views.views_data')->get($this->get('base_table')); + // Check whether the base table definition exists and contains a base table + // definition. For example, taxonomy_views_data_alter() defines + // node_field_data even if it doesn't exist as a base table. +@@ -530,4 +537,27 @@ public function onDependencyRemoval(array $dependencies) { + return $changed; + } + ++ /** ++ * {@inheritdoc} ++ */ ++ public function get($key) { ++ if (($key == 'base_table') && isset($this->mongodb_base_table)) { ++ return $this->mongodb_base_table; ++ } ++ return parent::get($key); ++ } ++ ++ /** ++ * {@inheritdoc} ++ */ ++ public function toArray() { ++ $properties = parent::toArray(); ++ ++ if (!empty($this->original_base_table)) { ++ $properties['base_table'] = $this->original_base_table; ++ } ++ ++ return $properties; ++ } ++ + } +diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php +index 67e30a95cf442f40150fb417612ee1eb90bff91c..e8afc1a90034845eadd1939901b11a3dfadb5420 100644 +--- a/core/modules/views/src/EntityViewsData.php ++++ b/core/modules/views/src/EntityViewsData.php +@@ -3,6 +3,7 @@ + namespace Drupal\views; + + use Drupal\Component\Utility\NestedArray; ++use Drupal\Core\Database\Connection; + use Drupal\Core\Entity\ContentEntityType; + use Drupal\Core\Entity\EntityFieldManagerInterface; + use Drupal\Core\Entity\EntityHandlerInterface; +@@ -73,6 +74,13 @@ class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterfac + */ + protected $entityFieldManager; + ++ /** ++ * The database connection. ++ * ++ * @var \Drupal\Core\Database\Connection ++ */ ++ protected $connection; ++ + /** + * Constructs an EntityViewsData object. + * +@@ -88,14 +96,17 @@ class EntityViewsData implements EntityHandlerInterface, EntityViewsDataInterfac + * The translation manager. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. ++ * @param \Drupal\Core\Database\Connection $connection ++ * The database connection. + */ +- public function __construct(EntityTypeInterface $entity_type, SqlEntityStorageInterface $storage_controller, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager, EntityFieldManagerInterface $entity_field_manager) { ++ public function __construct(EntityTypeInterface $entity_type, SqlEntityStorageInterface $storage_controller, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager, EntityFieldManagerInterface $entity_field_manager, Connection $connection) { + $this->entityType = $entity_type; + $this->entityTypeManager = $entity_type_manager; + $this->storage = $storage_controller; + $this->moduleHandler = $module_handler; + $this->setStringTranslation($translation_manager); + $this->entityFieldManager = $entity_field_manager; ++ $this->connection = $connection; + } + + /** +@@ -108,7 +119,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('string_translation'), +- $container->get('entity_field.manager') ++ $container->get('entity_field.manager'), ++ $container->get('database') + ); + } + +@@ -131,75 +143,58 @@ public function getViewsData() { + $data = []; + + $base_table = $this->entityType->getBaseTable() ?: $this->entityType->id(); +- $views_revision_base_table = NULL; +- $revisionable = $this->entityType->isRevisionable(); + $entity_id_key = $this->entityType->getKey('id'); ++ $entity_revision_key = $this->entityType->getKey('revision'); ++ $revision_field = $entity_revision_key; ++ $revisionable = $this->entityType->isRevisionable(); ++ $translatable = $this->entityType->isTranslatable(); + $entity_keys = $this->entityType->getKeys(); + +- $revision_table = ''; +- if ($revisionable) { +- $revision_table = $this->entityType->getRevisionTable() ?: $this->entityType->id() . '_revision'; +- } ++ if ($this->connection->driver() == 'mongodb') { ++ // Setup base information of the views data. ++ $data[$base_table]['table']['group'] = $this->entityType->getLabel(); ++ $data[$base_table]['table']['provider'] = $this->entityType->getProvider(); + +- $translatable = $this->entityType->isTranslatable(); +- $data_table = ''; +- if ($translatable) { +- $data_table = $this->entityType->getDataTable() ?: $this->entityType->id() . '_field_data'; +- } ++ $all_revisions_table = ''; ++ $current_revision_table = ''; ++ if ($revisionable) { ++ $all_revisions_table = $this->storage->getJsonStorageAllRevisionsTable(); ++ $data[$base_table]['table']['all revisions table'] = $all_revisions_table; + +- // Some entity types do not have a revision data table defined, but still +- // have a revision table name set in +- // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() so we +- // apply the same kind of logic. +- $revision_data_table = ''; +- if ($revisionable && $translatable) { +- $revision_data_table = $this->entityType->getRevisionDataTable() ?: $this->entityType->id() . '_field_revision'; +- } +- $entity_revision_key = $this->entityType->getKey('revision'); +- $revision_field = $entity_revision_key; ++ $current_revision_table = $this->storage->getJsonStorageCurrentRevisionTable(); ++ $data[$base_table]['table']['current revision table'] = $current_revision_table; + +- // Setup base information of the views data. +- $data[$base_table]['table']['group'] = $this->entityType->getLabel(); +- $data[$base_table]['table']['provider'] = $this->entityType->getProvider(); ++ $data[$base_table]['table']['entity revision field'] = $revision_field; ++ } ++ else { ++ $data[$base_table]['table']['all revisions table'] = FALSE; ++ $data[$base_table]['table']['current revision table'] = FALSE; ++ $data[$base_table]['table']['entity revision field'] = FALSE; ++ } + +- $views_base_table = $base_table; +- if ($data_table) { +- $views_base_table = $data_table; +- } +- $data[$views_base_table]['table']['base'] = [ +- 'field' => $entity_id_key, +- 'title' => $this->entityType->getLabel(), +- 'cache_contexts' => $this->entityType->getListCacheContexts(), +- 'access query tag' => $this->entityType->id() . '_access', +- ]; +- $data[$base_table]['table']['entity revision'] = FALSE; +- +- if ($label_key = $this->entityType->getKey('label')) { +- if ($data_table) { +- $data[$views_base_table]['table']['base']['defaults'] = [ +- 'field' => $label_key, +- 'table' => $data_table, +- ]; ++ if ($translatable && !$revisionable) { ++ $data[$base_table]['table']['translations table'] = $this->storage->getJsonStorageTranslationsTable(); + } + else { +- $data[$views_base_table]['table']['base']['defaults'] = [ ++ $data[$base_table]['table']['translations table'] = FALSE; ++ } ++ ++ $data[$base_table]['table']['base'] = [ ++ 'field' => $entity_id_key, ++ 'title' => $this->entityType->getLabel(), ++ 'cache_contexts' => $this->entityType->getListCacheContexts(), ++ 'access query tag' => $this->entityType->id() . '_access', ++ ]; ++ $data[$base_table]['table']['entity revision'] = $revisionable; ++ ++ if ($label_key = $this->entityType->getKey('label')) { ++ $data[$base_table]['table']['base']['defaults'] = [ + 'field' => $label_key, + ]; + } +- } + +- // Entity types must implement a list_builder in order to use Views' +- // entity operations field. +- if ($this->entityType->hasListBuilderClass()) { +- $data[$base_table]['operations'] = [ +- 'field' => [ +- 'title' => $this->t('Operations links'), +- 'help' => $this->t('Provides links to perform entity operations.'), +- 'id' => 'entity_operations', +- ], +- ]; +- if ($revision_table) { +- $data[$revision_table]['operations'] = [ ++ if ($this->entityType->hasListBuilderClass()) { ++ $data[$base_table]['operations'] = [ + 'field' => [ + 'title' => $this->t('Operations links'), + 'help' => $this->t('Provides links to perform entity operations.'), +@@ -207,168 +202,300 @@ public function getViewsData() { + ], + ]; + } +- } + +- if ($this->entityType->hasViewBuilderClass()) { +- $data[$base_table]['rendered_entity'] = [ +- 'field' => [ +- 'title' => $this->t('Rendered entity'), +- 'help' => $this->t('Renders an entity in a view mode.'), +- 'id' => 'rendered_entity', +- ], +- ]; +- } ++ if ($this->entityType->hasViewBuilderClass()) { ++ $data[$base_table]['rendered_entity'] = [ ++ 'field' => [ ++ 'title' => $this->t('Rendered entity'), ++ 'help' => $this->t('Renders an entity in a view mode.'), ++ 'id' => 'rendered_entity', ++ ], ++ ]; ++ } + +- // Setup relations to the revisions/property data. +- if ($data_table) { +- $data[$base_table]['table']['join'][$data_table] = [ +- 'left_field' => $entity_id_key, +- 'field' => $entity_id_key, +- 'type' => 'INNER', +- ]; +- $data[$data_table]['table']['group'] = $this->entityType->getLabel(); +- $data[$data_table]['table']['provider'] = $this->entityType->getProvider(); +- $data[$data_table]['table']['entity revision'] = FALSE; ++ if ($revisionable) { ++ $data[$base_table]['latest_revision'] = [ ++ 'title' => $this->t('Is Latest Revision'), ++ 'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'), ++ 'filter' => ['id' => 'latest_revision'], ++ ]; ++ if ($translatable) { ++ $data[$base_table]['latest_translation_affected_revision'] = [ ++ 'title' => $this->t('Is Latest Translation Affected Revision'), ++ 'help' => $this->t('Restrict the view to only revisions that are the latest translation affected revision of their entity.'), ++ 'filter' => ['id' => 'latest_translation_affected_revision'], ++ ]; ++ } ++ } ++ ++ $this->addEntityLinks($data[$base_table]); ++ ++ $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id()); ++ ++ $field_storage_definitions = array_map(function (FieldDefinitionInterface $definition) { ++ return $definition->getFieldStorageDefinition(); ++ }, $field_definitions); ++ ++ if ($table_mapping = $this->storage->getTableMapping($field_storage_definitions)) { ++ $duplicate_fields = array_intersect_key($entity_keys, array_flip(['id', 'bundle'])); ++ ++ foreach ($table_mapping->getTableNames() as $table) { ++ foreach ($table_mapping->getFieldNames($table) as $field_name) { ++ if (($table === $current_revision_table) && in_array($field_name, $duplicate_fields)) { ++ continue; ++ } ++ if ($table === $all_revisions_table) { ++ continue; ++ } ++ $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$base_table]); ++ } ++ } ++ } ++ ++ if (($uid_key = $entity_keys['uid'] ?? '')) { ++ $data[$base_table][$uid_key]['filter']['id'] = 'user_name'; ++ } ++ if ($revision_uid_key = $this->entityType->getRevisionMetadataKeys()['revision_user'] ?? '') { ++ $data[$base_table][$revision_uid_key]['filter']['id'] = 'user_name'; ++ } + } +- if ($revision_table) { +- $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); +- $data[$revision_table]['table']['provider'] = $this->entityType->getProvider(); +- $data[$revision_table]['table']['entity revision'] = TRUE; +- +- $views_revision_base_table = $revision_table; +- if ($revision_data_table) { +- $views_revision_base_table = $revision_data_table; ++ else { ++ $views_revision_base_table = NULL; ++ ++ $revision_table = ''; ++ if ($revisionable) { ++ $revision_table = $this->entityType->getRevisionTable() ?: $this->entityType->id() . '_revision'; + } +- $data[$views_revision_base_table]['table']['entity revision'] = TRUE; +- $data[$views_revision_base_table]['table']['base'] = [ +- 'field' => $revision_field, +- 'title' => $this->t('@entity_type revisions', ['@entity_type' => $this->entityType->getLabel()]), +- ]; +- // Join the revision table to the base table. +- $data[$views_revision_base_table]['table']['join'][$views_base_table] = [ +- 'left_field' => $revision_field, +- 'field' => $revision_field, +- 'type' => 'INNER', ++ ++ $translatable = $this->entityType->isTranslatable(); ++ $data_table = ''; ++ if ($translatable) { ++ $data_table = $this->entityType->getDataTable() ?: $this->entityType->id() . '_field_data'; ++ } ++ ++ // Some entity types do not have a revision data table defined, but still ++ // have a revision table name set in ++ // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() so we ++ // apply the same kind of logic. ++ $revision_data_table = ''; ++ if ($revisionable && $translatable) { ++ $revision_data_table = $this->entityType->getRevisionDataTable() ?: $this->entityType->id() . '_field_revision'; ++ } ++ $entity_revision_key = $this->entityType->getKey('revision'); ++ $revision_field = $entity_revision_key; ++ ++ // Setup base information of the views data. ++ $data[$base_table]['table']['group'] = $this->entityType->getLabel(); ++ $data[$base_table]['table']['provider'] = $this->entityType->getProvider(); ++ ++ $views_base_table = $base_table; ++ if ($data_table) { ++ $views_base_table = $data_table; ++ } ++ $data[$views_base_table]['table']['base'] = [ ++ 'field' => $entity_id_key, ++ 'title' => $this->entityType->getLabel(), ++ 'cache_contexts' => $this->entityType->getListCacheContexts(), ++ 'access query tag' => $this->entityType->id() . '_access', + ]; ++ $data[$base_table]['table']['entity revision'] = FALSE; + +- if ($revision_data_table) { +- $data[$revision_data_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); +- $data[$revision_data_table]['table']['entity revision'] = TRUE; ++ if ($label_key = $this->entityType->getKey('label')) { ++ if ($data_table) { ++ $data[$views_base_table]['table']['base']['defaults'] = [ ++ 'field' => $label_key, ++ 'table' => $data_table, ++ ]; ++ } ++ else { ++ $data[$views_base_table]['table']['base']['defaults'] = [ ++ 'field' => $label_key, ++ ]; ++ } ++ } + +- $data[$revision_table]['table']['join'][$revision_data_table] = [ +- 'left_field' => $revision_field, +- 'field' => $revision_field, +- 'type' => 'INNER', ++ // Entity types must implement a list_builder in order to use Views' ++ // entity operations field. ++ if ($this->entityType->hasListBuilderClass()) { ++ $data[$base_table]['operations'] = [ ++ 'field' => [ ++ 'title' => $this->t('Operations links'), ++ 'help' => $this->t('Provides links to perform entity operations.'), ++ 'id' => 'entity_operations', ++ ], + ]; ++ if ($revision_table) { ++ $data[$revision_table]['operations'] = [ ++ 'field' => [ ++ 'title' => $this->t('Operations links'), ++ 'help' => $this->t('Provides links to perform entity operations.'), ++ 'id' => 'entity_operations', ++ ], ++ ]; ++ } + } + +- // Add a filter for showing only the latest revisions of an entity. +- $data[$revision_table]['latest_revision'] = [ +- 'title' => $this->t('Is Latest Revision'), +- 'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'), +- 'filter' => ['id' => 'latest_revision'], +- ]; +- if ($this->entityType->isTranslatable()) { +- $data[$revision_table]['latest_translation_affected_revision'] = [ +- 'title' => $this->t('Is Latest Translation Affected Revision'), +- 'help' => $this->t('Restrict the view to only revisions that are the latest translation affected revision of their entity.'), +- 'filter' => ['id' => 'latest_translation_affected_revision'], ++ if ($this->entityType->hasViewBuilderClass()) { ++ $data[$base_table]['rendered_entity'] = [ ++ 'field' => [ ++ 'title' => $this->t('Rendered entity'), ++ 'help' => $this->t('Renders an entity in a view mode.'), ++ 'id' => 'rendered_entity', ++ ], + ]; + } +- // Add a relationship from the revision table back to the main table. +- $entity_type_label = $this->entityType->getLabel(); +- $data[$views_revision_base_table][$entity_id_key]['relationship'] = [ +- 'id' => 'standard', +- 'base' => $views_base_table, +- 'base field' => $entity_id_key, +- 'title' => $entity_type_label, +- 'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]), +- ]; +- $data[$views_revision_base_table][$entity_revision_key]['relationship'] = [ +- 'id' => 'standard', +- 'base' => $views_base_table, +- 'base field' => $entity_revision_key, +- 'title' => $this->t('@label revision', ['@label' => $entity_type_label]), +- 'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]), +- ]; +- if ($translatable) { +- $extra = [ +- 'field' => $entity_keys['langcode'], +- 'left_field' => $entity_keys['langcode'], ++ ++ // Setup relations to the revisions/property data. ++ if ($data_table) { ++ $data[$base_table]['table']['join'][$data_table] = [ ++ 'left_field' => $entity_id_key, ++ 'field' => $entity_id_key, ++ 'type' => 'INNER', + ]; +- $data[$views_revision_base_table][$entity_id_key]['relationship']['extra'][] = $extra; +- $data[$views_revision_base_table][$entity_revision_key]['relationship']['extra'][] = $extra; +- $data[$revision_table]['table']['join'][$views_base_table]['left_field'] = $entity_revision_key; +- $data[$revision_table]['table']['join'][$views_base_table]['field'] = $entity_revision_key; ++ $data[$data_table]['table']['group'] = $this->entityType->getLabel(); ++ $data[$data_table]['table']['provider'] = $this->entityType->getProvider(); ++ $data[$data_table]['table']['entity revision'] = FALSE; + } ++ if ($revision_table) { ++ $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); ++ $data[$revision_table]['table']['provider'] = $this->entityType->getProvider(); ++ $data[$revision_table]['table']['entity revision'] = TRUE; + +- } ++ $views_revision_base_table = $revision_table; ++ if ($revision_data_table) { ++ $views_revision_base_table = $revision_data_table; ++ } ++ $data[$views_revision_base_table]['table']['entity revision'] = TRUE; ++ $data[$views_revision_base_table]['table']['base'] = [ ++ 'field' => $revision_field, ++ 'title' => $this->t('@entity_type revisions', ['@entity_type' => $this->entityType->getLabel()]), ++ ]; ++ // Join the revision table to the base table. ++ $data[$views_revision_base_table]['table']['join'][$views_base_table] = [ ++ 'left_field' => $revision_field, ++ 'field' => $revision_field, ++ 'type' => 'INNER', ++ ]; + +- $this->addEntityLinks($data[$base_table]); +- if ($views_revision_base_table) { +- $this->addEntityLinks($data[$views_revision_base_table]); +- } ++ if ($revision_data_table) { ++ $data[$revision_data_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); ++ $data[$revision_data_table]['table']['entity revision'] = TRUE; + +- // Load all typed data definitions of all fields. This should cover each of +- // the entity base, revision, data tables. +- $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id()); +- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ +- $table_mapping = $this->storage->getTableMapping($field_definitions); +- // Fetch all fields that can appear in both the base table and the data +- // table. +- $duplicate_fields = array_intersect_key($entity_keys, array_flip(['id', 'revision', 'bundle'])); +- // Iterate over each table we have so far and collect field data for each. +- // Based on whether the field is in the field_definitions provided by the +- // entity field manager. +- // @todo We should better just rely on information coming from the entity +- // storage. +- // @todo https://www.drupal.org/node/2337511 +- foreach ($table_mapping->getTableNames() as $table) { +- foreach ($table_mapping->getFieldNames($table) as $field_name) { +- // To avoid confusing duplication in the user interface, for fields +- // that are on both base and data tables, only add them on the data +- // table (same for revision vs. revision data). +- if ($data_table && ($table === $base_table || $table === $revision_table) && in_array($field_name, $duplicate_fields)) { +- continue; ++ $data[$revision_table]['table']['join'][$revision_data_table] = [ ++ 'left_field' => $revision_field, ++ 'field' => $revision_field, ++ 'type' => 'INNER', ++ ]; ++ } ++ ++ // Add a filter for showing only the latest revisions of an entity. ++ $data[$revision_table]['latest_revision'] = [ ++ 'title' => $this->t('Is Latest Revision'), ++ 'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'), ++ 'filter' => ['id' => 'latest_revision'], ++ ]; ++ if ($this->entityType->isTranslatable()) { ++ $data[$revision_table]['latest_translation_affected_revision'] = [ ++ 'title' => $this->t('Is Latest Translation Affected Revision'), ++ 'help' => $this->t('Restrict the view to only revisions that are the latest translation affected revision of their entity.'), ++ 'filter' => ['id' => 'latest_translation_affected_revision'], ++ ]; ++ } ++ // Add a relationship from the revision table back to the main table. ++ $entity_type_label = $this->entityType->getLabel(); ++ $data[$views_revision_base_table][$entity_id_key]['relationship'] = [ ++ 'id' => 'standard', ++ 'base' => $views_base_table, ++ 'base field' => $entity_id_key, ++ 'title' => $entity_type_label, ++ 'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]), ++ ]; ++ $data[$views_revision_base_table][$entity_revision_key]['relationship'] = [ ++ 'id' => 'standard', ++ 'base' => $views_base_table, ++ 'base field' => $entity_revision_key, ++ 'title' => $this->t('@label revision', ['@label' => $entity_type_label]), ++ 'help' => $this->t('Get the actual @label from a @label revision', ['@label' => $entity_type_label]), ++ ]; ++ if ($translatable) { ++ $extra = [ ++ 'field' => $entity_keys['langcode'], ++ 'left_field' => $entity_keys['langcode'], ++ ]; ++ $data[$views_revision_base_table][$entity_id_key]['relationship']['extra'][] = $extra; ++ $data[$views_revision_base_table][$entity_revision_key]['relationship']['extra'][] = $extra; ++ $data[$revision_table]['table']['join'][$views_base_table]['left_field'] = $entity_revision_key; ++ $data[$revision_table]['table']['join'][$views_base_table]['field'] = $entity_revision_key; + } +- $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]); ++ + } +- } + +- foreach ($field_definitions as $field_definition) { +- if ($table_mapping->requiresDedicatedTableStorage($field_definition->getFieldStorageDefinition())) { +- $table = $table_mapping->getDedicatedDataTableName($field_definition->getFieldStorageDefinition()); ++ $this->addEntityLinks($data[$base_table]); ++ if ($views_revision_base_table) { ++ $this->addEntityLinks($data[$views_revision_base_table]); ++ } + +- $data[$table]['table']['group'] = $this->entityType->getLabel(); +- $data[$table]['table']['provider'] = $this->entityType->getProvider(); +- $data[$table]['table']['join'][$views_base_table] = [ +- 'left_field' => $entity_id_key, +- 'field' => 'entity_id', +- 'extra' => [ +- ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], +- ], +- ]; ++ // Load all typed data definitions of all fields. This should cover each of ++ // the entity base, revision, data tables. ++ $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id()); ++ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ ++ $table_mapping = $this->storage->getTableMapping($field_definitions); ++ // Fetch all fields that can appear in both the base table and the data ++ // table. ++ $duplicate_fields = array_intersect_key($entity_keys, array_flip(['id', 'revision', 'bundle'])); ++ // Iterate over each table we have so far and collect field data for each. ++ // Based on whether the field is in the field_definitions provided by the ++ // entity field manager. ++ // @todo We should better just rely on information coming from the entity ++ // storage. ++ // @todo https://www.drupal.org/node/2337511 ++ foreach ($table_mapping->getTableNames() as $table) { ++ foreach ($table_mapping->getFieldNames($table) as $field_name) { ++ // To avoid confusing duplication in the user interface, for fields ++ // that are on both base and data tables, only add them on the data ++ // table (same for revision vs. revision data). ++ if ($data_table && ($table === $base_table || $table === $revision_table) && in_array($field_name, $duplicate_fields)) { ++ continue; ++ } ++ $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]); ++ } ++ } + +- if ($revisionable) { +- $revision_table = $table_mapping->getDedicatedRevisionTableName($field_definition->getFieldStorageDefinition()); ++ foreach ($field_definitions as $field_definition) { ++ if ($table_mapping->requiresDedicatedTableStorage($field_definition->getFieldStorageDefinition())) { ++ $table = $table_mapping->getDedicatedDataTableName($field_definition->getFieldStorageDefinition()); + +- $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); +- $data[$revision_table]['table']['provider'] = $this->entityType->getProvider(); +- $data[$revision_table]['table']['join'][$views_revision_base_table] = [ +- 'left_field' => $revision_field, ++ $data[$table]['table']['group'] = $this->entityType->getLabel(); ++ $data[$table]['table']['provider'] = $this->entityType->getProvider(); ++ $data[$table]['table']['join'][$views_base_table] = [ ++ 'left_field' => $entity_id_key, + 'field' => 'entity_id', + 'extra' => [ + ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], + ], + ]; ++ ++ if ($revisionable) { ++ $revision_table = $table_mapping->getDedicatedRevisionTableName($field_definition->getFieldStorageDefinition()); ++ ++ $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); ++ $data[$revision_table]['table']['provider'] = $this->entityType->getProvider(); ++ $data[$revision_table]['table']['join'][$views_revision_base_table] = [ ++ 'left_field' => $revision_field, ++ 'field' => 'entity_id', ++ 'extra' => [ ++ ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], ++ ], ++ ]; ++ } + } + } +- } +- if (($uid_key = $entity_keys['uid'] ?? '')) { +- $data[$data_table][$uid_key]['filter']['id'] = 'user_name'; +- } +- if ($revision_table && ($revision_uid_key = $this->entityType->getRevisionMetadataKeys()['revision_user'] ?? '')) { +- $data[$revision_table][$revision_uid_key]['filter']['id'] = 'user_name'; ++ if (($uid_key = $entity_keys['uid'] ?? '')) { ++ $data[$data_table][$uid_key]['filter']['id'] = 'user_name'; ++ } ++ if ($revision_table && ($revision_uid_key = $this->entityType->getRevisionMetadataKeys()['revision_user'] ?? '')) { ++ $data[$revision_table][$revision_uid_key]['filter']['id'] = 'user_name'; ++ } + } + + // Add the entity type key to each table generated. +@@ -438,20 +565,73 @@ protected function mapFieldDefinition($table, $field_name, FieldDefinitionInterf + $field_column_mapping = $table_mapping->getColumnNames($field_name); + $field_schema = $this->getFieldStorageDefinitions()[$field_name]->getSchema(); + +- $field_definition_type = $field_definition->getType(); +- // Add all properties to views table data. We need an entry for each +- // column of each field, with the first one given special treatment. +- // @todo Introduce concept of the "main" column for a field, rather than +- // assuming the first one is the main column. See also what the +- // mapSingleFieldViewsData() method does with $first. +- $first = TRUE; +- foreach ($field_column_mapping as $field_column_name => $schema_field_name) { +- // The fields might be defined before the actual table. +- $table_data = $table_data ?: []; +- $table_data += [$schema_field_name => []]; +- $table_data[$schema_field_name] = NestedArray::mergeDeep($table_data[$schema_field_name], $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $field_column_name, $field_schema['columns'][$field_column_name]['type'], $first, $field_definition)); +- $table_data[$schema_field_name]['entity field'] = $field_name; +- $first = FALSE; ++ if ($this->connection->driver() == 'mongodb') { ++ $base_table = $this->entityType->getBaseTable() ?: $this->entityType->id(); ++ $field_definition_type = $field_definition->getType(); ++ ++ // $multiple = (count($field_column_mapping) > 1); ++ $first = TRUE; ++ foreach ($field_column_mapping as $field_column_name => $schema_field_name) { ++ // $views_field_name = ($multiple) ? $field_name . '__' . $field_column_name : $field_name; ++ // $table_data[$views_field_name] = $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $field_column_name, $field_schema['columns'][$field_column_name]['type'], $first, $field_definition); ++ // $table_data[$views_field_name]['entity field'] = $field_name; ++ ++ $table_data[$schema_field_name] = $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $field_column_name, $field_schema['columns'][$field_column_name]['type'], $first, $field_definition); ++ $table_data[$schema_field_name]['entity field'] = $field_name; ++ $first = FALSE; ++ ++ if ($table != $base_table) { ++ if ($table_mapping->requiresDedicatedTableStorage($field_definition->getFieldStorageDefinition())) { ++ if ($this->entityType->isRevisionable()) { ++ // $table_data[$views_field_name]['real field'] = $this->storage->getCurrentRevisionTable() . '.' . $table . '.' . $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $this->storage->getJsonStorageCurrentRevisionTable() . '.' . $table . '.' . $schema_field_name; ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ // $table_data[$views_field_name]['real field'] = $this->storage->getTranslationsTable() . '.' . $table . '.' . $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $this->storage->getJsonStorageTranslationsTable() . '.' . $table . '.' . $schema_field_name; ++ } ++ else { ++ // $table_data[$views_field_name]['real field'] = $table . '.' . $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $table . '.' . $schema_field_name; ++ } ++ } ++ elseif ($field_name != $this->entityType->getKey('id')) { ++ if ($this->entityType->isRevisionable()) { ++ // $table_data[$views_field_name]['real field'] = $this->storage->getCurrentRevisionTable() . '.' . $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $this->storage->getJsonStorageCurrentRevisionTable() . '.' . $schema_field_name; ++ } ++ elseif ($this->entityType->isTranslatable()) { ++ // $table_data[$views_field_name]['real field'] = $this->storage->getTranslationsTable() . '.' . $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $this->storage->getJsonStorageTranslationsTable() . '.' . $schema_field_name; ++ } ++ else { ++ // $table_data[$views_field_name]['real field'] = $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $schema_field_name; ++ } ++ } ++ else { ++ // $table_data[$views_field_name]['real field'] = $views_field_name; ++ $table_data[$schema_field_name]['real field'] = $schema_field_name; ++ } ++ } ++ } ++ } ++ else { ++ $field_definition_type = $field_definition->getType(); ++ // Add all properties to views table data. We need an entry for each ++ // column of each field, with the first one given special treatment. ++ // @todo Introduce concept of the "main" column for a field, rather than ++ // assuming the first one is the main column. See also what the ++ // mapSingleFieldViewsData() method does with $first. ++ $first = TRUE; ++ foreach ($field_column_mapping as $field_column_name => $schema_field_name) { ++ // The fields might be defined before the actual table. ++ $table_data = $table_data ?: []; ++ $table_data += [$schema_field_name => []]; ++ $table_data[$schema_field_name] = NestedArray::mergeDeep($table_data[$schema_field_name], $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $field_column_name, $field_schema['columns'][$field_column_name]['type'], $first, $field_definition)); ++ $table_data[$schema_field_name]['entity field'] = $field_name; ++ $first = FALSE; ++ } + } + } + +@@ -705,7 +885,13 @@ protected function processViewsDataForUuid($table, FieldDefinitionInterface $fie + * {@inheritdoc} + */ + public function getViewsTableForEntityType(EntityTypeInterface $entity_type) { +- return $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ if ($this->connection->driver() == 'mongodb') { ++ // For MongoDB this is always the entity base table. ++ return $entity_type->getBaseTable(); ++ } ++ else { ++ return $entity_type->getDataTable() ?: $entity_type->getBaseTable(); ++ } + } + + } +diff --git a/core/modules/views/src/Hook/ViewsHooks.php b/core/modules/views/src/Hook/ViewsHooks.php +index f26b51ef05d1c6d3813a06472568861b1f85ed85..c4aa8533d779156de1af27e8a8d1bc496277214d 100644 +--- a/core/modules/views/src/Hook/ViewsHooks.php ++++ b/core/modules/views/src/Hook/ViewsHooks.php +@@ -6,6 +6,7 @@ + use Drupal\views\ViewEntityInterface; + use Drupal\views\Plugin\Derivative\ViewsLocalTask; + use Drupal\Core\Database\Query\AlterableInterface; ++use Drupal\Core\Database\Query\ConditionInterface; + use Drupal\Core\Form\FormStateInterface; + use Drupal\Core\Entity\EntityInterface; + use Drupal\views\ViewExecutable; +@@ -350,6 +351,10 @@ public function queryViewsAlter(AlterableInterface $query): void { + } + } + } ++ if (isset($table_metadata['condition']) && ($table_metadata['condition'] instanceof ConditionInterface)) { ++ $table_conditions = &$tables[$table_name]['condition']->conditions(); ++ _views_query_tag_alter_condition($query, $table_conditions, $substitutions); ++ } + } + // Replaces substitutions in filter criteria. + _views_query_tag_alter_condition($query, $where, $substitutions); +diff --git a/core/modules/views/src/Hook/ViewsViewsHooks.php b/core/modules/views/src/Hook/ViewsViewsHooks.php +index 623d32e9c0594ff47140d84cca3a80950c7ba266..3f4aa4e43061c94515e15ff037977306ca73adcc 100644 +--- a/core/modules/views/src/Hook/ViewsViewsHooks.php ++++ b/core/modules/views/src/Hook/ViewsViewsHooks.php +@@ -218,6 +218,7 @@ public function viewsDataAlter(&$data): void { + */ + #[Hook('field_views_data')] + public function fieldViewsData(FieldStorageConfigInterface $field_storage): array { ++ $driver = \Drupal::database()->driver(); + $data = views_field_default_views_data($field_storage); + // The code below only deals with the Entity reference field type. + if ($field_storage->getType() != 'entity_reference') { +@@ -233,8 +234,19 @@ public function fieldViewsData(FieldStorageConfigInterface $field_storage): arra + $target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_type = $entity_type_manager->getDefinition($entity_type_id); +- $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); ++ if ($driver === 'mongodb') { ++ $target_base_table = $target_entity_type->getBaseTable(); ++ } ++ else { ++ $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); ++ } + $field_name = $field_storage->getName(); ++ ++ $relationship_field = $field_name . '_target_id'; ++ if (($driver === 'mongodb') && isset($table_data[$field_name]['field']['real field'])) { ++ $relationship_field = $table_data[$field_name]['field']['real field']; ++ } ++ + if ($target_entity_type instanceof ContentEntityTypeInterface) { + // Provide a relationship for the entity type with the entity reference + // field. +@@ -250,35 +262,38 @@ public function fieldViewsData(FieldStorageConfigInterface $field_storage): arra + 'base' => $target_base_table, + 'entity type' => $target_entity_type_id, + 'base field' => $target_entity_type->getKey('id'), +- 'relationship field' => $field_name . '_target_id', +- ]; +- // Provide a reverse relationship for the entity type that is referenced by +- // the field. +- $args['@entity'] = $entity_type->getLabel(); +- $args['@label'] = $target_entity_type->getSingularLabel(); +- $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; +- $data[$target_base_table][$pseudo_field_name]['relationship'] = [ +- 'title' => t('@entity using @field_name', $args), +- 'label' => t('@field_name', [ +- '@field_name' => $field_name, +- ]), +- 'group' => $target_entity_type->getLabel(), +- 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), +- 'id' => 'entity_reverse', +- 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), +- 'entity_type' => $entity_type_id, +- 'base field' => $entity_type->getKey('id'), +- 'field_name' => $field_name, +- 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), +- 'field field' => $field_name . '_target_id', +- 'join_extra' => [ +- [ +- 'field' => 'deleted', +- 'value' => 0, +- 'numeric' => TRUE, +- ], +- ], ++ 'relationship field' => $relationship_field, + ]; ++ // MongoDB does not need reverse relationships. ++ if ($driver != 'mongodb') { ++ // Provide a reverse relationship for the entity type that is referenced by ++ // the field. ++ $args['@entity'] = $entity_type->getLabel(); ++ $args['@label'] = $target_entity_type->getSingularLabel(); ++ $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; ++ $data[$target_base_table][$pseudo_field_name]['relationship'] = [ ++ 'title' => t('@entity using @field_name', $args), ++ 'label' => t('@field_name', [ ++ '@field_name' => $field_name, ++ ]), ++ 'group' => $target_entity_type->getLabel(), ++ 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), ++ 'id' => 'entity_reverse', ++ 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), ++ 'entity_type' => $entity_type_id, ++ 'base field' => $entity_type->getKey('id'), ++ 'field_name' => $field_name, ++ 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), ++ 'field field' => $field_name . '_target_id', ++ 'join_extra' => [ ++ [ ++ 'field' => 'deleted', ++ 'value' => 0, ++ 'numeric' => TRUE, ++ ], ++ ], ++ ]; ++ } + } + // Provide an argument plugin that has a meaningful titleQuery() + // implementation getting the entity label. +diff --git a/core/modules/views/src/ManyToOneHelper.php b/core/modules/views/src/ManyToOneHelper.php +index b1583b2a9f00500f6ae1cd1e2884843d5640c488..53274210260c8143b1c91817c9d16596bddf586f 100644 +--- a/core/modules/views/src/ManyToOneHelper.php ++++ b/core/modules/views/src/ManyToOneHelper.php +@@ -65,7 +65,12 @@ public function getField() { + return $this->handler->getFormula(); + } + else { +- return $this->handler->tableAlias . '.' . $this->handler->realField; ++ if (($this->handler->view->getDatabaseDriver() == 'mongodb') && ($this->handler->table == $this->handler->view->storage->get('base_table'))) { ++ return $this->handler->realField; ++ } ++ else { ++ return $this->handler->tableAlias . '.' . $this->handler->realField; ++ } + } + } + +@@ -330,18 +335,44 @@ public function addFilter() { + $placeholder .= '[]'; + + if ($operator == 'IS NULL') { +- $this->handler->query->addWhereExpression($options['group'], "$field $operator"); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb') { ++ $condition = $this->handler->view->getDatabaseCondition('AND'); ++ $condition->condition($field, $this->handler->value, 'NOT IN'); ++ $this->handler->query->addCondition($options['group'], $condition); ++ } ++ else { ++ $this->handler->query->addWhereExpression($options['group'], "$field $operator"); ++ } + } + else { +- $this->handler->query->addWhereExpression($options['group'], "$field $operator($placeholder)", [$placeholder => $value]); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb') { ++ $condition = $this->handler->view->getDatabaseCondition('AND'); ++ $condition->condition($field, $this->handler->value, 'IN'); ++ $this->handler->query->addCondition($options['group'], $condition); ++ } ++ else { ++ $this->handler->query->addWhereExpression($options['group'], "$field $operator($placeholder)", [$placeholder => $value]); ++ } + } + } + else { + if ($operator == 'IS NULL') { +- $this->handler->query->addWhereExpression($options['group'], "$field $operator"); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb') { ++ $condition = $this->handler->view->getDatabaseCondition('AND'); ++ $condition->condition($field, $this->handler->value, 'NOT IN'); ++ $this->handler->query->addCondition($options['group'], $condition); ++ } ++ else { ++ $this->handler->query->addWhereExpression($options['group'], "$field $operator"); ++ } + } + else { +- $this->handler->query->addWhereExpression($options['group'], "$field $operator $placeholder", [$placeholder => $value]); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb') { ++ $this->handler->query->addCondition($options['group'], $field, $value, $operator); ++ } ++ else { ++ $this->handler->query->addWhereExpression($options['group'], "$field $operator $placeholder", [$placeholder => $value]); ++ } + } + } + } +@@ -351,11 +382,22 @@ public function addFilter() { + $field = $this->handler->realField; + $clause = $operator == 'or' ? $this->handler->query->getConnection()->condition('OR') : $this->handler->query->getConnection()->condition('AND'); + foreach ($this->handler->tableAliases as $value => $alias) { +- $clause->condition("$alias.$field", $value); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb' && (in_array($alias, [NULL, $this->handler->table, $this->handler->tableAlias], TRUE))) { ++ $clause->condition($field, $value); ++ } ++ else { ++ $clause->condition("$alias.$field", $value); ++ } + } + +- // Implode on either AND or OR. +- $this->handler->query->addWhere($options['group'], $clause); ++ if ($this->handler->view->getDatabaseDriver() == 'mongodb') { ++ $clause->useElementMatch(FALSE); ++ $this->handler->query->addCondition($options['group'], $clause); ++ } ++ else { ++ // Implode on either AND or OR. ++ $this->handler->query->addWhere($options['group'], $clause); ++ } + } + } + +diff --git a/core/modules/views/src/Plugin/Derivative/ViewsEntityRow.php b/core/modules/views/src/Plugin/Derivative/ViewsEntityRow.php +index 757b574189b48bf2a73098d0fb3099e657a527f3..0b78129a34a9a6740df924c84eb1889d76ea58a1 100644 +--- a/core/modules/views/src/Plugin/Derivative/ViewsEntityRow.php ++++ b/core/modules/views/src/Plugin/Derivative/ViewsEntityRow.php +@@ -2,6 +2,7 @@ + + namespace Drupal\views\Plugin\Derivative; + ++use Drupal\Core\Database\Connection; + use Drupal\Core\Entity\EntityTypeManagerInterface; + use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; + use Drupal\Core\StringTranslation\StringTranslationTrait; +@@ -47,6 +48,13 @@ class ViewsEntityRow implements ContainerDeriverInterface { + */ + protected $viewsData; + ++ /** ++ * The database connection. ++ * ++ * @var \Drupal\Core\Database\Connection ++ */ ++ protected $connection; ++ + /** + * Constructs a ViewsEntityRow object. + * +@@ -56,11 +64,14 @@ class ViewsEntityRow implements ContainerDeriverInterface { + * The entity type manager. + * @param \Drupal\views\ViewsData $views_data + * The views data service. ++ * @param \Drupal\Core\Database\Connection $connection ++ * The database connection. + */ +- public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, ViewsData $views_data) { ++ public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, ViewsData $views_data, Connection $connection) { + $this->basePluginId = $base_plugin_id; + $this->entityTypeManager = $entity_type_manager; + $this->viewsData = $views_data; ++ $this->connection = $connection; + } + + /** +@@ -70,7 +81,8 @@ public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('entity_type.manager'), +- $container->get('views.views_data') ++ $container->get('views.views_data'), ++ $container->get('database') + ); + } + +@@ -102,6 +114,9 @@ public function getDerivativeDefinitions($base_plugin_definition) { + 'display_types' => ['normal'], + 'class' => $base_plugin_definition['class'], + ]; ++ if ($this->connection->driver() == 'mongodb') { ++ $this->derivatives[$entity_type_id]['base'] = [$entity_type->getBaseTable()]; ++ } + } + } + +diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +index ab4f399f72b0e0efce06f3a082d759ec41ee53ae..09c18178d7584f9f848bf64430546d959b4154fe 100644 +--- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php ++++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +@@ -1022,7 +1022,18 @@ public function summaryName($data) { + */ + public function query($group_by = FALSE) { + $this->ensureMyTable(); +- $this->query->addWhere(0, "$this->tableAlias.$this->realField", $this->argument); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if ($this->table == $this->view->storage->get('base_table')) { ++ $field = $this->realField; ++ } ++ else { ++ $field = "$this->tableAlias.$this->realField"; ++ } ++ $this->query->addCondition(0, $field, $this->argument); ++ } ++ else { ++ $this->query->addWhere(0, "$this->tableAlias.$this->realField", $this->argument); ++ } + } + + /** +diff --git a/core/modules/views/src/Plugin/views/argument/Formula.php b/core/modules/views/src/Plugin/views/argument/Formula.php +index 1bfc914d5ae960074c8df473177992cc073b08e3..434fc2d49a10f2897df6e7d5f87146615685b22e 100644 +--- a/core/modules/views/src/Plugin/views/argument/Formula.php ++++ b/core/modules/views/src/Plugin/views/argument/Formula.php +@@ -43,12 +43,19 @@ public function getFormula() { + */ + protected function summaryQuery() { + $this->ensureMyTable(); +- // Now that our table is secure, get our formula. +- $formula = $this->getFormula(); + +- // Add the field. +- $this->base_alias = $this->name_alias = $this->query->addField(NULL, $formula, $this->field); +- $this->query->setCountField(NULL, $formula, $this->field); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $this->query->addDateDateFormattedField($this->field, $this->realField, $this->getDateFormat($this->argFormat)); ++ $this->base_alias = $this->name_alias = $this->query->addField(NULL, $this->realField, $this->field); ++ } ++ else { ++ // Now that our table is secure, get our formula. ++ $formula = $this->getFormula(); ++ ++ // Add the field. ++ $this->base_alias = $this->name_alias = $this->query->addField(NULL, $formula, $this->field); ++ $this->query->setCountField(NULL, $formula, $this->field); ++ } + + return $this->summaryBasics(FALSE); + } +@@ -58,13 +65,32 @@ protected function summaryQuery() { + */ + public function query($group_by = FALSE) { + $this->ensureMyTable(); +- // Now that our table is secure, get our formula. +- $placeholder = $this->placeholder(); +- $formula = $this->getFormula() . ' = ' . $placeholder; +- $placeholders = [ +- $placeholder => $this->argument, +- ]; +- $this->query->addWhere(0, $formula, $placeholders, 'formula'); ++ ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if ($this->relationship) { ++ $field = "$this->tableAlias.$this->realField"; ++ } ++ else { ++ $field = $this->realField; ++ } ++ ++ $values = [ ++ 'format' => $this->getDateFormat($this->argFormat), ++ 'value' => $this->argument, ++ 'timezone' => $this->query->setupTimezone(), ++ ]; ++ ++ $this->query->addCondition(0, $field, $values, $this->mongodbOperator); ++ } ++ else { ++ // Now that our table is secure, get our formula. ++ $placeholder = $this->placeholder(); ++ $formula = $this->getFormula() . ' = ' . $placeholder; ++ $placeholders = [ ++ $placeholder => $this->argument, ++ ]; ++ $this->query->addWhere(0, $formula, $placeholders, 'formula'); ++ } + } + + } +diff --git a/core/modules/views/src/Plugin/views/argument/NumericArgument.php b/core/modules/views/src/Plugin/views/argument/NumericArgument.php +index 39bb662ec7980fd3ec16989a8a86d4ebc8039fcc..d12a211169cd2eb728b3d92e92ee4bc1b9a2bb8c 100644 +--- a/core/modules/views/src/Plugin/views/argument/NumericArgument.php ++++ b/core/modules/views/src/Plugin/views/argument/NumericArgument.php +@@ -104,14 +104,47 @@ public function query($group_by = FALSE) { + $placeholder = $this->placeholder(); + $null_check = empty($this->options['not']) ? '' : " OR $this->tableAlias.$this->realField IS NULL"; + ++ if (($this->view->getDatabaseDriver() == 'mongodb') && ($this->table == $this->view->storage->get('base_table'))) { ++ $field = $this->realField; ++ } ++ else { ++ $field = "$this->tableAlias.$this->realField"; ++ } ++ + if (count($this->value) > 1) { +- $operator = empty($this->options['not']) ? 'IN' : 'NOT IN'; +- $placeholder .= '[]'; +- $this->query->addWhereExpression(0, "$this->tableAlias.$this->realField $operator($placeholder)" . $null_check, [$placeholder => $this->value]); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if (empty($this->options['not'])) { ++ $this->query->addCondition(0, $field, $this->value, 'IN'); ++ } ++ else { ++ $or_condition = $this->view->getDatabaseCondition('OR'); ++ $or_condition->condition($field, $this->value, 'NOT IN'); ++ $or_condition->isNull($field); ++ $this->query->addCondition(0, $or_condition); ++ } ++ } ++ else { ++ $operator = empty($this->options['not']) ? 'IN' : 'NOT IN'; ++ $placeholder .= '[]'; ++ $this->query->addWhereExpression(0, "$this->tableAlias.$this->realField $operator($placeholder)" . $null_check, [$placeholder => $this->value]); ++ } + } + else { +- $operator = empty($this->options['not']) ? '=' : '!='; +- $this->query->addWhereExpression(0, "$this->tableAlias.$this->realField $operator $placeholder" . $null_check, [$placeholder => $this->argument]); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if (empty($this->options['not'])) { ++ $this->query->addCondition(0, $field, $this->argument, '='); ++ } ++ else { ++ $or_condition = $this->view->getDatabaseCondition('OR'); ++ $or_condition->condition($field, $this->argument, '!='); ++ $or_condition->isNull($field); ++ $this->query->addCondition(0, $or_condition); ++ } ++ } ++ else { ++ $operator = empty($this->options['not']) ? '=' : '!='; ++ $this->query->addWhereExpression(0, "$this->tableAlias.$this->realField $operator $placeholder" . $null_check, [$placeholder => $this->argument]); ++ } + } + } + +diff --git a/core/modules/views/src/Plugin/views/argument/StringArgument.php b/core/modules/views/src/Plugin/views/argument/StringArgument.php +index 15a68e0e608d43737407a7385f31f636a0e844de..bcc7b2859ea992d79dc3a05229758253adb19593 100644 +--- a/core/modules/views/src/Plugin/views/argument/StringArgument.php ++++ b/core/modules/views/src/Plugin/views/argument/StringArgument.php +@@ -167,9 +167,16 @@ protected function summaryQuery() { + } + else { + // Add the field. +- $formula = $this->getFormula(); +- $this->base_alias = $this->query->addField(NULL, $formula, $this->field . '_truncated'); +- $this->query->setCountField(NULL, $formula, $this->field, $this->field . '_truncated'); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $this->base_alias = $this->field . '_truncated'; ++ $this->query->addSubstringField($this->base_alias, $this->field, 0, intval($this->options['limit'])); ++ } ++ else { ++ // Add the field. ++ $formula = $this->getFormula(); ++ $this->base_alias = $this->query->addField(NULL, $formula, $this->field . '_truncated'); ++ $this->query->setCountField(NULL, $formula, $this->field, $this->field . '_truncated'); ++ } + } + + $this->summaryNameField(); +@@ -238,7 +245,12 @@ public function query($group_by = FALSE) { + $this->ensureMyTable(); + $formula = FALSE; + if (empty($this->options['glossary'])) { +- $field = "$this->tableAlias.$this->realField"; ++ if (($this->view->getDatabaseDriver() == 'mongodb') && ($this->table == $this->view->storage->get('base_table'))) { ++ $field = $this->realField; ++ } ++ else { ++ $field = "$this->tableAlias.$this->realField"; ++ } + } + else { + $formula = TRUE; +@@ -264,10 +276,20 @@ public function query($group_by = FALSE) { + $placeholders = [ + $placeholder => $argument, + ]; +- $this->query->addWhereExpression(0, $field, $placeholders); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $this->query->addSubstringField($this->realField . '_truncated', $field, 0, intval($this->options['limit'])); ++ } ++ else { ++ $this->query->addWhereExpression(0, $field, $placeholders); ++ } + } + else { +- $this->query->addWhere(0, $field, $argument, $operator); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $this->query->addCondition(0, $field, $argument, $operator); ++ } ++ else { ++ $this->query->addWhere(0, $field, $argument, $operator); ++ } + } + } + +diff --git a/core/modules/views/src/Plugin/views/display/EntityReference.php b/core/modules/views/src/Plugin/views/display/EntityReference.php +index 177e478b19f9b7d8d2dcbc7b59a602f277ec12d9..5cc5209b3c0411fdc7d054156fa9aa7703c5d482 100644 +--- a/core/modules/views/src/Plugin/views/display/EntityReference.php ++++ b/core/modules/views/src/Plugin/views/display/EntityReference.php +@@ -202,12 +202,14 @@ public function query() { + } + } + +- $this->view->query->addWhere(0, $conditions); ++ $this->view->query->addCondition(0, $conditions); + } + + // Add an IN condition for validation. + if (!empty($options['ids'])) { +- $this->view->query->addWhere(0, $id_table . '.' . $id_field, $options['ids'], 'IN'); ++ $condition = $this->view->query->getConnection()->condition('AND'); ++ $condition->condition($id_table . '.' . $id_field, $options['ids'], 'IN'); ++ $this->view->query->addCondition(0, $condition); + } + + $this->view->setItemsPerPage($options['limit']); +diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +index 4b0e4d57fd446550923a03487cc9b28f9d373c26..4fbbaf90fafba59db3e15f18ffa5e1b0f01669a6 100644 +--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php ++++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +@@ -232,7 +232,24 @@ protected function addAdditionalFields($fields = NULL) { + $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params); + } + else { +- $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $real_field_last_part = ''; ++ if (!empty($this->realField)) { ++ $real_field_parts = explode('.', $this->realField); ++ $real_field_last_part = end($real_field_parts); ++ } ++ ++ if (!empty($real_field_last_part) && ($real_field_last_part == $info)) { ++ $alias = $this->tableAlias . '_' . str_replace('.', '_', $info); ++ $this->aliases[$info] = $this->query->addField($this->tableAlias, $this->realField, $alias, $group_params); ++ } ++ else { ++ $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params); ++ } ++ } ++ else { ++ $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params); ++ } + } + } + } +diff --git a/core/modules/views/src/Plugin/views/filter/LatestRevision.php b/core/modules/views/src/Plugin/views/filter/LatestRevision.php +index 90a6d202697f65687bf1d0b1ff69d71c33fb7797..e9013fa538952ea78835aeef925b7bac4697dd9a 100644 +--- a/core/modules/views/src/Plugin/views/filter/LatestRevision.php ++++ b/core/modules/views/src/Plugin/views/filter/LatestRevision.php +@@ -94,7 +94,7 @@ public function query() { + $keys = $entity_type->getKeys(); + + $subquery = $query->getConnection()->select($query_base_table, 'base_table'); +- $subquery->addExpression("MAX(base_table.{$keys['revision']})", $keys['revision']); ++ $subquery->addExpressionMax("base_table.{$keys['revision']}", $keys['revision']); + $subquery->groupBy("base_table.{$keys['id']}"); + $query->addWhere($this->options['group'], "$query_base_table.{$keys['revision']}", $subquery, 'IN'); + } +diff --git a/core/modules/views/src/Plugin/views/filter/LatestTranslationAffectedRevision.php b/core/modules/views/src/Plugin/views/filter/LatestTranslationAffectedRevision.php +index 9afc64e929ead7ff49cc304922cc511b47fcf23d..b6d6b330a8c05104d569af080412962cfefe0fcf 100644 +--- a/core/modules/views/src/Plugin/views/filter/LatestTranslationAffectedRevision.php ++++ b/core/modules/views/src/Plugin/views/filter/LatestTranslationAffectedRevision.php +@@ -94,7 +94,7 @@ public function query() { + $keys = $entity_type->getKeys(); + + $subquery = $query->getConnection()->select($query_base_table, 'base_table'); +- $subquery->addExpression("MAX(base_table.{$keys['revision']})", $keys['revision']); ++ $subquery->addExpressionMax("base_table.{$keys['revision']}", $keys['revision']); + $subquery->fields('base_table', [$keys['id'], 'langcode']); + $subquery->groupBy("base_table.{$keys['id']}"); + $subquery->groupBy('base_table.langcode'); +diff --git a/core/modules/views/src/Plugin/views/HandlerBase.php b/core/modules/views/src/Plugin/views/HandlerBase.php +index 7f40d5ec2a94ebe5c4bace1be898cfd7791e573c..fec84a04f7b62e222d4cda5426683d67b28911ff 100644 +--- a/core/modules/views/src/Plugin/views/HandlerBase.php ++++ b/core/modules/views/src/Plugin/views/HandlerBase.php +@@ -797,7 +797,19 @@ public function getEntityType() { + if (!empty($this->options['relationship']) && $this->options['relationship'] != 'none') { + $relationship = $this->displayHandler->getOption('relationships')[$this->options['relationship']]; + $table_data = $this->getViewsData()->get($relationship['table']); +- $views_data = $this->getViewsData()->get($table_data[$relationship['field']]['relationship']['base']); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ if (isset($table_data[$relationship['field']]['relationship']['base'])) { ++ $views_data = $this->getViewsData()->get($table_data[$relationship['field']]['relationship']['base']); ++ } ++ elseif (isset($relationship['relationship']) && ($relationship['relationship'] == 'none')) { ++ // Some relationships are removed, because in MongoDB's entity storage ++ // they live in the same document instead of in separate tables. ++ $views_data = $this->getViewsData()->get($this->view->storage->get('base_table')); ++ } ++ } ++ else { ++ $views_data = $this->getViewsData()->get($table_data[$relationship['field']]['relationship']['base']); ++ } + } + else { + $views_data = $this->getViewsData()->get($this->view->storage->get('base_table')); +diff --git a/core/modules/views/src/Plugin/views/join/CastedIntFieldJoin.php b/core/modules/views/src/Plugin/views/join/CastedIntFieldJoin.php +index c091222ae51545e230444addd2e9f8e6913d7980..39c9e6ddbe7ca740a8b4a6d6dc0a55bb38f3193b 100644 +--- a/core/modules/views/src/Plugin/views/join/CastedIntFieldJoin.php ++++ b/core/modules/views/src/Plugin/views/join/CastedIntFieldJoin.php +@@ -49,7 +49,7 @@ public function buildJoin($select_query, $table, $view_query) { + $right_field = \Drupal::service('views.cast_sql')->getFieldAsInt($right_field); + } + +- $condition = "$left_field {$this->configuration['operator']} $right_field"; ++ $condition = $select_query->joinCondition()->where("$left_field {$this->configuration['operator']} $right_field"); + $arguments = []; + + // Tack on the extra. +diff --git a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php +index f2b61f306c09eff445844413304b5a5d7622632d..506010fdb1b1d659eeed48f1aa3110d9cd47fa58 100644 +--- a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php ++++ b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php +@@ -2,6 +2,7 @@ + + namespace Drupal\views\Plugin\views\join; + ++use Drupal\Component\Assertion\Inspector; + use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Plugin\PluginBase; + +@@ -311,15 +312,20 @@ public function buildJoin($select_query, $table, $view_query) { + $left_table = NULL; + } + +- $condition = "$left_field " . $this->configuration['operator'] . " $table[alias].$this->field"; + $arguments = []; ++ if ($this->leftFormula || is_null($this->leftTable)) { ++ $condition = $select_query->joinCondition()->where("$left_field " . $this->configuration['operator'] . " $table[alias].$this->field"); ++ } ++ else { ++ $condition = $select_query->joinCondition()->compare($left_field, "$table[alias].$this->field", $this->configuration['operator']); ++ } + + // Tack on the extra. + if (isset($this->extra)) { + $this->joinAddExtra($arguments, $condition, $table, $select_query, $left_table); + } + +- $select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments); ++ $select_query->addJoin($this->type, $right_table, $table['alias'], $condition); + } + + /** +@@ -340,15 +346,40 @@ protected function joinAddExtra(&$arguments, &$condition, $table, SelectInterfac + if (is_array($this->extra)) { + $extras = []; + foreach ($this->extra as $info) { +- $extras[] = $this->buildExtra($info, $arguments, $table, $select_query, $left_table); ++ $extras[] = $this->buildExtra($info, $arguments, $table, $select_query, $left_table, is_string($condition)); + } + + if ($extras) { + if (count($extras) == 1) { +- $condition .= ' AND ' . array_shift($extras); ++ $extra = array_shift($extras); ++ if (is_string($extra)) { ++ $condition .= ' AND ' . $extra; ++ } ++ else { ++ if (isset($extra['field2'])) { ++ $condition->compare($extra['field'], $extra['field2'], $extra['operator']); ++ } ++ else { ++ $condition->condition($extra['field'], $extra['value'], $extra['operator']); ++ } ++ } + } + else { +- $condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')'; ++ if (Inspector::assertAllStrings($extras)) { ++ $condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')'; ++ } ++ else { ++ $inner_condition = $select_query->getConnection()->condition($this->extraOperator); ++ foreach ($extras as $extra) { ++ if (isset($extra['field2'])) { ++ $inner_condition->compare($extra['field'], $extra['field2'], $extra['operator']); ++ } ++ else { ++ $inner_condition->condition($extra['field'], $extra['value'], $extra['operator']); ++ } ++ } ++ $condition->condition($inner_condition); ++ } + } + } + } +@@ -370,11 +401,13 @@ protected function joinAddExtra(&$arguments, &$condition, $table, SelectInterfac + * The current select query being built. + * @param array $left + * The left table. ++ * @param bool $condition_as_string ++ * (optional) Return the condition as a string value. + * +- * @return string ++ * @return array|string + * The extra condition + */ +- protected function buildExtra($info, &$arguments, $table, SelectInterface $select_query, $left) { ++ protected function buildExtra($info, &$arguments, $table, SelectInterface $select_query, $left, $condition_as_string = FALSE) { + // Do not require 'value' to be set; allow for field syntax instead. + $info += [ + 'value' => NULL, +@@ -414,26 +447,51 @@ protected function buildExtra($info, &$arguments, $table, SelectInterface $selec + $operator = !empty($info['operator']) ? $info['operator'] : '='; + $placeholder = $placeholder_sql = ':views_join_condition_' . $select_query->nextPlaceholder(); + } ++ + // Set 'field' as join table field if available or set 'left field' as + // join table field is not set. + if (isset($info['field'])) { + $join_table_field = "$join_table$info[field]"; + // Allow the value to be set either with the 'value' element or +- // with 'left_field'. ++ // with 'left_field' or 'field2'. + if (isset($info['left_field'])) { +- $placeholder_sql = "$left[alias].$info[left_field]"; ++ $field2 = $placeholder_sql = "$left[alias].$info[left_field]"; + } +- else { ++ elseif ($condition_as_string) { + $arguments[$placeholder] = $info['value']; + } ++ if (isset($info['field2'])) { ++ if (isset($left['alias'])) { ++ $field2 = "$left[alias].$info[field2]"; ++ } ++ else { ++ $field2 = "$info[field2]"; ++ } ++ } + } + // Set 'left field' as join table field is not set. + else { + $join_table_field = "$left[alias].$info[left_field]"; +- $arguments[$placeholder] = $info['value']; + } +- // Render out the SQL fragment with parameters. +- return "$join_table_field $operator $placeholder_sql"; ++ ++ if ($condition_as_string) { ++ // Render out the SQL fragment with parameters. ++ return "$join_table_field $operator $placeholder_sql"; ++ } ++ elseif (isset($field2)) { ++ return [ ++ 'field' => $join_table_field, ++ 'field2' => $field2, ++ 'operator' => $operator, ++ ]; ++ } ++ else { ++ return [ ++ 'field' => $join_table_field, ++ 'value' => $info['value'], ++ 'operator' => $operator, ++ ]; ++ } + } + + } +diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +index 446b636f37571b24b5bc225909432053f0444b73..60e2e5aceaffeb04ca0734bea5c8627b6f3d4af8 100644 +--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php ++++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +@@ -299,27 +299,29 @@ public function getEntityTableInfo() { + + // Include all relationships. + foreach ((array) $this->view->relationship as $relationship_id => $relationship) { +- $table_data = $views_data->get($relationship->definition['base']); +- if (isset($table_data['table']['entity type'])) { +- +- // If this is not one of the entity base tables, skip it. +- $entity_type = \Drupal::entityTypeManager()->getDefinition($table_data['table']['entity type']); +- $entity_base_tables = [$entity_type->getBaseTable(), $entity_type->getDataTable(), $entity_type->getRevisionTable(), $entity_type->getRevisionDataTable()]; +- if (!in_array($relationship->definition['base'], $entity_base_tables)) { +- continue; +- } +- +- $entity_tables[$relationship_id . '__' . $relationship->tableAlias] = [ +- 'base' => $relationship->definition['base'], +- 'relationship_id' => $relationship_id, +- 'alias' => $relationship->alias, +- 'entity_type' => $table_data['table']['entity type'], +- 'revision' => $table_data['table']['entity revision'], +- ]; +- +- // Include the entity provider. +- if (!empty($table_data['table']['provider'])) { +- $entity_tables[$relationship_id . '__' . $relationship->tableAlias]['provider'] = $table_data['table']['provider']; ++ if (isset($relationship->definition['base'])) { ++ $table_data = $views_data->get($relationship->definition['base']); ++ if (isset($table_data['table']['entity type'])) { ++ ++ // If this is not one of the entity base tables, skip it. ++ $entity_type = \Drupal::entityTypeManager()->getDefinition($table_data['table']['entity type']); ++ $entity_base_tables = [$entity_type->getBaseTable(), $entity_type->getDataTable(), $entity_type->getRevisionTable(), $entity_type->getRevisionDataTable()]; ++ if (!in_array($relationship->definition['base'], $entity_base_tables)) { ++ continue; ++ } ++ ++ $entity_tables[$relationship_id . '__' . $relationship->tableAlias] = [ ++ 'base' => $relationship->definition['base'], ++ 'relationship_id' => $relationship_id, ++ 'alias' => $relationship->alias, ++ 'entity_type' => $table_data['table']['entity type'], ++ 'revision' => $table_data['table']['entity revision'], ++ ]; ++ ++ // Include the entity provider. ++ if (!empty($table_data['table']['provider'])) { ++ $entity_tables[$relationship_id . '__' . $relationship->tableAlias]['provider'] = $table_data['table']['provider']; ++ } + } + } + } +diff --git a/core/modules/views/src/Plugin/views/row/EntityRow.php b/core/modules/views/src/Plugin/views/row/EntityRow.php +index eba9d3fc0a39254f76fdddf6d105e5a1b118b335..3e00464df6ae256a76799a0b4703463f27c01d59 100644 +--- a/core/modules/views/src/Plugin/views/row/EntityRow.php ++++ b/core/modules/views/src/Plugin/views/row/EntityRow.php +@@ -109,7 +109,12 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$ + + $this->entityTypeId = $this->definition['entity_type']; + $this->entityType = $this->entityTypeManager->getDefinition($this->entityTypeId); +- $this->base_table = $this->entityType->getDataTable() ?: $this->entityType->getBaseTable(); ++ if (($view->getDatabaseDriver() != 'mongodb') && $this->entityType->getDataTable()) { ++ $this->base_table = $this->entityType->getDataTable(); ++ } ++ else { ++ $this->base_table = $this->entityType->getBaseTable(); ++ } + $this->base_field = $this->entityType->getKey('id'); + } + +diff --git a/core/modules/views/src/Plugin/views/sort/Date.php b/core/modules/views/src/Plugin/views/sort/Date.php +index 7c2719c6316febd75ccda15a2985ae36b21e6a17..34db1b8b02227ae6290d77d717409df1cca8309f 100644 +--- a/core/modules/views/src/Plugin/views/sort/Date.php ++++ b/core/modules/views/src/Plugin/views/sort/Date.php +@@ -74,7 +74,20 @@ public function query() { + } + + // Add the field. +- $this->query->addOrderBy(NULL, $formula, $this->options['order'], $this->tableAlias . '_' . $this->field . '_' . $this->options['granularity']); ++ if ($this->view->getDatabaseDriver() == 'mongodb') { ++ $placeholder = $this->placeholder(); ++ if ($this->getPluginId() == 'datetime') { ++ $this->query->addDateStringFormattedField($placeholder, $this->realField, $formula); ++ } ++ else { ++ $this->query->addDateDateFormattedField($placeholder, $this->realField, $formula); ++ } ++ $this->query->addOrderBy($this->tableAlias, $placeholder, $this->options['order']); ++ } ++ else { ++ // Add the field. ++ $this->query->addOrderBy(NULL, $formula, $this->options['order'], $this->tableAlias . '_' . $this->field . '_' . $this->options['granularity']); ++ } + } + + } +diff --git a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +index f625dc6cb8709b08c10036c707a4036c991e3f4e..b4604428b7d1591176fc236a75063e67684141e2 100644 +--- a/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php ++++ b/core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +@@ -3,6 +3,7 @@ + namespace Drupal\views\Plugin\views\wizard; + + use Drupal\Component\Utility\NestedArray; ++use Drupal\Core\Database\Connection; + use Drupal\Core\Entity\EntityPublishedInterface; + use Drupal\Core\Entity\EntityTypeBundleInfoInterface; + use Drupal\Core\Form\FormStateInterface; +@@ -127,6 +128,13 @@ abstract class WizardPluginBase extends PluginBase implements WizardInterface { + */ + protected $parentFormSelector; + ++ /** ++ * The database connection. ++ * ++ * @var \Drupal\Core\Database\Connection ++ */ ++ protected $connection; ++ + /** + * {@inheritdoc} + */ +@@ -136,18 +144,20 @@ public static function create(ContainerInterface $container, array $configuratio + $plugin_id, + $plugin_definition, + $container->get('entity_type.bundle.info'), +- $container->get('menu.parent_form_selector') ++ $container->get('menu.parent_form_selector'), ++ $container->get('database') + ); + } + + /** + * Constructs a WizardPluginBase object. + */ +- public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector) { ++ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeBundleInfoInterface $bundle_info_service, MenuParentFormSelectorInterface $parent_form_selector, Connection $connection) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->bundleInfoService = $bundle_info_service; + $this->base_table = $this->definition['base_table']; ++ $this->connection = $connection; + + $this->parentFormSelector = $parent_form_selector; + +@@ -156,6 +166,9 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition + if (in_array($this->base_table, [$entity_type->getBaseTable(), $entity_type->getDataTable(), $entity_type->getRevisionTable(), $entity_type->getRevisionDataTable()], TRUE)) { + $this->entityType = $entity_type; + $this->entityTypeId = $entity_type_id; ++ if ($this->connection->driver() == 'mongodb') { ++ $this->base_table = $entity_type->getBaseTable(); ++ } + } + } + } +diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php +index 87739a90c96c9381a6b6936abafdcb39336c9e95..ae906c6a732dbdb91d41b92233bf827cbd623f9f 100644 +--- a/core/modules/views/src/ViewExecutable.php ++++ b/core/modules/views/src/ViewExecutable.php +@@ -494,6 +494,13 @@ class ViewExecutable { + */ + protected $serializationData; + ++ /** ++ * The database connection. Needed for MongoDB. ++ * ++ * @var \Drupal\Core\Database\Connection|false ++ */ ++ protected $database; ++ + /** + * Constructs a new ViewExecutable object. + * +@@ -522,6 +529,37 @@ public function __construct(ViewEntityInterface $storage, AccountInterface $user + + } + ++ /** ++ * Returns the database driver. Needed for MongoDB. ++ * ++ * @return string ++ * The database driver. ++ */ ++ public function getDatabaseDriver() { ++ if (empty($this->database)) { ++ $this->database = \Drupal::service('database'); ++ } ++ ++ return $this->database->driver(); ++ } ++ ++ /** ++ * Returns a new database condition object. ++ * ++ * @param string $conjunction ++ * The operator to use to combine conditions: 'AND' or 'OR'. ++ * ++ * @return Drupal\Core\Database\Query\Condition ++ * A new database condition object. ++ */ ++ public function getDatabaseCondition($conjunction) { ++ if (empty($this->database)) { ++ $this->database = \Drupal::service('database'); ++ } ++ ++ return $this->database->condition($conjunction); ++ } ++ + /** + * Returns the identifier. + * +@@ -2137,6 +2175,8 @@ public function destroy() { + foreach ($defaults as $property => $default) { + $this->{$property} = $default; + } ++ ++ $this->database = NULL; + } + + /** +@@ -2537,6 +2577,8 @@ public function getDependencies() { + * The names of all variables that should be serialized. + */ + public function __sleep(): array { ++ $this->database = NULL; ++ + // Limit to only the required data which is needed to properly restore the + // state during unserialization. + $this->serializationData = [ +diff --git a/core/modules/views/views.module b/core/modules/views/views.module +index 964b1657c61372b713eb546a4d8bae630cada9d1..0e2c91bc9e439b123b8881ae9b0ec64a034b63fd 100644 +--- a/core/modules/views/views.module ++++ b/core/modules/views/views.module +@@ -5,6 +5,7 @@ + */ + + use Drupal\Core\Database\Query\AlterableInterface; ++use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Form\FormStateInterface; + use Drupal\views\ViewExecutable; + use Drupal\views\Entity\View; +@@ -339,13 +340,16 @@ function _views_query_tag_alter_condition(AlterableInterface $query, &$condition + if (is_string($condition['field'])) { + $condition['field'] = str_replace(array_keys($substitutions), array_values($substitutions), $condition['field']); + } +- elseif (is_object($condition['field'])) { ++ elseif (is_object($condition['field']) && ($condition['value'] instanceof SelectInterface)) { + $sub_conditions = &$condition['field']->conditions(); + _views_query_tag_alter_condition($query, $sub_conditions, $substitutions); + } ++ if (isset($condition['field2']) && is_string($condition['field2'])) { ++ $condition['field2'] = str_replace(array_keys($substitutions), array_values($substitutions), $condition['field2']); ++ } + // $condition['value'] is a subquery so alter the subquery recursive. + // Therefore make sure to get the metadata of the main query. +- if (is_object($condition['value'])) { ++ if (isset($condition['value']) && is_object($condition['value']) && ($condition['value'] instanceof SelectInterface)) { + $subquery = $condition['value']; + $subquery->addMetaData('views_substitutions', $query->getMetaData('views_substitutions')); + \Drupal::moduleHandler()->invoke('views', 'query_views_alter', [$condition['value']]); +diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc +index 3a08f62c900949377212658a26a4775357601f16..842d737409ce86e1cfaa20bc081e24ab5c5a2dcd 100644 +--- a/core/modules/views/views.views.inc ++++ b/core/modules/views/views.views.inc +@@ -99,6 +99,8 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + return $data; + } + ++ $driver = \Drupal::database()->driver(); ++ + $field_name = $field_storage->getName(); + $field_columns = $field_storage->getColumns(); + +@@ -111,19 +113,24 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + // We cannot do anything if for some reason there is no base table. + return $data; + } +- $entity_tables = [$base_table => $entity_type_id]; +- // Some entities may not have a data table. +- $data_table = $entity_type->getDataTable(); +- if ($data_table) { +- $entity_tables[$data_table] = $entity_type_id; ++ if ($driver == 'mongodb') { ++ $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); + } +- $entity_revision_table = $entity_type->getRevisionTable(); +- $supports_revisions = $entity_type->hasKey('revision') && $entity_revision_table; +- if ($supports_revisions) { +- $entity_tables[$entity_revision_table] = $entity_type_id; +- $entity_revision_data_table = $entity_type->getRevisionDataTable(); +- if ($entity_revision_data_table) { +- $entity_tables[$entity_revision_data_table] = $entity_type_id; ++ else { ++ $entity_tables = [$base_table => $entity_type_id]; ++ // Some entities may not have a data table. ++ $data_table = $entity_type->getDataTable(); ++ if ($data_table) { ++ $entity_tables[$data_table] = $entity_type_id; ++ } ++ $entity_revision_table = $entity_type->getRevisionTable(); ++ $supports_revisions = $entity_type->hasKey('revision') && $entity_revision_table; ++ if ($supports_revisions) { ++ $entity_tables[$entity_revision_table] = $entity_type_id; ++ $entity_revision_data_table = $entity_type->getRevisionDataTable(); ++ if ($entity_revision_data_table) { ++ $entity_tables[$entity_revision_data_table] = $entity_type_id; ++ } + } + } + +@@ -131,18 +138,28 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + // @todo Generalize this code to make it work with any table layout. See + // https://www.drupal.org/node/2079019. + $table_mapping = $storage->getTableMapping(); +- $field_tables = [ +- EntityStorageInterface::FIELD_LOAD_CURRENT => [ +- 'table' => $table_mapping->getDedicatedDataTableName($field_storage), +- 'alias' => "{$entity_type_id}__{$field_name}", +- ], +- ]; +- if ($supports_revisions) { +- $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = [ +- 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), +- 'alias' => "{$entity_type_id}_revision__{$field_name}", ++ if ($driver == 'mongodb') { ++ $field_tables = [ ++ EntityStorageInterface::FIELD_LOAD_CURRENT => [ ++ 'table' => $table_mapping->getJsonStorageDedicatedTableName($field_storage, $base_table), ++ 'alias' => "{$entity_type_id}__{$field_name}", ++ ], + ]; + } ++ else { ++ $field_tables = [ ++ EntityStorageInterface::FIELD_LOAD_CURRENT => [ ++ 'table' => $table_mapping->getDedicatedDataTableName($field_storage), ++ 'alias' => "{$entity_type_id}__{$field_name}", ++ ], ++ ]; ++ if ($supports_revisions) { ++ $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = [ ++ 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), ++ 'alias' => "{$entity_type_id}_revision__{$field_name}", ++ ]; ++ } ++ } + + // Determine if the fields are translatable. + $bundles_names = $field_storage->getBundles(); +@@ -191,57 +208,15 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + $translation_join_type = 'language_bundle'; + } + +- // Build the relationships between the field table and the entity tables. +- $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias']; +- if ($data_table) { +- // Tell Views how to join to the base table, via the data table. +- $data[$table_alias]['table']['join'][$data_table] = [ +- 'table' => $table_mapping->getDedicatedDataTableName($field_storage), +- 'left_field' => $entity_type->getKey('id'), +- 'field' => 'entity_id', +- 'extra' => [ +- ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], +- ], +- ]; +- } +- else { +- // If there is no data table, just join directly. +- $data[$table_alias]['table']['join'][$base_table] = [ +- 'table' => $table_mapping->getDedicatedDataTableName($field_storage), +- 'left_field' => $entity_type->getKey('id'), +- 'field' => 'entity_id', +- 'extra' => [ +- ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], +- ], +- ]; +- } +- +- if ($translation_join_type === 'language_bundle') { +- $data[$table_alias]['table']['join'][$data_table]['join_id'] = 'field_or_language_join'; +- $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ +- 'left_field' => 'langcode', +- 'field' => 'langcode', +- ]; +- $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ +- 'field' => 'bundle', +- 'value' => $untranslatable_config_bundles, +- ]; +- } +- elseif ($translation_join_type === 'language') { +- $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ +- 'left_field' => 'langcode', +- 'field' => 'langcode', +- ]; +- } +- +- if ($supports_revisions) { +- $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias']; +- if ($entity_revision_data_table) { +- // Tell Views how to join to the revision table, via the data table. +- $data[$table_alias]['table']['join'][$entity_revision_data_table] = [ +- 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), +- 'left_field' => $entity_type->getKey('revision'), +- 'field' => 'revision_id', ++ if ($driver != 'mongodb') { ++ // Build the relationships between the field table and the entity tables. ++ $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias']; ++ if ($data_table) { ++ // Tell Views how to join to the base table, via the data table. ++ $data[$table_alias]['table']['join'][$data_table] = [ ++ 'table' => $table_mapping->getDedicatedDataTableName($field_storage), ++ 'left_field' => $entity_type->getKey('id'), ++ 'field' => 'entity_id', + 'extra' => [ + ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], + ], +@@ -249,32 +224,76 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + } + else { + // If there is no data table, just join directly. +- $data[$table_alias]['table']['join'][$entity_revision_table] = [ +- 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), +- 'left_field' => $entity_type->getKey('revision'), +- 'field' => 'revision_id', ++ $data[$table_alias]['table']['join'][$base_table] = [ ++ 'table' => $table_mapping->getDedicatedDataTableName($field_storage), ++ 'left_field' => $entity_type->getKey('id'), ++ 'field' => 'entity_id', + 'extra' => [ + ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], + ], + ]; + } ++ + if ($translation_join_type === 'language_bundle') { +- $data[$table_alias]['table']['join'][$entity_revision_data_table]['join_id'] = 'field_or_language_join'; +- $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ ++ $data[$table_alias]['table']['join'][$data_table]['join_id'] = 'field_or_language_join'; ++ $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ + 'left_field' => 'langcode', + 'field' => 'langcode', + ]; +- $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ +- 'value' => $untranslatable_config_bundles, ++ $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ + 'field' => 'bundle', ++ 'value' => $untranslatable_config_bundles, + ]; + } + elseif ($translation_join_type === 'language') { +- $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ ++ $data[$table_alias]['table']['join'][$data_table]['extra'][] = [ + 'left_field' => 'langcode', + 'field' => 'langcode', + ]; + } ++ ++ if ($supports_revisions) { ++ $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias']; ++ if ($entity_revision_data_table) { ++ // Tell Views how to join to the revision table, via the data table. ++ $data[$table_alias]['table']['join'][$entity_revision_data_table] = [ ++ 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), ++ 'left_field' => $entity_type->getKey('revision'), ++ 'field' => 'revision_id', ++ 'extra' => [ ++ ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], ++ ], ++ ]; ++ } ++ else { ++ // If there is no data table, just join directly. ++ $data[$table_alias]['table']['join'][$entity_revision_table] = [ ++ 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage), ++ 'left_field' => $entity_type->getKey('revision'), ++ 'field' => 'revision_id', ++ 'extra' => [ ++ ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], ++ ], ++ ]; ++ } ++ if ($translation_join_type === 'language_bundle') { ++ $data[$table_alias]['table']['join'][$entity_revision_data_table]['join_id'] = 'field_or_language_join'; ++ $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ ++ 'left_field' => 'langcode', ++ 'field' => 'langcode', ++ ]; ++ $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ ++ 'value' => $untranslatable_config_bundles, ++ 'field' => 'bundle', ++ ]; ++ } ++ elseif ($translation_join_type === 'language') { ++ $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = [ ++ 'left_field' => 'langcode', ++ 'field' => 'langcode', ++ ]; ++ } ++ } + } + + $group_name = $entity_type->getLabel(); +@@ -295,50 +314,81 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + $table = $table_info['table']; + $table_alias = $table_info['alias']; + +- if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ if ($driver === 'mongodb') { + $group = $group_name; + $field_alias = $field_name; ++ ++ $data[$base_table][$field_alias] = [ ++ 'group' => $group, ++ 'title' => $label, ++ 'title short' => $label, ++ 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), ++ ]; + } + else { +- $group = t('@group (historical data)', ['@group' => $group_name]); +- $field_alias = $field_name . '__revision_id'; +- } ++ if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ $group = $group_name; ++ $field_alias = $field_name; ++ } ++ else { ++ $group = t('@group (historical data)', ['@group' => $group_name]); ++ $field_alias = $field_name . '__revision_id'; ++ } + +- $data[$table_alias][$field_alias] = [ +- 'group' => $group, +- 'title' => $label, +- 'title short' => $label, +- 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), +- ]; ++ $data[$table_alias][$field_alias] = [ ++ 'group' => $group, ++ 'title' => $label, ++ 'title short' => $label, ++ 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), ++ ]; ++ } + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. + $aliases = []; + $also_known = []; + foreach ($all_labels as $label_name => $true) { +- if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ if ($driver === 'mongodb') { + if ($label != $label_name) { + $aliases[] = [ + 'base' => $base_table, +- 'group' => $group_name, ++ 'group' => t('@group (historical data)', ['@group' => $group_name]), + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]), + ]; + $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $label_name]); + } + } +- elseif ($supports_revisions && $label != $label_name) { +- $aliases[] = [ +- 'base' => $table, +- 'group' => t('@group (historical data)', ['@group' => $group_name]), +- 'title' => $label_name, +- 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]), +- ]; +- $also_known[] = t('@group (historical data): @field', ['@group' => $group_name, '@field' => $label_name]); ++ else { ++ if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ if ($label != $label_name) { ++ $aliases[] = [ ++ 'base' => $base_table, ++ 'group' => $group_name, ++ 'title' => $label_name, ++ 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]), ++ ]; ++ $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $label_name]); ++ } ++ } ++ elseif ($supports_revisions && $label != $label_name) { ++ $aliases[] = [ ++ 'base' => $table, ++ 'group' => t('@group (historical data)', ['@group' => $group_name]), ++ 'title' => $label_name, ++ 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]), ++ ]; ++ $also_known[] = t('@group (historical data): @field', ['@group' => $group_name, '@field' => $label_name]); ++ } + } + } + if ($aliases) { +- $data[$table_alias][$field_alias]['aliases'] = $aliases; ++ if ($driver === 'mongodb') { ++ $data[$base_table][$field_alias]['aliases'] = $aliases; ++ } ++ else { ++ $data[$table_alias][$field_alias]['aliases'] = $aliases; ++ } + // The $also_known variable contains markup that is HTML escaped and that + // loses safeness when imploded. The help text is used in #description + // and therefore XSS admin filtered by default. Escaped HTML is not +@@ -348,23 +398,56 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + // Considering the dual use of this help data (both as metadata and as + // help text), other patterns such as use of #markup would not be correct + // here. +- $data[$table_alias][$field_alias]['help'] = Markup::create($data[$table_alias][$field_alias]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ if ($driver === 'mongodb') { ++ $data[$base_table][$field_alias]['help'] = Markup::create($data[$base_table][$field_alias]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ } ++ else { ++ $data[$table_alias][$field_alias]['help'] = Markup::create($data[$table_alias][$field_alias]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ } + } + + $keys = array_keys($field_columns); + $real_field = reset($keys); +- $data[$table_alias][$field_alias]['field'] = [ +- 'table' => $table, +- 'id' => 'field', +- 'field_name' => $field_name, +- 'entity_type' => $entity_type_id, +- // Provide a real field for group by. +- 'real field' => $field_name . '_' . $real_field, +- 'additional fields' => $add_fields, +- // Default the element type to div, let the UI change it if necessary. +- 'element type' => 'div', +- 'is revision' => $type == EntityStorageInterface::FIELD_LOAD_REVISION, +- ]; ++ if ($driver == 'mongodb') { ++ $real_field = $field_alias . '_' . $real_field; ++ if ($entity_type->isRevisionable()) { ++ $current_revision_table = $entity_storage->getJsonStorageCurrentRevisionTable(); ++ $real_field = $current_revision_table . '.' . $table_mapping->getJsonStorageDedicatedTableName($field_storage, $current_revision_table) . '.' . $real_field; ++ } ++ elseif ($entity_type->isTranslatable()) { ++ $translations_table = $entity_storage->getJsonStorageTranslationsTable(); ++ $real_field = $translations_table . '.' . $table_mapping->getJsonStorageDedicatedTableName($field_storage, $translations_table) . '.' . $real_field; ++ } ++ else { ++ $real_field = $table_mapping->getJsonStorageDedicatedTableName($field_storage, $base_table) . '.' . $real_field; ++ } ++ ++ $data[$base_table][$field_alias]['field'] = [ ++ 'id' => 'field', ++ 'field_name' => $field_alias, ++ 'entity field' => $field_alias, ++ // Provide a real field for group by. ++ // Testing to see if we can remove the real field here. ++ 'real field' => $real_field, ++ 'additional fields' => $add_fields, ++ // Default the element type to div, let the UI change it if necessary. ++ 'element type' => 'div', ++ ]; ++ } ++ else { ++ $data[$table_alias][$field_alias]['field'] = [ ++ 'table' => $table, ++ 'id' => 'field', ++ 'field_name' => $field_name, ++ 'entity_type' => $entity_type_id, ++ // Provide a real field for group by. ++ 'real field' => $field_name . '_' . $real_field, ++ 'additional fields' => $add_fields, ++ // Default the element type to div, let the UI change it if necessary. ++ 'element type' => 'div', ++ 'is revision' => $type == EntityStorageInterface::FIELD_LOAD_REVISION, ++ ]; ++ } + } + + // Expose data for each field property individually. +@@ -391,11 +474,20 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + case 'blob': + // It does not make sense to sort by blob. + $allow_sort = FALSE; ++ case 'bool': + default: +- $filter = 'string'; +- $argument = 'string'; +- $sort = 'standard'; +- break; ++ if (\Drupal::database()->driver() == 'mongodb' && $attributes['type'] == 'bool') { ++ $filter = 'boolean'; ++ $argument = 'numeric'; ++ $sort = 'standard'; ++ break; ++ } ++ else { ++ $filter = 'string'; ++ $argument = 'string'; ++ $sort = 'standard'; ++ break; ++ } + } + + if (count($field_columns) == 1 || $column == 'value') { +@@ -409,26 +501,56 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + + // Expose data for the property. + foreach ($field_tables as $type => $table_info) { +- $table = $table_info['table']; +- $table_alias = $table_info['alias']; +- +- if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ if ($driver === 'mongodb') { + $group = $group_name; ++ $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column); ++ ++ // Load all the fields from the table by default. ++ $additional_fields = $table_mapping->getAllColumns($base_table); ++ ++ if ($entity_type->isRevisionable()) { ++ $current_revision_table = $entity_storage->getJsonStorageCurrentRevisionTable(); ++ $real_field = $current_revision_table . '.' . $table_mapping->getJsonStorageDedicatedTableName($field_storage, $current_revision_table) . '.' . $column_real_name; ++ } ++ elseif ($entity_type->isTranslatable()) { ++ $translations_table = $entity_storage->getJsonStorageTranslationsTable(); ++ $real_field = $translations_table . '.' . $table_mapping->getJsonStorageDedicatedTableName($field_storage, $translations_table) . '.' . $column_real_name; ++ } ++ else { ++ $real_field = $table_mapping->getJsonStorageDedicatedTableName($field_storage, $base_table) . '.' . $column_real_name; ++ } ++ ++ $data[$base_table][$column_real_name] = [ ++ 'field' => ['id' => 'field'], ++ 'real field' => $real_field, ++ 'group' => $group, ++ 'title' => $title, ++ 'title short' => $title_short, ++ 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), ++ ]; + } + else { +- $group = t('@group (historical data)', ['@group' => $group_name]); +- } +- $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column); ++ $table = $table_info['table']; ++ $table_alias = $table_info['alias']; + +- // Load all the fields from the table by default. +- $additional_fields = $table_mapping->getAllColumns($table); ++ if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { ++ $group = $group_name; ++ } ++ else { ++ $group = t('@group (historical data)', ['@group' => $group_name]); ++ } ++ $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column); + +- $data[$table_alias][$column_real_name] = [ +- 'group' => $group, +- 'title' => $title, +- 'title short' => $title_short, +- 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), +- ]; ++ // Load all the fields from the table by default. ++ $additional_fields = $table_mapping->getAllColumns($table); ++ ++ $data[$table_alias][$column_real_name] = [ ++ 'group' => $group, ++ 'title' => $title, ++ 'title short' => $title_short, ++ 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), ++ ]; ++ } + + // Go through and create a list of aliases for all possible combinations of + // entity type + name. +@@ -451,7 +573,12 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + } + } + if ($aliases) { +- $data[$table_alias][$column_real_name]['aliases'] = $aliases; ++ if ($driver === 'mongodb') { ++ $data[$base_table][$column_real_name]['aliases'] = $aliases; ++ } ++ else { ++ $data[$table_alias][$column_real_name]['aliases'] = $aliases; ++ } + // The $also_known variable contains markup that is HTML escaped and + // that loses safeness when imploded. The help text is used in + // #description and therefore XSS admin filtered by default. Escaped +@@ -461,83 +588,112 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora + // Considering the dual use of this help data (both as metadata and as + // help text), other patterns such as use of #markup would not be + // correct here. +- $data[$table_alias][$column_real_name]['help'] = Markup::create($data[$table_alias][$column_real_name]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ if ($driver === 'mongodb') { ++ $data[$base_table][$column_real_name]['help'] = Markup::create($data[$base_table][$column_real_name]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ } ++ else { ++ $data[$table_alias][$column_real_name]['help'] = Markup::create($data[$table_alias][$column_real_name]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); ++ } + } + +- $data[$table_alias][$column_real_name]['argument'] = [ +- 'field' => $column_real_name, +- 'table' => $table, +- 'id' => $argument, +- 'additional fields' => $additional_fields, +- 'field_name' => $field_name, +- 'entity_type' => $entity_type_id, +- 'empty field name' => t('- No value -'), +- ]; +- $data[$table_alias][$column_real_name]['filter'] = [ +- 'field' => $column_real_name, +- 'table' => $table, +- 'id' => $filter, +- 'additional fields' => $additional_fields, +- 'field_name' => $field_name, +- 'entity_type' => $entity_type_id, +- 'allow empty' => TRUE, +- ]; +- if (!empty($allow_sort)) { +- $data[$table_alias][$column_real_name]['sort'] = [ +- 'field' => $column_real_name, +- 'table' => $table, +- 'id' => $sort, +- 'additional fields' => $additional_fields, ++ if ($driver == 'mongodb') { ++ $data[$base_table][$column_real_name]['argument'] = [ ++ 'id' => $argument, + 'field_name' => $field_name, +- 'entity_type' => $entity_type_id, + ]; +- } ++ $data[$base_table][$column_real_name]['filter'] = [ ++ 'id' => $filter, ++ 'field_name' => $field_name, ++ 'allow empty' => TRUE, ++ ]; ++ if (!empty($allow_sort)) { ++ $data[$base_table][$column_real_name]['sort'] = [ ++ 'id' => $sort, ++ 'field_name' => $field_name, ++ ]; ++ } + +- // Set click sortable if there is a field definition. +- if (isset($data[$table_alias][$field_name]['field'])) { +- $data[$table_alias][$field_name]['field']['click sortable'] = $allow_sort; ++ // Set click sortable if there is a field definition. ++ if (isset($data[$base_table][$field_name]['field'])) { ++ $data[$base_table][$field_name]['field']['click sortable'] = $allow_sort; ++ } + } +- +- // Expose additional delta column for multiple value fields. +- if ($field_storage->isMultiple()) { +- $title_delta = t('@label (@name:delta)', ['@label' => $label, '@name' => $field_name]); +- $title_short_delta = t('@label:delta', ['@label' => $label]); +- +- $data[$table_alias]['delta'] = [ +- 'group' => $group, +- 'title' => $title_delta, +- 'title short' => $title_short_delta, +- 'help' => t('Delta - Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), +- ]; +- $data[$table_alias]['delta']['field'] = [ +- 'id' => 'numeric', +- ]; +- $data[$table_alias]['delta']['argument'] = [ +- 'field' => 'delta', ++ else { ++ $data[$table_alias][$column_real_name]['argument'] = [ ++ 'field' => $column_real_name, + 'table' => $table, +- 'id' => 'numeric', ++ 'id' => $argument, + 'additional fields' => $additional_fields, +- 'empty field name' => t('- No value -'), + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, ++ 'empty field name' => t('- No value -'), + ]; +- $data[$table_alias]['delta']['filter'] = [ +- 'field' => 'delta', ++ $data[$table_alias][$column_real_name]['filter'] = [ ++ 'field' => $column_real_name, + 'table' => $table, +- 'id' => 'numeric', ++ 'id' => $filter, + 'additional fields' => $additional_fields, + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, + 'allow empty' => TRUE, + ]; +- $data[$table_alias]['delta']['sort'] = [ +- 'field' => 'delta', +- 'table' => $table, +- 'id' => 'standard', +- 'additional fields' => $additional_fields, +- 'field_name' => $field_name, +- 'entity_type' => $entity_type_id, +- ]; ++ if (!empty($allow_sort)) { ++ $data[$table_alias][$column_real_name]['sort'] = [ ++ 'field' => $column_real_name, ++ 'table' => $table, ++ 'id' => $sort, ++ 'additional fields' => $additional_fields, ++ 'field_name' => $field_name, ++ 'entity_type' => $entity_type_id, ++ ]; ++ } ++ ++ // Set click sortable if there is a field definition. ++ if (isset($data[$table_alias][$field_name]['field'])) { ++ $data[$table_alias][$field_name]['field']['click sortable'] = $allow_sort; ++ } ++ ++ // Expose additional delta column for multiple value fields. ++ if ($field_storage->isMultiple()) { ++ $title_delta = t('@label (@name:delta)', ['@label' => $label, '@name' => $field_name]); ++ $title_short_delta = t('@label:delta', ['@label' => $label]); ++ ++ $data[$table_alias]['delta'] = [ ++ 'group' => $group, ++ 'title' => $title_delta, ++ 'title short' => $title_short_delta, ++ 'help' => t('Delta - Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]), ++ ]; ++ $data[$table_alias]['delta']['field'] = [ ++ 'id' => 'numeric', ++ ]; ++ $data[$table_alias]['delta']['argument'] = [ ++ 'field' => 'delta', ++ 'table' => $table, ++ 'id' => 'numeric', ++ 'additional fields' => $additional_fields, ++ 'empty field name' => t('- No value -'), ++ 'field_name' => $field_name, ++ 'entity_type' => $entity_type_id, ++ ]; ++ $data[$table_alias]['delta']['filter'] = [ ++ 'field' => 'delta', ++ 'table' => $table, ++ 'id' => 'numeric', ++ 'additional fields' => $additional_fields, ++ 'field_name' => $field_name, ++ 'entity_type' => $entity_type_id, ++ 'allow empty' => TRUE, ++ ]; ++ $data[$table_alias]['delta']['sort'] = [ ++ 'field' => 'delta', ++ 'table' => $table, ++ 'id' => 'standard', ++ 'additional fields' => $additional_fields, ++ 'field_name' => $field_name, ++ 'entity_type' => $entity_type_id, ++ ]; ++ } + } + } + } +diff --git a/core/modules/workspaces/src/EntityQuery/Query.php b/core/modules/workspaces/src/EntityQuery/Query.php +index c7aebf61047d4addff1ef7a2737582359b6dfdcb..23bda9da5010e552404ff37dfc6151485900b613 100644 +--- a/core/modules/workspaces/src/EntityQuery/Query.php ++++ b/core/modules/workspaces/src/EntityQuery/Query.php +@@ -31,8 +31,8 @@ public function prepare() { + // relationship, and, as a consequence, the revision ID field is no longer + // a simple SQL field but an expression. + $this->sqlFields = []; +- $this->sqlQuery->addExpression("COALESCE([workspace_association].[target_entity_revision_id], [base_table].[$revision_field])", $revision_field); +- $this->sqlQuery->addExpression("[base_table].[$id_field]", $id_field); ++ $this->sqlQuery->addExpressionCoalesce(['workspace_association.target_entity_revision_id', "base_table.$revision_field"], $revision_field); ++ $this->sqlQuery->addExpressionField("base_table.$id_field", $id_field); + + $this->sqlGroupBy['workspace_association.target_entity_revision_id'] = 'workspace_association.target_entity_revision_id'; + $this->sqlGroupBy["base_table.$id_field"] = "base_table.$id_field"; +diff --git a/core/modules/workspaces/src/EntityQuery/QueryTrait.php b/core/modules/workspaces/src/EntityQuery/QueryTrait.php +index eef973cc45639c56507af443be2586153b0014a4..b33325f079946c01d70beb9ff741a667004fe1b0 100644 +--- a/core/modules/workspaces/src/EntityQuery/QueryTrait.php ++++ b/core/modules/workspaces/src/EntityQuery/QueryTrait.php +@@ -78,7 +78,11 @@ public function prepare() { + // revision. + $id_field = $this->entityType->getKey('id'); + $target_id_field = WorkspaceAssociation::getIdField($this->entityTypeId); +- $this->sqlQuery->leftJoin('workspace_association', 'workspace_association', "[%alias].[target_entity_type_id] = '{$this->entityTypeId}' AND [%alias].[$target_id_field] = [base_table].[$id_field] AND [%alias].[workspace] = '{$active_workspace->id()}'"); ++ $this->sqlQuery->leftJoin('workspace_association', 'workspace_association', $this->sqlQuery->joinCondition() ++ ->condition("%alias.target_entity_type_id", $this->entityTypeId) ++ ->compare("%alias.$target_id_field", "base_table.$id_field") ++ ->condition("%alias.workspace", $active_workspace->id()) ++ ); + } + + return $this; +diff --git a/core/modules/workspaces/src/EntityQuery/Tables.php b/core/modules/workspaces/src/EntityQuery/Tables.php +index 199d5cc1559729921f1263198464db62162cf1f6..d15826102a3bf71e015b6de974f0561d89a22d85 100644 +--- a/core/modules/workspaces/src/EntityQuery/Tables.php ++++ b/core/modules/workspaces/src/EntityQuery/Tables.php +@@ -2,6 +2,7 @@ + + namespace Drupal\workspaces\EntityQuery; + ++use Drupal\Core\Database\Query\ConditionInterface; + use Drupal\Core\Database\Query\SelectInterface; + use Drupal\Core\Entity\EntityType; + use Drupal\Core\Entity\Query\Sql\Tables as BaseTables; +@@ -93,9 +94,18 @@ protected function addJoin($type, $table, $join_condition, $langcode, $delta = N + // 'revision_id' string used when joining dedicated field tables. + // If those two conditions are met, we have to update the join condition + // to also look for a possible workspace-specific revision using COALESCE. +- $condition_parts = explode(' = ', $join_condition); +- $condition_parts_1 = str_replace(['[', ']'], '', $condition_parts[1]); +- [$base_table, $id_field] = explode('.', $condition_parts_1); ++ if ($join_condition instanceof ConditionInterface) { ++ $first_condition = $join_condition->conditions()[0]; ++ $field = $first_condition['field']; ++ $field2 = $first_condition['field2']; ++ [$base_table, $id_field] = explode('.', $field2); ++ $condition_parts = []; ++ } ++ else { ++ $condition_parts = explode(' = ', $join_condition); ++ $condition_parts_1 = str_replace(['[', ']'], '', $condition_parts[1]); ++ [$base_table, $id_field] = explode('.', $condition_parts_1); ++ } + + if (isset($this->baseTablesEntityType[$base_table])) { + $entity_type_id = $this->baseTablesEntityType[$base_table]; +@@ -103,7 +113,12 @@ protected function addJoin($type, $table, $join_condition, $langcode, $delta = N + + if ($id_field === $revision_key || $id_field === 'revision_id') { + $workspace_association_table = $this->contentWorkspaceTables[$base_table]; +- $join_condition = "{$condition_parts[0]} = COALESCE($workspace_association_table.target_entity_revision_id, {$condition_parts[1]})"; ++ if ($join_condition instanceof ConditionInterface) { ++ $join_condition = $this->sqlQuery->joinCondition()->where("$field = COALESCE($workspace_association_table.target_entity_revision_id, $field2)"); ++ } ++ else { ++ $join_condition = "{$condition_parts[0]} = COALESCE($workspace_association_table.target_entity_revision_id, {$condition_parts[1]})"; ++ } + } + } + } +@@ -149,7 +164,13 @@ public function addWorkspaceAssociationJoin($entity_type_id, $base_table_alias, + + // LEFT join the Workspace association entity's table so we can properly + // include live content along with a possible workspace-specific revision. +- $this->contentWorkspaceTables[$base_table_alias] = $this->sqlQuery->leftJoin('workspace_association', NULL, "[%alias].[target_entity_type_id] = '$entity_type_id' AND [%alias].[$target_id_field] = [$base_table_alias].[$id_field] AND [%alias].[workspace] = '$active_workspace_id'"); ++ ++ $this->contentWorkspaceTables[$base_table_alias] = $this->sqlQuery->leftJoin('workspace_association', NULL, ++ $this->sqlQuery->joinCondition() ++ ->condition("%alias.target_entity_type_id", $entity_type_id) ++ ->compare("%alias.$target_id_field", "$base_table_alias.$id_field") ++ ->condition("%alias.workspace", $active_workspace_id) ++ ); + + $this->baseTablesEntityType[$base_table_alias] = $entity_type->id(); + } +diff --git a/core/modules/workspaces/src/ViewsQueryAlter.php b/core/modules/workspaces/src/ViewsQueryAlter.php +index f409a20b0d9c5fc52793bda381dc25b9d2e16501..46703743b1c674e3a8990507ace19aa1fcc2205a 100644 +--- a/core/modules/workspaces/src/ViewsQueryAlter.php ++++ b/core/modules/workspaces/src/ViewsQueryAlter.php +@@ -411,7 +411,11 @@ protected function getRevisionTableJoin($relationship, $table, $field, $workspac + if ($entity_type->isTranslatable() && $this->languageManager->isMultilingual()) { + $langcode_field = $entity_type->getKey('langcode'); + $definition['extra'] = [ +- ['field' => $langcode_field, 'left_field' => $langcode_field], ++ [ ++ 'field' => $langcode_field, ++ 'field2' => "$relationship.$langcode_field", ++ 'operator' => '=', ++ ], + ]; + } + +diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php +index 25b476fab864e4c0ef65a82be426c613fb08bcf3..018b522cd7eb4113c98757ebb5156a36b3126408 100644 +--- a/core/modules/workspaces/src/WorkspaceAssociation.php ++++ b/core/modules/workspaces/src/WorkspaceAssociation.php +@@ -64,20 +64,37 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w + $id_field = static::getIdField($entity->getEntityTypeId()); + + try { +- $transaction = $this->database->startTransaction(); ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } ++ + // Update all affected workspaces that were tracking the current revision. + // This means they are inheriting content and should be updated. + if ($tracked_revision_id) { ++ if ($id_field === 'target_entity_id') { ++ $entity_id = (int) $entity->id(); ++ } ++ else { ++ $entity_id = (string) $entity->id(); ++ } + $this->database->update(static::TABLE) + ->fields([ + 'target_entity_revision_id' => $entity->getRevisionId(), + ]) + ->condition('workspace', $affected_workspaces, 'IN') + ->condition('target_entity_type_id', $entity->getEntityTypeId()) +- ->condition($id_field, $entity->id()) ++ ->condition($id_field, $entity_id) + // Only update descendant workspaces if they have the same initial + // revision, which means they are currently inheriting content. +- ->condition('target_entity_revision_id', $tracked_revision_id) ++ ->condition('target_entity_revision_id', (int) $tracked_revision_id) + ->execute(); + } + +@@ -102,11 +119,18 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w + } + $insert_query->execute(); + } ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException($this->logger, $e); + throw $e; + } +@@ -141,10 +165,21 @@ public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entit + ->condition('workspace', $workspace_id); + + if ($entity_type_id) { +- $query->condition('target_entity_type_id', $entity_type_id, '='); ++ $query->condition('target_entity_type_id', $entity_type_id); + + if ($entity_ids) { +- $query->condition(static::getIdField($entity_type_id), $entity_ids, 'IN'); ++ $id_field = static::getIdField($entity_type_id); ++ if ($id_field === 'target_entity_id') { ++ foreach ($entity_ids as &$entity_id) { ++ $entity_id = (int) $entity_id; ++ } ++ } ++ else { ++ foreach ($entity_ids as &$entity_id) { ++ $entity_id = (string) $entity_id; ++ } ++ } ++ $query->condition($id_field, $entity_ids, 'IN'); + } + } + +@@ -225,21 +260,57 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i + $workspace_candidates = [$workspace_id]; + } + +- $query = $this->database->select($entity_type->getRevisionTable(), 'revision'); +- $query->leftJoin($entity_type->getBaseTable(), 'base', "[revision].[$id_field] = [base].[$id_field]"); ++ if ($this->database->driver() == 'mongodb') { ++ $all_revisions_table = $table_mapping->getJsonStorageAllRevisionsTable(); + +- $query +- ->fields('revision', [$revision_id_field, $id_field]) +- ->condition("revision.$workspace_field", $workspace_candidates, 'IN') +- ->where("[revision].[$revision_id_field] >= [base].[$revision_id_field]") +- ->orderBy("revision.$revision_id_field", 'ASC'); +- +- // Restrict the result to a set of entity ID's if provided. +- if ($entity_ids) { +- $query->condition("revision.$id_field", $entity_ids, 'IN'); ++ $query = $this->database->select($entity_type->getBaseTable(), 'base'); ++ $query ++ ->fields('base', [$revision_id_field, $id_field, $all_revisions_table]) ++ ->condition("$all_revisions_table.$workspace_field", $workspace_candidates, 'IN') ++ ->orderBy("$all_revisions_table.$revision_id_field", 'ASC'); ++ ++ // Restrict the result to a set of entity ID's if provided. ++ if ($entity_ids) { ++ foreach ($entity_ids as & $entity_id) { ++ $entity_id = (int) $entity_id; ++ } ++ $query->condition($id_field, $entity_ids, 'IN'); ++ } ++ ++ $result = []; ++ ++ $rows = $query->execute()->fetchAll(); ++ foreach ($rows as $row) { ++ $id = $row->{$id_field}; ++ $revision_id = $row->{$revision_id_field}; ++ $all_revisions = $row->{$all_revisions_table}; ++ foreach ($all_revisions as $all_revision) { ++ $all_revision_revision_id = $all_revision[$revision_id_field] ?? NULL; ++ $all_revision_workspace = $all_revision[$workspace_field] ?? NULL; ++ // @todo the next if-statement should be moved to the query. ++ if ($all_revision_revision_id && $all_revision_workspace && ($all_revision_revision_id >= $revision_id) && (in_array($all_revision_workspace, $workspace_candidates, TRUE))) { ++ $result[$all_revision_revision_id] = $id; ++ } ++ } ++ } + } ++ else { ++ $query = $this->database->select($entity_type->getRevisionTable(), 'revision'); ++ $query->leftJoin($entity_type->getBaseTable(), 'base', $query->joinCondition()->compare("revision.$id_field", "base.$id_field")); + +- $result = $query->execute()->fetchAllKeyed(); ++ $query ++ ->fields('revision', [$revision_id_field, $id_field]) ++ ->condition("revision.$workspace_field", $workspace_candidates, 'IN') ++ ->where("[revision].[$revision_id_field] >= [base].[$revision_id_field]") ++ ->orderBy("revision.$revision_id_field", 'ASC'); ++ ++ // Restrict the result to a set of entity ID's if provided. ++ if ($entity_ids) { ++ $query->condition("revision.$id_field", $entity_ids, 'IN'); ++ } ++ ++ $result = $query->execute()->fetchAllKeyed(); ++ } + + // Cache the list of associated entity IDs if the full list was requested. + if (!$entity_ids) { +@@ -279,19 +350,52 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti + $revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value']; + + $query = $this->database->select($entity_type->getBaseTable(), 'base'); +- $query->leftJoin($entity_type->getRevisionTable(), 'revision', "[base].[$revision_id_field] = [revision].[$revision_id_field]"); ++ if ($this->database->driver() == 'mongodb') { ++ $current_revision_table = $table_mapping->getJsonStorageCurrentRevisionTable(); + +- $query +- ->fields('base', [$revision_id_field, $id_field]) +- ->condition("revision.$workspace_field", $workspace_id, '=') +- ->orderBy("base.$revision_id_field", 'ASC'); ++ $query ++ ->fields('base', [$revision_id_field, $id_field, $current_revision_table]) ++ ->condition("$current_revision_table.$workspace_field", $workspace_id) ++ ->orderBy("$current_revision_table.$revision_id_field", 'ASC'); + +- // Restrict the result to a set of entity ID's if provided. +- if ($entity_ids) { +- $query->condition("base.$id_field", $entity_ids, 'IN'); ++ // Restrict the result to a set of entity ID's if provided. ++ if ($entity_ids) { ++ foreach ($entity_ids as & $entity_id) { ++ $entity_id = (int) $entity_id; ++ } ++ $query->condition("$current_revision_table.$id_field", $entity_ids, 'IN'); ++ } ++ ++ $rows = $query->execute()->fetchAll(); ++ $result = []; ++ foreach ($rows as $row) { ++ if (isset($row->{$current_revision_table})) { ++ $current_revisions = $row->{$current_revision_table}; ++ foreach ($current_revisions as $current_revision) { ++ if (isset($current_revision[$revision_id_field]) && isset($current_revision[$id_field])) { ++ $revision_id = $current_revision[$revision_id_field]; ++ $id = $current_revision[$id_field]; ++ $result[$revision_id] = $id; ++ } ++ } ++ } ++ } + } ++ else { ++ $query->leftJoin($entity_type->getRevisionTable(), 'revision', $query->joinCondition()->compare("base.$revision_id_field", "revision.$revision_id_field")); ++ ++ $query ++ ->fields('base', [$revision_id_field, $id_field]) ++ ->condition("revision.$workspace_field", $workspace_id, '=') ++ ->orderBy("base.$revision_id_field", 'ASC'); + +- $result = $query->execute()->fetchAllKeyed(); ++ // Restrict the result to a set of entity ID's if provided. ++ if ($entity_ids) { ++ $query->condition("base.$id_field", $entity_ids, 'IN'); ++ } ++ ++ $result = $query->execute()->fetchAllKeyed(); ++ } + + // Cache the list of associated entity IDs if the full list was requested. + if (!$entity_ids) { +@@ -306,20 +410,38 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti + */ + public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) { + $id_field = static::getIdField($entity->getEntityTypeId()); ++ if ($id_field === 'target_entity_id') { ++ $entity_id = (int) $entity->id(); ++ } ++ else { ++ $entity_id = (string) $entity->id(); ++ } + $query = $this->database->select(static::TABLE, 'wa') + ->fields('wa', ['workspace']) +- ->condition('[wa].[target_entity_type_id]', $entity->getEntityTypeId()) +- ->condition("[wa].[$id_field]", $entity->id()); ++ ->condition('wa.target_entity_type_id', $entity->getEntityTypeId()) ++ ->condition("wa.$id_field", $entity_id); + + // Use a self-join to get only the workspaces in which the latest revision + // of the entity is tracked. + if ($latest_revision) { +- $inner_select = $this->database->select(static::TABLE, 'wai') +- ->condition('[wai].[target_entity_type_id]', $entity->getEntityTypeId()) +- ->condition("[wai].[$id_field]", $entity->id()); +- $inner_select->addExpression('MAX([wai].[target_entity_revision_id])', 'max_revision_id'); ++ if ($this->database->driver() == 'mongodb') { ++ $inner_select = $this->database->select(static::TABLE, 'wai') ++ ->condition('wai.target_entity_type_id', $entity->getEntityTypeId()) ++ ->condition("wai.$id_field", $entity_id); ++ $inner_select->addExpressionMax('wai.target_entity_revision_id', 'max_revision_id'); ++ $max_revision_id = $inner_select->execute()->fetchField(); ++ if (!empty($max_revision_id)) { ++ $query->condition('wa.target_entity_revision_id', (int) $max_revision_id); ++ } ++ } ++ else { ++ $inner_select = $this->database->select(static::TABLE, 'wai') ++ ->condition('[wai].[target_entity_type_id]', $entity->getEntityTypeId()) ++ ->condition("[wai].[$id_field]", $entity->id()); ++ $inner_select->addExpression('MAX([wai].[target_entity_revision_id])', 'max_revision_id'); + +- $query->join($inner_select, 'waj', '[wa].[target_entity_revision_id] = [waj].[max_revision_id]'); ++ $query->join($inner_select, 'waj', '[wa].[target_entity_revision_id] = [waj].[max_revision_id]'); ++ } + } + + $result = $query->execute()->fetchCol(); +@@ -356,6 +478,13 @@ public function deleteAssociations($workspace_id = NULL, $entity_type_id = NULL, + $query->condition('target_entity_type_id', $entity_type_id, '='); + + if ($entity_ids) { ++ $entity_ids_as_integers = []; ++ $entity_ids_as_strings = []; ++ foreach ($entity_ids as & $entity_id) { ++ $entity_id = (int) $entity_id; ++ $entity_ids_as_integers[] = (int) $entity_id; ++ $entity_ids_as_strings[] = (string) $entity_id; ++ } + try { + $query->condition(static::getIdField($entity_type_id), $entity_ids, 'IN'); + } +@@ -364,13 +493,16 @@ public function deleteAssociations($workspace_id = NULL, $entity_type_id = NULL, + // to retrieve its identifier field type, so we try both. + $query->condition( + $query->orConditionGroup() +- ->condition('target_entity_id', $entity_ids, 'IN') +- ->condition('target_entity_id_string', $entity_ids, 'IN') ++ ->condition('target_entity_id', $entity_ids_as_integers, 'IN') ++ ->condition('target_entity_id_string', $entity_ids_as_strings, 'IN') + ); + } + } + + if ($revision_ids) { ++ foreach ($revision_ids as &$revision_id) { ++ $revision_id = (int) $revision_id; ++ } + $query->condition('target_entity_revision_id', $revision_ids, 'IN'); + } + } +@@ -385,18 +517,50 @@ public function deleteAssociations($workspace_id = NULL, $entity_type_id = NULL, + */ + public function initializeWorkspace(WorkspaceInterface $workspace) { + if ($parent_id = $workspace->parent->target_id) { +- $indexed_rows = $this->database->select(static::TABLE); +- $indexed_rows->addExpression(':new_id', 'workspace', [ +- ':new_id' => $workspace->id(), +- ]); +- $indexed_rows->fields(static::TABLE, [ +- 'target_entity_type_id', +- 'target_entity_id', +- 'target_entity_id_string', +- 'target_entity_revision_id', +- ]); +- $indexed_rows->condition('workspace', $parent_id); +- $this->database->insert(static::TABLE)->from($indexed_rows)->execute(); ++ if ($this->database->driver() == 'mongodb') { ++ $indexed_rows = $this->database->select(static::TABLE); ++ $indexed_rows->fields(static::TABLE, [ ++ 'target_entity_type_id', ++ 'target_entity_id', ++ 'target_entity_id_string', ++ 'target_entity_revision_id', ++ ]); ++ $indexed_rows->condition('workspace', $parent_id); ++ $result = $indexed_rows->execute()->fetchAll(); ++ if (!empty($result)) { ++ $query = $this->database->insert(static::TABLE)->fields([ ++ 'workspace', ++ 'target_entity_type_id', ++ 'target_entity_id', ++ 'target_entity_id_string', ++ 'target_entity_revision_id', ++ ]); ++ foreach ($result as $row) { ++ $query->values([ ++ $workspace->id(), ++ $row->target_entity_type_id, ++ $row->target_entity_id, ++ $row->target_entity_id, ++ $row->target_entity_revision_id, ++ ]); ++ } ++ $query->execute(); ++ } ++ } ++ else { ++ $indexed_rows = $this->database->select(static::TABLE); ++ $indexed_rows->addExpression(':new_id', 'workspace', [ ++ ':new_id' => $workspace->id(), ++ ]); ++ $indexed_rows->fields(static::TABLE, [ ++ 'target_entity_type_id', ++ 'target_entity_id', ++ 'target_entity_id_string', ++ 'target_entity_revision_id', ++ ]); ++ $indexed_rows->condition('workspace', $parent_id); ++ $this->database->insert(static::TABLE)->from($indexed_rows)->execute(); ++ } + } + + $this->associatedRevisions = $this->associatedInitialRevisions = []; +diff --git a/core/modules/workspaces/src/WorkspaceManager.php b/core/modules/workspaces/src/WorkspaceManager.php +index d6f2e3327c479f65673b8fc01fc539ebb04c39e6..1808dc0484ddb7d1427e62cbb34d6809152bea35 100644 +--- a/core/modules/workspaces/src/WorkspaceManager.php ++++ b/core/modules/workspaces/src/WorkspaceManager.php +@@ -222,7 +222,10 @@ public function purgeDeletedWorkspacesBatch() { + // entity was created inside that workspace), we need to delete the + // whole entity after all of its pending revisions are gone. + if (isset($initial_revision_ids[$revision_id])) { +- $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]); ++ $associated_entity = $associated_entity_storage->load($initial_revision_ids[$revision_id]); ++ if ($associated_entity) { ++ $associated_entity_storage->delete([$associated_entity]); ++ } + } + else { + // Delete the associated entity revision. +diff --git a/core/modules/workspaces/src/WorkspaceMerger.php b/core/modules/workspaces/src/WorkspaceMerger.php +index 56a198ee0d898e82e68c6982772973247e341022..e2c7278fc52b579ee02c24331903806cb1ceb2a9 100644 +--- a/core/modules/workspaces/src/WorkspaceMerger.php ++++ b/core/modules/workspaces/src/WorkspaceMerger.php +@@ -31,7 +31,17 @@ public function merge() { + } + + try { +- $transaction = $this->database->startTransaction(); ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } + $max_execution_time = ini_get('max_execution_time'); + $step_size = Settings::get('entity_update_batch_size', 50); + $counter = 0; +@@ -63,11 +73,18 @@ public function merge() { + } + } + } ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException($this->logger, $e); + throw $e; + } +diff --git a/core/modules/workspaces/src/WorkspacePublisher.php b/core/modules/workspaces/src/WorkspacePublisher.php +index f8610247269386eb1fe135a547458a27bea303a6..522e9bb308435b9aa5b56cf91be109c200718a27 100644 +--- a/core/modules/workspaces/src/WorkspacePublisher.php ++++ b/core/modules/workspaces/src/WorkspacePublisher.php +@@ -45,7 +45,20 @@ public function publish() { + } + + try { +- $transaction = $this->database->startTransaction(); ++ if ($this->database->driver() == 'mongodb') { ++ $session = $this->database->getMongodbSession(); ++ $session_started = FALSE; ++ if (!$session->isInTransaction()) { ++ $session->startTransaction(); ++ $session_started = TRUE; ++ } ++ } ++ else { ++ $transaction = $this->database->startTransaction(); ++ } ++ ++ // @todo Handle the publishing of a workspace with a batch operation in ++ // https://www.drupal.org/node/2958752. + $this->workspaceManager->executeOutsideWorkspace(function () use ($tracked_entities) { + $max_execution_time = ini_get('max_execution_time'); + $step_size = Settings::get('entity_update_batch_size', 50); +@@ -82,11 +95,18 @@ public function publish() { + } + } + }); ++ ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->commitTransaction(); ++ } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } ++ if (isset($session) && $session->isInTransaction() && $session_started) { ++ $session->abortTransaction(); ++ } + Error::logException($this->logger, $e); + throw $e; + } +diff --git a/core/modules/workspaces/src/WorkspacesAliasRepository.php b/core/modules/workspaces/src/WorkspacesAliasRepository.php +index 8ed057f6bd58243c6cfc069809f543623f546f13..f42c952687061b3cb797e299537b5d8738fcac06 100644 +--- a/core/modules/workspaces/src/WorkspacesAliasRepository.php ++++ b/core/modules/workspaces/src/WorkspacesAliasRepository.php +@@ -41,11 +41,34 @@ protected function getBaseQuery() { + $active_workspace = $this->workspaceManager->getActiveWorkspace(); + + $query = $this->connection->select('path_alias', 'original_base_table'); +- $wa_join = $query->leftJoin('workspace_association', NULL, "[%alias].[target_entity_type_id] = 'path_alias' AND [%alias].[target_entity_id] = [original_base_table].[id] AND [%alias].[workspace] = :active_workspace_id", [ +- ':active_workspace_id' => $active_workspace->id(), +- ]); +- $query->innerJoin('path_alias_revision', 'base_table', "[%alias].[revision_id] = COALESCE([$wa_join].[target_entity_revision_id], [original_base_table].[revision_id])"); +- $query->condition('base_table.status', 1); ++ if ($this->connection->driver() == 'mongodb') { ++ $query->leftJoin('workspace_association', 'wa', ++ $query->joinCondition() ++ ->condition("%alias.target_entity_type_id", 'path_alias') ++ ->compare("%alias.target_entity_id", "original_base_table.id") ++ ->condition("%alias.workspace", $active_workspace->id()) ++ ); ++ ++ $coalesce_field = [ ++ '$ifNull' => [ ++ '$' . $this->connection->escapeField('wa.target_entity_revision_id'), ++ '$' . $this->connection->escapeField('original_base_table.revision_id'), ++ ], ++ ]; ++ ++ $query->innerJoin('path_alias', 'base_table', ++ $query->joinCondition() ++ ->compare("base_table.path_alias_current_revision.revision_id", serialize($coalesce_field)) ++ ->condition('base_table.path_alias_current_revision.status', TRUE), ++ ); ++ } ++ else { ++ $wa_join = $query->leftJoin('workspace_association', NULL, "[%alias].[target_entity_type_id] = 'path_alias' AND [%alias].[target_entity_id] = [original_base_table].[id] AND [%alias].[workspace] = :active_workspace_id", [ ++ ':active_workspace_id' => $active_workspace->id(), ++ ]); ++ $query->innerJoin('path_alias_revision', 'base_table', "[%alias].[revision_id] = COALESCE([$wa_join].[target_entity_revision_id], [original_base_table].[revision_id])"); ++ $query->condition('base_table.status', 1); ++ } + + return $query; + } +diff --git a/core/modules/workspaces/workspaces.install b/core/modules/workspaces/workspaces.install +index 6dfb3312fb895554f6a528c4b9f2ef867320abb9..c0b3343bfd34c26dbc294921c5a018beaf144f69 100644 +--- a/core/modules/workspaces/workspaces.install ++++ b/core/modules/workspaces/workspaces.install +@@ -41,7 +41,7 @@ function workspaces_install(): void { + $query = \Drupal::entityTypeManager()->getStorage('user')->getQuery() + ->accessCheck(FALSE) + ->condition('roles', $admin_roles, 'IN') +- ->condition('status', 1) ++ ->condition('status', TRUE) + ->sort('uid', 'ASC') + ->range(0, 1); + $result = $query->execute(); diff --git a/readme.MD b/readme.MD index c49da0784a67f42fc3ea4f813193a16eeb830524..63d32a8364e9331adb0deeb266e3106f184c9b0d 100644 --- a/readme.MD +++ b/readme.MD @@ -83,7 +83,7 @@ When entity instances are revisionable, translatable and/or have field data atta -## Install Drupal on MongoDB (for Drupal Core [11.1.2](https://www.drupal.org/project/drupal/releases/11.1.2)) +## Install Drupal on MongoDB (for Drupal Core [11.1.3](https://www.drupal.org/project/drupal/releases/11.1.3)) This install guide uses the DDEV development environment. @@ -97,7 +97,7 @@ This install guide is based on the following DDEV [guide](https://ddev.readthedo ```ddev start``` -```ddev composer create drupal/recommended-project:11.1.2``` +```ddev composer create drupal/recommended-project:11.1.3``` ```ddev config --update``` @@ -115,7 +115,7 @@ This install guide is based on the following DDEV [guide](https://ddev.readthedo ### 5. Drupal Core needs to be patched to make it all work. ```cd web``` -```git apply -v modules/contrib/mongodb/patches/drupal-core-11.1.2.patch``` +```git apply -v modules/contrib/mongodb/patches/drupal-core-11.1.3.patch``` ```cd ..``` diff --git a/src/modules/paragraphs/ParagraphStorageSchema.php b/src/modules/paragraphs/ParagraphStorageSchema.php index b82f3536dea9a4a1cd084aac6f0ca45576a7be6a..3d4b2f1eadc889d73be75ccd5e1ce40aa12af1cd 100644 --- a/src/modules/paragraphs/ParagraphStorageSchema.php +++ b/src/modules/paragraphs/ParagraphStorageSchema.php @@ -28,4 +28,3 @@ class ParagraphStorageSchema extends SqlContentEntityStorageSchema { } } -