From 3801adceb86e8fdeddec5c8b150d9985181a9cc2 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Mon, 22 Nov 2021 17:01:11 +0100
Subject: [PATCH] [BUGFIX] Better sys_refindex with workspace mm
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This fixes issues regarding sys_refindex handling when dealing
with workspace mm relations. Various DataHandler tests run with
disabled refindex check after performing mm related operations.
All those are enabled now, so all known issues in this area
are fixed.

The patch touches various areas in DataHandler, ReferenceIndex
and RelationHandler to achieve this.

The key changes are triggered by a database scenario that is
unique for mm relations. Usually, when creating a workspace record
or overlay, child record overlays (for instance inline children)
are also created in this workspace.

For mm records, this can be different: When the local or foreign
side of a record is created in a workspace, it does not necessarily
mean that a connected opposite side is also created as workspace
overlay.

This would otherwise create the need for a recursive operation that
would basically end up with "everything" being created as workspace
overlay. However, mm table entries are created for these relations,
leading to a situation that a workspace record can point to a
non-workspace record through the mm table.

Reference index entries for mm table connections are always handled
from the 'local' side. For instance, with categories, 'sys_category'
table is the local side, with 'pages', 'tt_content' and others
being the foreign side. If now one of the records on the foreign
side gets a workspace overlay record, sys_refindex rows of all
connected records of the affected local side need to be created.
This can lead to the funny situation that we end up with refindex
rows that point from a non-workspace to a non-workspace record,
but have a non-0 workspace entry. Those additional rows are needed
to have "the full set" and proper sorting when looking at refindex
of such a relation from the local side.

The patch basically handles especially these 'foreign side has
a workspace overlay' scenarios, plus the side effects that
have to be taken care of when discarding and publishing these
records.

Additionally, a couple of side effects are tackled: First, the
ReferenceIndex->updateIndex() - that's the main logic when
running cli referenceindex:update command - is tuned to drop
reference index entries related to meanwhile deleted workspaces.
This is covered with additional tests and is done now since
the code needs to iterate over existing workspaces for local-side
mm records that may have workspace overlays on the foreign side.
So most of the code had to be created now anyways.

RelationHandler->readMM() receives a fix for a long standing bug
(Issue #83582, introduced by #57169), to no longer show workspace
relations in live when looking at local-side records, for instance
when looking at category items in live when one of the connected
items has a workspace overlay.

Next to lots of comments explaining details in place and
referencing the test cases that nail specific scenarios, a
couple of @todo's are added to point out things we may
want to tackle in the future.

All in all, the patch resolves a series of issues that will
especially lead to 'categories' being much more reliable in
workspaces.

Change-Id: I24407f93665852fa761f6fbe6c5ab249473468d2
Related: #57169
Resolves: #83582
Resolves: #96067
Releases: master, 11.5
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72250
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../core/Classes/DataHandling/DataHandler.php |  41 +++-
 .../DataHandling/ReferenceIndexUpdater.php    |   4 +-
 .../core/Classes/Database/ReferenceIndex.php  | 202 +++++++++++++++---
 .../core/Classes/Database/RelationHandler.php |  14 +-
 .../DataSet/changeCategoryRelationSorting.csv |   1 +
 ...eIndexRemoveNonExistingWorkspaceImport.csv |  16 ++
 ...eIndexRemoveNonExistingWorkspaceResult.csv |  14 ++
 ...deMmHavingForeignWorkspaceRecordImport.csv |  32 +++
 ...deMmHavingForeignWorkspaceRecordResult.csv |  39 ++++
 ...eIndexRemoveNonExistingWorkspaceImport.csv |  31 +++
 ...eIndexRemoveNonExistingWorkspaceResult.csv |  26 +++
 .../Database/ReferenceIndexTest.php           |  39 ++++
 .../ReferenceIndexWorkspaceLoadedTest.php     |  53 +++++
 .../Classes/Hook/DataHandlerHook.php          |  19 +-
 .../ManyToMany/Modify/ActionTest.php          |  33 ---
 .../Modify/DataSet/addCategoryRelation.csv    |   8 +
 .../DataSet/changeCategoryRelationSorting.csv |   4 +
 .../Modify/DataSet/copyContentOfRelation.csv  |   5 +
 .../ManyToMany/Modify/DataSet/copyPage.csv    |   8 +
 .../DataSet/createContentNAddRelation.csv     |   3 +
 .../Modify/DataSet/deleteCategoryRelation.csv |   9 +-
 .../DataSet/deleteContentOfRelation.csv       |   3 +
 .../DataSet/localizeContentOfRelation.csv     |   5 +
 .../Modify/DataSet/modifyBothsOfRelation.csv  |   2 +
 .../DataSet/modifyContentOfRelation.csv       |   3 +
 .../moveContentOfRelationToDifferentPage.csv  |   3 +
 .../ManyToMany/Publish/ActionTest.php         |  33 ---
 .../Publish/DataSet/addCategoryRelation.csv   |   1 +
 .../Publish/DataSet/copyContentOfRelation.csv |   2 +
 .../ManyToMany/Publish/DataSet/copyPage.csv   |   4 +
 .../DataSet/createContentNAddRelation.csv     |   1 +
 .../DataSet/deleteCategoryRelation.csv        |   3 +-
 .../DataSet/localizeContentOfRelation.csv     |   2 +
 .../ManyToMany/PublishAll/ActionTest.php      |  33 ---
 .../DataSet/addCategoryRelation.csv           |   1 +
 .../DataSet/copyContentOfRelation.csv         |   2 +
 .../PublishAll/DataSet/copyPage.csv           |   4 +
 .../DataSet/createContentNAddRelation.csv     |   1 +
 .../DataSet/deleteCategoryRelation.csv        |   3 +-
 .../DataSet/localizeContentOfRelation.csv     |   2 +
 40 files changed, 564 insertions(+), 145 deletions(-)
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceImport.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceResult.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordImport.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordResult.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceImport.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceResult.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/ReferenceIndexTest.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/ReferenceIndexWorkspaceLoadedTest.php

diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index fff706a18a1d..d83d8e31d5fe 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -5703,7 +5703,7 @@ class DataHandler implements LoggerAwareInterface
                     }
                 }
             } elseif ($this->isReferenceField($fieldConfig) && !empty($fieldConfig['MM'])) {
-                $this->discardMmRelations($fieldConfig, $record);
+                $this->discardMmRelations($table, $fieldConfig, $record);
             }
             // @todo not inline and not mm - probably not handled correctly and has no proper test coverage yet
         }
@@ -5713,10 +5713,11 @@ class DataHandler implements LoggerAwareInterface
      * When a workspace record row is discarded that has mm relations, existing mm table rows need
      * to be deleted. The method performs the delete operation depending on TCA field configuration.
      *
+     * @param string $table Table name of this record
      * @param array $fieldConfig TCA configuration of this field
      * @param array $record The full record of a left- or ride-side relation
      */
-    protected function discardMmRelations(array $fieldConfig, array $record): void
+    protected function discardMmRelations(string $table, array $fieldConfig, array $record): void
     {
         $recordUid = (int)$record['uid'];
         $mmTableName = $fieldConfig['MM'];
@@ -5745,6 +5746,14 @@ class DataHandler implements LoggerAwareInterface
             );
         }
         $queryBuilder->execute();
+
+        // refindex treatment for mm relation handling: If the to discard record is foreign side of an mm relation,
+        // there may be other refindex rows that become obsolete when that record is discarded. See Modify
+        // addCategoryRelation sys_category-29->tt_content-298. We thus register an update for references
+        // to this item (right side - ref_table, ref_uid) in reference index updater to catch these.
+        if ($relationUidFieldName === 'uid_foreign') {
+            $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $recordUid, (int)$record['t3ver_wsid']);
+        }
     }
 
     /**
@@ -5877,7 +5886,7 @@ class DataHandler implements LoggerAwareInterface
      *
      * @internal should only be used from within DataHandler
      */
