From f1dac228c27bb08a9cf7d6b85dd979ca657df300 Mon Sep 17 00:00:00 2001 From: David Bekker <david.bekker@finalist.nl> Date: Wed, 12 Mar 2025 09:07:57 +0100 Subject: [PATCH] update for Drupal 11.1.4 --- database-settings-example2.png | Bin 0 -> 109005 bytes patches/drupal-core-11.1.4.patch | 16723 +++++++++++++++++++++++++++++ src/Plugin/views/field/Date.php | 2 +- src/Plugin/views/filter/Date.php | 6 +- 4 files changed, 16727 insertions(+), 4 deletions(-) create mode 100644 database-settings-example2.png create mode 100644 patches/drupal-core-11.1.4.patch diff --git a/database-settings-example2.png b/database-settings-example2.png new file mode 100644 index 0000000000000000000000000000000000000000..443600948bd0df1037e2aa92bf3ac233d2abd021 GIT binary patch literal 109005 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	V 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 Mp00i_>zopr0K$9B!vFvP literal 0 HcmV?d00001 diff --git a/patches/drupal-core-11.1.4.patch b/patches/drupal-core-11.1.4.patch new file mode 100644 index 0000000..fe080e2 --- /dev/null +++ b/patches/drupal-core-11.1.4.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/src/Plugin/views/field/Date.php b/src/Plugin/views/field/Date.php index 6725ef7..3659b3f 100644 --- a/src/Plugin/views/field/Date.php +++ b/src/Plugin/views/field/Date.php @@ -35,7 +35,7 @@ class Date extends CoreDate { $timezone = !empty($this->options['timezone']) ? $this->options['timezone'] : NULL; // Will be positive for a datetime in the past (ago), and negative for a // datetime in the future (hence). - $time_diff = \Drupal::time()->getRequestMicroTime() - $value; + $time_diff = intval(\Drupal::time()->getRequestMicroTime() - $value); switch ($format) { case 'raw time ago': return $this->dateFormatter->formatTimeDiffSince($value, ['granularity' => is_numeric($custom_format) ? $custom_format : 2]); diff --git a/src/Plugin/views/filter/Date.php b/src/Plugin/views/filter/Date.php index 9ceac91..54ce9ae 100644 --- a/src/Plugin/views/filter/Date.php +++ b/src/Plugin/views/filter/Date.php @@ -21,8 +21,8 @@ class Date extends CoreDate { if (!empty($this->value['type']) && $this->value['type'] == 'offset') { $time = \Drupal::time()->getRequestMicroTime(); - $a = new UTCDateTime(($time - $a) * 1000); - $b = new UTCDateTime(($time + $b) * 1000); + $a = new UTCDateTime(intval(($time - $a) * 1000)); + $b = new UTCDateTime(intval(($time + $b) * 1000)); } else { $a = new UTCDateTime($a * 1000); @@ -48,7 +48,7 @@ class Date extends CoreDate { if (!empty($this->value['type']) && $this->value['type'] == 'offset') { $time = \Drupal::time()->getRequestMicroTime(); - $value = new UTCDateTime(($time + $value) * 1000); + $value = new UTCDateTime(intval(($time + $value) * 1000)); } else { $value = new UTCDateTime($value * 1000); -- GitLab