-    public function versionPublishManyToManyRelations(string $table, array $liveRecord, array $workspaceRecord): void
+    public function versionPublishManyToManyRelations(string $table, array $liveRecord, array $workspaceRecord, int $fromWorkspace): void
     {
         if (!is_array($GLOBALS['TCA'][$table]['columns'])) {
             return;
@@ -5938,7 +5947,8 @@ class DataHandler implements LoggerAwareInterface
 
         // Update mm table relations of workspace record to uid of live record
         foreach ($toUpdateRegistry as $config) {
-            $uidFieldName = $this->mmRelationIsLocalSide($config) ? 'uid_local' : 'uid_foreign';
+            $mmRelationIsLocalSide = $this->mmRelationIsLocalSide($config);
+            $uidFieldName = $mmRelationIsLocalSide ? 'uid_local' : 'uid_foreign';
             $mmTableName = $config['MM'];
             $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($mmTableName);
             $queryBuilder->update($mmTableName);
@@ -5954,6 +5964,17 @@ class DataHandler implements LoggerAwareInterface
                 ));
             }
             $queryBuilder->execute();
+
+            if (!$mmRelationIsLocalSide) {
+                // refindex treatment for mm relation handling: If the to publish record is foreign side of an mm relation, we need
+                // to instruct refindex updater to update all local side references for the live record the current workspace record
+                // has on foreign side. See ManyToMany Publish addCategoryRelation, this will create the sys_category-31->tt_content-297 entry.
+                $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$workspaceRecord['uid'], $fromWorkspace, 0);
+                // Similar, when in mm foreign side and relations are deleted in live during publish, other relations pointing to the
+                // same local side record may need updates due to different sorting, and the former refindex entry of the live record
+                // needs updates. See ManyToMany Publish deleteCategoryRelation scenario.
+                $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, (int)$liveRecord['uid'], 0);
+            }
         }
     }
 
@@ -7452,12 +7473,24 @@ class DataHandler implements LoggerAwareInterface
      * @param string $table Table name, used as tablename and ref_table
      * @param int $uid Record uid, used as recuid and ref_uid
      * @param int $workspace Workspace the record lives in
+     * @internal should only be used from within DataHandler
      */
     public function registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace): void
     {
         $this->referenceIndexUpdater->registerForDrop($table, $uid, $workspace);
     }
 
+    /**
+     * Helper method to access referenceIndexUpdater->registerUpdateForReferencesToItem()
+     * from within workspace DataHandlerHook.
+     *
+     * @internal Exists only for workspace DataHandlerHook. May vanish any time.
+     */
+    public function registerReferenceIndexUpdateForReferencesToItem(string $table, int $uid, int $workspace, int $targetWorkspace = null): void
+    {
+        $this->referenceIndexUpdater->registerUpdateForReferencesToItem($table, $uid, $workspace, $targetWorkspace);
+    }
+
     /*********************************************
      *
      * Misc functions
diff --git a/typo3/sysext/core/Classes/DataHandling/ReferenceIndexUpdater.php b/typo3/sysext/core/Classes/DataHandling/ReferenceIndexUpdater.php
index 36a5286e39f6..d3f61710a853 100644
--- a/typo3/sysext/core/Classes/DataHandling/ReferenceIndexUpdater.php
+++ b/typo3/sysext/core/Classes/DataHandling/ReferenceIndexUpdater.php
@@ -82,13 +82,13 @@ class ReferenceIndexUpdater
     /**
      * Find reference index rows pointing to given table/uid combination and register them for update. Important in
      * delete and publish scenarios where a child is deleted to make sure any references to this child are dropped, too.
-     * In publish scenarios reference index may exist for a non live workspace, but should be updated for live workspace.
+     * In publish scenarios reference index may exist for a non-live workspace, but should be updated for live workspace.
      * The optional $targetWorkspace argument is used for this.
      *
      * @param string $table Table name, used as ref_table
      * @param int $uid Record uid, used as ref_uid
      * @param int $workspace The workspace given record lives in
-     * @param int $targetWorkspace The target workspace the record has been swapped to
+     * @param int|null $targetWorkspace The target workspace the record has been swapped to
      */
     public function registerUpdateForReferencesToItem(string $table, int $uid, int $workspace, int $targetWorkspace = null): void
     {
diff --git a/typo3/sysext/core/Classes/Database/ReferenceIndex.php b/typo3/sysext/core/Classes/Database/ReferenceIndex.php
index 5539abc4fb64..d65d56024ddd 100644
--- a/typo3/sysext/core/Classes/Database/ReferenceIndex.php
+++ b/typo3/sysext/core/Classes/Database/ReferenceIndex.php
@@ -24,25 +24,19 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Backend\View\ProgressListenerInterface;
 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\DataHandling\Event\IsTableExcludedFromReferenceIndexEvent;
 use TYPO3\CMS\Core\DataHandling\SoftReference\SoftReferenceParserFactory;
 use TYPO3\CMS\Core\Registry;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Reference index processing and relation extraction
  *
- * NOTICE: When the reference index is updated for an offline version the results may not be correct.
- * First, lets assumed that the reference update happens in LIVE workspace (ALWAYS update from Live workspace if you analyze whole database!)
- * Secondly, lets assume that in a Draft workspace you have changed the data structure of a parent page record - this is (in TemplaVoila) inherited by subpages.
- * When in the LIVE workspace the data structure for the records/pages in the offline workspace will not be evaluated to the right one simply because the data
- * structure is taken from a rootline traversal and in the Live workspace that will NOT include the changed DataStructure! Thus the evaluation will be based
- * on the Data Structure set in the Live workspace!
- * Somehow this scenario is rarely going to happen. Yet, it is an inconsistency and I see now practical way to handle it - other than simply ignoring
- * maintaining the index for workspace records. Or we can say that the index is precise for all Live elements while glitches might happen in an offline workspace?
- * Anyway, I just wanted to document this finding - I don't think we can find a solution for it. And its very TemplaVoila specific.
+ * @internal Extensions shouldn't fiddle with the reference index themselves, it's task of DataHandler to do this.
  */
 class ReferenceIndex implements LoggerAwareInterface
 {
@@ -359,11 +353,13 @@ class ReferenceIndex implements LoggerAwareInterface
      */
     protected function createEntryDataUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, string $referencedTable, int $referencedUid, string $referenceString = '', int $sort = -1, string $softReferenceKey = '', string $softReferenceId = '')
     {
-        $workspaceId = 0;
+        $currentWorkspace = $this->getWorkspaceId();
         if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
-            $workspaceId = $this->getWorkspaceId();
-            if (isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $workspaceId) {
-                // The given record is workspace-enabled but doesn't live in the selected workspace => don't add index as it's not actually there
+            $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
+            if (isset($record['t3ver_wsid']) && (int)$record['t3ver_wsid'] !== $currentWorkspace && empty($fieldConfig['MM'])) {
+                // The given record is workspace-enabled but doesn't live in the selected workspace. Don't add index, it's not actually there.
+                // We still add those rows if the record is a local side live record of an MM relation and can be a target of a workspace record.
+                // See workspaces ManyToMany Modify addCategoryRelation for details on this case.
                 return false;
             }
         }
@@ -375,7 +371,7 @@ class ReferenceIndex implements LoggerAwareInterface
             'softref_key' => $softReferenceKey,
             'softref_id' => $softReferenceId,
             'sorting' => $sort,
-            'workspace' => $workspaceId,
+            'workspace' => $currentWorkspace,
             'ref_table' => $referencedTable,
             'ref_uid' => $referencedUid,
             'ref_string' => mb_substr($referenceString, 0, 1024),
@@ -458,7 +454,7 @@ class ReferenceIndex implements LoggerAwareInterface
                     $conf['softref'] = 'typolink';
                 }
                 // Add DB:
-                $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table);
+                $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table, $row);
                 if (!empty($resultsFromDatabase)) {
                     // Create an entry for the field with all DB relations:
                     $outRow[$field] = [
@@ -567,9 +563,10 @@ class ReferenceIndex implements LoggerAwareInterface
      * @param array $conf Field configuration array of type "TCA/columns
      * @param int $uid Field uid
      * @param string $table Table name
+     * @param array $row
      * @return array|bool If field type is OK it will return an array with the database relations. Else FALSE
      */
-    protected function getRelations_procDB($value, $conf, $uid, $table = '')
+    protected function getRelations_procDB($value, $conf, $uid, $table = '', array $row = [])
     {
         // Get IRRE relations
         if (empty($conf)) {
@@ -586,11 +583,36 @@ class ReferenceIndex implements LoggerAwareInterface
         if ($this->isDbReferenceField($conf)) {
             $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
             if ($conf['MM_opposite_field'] ?? false) {
+                // Never handle sys_refindex when looking at MM from foreign side
                 return [];
             }
+
             $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
+            $dbAnalysis->setWorkspaceId($this->getWorkspaceId());
             $dbAnalysis->start($value, $allowedTables, $conf['MM'] ?? '', $uid, $table, $conf);
-            return $dbAnalysis->itemArray;
+            $itemArray = $dbAnalysis->itemArray;
+
+            if (ExtensionManagementUtility::isLoaded('workspaces')
+                && $this->getWorkspaceId() > 0
+                && !empty($conf['MM'] ?? '')
+                && !empty($conf['allowed'] ?? '')
+                && empty($conf['MM_opposite_field'] ?? '')
+                && (int)($row['t3ver_wsid'] ?? 0) === 0
+            ) {
+                // When dealing with local side mm relations in workspace 0, there may be workspace records on the foreign
+                // side, for instance when those got an additional category. See ManyToMany Modify addCategoryRelations test.
+                // In those cases, the full set of relations must be written to sys_refindex as workspace rows.
+                // But, if the relations in this workspace and live are identical, no sys_refindex workspace rows
+                // have to be added.
+                $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
+                $dbAnalysis->setWorkspaceId(0);
+                $dbAnalysis->start($value, $allowedTables, $conf['MM'], $uid, $table, $conf);
+                $itemArrayLive = $dbAnalysis->itemArray;
+                if ($itemArrayLive === $itemArray) {
+                    $itemArray = false;
+                }
+            }
+            return $itemArray;
         }
         return false;
     }
@@ -890,18 +912,34 @@ class ReferenceIndex implements LoggerAwareInterface
      * @param bool $testOnly If set, only a test
      * @param ProgressListenerInterface|null $progressListener If set, the current progress is added to the listener
      * @return array Header and body status content
+     * @todo: Consider moving this together with the helper methods to a dedicated class.
      */
     public function updateIndex($testOnly, ?ProgressListenerInterface $progressListener = null)
     {
         $errors = [];
         $tableNames = [];
         $recCount = 0;
-        // Traverse all tables:
+        $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces');
         $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
         $refIndexConnectionName = empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']['sys_refindex'])
                 ? ConnectionPool::DEFAULT_CONNECTION_NAME
                 : $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping']['sys_refindex'];
 
+        // Drop sys_refindex rows from deleted workspaces
+        $listOfActiveWorkspaces = $this->getListOfActiveWorkspaces();
+        $unusedWorkspaceRows = $this->getAmountOfUnusedWorkspaceRowsInReferenceIndex($listOfActiveWorkspaces);
+        if ($unusedWorkspaceRows > 0) {
+            $error = 'Index table hosted ' . $unusedWorkspaceRows . ' indexes for non-existing or deleted workspaces, now removed.';
+            $errors[] = $error;
+            if ($progressListener) {
+                $progressListener->log($error, LogLevel::WARNING);
+            }
+            if (!$testOnly) {
+                $this->removeUnusedWorkspaceRowsFromReferenceIndex($listOfActiveWorkspaces);
+            }
+        }
+
+        // Main loop traverses all records of all TCA tables
         foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
             if ($this->shouldExcludeTableFromReferenceIndex($tableName)) {
                 continue;
@@ -910,6 +948,18 @@ class ReferenceIndex implements LoggerAwareInterface
                 ? ConnectionPool::DEFAULT_CONNECTION_NAME
                 : $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
 
+            // Some additional magic is needed if the table has a field that is the local side of
+            // a mm relation. See the variable usage below for details.
+            $tableHasLocalSideMmRelation = false;
+            foreach (($cfg['columns'] ?? []) as $fieldConfig) {
+                if (!empty($fieldConfig['config']['MM'] ?? '')
+                    && !empty($fieldConfig['config']['allowed'] ?? '')
+                    && empty($fieldConfig['config']['MM_opposite_field'] ?? '')
+                ) {
+                    $tableHasLocalSideMmRelation = true;
+                }
+            }
+
             $fields = ['uid'];
             if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
                 $fields[] = 't3ver_wsid';
@@ -940,18 +990,38 @@ class ReferenceIndex implements LoggerAwareInterface
                 if ($progressListener) {
                     $progressListener->advance();
                 }
-                /** @var ReferenceIndex $refIndexObj */
-                $refIndexObj = GeneralUtility::makeInstance(self::class);
-                if (isset($record['t3ver_wsid'])) {
-                    $refIndexObj->setWorkspaceId($record['t3ver_wsid']);
-                }
-                $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly);
-                $recCount++;
-                if ($result['addedNodes'] || $result['deletedNodes']) {
-                    $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
-                    $errors[] = $error;
-                    if ($progressListener) {
-                        $progressListener->log($error, LogLevel::WARNING);
+
+                if ($isWorkspacesLoaded && $tableHasLocalSideMmRelation && (int)($record['t3ver_wsid'] ?? 0) === 0) {
+                    // If we have record that can be the local side of a workspace relation, workspace records
+                    // may point to it, even though the record has no workspace overlay. See workspace ManyToMany
+                    // Modify addCategoryRelation as example. In those cases, we need to iterate all active workspaces
+                    // and update refindex for all foreign workspace records that point to it.
+                    foreach ($listOfActiveWorkspaces as $workspaceId) {
+                        $refIndexObj = GeneralUtility::makeInstance(self::class);
+                        $refIndexObj->setWorkspaceId($workspaceId);
+                        $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly);
+                        $recCount++;
+                        if ($result['addedNodes'] || $result['deletedNodes']) {
+                            $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
+                            $errors[] = $error;
+                            if ($progressListener) {
+                                $progressListener->log($error, LogLevel::WARNING);
+                            }
+                        }
+                    }
+                } else {
+                    $refIndexObj = GeneralUtility::makeInstance(self::class);
+                    if (isset($record['t3ver_wsid'])) {
+                        $refIndexObj->setWorkspaceId($record['t3ver_wsid']);
+                    }
+                    $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly);
+                    $recCount++;
+                    if ($result['addedNodes'] || $result['deletedNodes']) {
+                        $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
+                        $errors[] = $error;
+                        if ($progressListener) {
+                            $progressListener->log($error, LogLevel::WARNING);
+                        }
                     }
                 }
             }
@@ -960,6 +1030,13 @@ class ReferenceIndex implements LoggerAwareInterface
             }
 
             // Subselect based queries only work on the same connection
+            // @todo: Consider dropping this in v12 and always use sub select: The base set of tables should
+            //        be in exactly one DB and only tables like caches should be "extractable" to a different DB?!
+            //        Even though sys_refindex is a "cache-like" table since it only holds secondary information that
+            //        can always be re-created by analyzing the entire data set, it shouldn't be possible to run it
+            //        on a different database since that prevents quick joins between sys_refindex and target relations.
+            //        We should probably have some report and/or install tool check to make sure all main tables
+            //        are on the same connection in v12.
             if ($refIndexConnectionName !== $tableConnectionName) {
                 $this->logger->error('Not checking table {table_name} for lost indexes, "sys_refindex" table uses a different connection', ['table_name' => $tableName]);
                 continue;
@@ -1017,6 +1094,8 @@ class ReferenceIndex implements LoggerAwareInterface
         }
 
         // Searching lost indexes for non-existing tables
+        // @todo: Consider moving this *before* the main re-index logic to have a smaller
+        //        dataset when starting with heavy lifting.
         $lostTables = $this->getAmountOfUnusedTablesInReferenceIndex($tableNames);
         if ($lostTables > 0) {
             $error = 'Index table hosted ' . $lostTables . ' indexes for non-existing tables, now removed';
@@ -1044,6 +1123,69 @@ class ReferenceIndex implements LoggerAwareInterface
         return ['resultText' => trim($recordsCheckedString), 'errors' => $errors];
     }
 
+    /**
+     * Helper method of updateIndex().
+     * Create list of non-deleted "active" workspace uid's. This contains at least 0 "live workspace".
+     *
+     * @return int[]
+     */
+    private function getListOfActiveWorkspaces(): array
+    {
+        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
+            // If ext:workspaces is not loaded, "0" is the only valid one.
+            return [0];
+        }
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
+        // There are no "hidden" workspaces, which wouldn't make much sense anyways.
+        $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        $result = $queryBuilder->select('uid')->from('sys_workspace')->orderBy('uid')->execute();
+        // "0", plus non-deleted workspaces are active
+        $activeWorkspaces = [0];
+        while ($row = $result->fetchFirstColumn()) {
+            $activeWorkspaces[] = (int)$row[0];
+        }
+        return $activeWorkspaces;
+    }
+
+    /**
+     * Helper method of updateIndex() to find number of rows in sys_refindex that
+     * relate to a non-existing or deleted workspace record, even if workspaces is
+     * not loaded at all, but has been loaded somewhere in the past and sys_refindex
+     * rows have been created.
+     */
+    private function getAmountOfUnusedWorkspaceRowsInReferenceIndex(array $activeWorkspaces): int
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
+        $numberOfInvalidWorkspaceRecords = $queryBuilder->count('hash')
+            ->from('sys_refindex')
+            ->where(
+                $queryBuilder->expr()->notIn(
+                    'workspace',
+                    $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY)
+                )
+            )
+            ->execute()
+            ->fetchOne();
+        return (int)$numberOfInvalidWorkspaceRecords;
+    }
+
+    /**
+     * Pair method of getAmountOfUnusedWorkspaceRowsInReferenceIndex() to actually delete
+     * sys_refindex rows of deleted workspace records, or all if ext:workspace is not loaded.
+     */
+    private function removeUnusedWorkspaceRowsFromReferenceIndex(array $activeWorkspaces): void
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
+        $queryBuilder->delete('sys_refindex')
+            ->where(
+                $queryBuilder->expr()->notIn(
+                    'workspace',
+                    $queryBuilder->createNamedParameter($activeWorkspaces, Connection::PARAM_INT_ARRAY)
+                )
+            )
+            ->execute();
+    }
+
     protected function getAmountOfUnusedTablesInReferenceIndex(array $tableNames): int
     {
         $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
diff --git a/typo3/sysext/core/Classes/Database/RelationHandler.php b/typo3/sysext/core/Classes/Database/RelationHandler.php
index 6866abfd0aa0..3b2774b44b7d 100644
--- a/typo3/sysext/core/Classes/Database/RelationHandler.php
+++ b/typo3/sysext/core/Classes/Database/RelationHandler.php
@@ -512,6 +512,11 @@ class RelationHandler
     /**
      * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
      *
+     * @todo: The source record is not checked for correct workspace. Say there is a category 5 in
+     *        workspace 1. setWorkspace(0) is called, after that readMM('sys_category_record_mm', 5 ...).
+     *        readMM will *still* return the list of records connected to this workspace 1 item,
+     *        even though workspace 0 has been set.
+     *
      * @param string $tableName MM Tablename
      * @param int|string $uid Local UID
      * @param string $mmOppositeTable Opposite table name
@@ -1377,8 +1382,15 @@ class RelationHandler
     }
 
     /**
+     * @todo: It *should* be possible to drop all three 'purge' methods by using
+     *        a clever join within readMM - that sounds doable now with pid -1 and
+     *        ws-pair records being gone since v11. It would resolve this indirect
+     *        callback logic and would reduce some queries. The (workspace) mm tests
+     *        should be complete enough now to verify if a change like that would do.
+     *
      * @param int|null $workspaceId
      * @return bool Whether items have been purged
+     * @internal
      */
     public function purgeItemArray($workspaceId = null)
     {
@@ -1470,7 +1482,7 @@ class RelationHandler
                 ->from($tableName)
                 ->where(
                     $queryBuilder->expr()->in(
-                        't3ver_oid',
+                        'uid',
                         $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
                     ),
                     $queryBuilder->expr()->neq(
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv b/typo3/sysext/core/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
index f95798989d9a..af81ce46e960 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
@@ -26,6 +26,7 @@
 ,298,89,512,0,0,0,0,0,0,0,0,"Regular Element #2",0,2
 "sys_refindex",,,,,,,,,,,,,,
 ,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,
+# @todo: Broken. Both 28->297 and 29->297 have sorting 0 here and in mm table ... sys_refindex has no sorting_foreign
 ,"3c637501ab9c158daa933643bff8cc43","sys_category",28,"items",,,,0,0,"tt_content",297,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceImport.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceImport.csv
new file mode 100644
index 000000000000..21e2abc22467
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceImport.csv
@@ -0,0 +1,16 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","tx_testirreforeignfield_hotels",,,,,,
+,1,0,256,0,0,0,0,0,0,"FunctionalTest",0,,,,,,
+,88,1,256,0,0,0,0,0,0,"DataHandlerTest",0,,,,,,
+,89,88,256,0,0,0,0,0,0,"Relations",1,,,,,,
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","image","tx_testirreforeignfield_hotels",,,
+,297,89,256,0,0,0,0,0,0,0,0,"Regular Element #1",0,1,,,
+"tx_testirreforeignfield_hotel",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parentid","parenttable","parentidentifier","offers"
+,3,89,1,0,0,0,,0,0,0,0,0,"Hotel #1",297,"tt_content","1nff.hotels",0
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+,"b9be8f0166b062001c138957270e72e2","tt_content",297,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",3,,,,,,
+# workspace extension not loaded - ws 1 entry should be removed
+,"iShouldBeRemoved1c138957270e72e2","tt_content",297,"tx_testirreforeignfield_hotels",,,,0,1,"tx_testirreforeignfield_hotel",3,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceResult.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceResult.csv
new file mode 100644
index 000000000000..45ec68ee26af
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceResult.csv
@@ -0,0 +1,14 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","tx_testirreforeignfield_hotels",,,,,,
+,1,0,256,0,0,0,0,0,0,"FunctionalTest",0,,,,,,
+,88,1,256,0,0,0,0,0,0,"DataHandlerTest",0,,,,,,
+,89,88,256,0,0,0,0,0,0,"Relations",1,,,,,,
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","image","tx_testirreforeignfield_hotels",,,
+,297,89,256,0,0,0,0,0,0,0,0,"Regular Element #1",0,1,,,
+"tx_testirreforeignfield_hotel",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parentid","parenttable","parentidentifier","offers"
+,3,89,1,0,0,0,,0,0,0,0,0,"Hotel #1",297,"tt_content","1nff.hotels",0
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+,"b9be8f0166b062001c138957270e72e2","tt_content",297,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",3,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordImport.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordImport.csv
new file mode 100644
index 000000000000..ba2ce982ec4d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordImport.csv
@@ -0,0 +1,32 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title",,,,,,,,
+,1,0,256,0,0,0,0,0,"FunctionalTest",,,,,,,,
+,88,1,256,0,0,0,0,0,"DataHandlerTest",,,,,,,,
+,89,88,256,0,0,0,0,0,"Relations",,,,,,,,
+"sys_workspace",,,,,,,,,,,,,,,,,
+,"uid","pid","deleted","title","adminusers","members","db_mountpoints","file_mountpoints","freeze","live_edit","publish_access","custom_stages","stagechg_notification","edit_notification_defaults","edit_allow_notificaton_settings","publish_notification_defaults","publish_allow_notificaton_settings"
+,1,0,0,"Workspace #1",,,,,0,0,0,0,0,0,0,0,0
+"sys_category",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parent","items",,,,
+,28,0,256,0,0,0,0,0,0,0,"Category A",0,0,,,,
+,29,0,512,0,0,0,0,0,0,0,"Category B",0,0,,,,
+,30,0,768,0,0,0,0,0,0,0,"Category C",0,0,,,,
+,31,0,1024,0,0,0,0,0,0,0,"Category A.A",28,0,,,,
+"sys_category_record_mm",,,,,,,,,,,,,,,,,
+,"uid_local","uid_foreign","tablenames","sorting","sorting_foreign","fieldname",,,,,,,,,,,
+,28,297,"tt_content",0,1,"categories",,,,,,,,,,,
+,29,297,"tt_content",0,2,"categories",,,,,,,,,,,
+,29,298,"tt_content",0,1,"categories",,,,,,,,,,,
+,30,298,"tt_content",0,2,"categories",,,,,,,,,,,
+,28,299,"tt_content",0,1,"categories",,,,,,,,,,,
+,29,299,"tt_content",0,2,"categories",,,,,,,,,,,
+,31,299,"tt_content",0,3,"categories",,,,,,,,,,,
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","image","categories",,,,
+,297,89,256,0,0,0,0,0,0,0,"Regular Element #1",0,2,,,,
+,298,89,512,0,0,0,0,0,0,0,"Regular Element #2",0,2,,,,
+,299,89,256,0,0,0,1,0,0,297,"Regular Element #1",0,3,,,,
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+# Should be removed - it is a workspace entry with local & foreign not having ws overlays and there is no other relation to a workspace record
+,"d624da48d7d6427f385a8197cb90c391","sys_category",30,"items",,,,0,1,"tt_content",298,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordResult.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordResult.csv
new file mode 100644
index 000000000000..f8567a0fd726
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordResult.csv
@@ -0,0 +1,39 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title",,,,,,,,
+,1,0,256,0,0,0,0,0,"FunctionalTest",,,,,,,,
+,88,1,256,0,0,0,0,0,"DataHandlerTest",,,,,,,,
+,89,88,256,0,0,0,0,0,"Relations",,,,,,,,
+"sys_workspace",,,,,,,,,,,,,,,,,
+,"uid","pid","deleted","title","adminusers","members","db_mountpoints","file_mountpoints","freeze","live_edit","publish_access","custom_stages","stagechg_notification","edit_notification_defaults","edit_allow_notificaton_settings","publish_notification_defaults","publish_allow_notificaton_settings"
+,1,0,0,"Workspace #1",,,,,0,0,0,0,0,0,0,0,0
+"sys_category",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parent","items",,,,
+,28,0,256,0,0,0,0,0,0,0,"Category A",0,0,,,,
+,29,0,512,0,0,0,0,0,0,0,"Category B",0,0,,,,
+,30,0,768,0,0,0,0,0,0,0,"Category C",0,0,,,,
+,31,0,1024,0,0,0,0,0,0,0,"Category A.A",28,0,,,,
+"sys_category_record_mm",,,,,,,,,,,,,,,,,
+,"uid_local","uid_foreign","tablenames","sorting","sorting_foreign","fieldname",,,,,,,,,,,
+,28,297,"tt_content",0,1,"categories",,,,,,,,,,,
+,29,297,"tt_content",0,2,"categories",,,,,,,,,,,
+,29,298,"tt_content",0,1,"categories",,,,,,,,,,,
+,30,298,"tt_content",0,2,"categories",,,,,,,,,,,
+,28,299,"tt_content",0,1,"categories",,,,,,,,,,,
+,29,299,"tt_content",0,2,"categories",,,,,,,,,,,
+,31,299,"tt_content",0,3,"categories",,,,,,,,,,,
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","image","categories",,,,
+,297,89,256,0,0,0,0,0,0,0,"Regular Element #1",0,2,,,,
+,298,89,512,0,0,0,0,0,0,0,"Regular Element #2",0,2,,,,
+,299,89,256,0,0,0,1,0,0,297,"Regular Element #1",0,3,,,,
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+,"1b70a8e25c22645f7a49a79f57f3cf3f","sys_category",31,"parent",,,,0,0,"sys_category",28,,,,,,
+,"3c637501ab9c158daa933643bff8cc43","sys_category",28,"items",,,,0,0,"tt_content",297,,,,,,
+,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
+,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
+,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"7d23196104341ffb4a15728711c8da57","sys_category",28,"items",,,,1,1,"tt_content",299,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"55997c67816b709e1d0f0323fea3c39a","sys_category",31,"items",,,,0,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceImport.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceImport.csv
new file mode 100644
index 000000000000..3d5aaf5a1140
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceImport.csv
@@ -0,0 +1,31 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","tx_testirreforeignfield_hotels",,,,,,,
+,1,0,256,0,0,0,0,0,"FunctionalTest",0,,,,,,,
+,88,1,256,0,0,0,0,0,"DataHandlerTest",0,,,,,,,
+,89,88,256,0,0,0,0,0,"Relations",1,,,,,,,
+"sys_workspace",,,,,,,,,,,,,,,,,
+,"uid","pid","deleted","title","adminusers","members","db_mountpoints","file_mountpoints","freeze","live_edit","publish_access","custom_stages","stagechg_notification","edit_notification_defaults","edit_allow_notificaton_settings","publish_notification_defaults","publish_allow_notificaton_settings"
+,1,0,0,"Workspace #1",,,,,0,0,0,0,0,0,0,0,0
+,2,0,1,"Workspace #2",,,,,0,0,0,0,0,0,0,0,0
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","tx_testirreforeignfield_hotels",,,,,
+,297,89,256,0,0,0,0,0,0,0,"Regular Element #1",1,,,,,
+,298,89,512,0,0,0,0,0,0,0,"Regular Element #2",1,,,,,
+,299,89,512,0,0,0,1,0,0,298,"Testing #1",1,,,,,
+"tx_testirreforeignfield_hotel",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parentid","parenttable","parentidentifier","offers",,
+,2,89,1,0,0,0,0,0,0,0,"Hotel #0",89,"pages",,0,,
+,3,89,1,0,0,0,0,0,0,0,"Hotel #1",297,"tt_content","1nff.hotels",2,,
+,5,89,1,0,0,0,0,0,0,0,"Hotel #1",298,"tt_content","1nff.hotels",1,,
+,6,89,1,0,0,0,1,0,0,5,"Hotel #1",298,"tt_content","1nff.hotels",1,,
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+,"8309f9a3ae2a259bdcb29913b7101244","pages",89,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",2,,,,,,
+,"b9be8f0166b062001c138957270e72e2","tt_content",297,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",3,,,,,,
+,"4e8bfa4d76d4cec2d9f6ec62d2974371","tt_content",298,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",5,,,,,,
+# valid - ws exists and records are connected
+,"f735c7fc6863a29d827ca201f60c8055","tt_content",299,"tx_testirreforeignfield_hotels",,,,0,1,"tx_testirreforeignfield_hotel",6,,,,,,
+# invalid - ws 2 is deleted = 1 - should be removed
+,"deletetWorkspace827ca201f60c8055","tt_content",299,"tx_testirreforeignfield_hotels",,,,0,2,"tx_testirreforeignfield_hotel",6,,,,,,
+# invalid - ws 3 does not exist - should be removed
+,"notExistingWorkspacea201f60c8055","tt_content",299,"tx_testirreforeignfield_hotels",,,,0,3,"tx_testirreforeignfield_hotel",6,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceResult.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceResult.csv
new file mode 100644
index 000000000000..b5aef40c72c1
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceResult.csv
@@ -0,0 +1,26 @@
+"pages",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","tx_testirreforeignfield_hotels",,,,,,,
+,1,0,256,0,0,0,0,0,"FunctionalTest",0,,,,,,,
+,88,1,256,0,0,0,0,0,"DataHandlerTest",0,,,,,,,
+,89,88,256,0,0,0,0,0,"Relations",1,,,,,,,
+"sys_workspace",,,,,,,,,,,,,,,,,
+,"uid","pid","deleted","title","adminusers","members","db_mountpoints","file_mountpoints","freeze","live_edit","publish_access","custom_stages","stagechg_notification","edit_notification_defaults","edit_allow_notificaton_settings","publish_notification_defaults","publish_allow_notificaton_settings"
+,1,0,0,"Workspace #1",,,,,0,0,0,0,0,0,0,0,0
+,2,0,1,"Workspace #2",,,,,0,0,0,0,0,0,0,0,0
+"tt_content",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","header","tx_testirreforeignfield_hotels",,,,,
+,297,89,256,0,0,0,0,0,0,0,"Regular Element #1",1,,,,,
+,298,89,512,0,0,0,0,0,0,0,"Regular Element #2",1,,,,,
+,299,89,512,0,0,0,1,0,0,298,"Testing #1",1,,,,,
+"tx_testirreforeignfield_hotel",,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parentid","parenttable","parentidentifier","offers",,
+,2,89,1,0,0,0,0,0,0,0,"Hotel #0",89,"pages",,0,,
+,3,89,1,0,0,0,0,0,0,0,"Hotel #1",297,"tt_content","1nff.hotels",2,,
+,5,89,1,0,0,0,0,0,0,0,"Hotel #1",298,"tt_content","1nff.hotels",1,,
+,6,89,1,0,0,0,1,0,0,5,"Hotel #1",298,"tt_content","1nff.hotels",1,,
+"sys_refindex",,,,,,,,,,,,,,,,,
+,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
+,"8309f9a3ae2a259bdcb29913b7101244","pages",89,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",2,,,,,,
+,"b9be8f0166b062001c138957270e72e2","tt_content",297,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",3,,,,,,
+,"4e8bfa4d76d4cec2d9f6ec62d2974371","tt_content",298,"tx_testirreforeignfield_hotels",,,,0,0,"tx_testirreforeignfield_hotel",5,,,,,,
+,"f735c7fc6863a29d827ca201f60c8055","tt_content",299,"tx_testirreforeignfield_hotels",,,,0,1,"tx_testirreforeignfield_hotel",6,,,,,,
diff --git a/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexTest.php b/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexTest.php
new file mode 100644
index 000000000000..9e6c474bc529
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexTest.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Functional\Database;
+
+use TYPO3\CMS\Core\Database\ReferenceIndex;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ReferenceIndexTest extends FunctionalTestCase
+{
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield',
+    ];
+
+    /**
+     * @test
+     */
+    public function updateIndexRemovesRecordsOfNotExistingWorkspaces(): void
+    {
+        $this->importCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceImport.csv');
+        $result = (new ReferenceIndex())->updateIndex(false);
+        self::assertSame('Index table hosted 1 indexes for non-existing or deleted workspaces, now removed.', $result['errors'][0]);
+        $this->assertCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/UpdateIndexRemoveNonExistingWorkspaceResult.csv');
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexWorkspaceLoadedTest.php b/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexWorkspaceLoadedTest.php
new file mode 100644
index 000000000000..38516e56e334
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/ReferenceIndexWorkspaceLoadedTest.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Functional\Database;
+
+use TYPO3\CMS\Core\Database\ReferenceIndex;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class ReferenceIndexWorkspaceLoadedTest extends FunctionalTestCase
+{
+    protected $coreExtensionsToLoad = [
+        'workspaces',
+    ];
+
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_irre_foreignfield',
+    ];
+
+    /**
+     * @test
+     */
+    public function updateIndexRemovesRecordsOfNotExistingWorkspaces(): void
+    {
+        $this->importCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceImport.csv');
+        $result = (new ReferenceIndex())->updateIndex(false);
+        self::assertSame('Index table hosted 2 indexes for non-existing or deleted workspaces, now removed.', $result['errors'][0]);
+        $this->assertCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexRemoveNonExistingWorkspaceResult.csv');
+    }
+
+    /**
+     * @test
+     */
+    public function updateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecord(): void
+    {
+        $this->importCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordImport.csv');
+        $result = (new ReferenceIndex())->updateIndex(false);
+        $this->assertCSVDataSet('typo3/sysext/core/Tests/Functional/Database/Fixtures/ReferenceIndex/WorkspaceLoadedUpdateIndexAddsRowsForLocalSideMmHavingForeignWorkspaceRecordResult.csv');
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
index 0018116cd5a8..037fa6179e1e 100644
--- a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
+++ b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
@@ -562,6 +562,12 @@ class DataHandlerHook
         // Versioned records which contents will be moved into $curVersion
         $isNewRecord = ((int)($curVersion['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER);
         if ($isNewRecord && is_array($curVersion)) {
+            // @todo: This early return is odd. It means version_swap_processFields() and versionPublishManyToManyRelations()
+            //        below are not called for new records to be published. This is "fine" for mm since mm tables have no
+            //        t3ver_wsid and need no publish as such. For inline relation publishing, this is indirectly resolved by the
+            //        processCmdmap_beforeStart() hook, which adds additional commands for child records - a construct we
+            //        may want to avoid altogether due to its complexity. It would be easier to follow if publish here would
+            //        handle that instead.
             $this->publishNewRecord($table, $curVersion, $dataHandler, $comment, (array)$notificationAlternativeRecipients);
             return;
         }
@@ -632,6 +638,10 @@ class DataHandlerHook
         // In case of swapping and the offline record has a state
         // (like 2 or 4 for deleting or move-pointer) we set the
         // current workspace ID so the record is not deselected.
+        // @todo: It is odd these information are updated in $swapVersion *before* version_swap_processFields
+        //        version_swap_processFields() and versionPublishManyToManyRelations() are called. This leads
+        //        to the situation that versionPublishManyToManyRelations() needs another argument to transfer
+        //        the "from workspace" information which would usually be retrieved by accessing $swapVersion['t3ver_wsid']
         $swapVersion['t3ver_wsid'] = 0;
         $swapVersion['t3ver_stage'] = 0;
         $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
@@ -643,7 +653,7 @@ class DataHandlerHook
                 }
             }
         }
-        $dataHandler->versionPublishManyToManyRelations($table, $curVersion, $swapVersion);
+        $dataHandler->versionPublishManyToManyRelations($table, $curVersion, $swapVersion, $workspaceId);
         unset($swapVersion['uid']);
         // Modify online version to become offline:
         unset($curVersion['uid']);
@@ -987,6 +997,13 @@ class DataHandlerHook
         $dataHandler->registerReferenceIndexRowsForDrop($table, $id, $workspaceId);
         $dataHandler->updateRefIndex($table, $id, 0);
         $this->updateReferenceIndexForL10nOverlays($table, $id, $workspaceId, $dataHandler);
+
+        // When dealing with mm relations on local side, existing refindex rows of the new workspace record
+        // need to be re-calculated for the now live record. Scenario ManyToMany Publish createContentAndAddRelation
+        // These calls are similar to what is done in DH->versionPublishManyToManyRelations() and can not be
+        // used from there since publishing new records does not call that method, see @todo in version_swap().
+        $dataHandler->registerReferenceIndexUpdateForReferencesToItem($table, $id, $workspaceId, 0);
+        $dataHandler->registerReferenceIndexUpdateForReferencesToItem($table, $id, $workspaceId);
     }
 
     /**
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/ActionTest.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/ActionTest.php
index 5cc5be738077..d7d0aa63733a 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/ActionTest.php
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/ActionTest.php
@@ -57,9 +57,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -81,9 +78,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -102,9 +96,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -125,9 +116,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -230,9 +218,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -253,9 +238,6 @@ class ActionTest extends AbstractActionTestCase
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Testing #1', 'Category B'));
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -273,9 +255,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionDoesNotHaveRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -312,9 +291,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -351,9 +327,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -392,9 +365,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -420,8 +390,5 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentIdLast'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 }
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/addCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/addCategoryRelation.csv
index e4f10e839b7b..f77113537169 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/addCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/addCategoryRelation.csv
@@ -44,3 +44,11 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"7d23196104341ffb4a15728711c8da57","sys_category",28,"items",,,,1,1,"tt_content",299,,,,,,
+# Yes. This is a workspace entry for cat 29 (not a ws record) to tt_content 298 (not a ws record either).
+# It still makes sense: When looking at cat 29 in ws 1 - both records 298 and 299 (which is a ws record)
+# are connected to cat 29. So the refindex entry 29-298 is triggered together with the creation of ws
+# record 299 to "complete" the flat refindex table entries for cat 29.
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"55997c67816b709e1d0f0323fea3c39a","sys_category",31,"items",,,,0,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
index 19ea64671cb8..362781698850 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/changeCategoryRelationSorting.csv
@@ -43,3 +43,7 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+# @todo: Broken. 28->299 sorting 1 and 29->299 sorting 2 ... sys_refindex has no sorting_foreign
+,"7d23196104341ffb4a15728711c8da57","sys_category",28,"items",,,,1,1,"tt_content",299,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyContentOfRelation.csv
index 156b2a72409f..99291279c128 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyContentOfRelation.csv
@@ -43,3 +43,8 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"d624da48d7d6427f385a8197cb90c391","sys_category",30,"items",,,,0,1,"tt_content",298,,,,,,
+,"f65da53fb6e3b34333940d2cffe010a3","sys_category",30,"items",,,,1,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyPage.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyPage.csv
index 691b610e9fc2..96c05b87463b 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyPage.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/copyPage.csv
@@ -47,3 +47,11 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"bc70e11586947a0c445e348692fd80ba","sys_category",29,"items",,,,3,1,"tt_content",300,,,,,,
+,"d624da48d7d6427f385a8197cb90c391","sys_category",30,"items",,,,0,1,"tt_content",298,,,,,,
+,"f65da53fb6e3b34333940d2cffe010a3","sys_category",30,"items",,,,1,1,"tt_content",299,,,,,,
+,"538e7c228af54d2978dff4ff24d2962c","sys_category",28,"items",,,,0,1,"tt_content",297,,,,,,
+,"8567a86039b572b0c9ac1af154cfb9b1","sys_category",28,"items",,,,1,1,"tt_content",300,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/createContentNAddRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/createContentNAddRelation.csv
index 84c2667aad07..10d52784afeb 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/createContentNAddRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/createContentNAddRelation.csv
@@ -42,3 +42,6 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteCategoryRelation.csv
index beca40a2ff5e..e802862ead64 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteCategoryRelation.csv
@@ -35,10 +35,11 @@
 ,299,89,256,0,0,0,1,0,0,297,"Regular Element #1",0,1,,,,
 "sys_refindex",,,,,,,,,,,,,,,,,
 ,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,
-,"01a3ce8c4e3b2bb1aa439dc29081f996","sys_workspace_stage",1,"responsible_persons",,,,0,0,"be_users",3,,,,,,
-,"1b70a8e25c22645f7a49a79f57f3cf3f","sys_category",31,"parent",,,,0,0,"sys_category",28,,,,,,
-,"25426f92d44dd2ccf416108462b446e3","sys_workspace",1,"custom_stages",,,,0,0,"sys_workspace_stage",1,,,,,,
 ,"3c637501ab9c158daa933643bff8cc43","sys_category",28,"items",,,,0,0,"tt_content",297,,,,,,
+,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
-,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"1b70a8e25c22645f7a49a79f57f3cf3f","sys_category",31,"parent",,,,0,0,"sys_category",28,,,,,,
+,"25426f92d44dd2ccf416108462b446e3","sys_workspace",1,"custom_stages",,,,0,0,"sys_workspace_stage",1,,,,,,
+,"01a3ce8c4e3b2bb1aa439dc29081f996","sys_workspace_stage",1,"responsible_persons",,,,0,0,"be_users",3,,,,,,
+,"7d23196104341ffb4a15728711c8da57","sys_category",28,"items",,,,1,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteContentOfRelation.csv
index 70806e73c3eb..2dfd9a8dc0d0 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/deleteContentOfRelation.csv
@@ -43,3 +43,6 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"f65da53fb6e3b34333940d2cffe010a3","sys_category",30,"items",,,,1,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/localizeContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/localizeContentOfRelation.csv
index c4d1211220a5..0a06f261a61d 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/localizeContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/localizeContentOfRelation.csv
@@ -44,3 +44,8 @@
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
 ,"e4afda9b67d6ad42e03f5c797250235d","tt_content",299,"l18n_parent",,,,0,1,"tt_content",298,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"d624da48d7d6427f385a8197cb90c391","sys_category",30,"items",,,,0,1,"tt_content",298,,,,,,
+,"f65da53fb6e3b34333940d2cffe010a3","sys_category",30,"items",,,,1,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyBothsOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyBothsOfRelation.csv
index df1e4f57bcae..fe5e7562bd9f 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyBothsOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyBothsOfRelation.csv
@@ -45,3 +45,5 @@
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
 ,"e69807022342144ed786af9748b90623","sys_category",32,"items",,,,0,1,"tt_content",299,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyContentOfRelation.csv
index 0549c71ffff0..4916dacc0e79 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/modifyContentOfRelation.csv
@@ -43,3 +43,6 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"7d23196104341ffb4a15728711c8da57","sys_category",28,"items",,,,1,1,"tt_content",299,,,,,,
+,"6824a5635d464123a7a91770440e1d23","sys_category",29,"items",,,,1,1,"tt_content",298,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/moveContentOfRelationToDifferentPage.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/moveContentOfRelationToDifferentPage.csv
index fbc2d9150501..51f2272e506b 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/moveContentOfRelationToDifferentPage.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Modify/DataSet/moveContentOfRelationToDifferentPage.csv
@@ -43,3 +43,6 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a2784c59b73c994181170d02af23959c","sys_category",29,"items",,,,0,1,"tt_content",297,,,,,,
+,"92e328e25f38d92ecf90478f6b47e671","sys_category",29,"items",,,,2,1,"tt_content",299,,,,,,
+,"f65da53fb6e3b34333940d2cffe010a3","sys_category",30,"items",,,,1,1,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/ActionTest.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/ActionTest.php
index ef661b9add2e..0f4ce03e2073 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/ActionTest.php
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/ActionTest.php
@@ -54,9 +54,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -76,9 +73,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -95,9 +89,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -116,9 +107,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -229,9 +217,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -253,9 +238,6 @@ class ActionTest extends AbstractActionTestCase
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Testing #1', 'Category B'));
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -271,9 +253,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionDoesNotHaveRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -306,9 +285,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -341,9 +317,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -379,9 +352,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -408,8 +378,5 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentIdLast'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 }
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/addCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/addCategoryRelation.csv
index e2e31b6cd3c3..7ed9e2f23586 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/addCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/addCategoryRelation.csv
@@ -41,3 +41,4 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a8a91b196a05f3f11285176aebc2b2f5","sys_category",31,"items",,,,0,0,"tt_content",297,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyContentOfRelation.csv
index dbaf906b1eb3..48c4ca2f856c 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyContentOfRelation.csv
@@ -43,3 +43,5 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyPage.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyPage.csv
index 4f84e0afc1c7..56fbffe5042d 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyPage.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/copyPage.csv
@@ -47,3 +47,7 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"c9b6f46d28e208f172a6d23329c6ca99","sys_category",29,"items",,,,3,0,"tt_content",300,,,,,,
+,"ec0c2b2cf2e32e44f5bff20c4beefb21","sys_category",28,"items",,,,1,0,"tt_content",300,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/createContentNAddRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/createContentNAddRelation.csv
index 725b50de7b63..97a39fe705a1 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/createContentNAddRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/createContentNAddRelation.csv
@@ -42,3 +42,4 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/deleteCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/deleteCategoryRelation.csv
index cba2b0961217..62d4078c3a83 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/deleteCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/deleteCategoryRelation.csv
@@ -36,6 +36,5 @@
 ,"1b70a8e25c22645f7a49a79f57f3cf3f","sys_category",31,"parent",,,,0,0,"sys_category",28,,,,,,
 ,"25426f92d44dd2ccf416108462b446e3","sys_workspace",1,"custom_stages",,,,0,0,"sys_workspace_stage",1,,,,,,
 ,"3c637501ab9c158daa933643bff8cc43","sys_category",28,"items",,,,0,0,"tt_content",297,,,,,,
-,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
-,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"c5335005290004a81e512dfe6948bd69","sys_category",29,"items",,,,0,0,"tt_content",298,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/localizeContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/localizeContentOfRelation.csv
index 74a2d6113c3f..c1db7957b814 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/localizeContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/Publish/DataSet/localizeContentOfRelation.csv
@@ -44,3 +44,5 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/ActionTest.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/ActionTest.php
index 99a781dac3d3..94f64cf60430 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/ActionTest.php
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/ActionTest.php
@@ -54,9 +54,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -76,9 +73,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C', 'Category A.A'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -95,9 +89,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdFirst)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category A', 'Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -116,9 +107,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -216,9 +204,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -237,9 +222,6 @@ class ActionTest extends AbstractActionTestCase
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Testing #1', 'Category B'));
         self::assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -255,9 +237,6 @@ class ActionTest extends AbstractActionTestCase
         $responseSections = ResponseContent::fromString((string)$response->getBody())->getSections();
         self::assertThat($responseSections, $this->getRequestSectionDoesNotHaveRecordConstraint()
             ->setTable(self::TABLE_Content)->setField('header')->setValues('Testing #1'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -290,9 +269,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentId'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -325,9 +301,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -362,9 +335,6 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 
     /**
@@ -388,8 +358,5 @@ class ActionTest extends AbstractActionTestCase
         self::assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
             ->setRecordIdentifier(self::TABLE_Content . ':' . $this->recordIds['newContentIdLast'])->setRecordField('categories')
             ->setTable(self::TABLE_Category)->setField('title')->setValues('Category B', 'Category C'));
-
-        // @todo: reference index not clean after this test. Needs investigation.
-        $this->assertCleanReferenceIndex = false;
     }
 }
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/addCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/addCategoryRelation.csv
index b5c7aee31526..123cd040fd72 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/addCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/addCategoryRelation.csv
@@ -41,3 +41,4 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"a8a91b196a05f3f11285176aebc2b2f5","sys_category",31,"items",,,,0,0,"tt_content",297,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyContentOfRelation.csv
index dbaf906b1eb3..48c4ca2f856c 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyContentOfRelation.csv
@@ -43,3 +43,5 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyPage.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyPage.csv
index 4f84e0afc1c7..1fef88a342cf 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyPage.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/copyPage.csv
@@ -47,3 +47,7 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"c9b6f46d28e208f172a6d23329c6ca99","sys_category",29,"items",,,,3,0,"tt_content",300,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
+,"ec0c2b2cf2e32e44f5bff20c4beefb21","sys_category",28,"items",,,,1,0,"tt_content",300,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/createContentNAddRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/createContentNAddRelation.csv
index 725b50de7b63..97a39fe705a1 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/createContentNAddRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/createContentNAddRelation.csv
@@ -42,3 +42,4 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/deleteCategoryRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/deleteCategoryRelation.csv
index 10f49ae45c5e..51b8ff95248b 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/deleteCategoryRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/deleteCategoryRelation.csv
@@ -36,6 +36,5 @@
 ,"1b70a8e25c22645f7a49a79f57f3cf3f","sys_category",31,"parent",,,,0,0,"sys_category",28,,,,,,
 ,"25426f92d44dd2ccf416108462b446e3","sys_workspace",1,"custom_stages",,,,0,0,"sys_workspace_stage",1,,,,,,
 ,"3c637501ab9c158daa933643bff8cc43","sys_category",28,"items",,,,0,0,"tt_content",297,,,,,,
-,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
-,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"c5335005290004a81e512dfe6948bd69","sys_category",29,"items",,,,0,0,"tt_content",298,,,,,,
diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/localizeContentOfRelation.csv b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/localizeContentOfRelation.csv
index 74a2d6113c3f..c1db7957b814 100644
--- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/localizeContentOfRelation.csv
+++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/PublishAll/DataSet/localizeContentOfRelation.csv
@@ -44,3 +44,5 @@
 ,"aabda97b66f9b693f30d1faf442b37d6","sys_category",29,"items",,,,1,0,"tt_content",298,,,,,,
 ,"b102e2f9b1ed99b14d813363199cb281","sys_category",30,"items",,,,0,0,"tt_content",298,,,,,,
 ,"e19100d609a435906e16432a41b55c72","sys_category",29,"items",,,,0,0,"tt_content",297,,,,,,
+,"40880cf3db51a7c80d8714f9b60409eb","sys_category",29,"items",,,,2,0,"tt_content",299,,,,,,
+,"60e6778effa62e4edb070c954d9643ff","sys_category",30,"items",,,,1,0,"tt_content",299,,,,,,
-- 
GitLab