[BUGFIX] Synchronize TCA enable columns from workspace version
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Database / ReferenceIndex.php
index 4512c4a..973d83c 100644 (file)
@@ -15,24 +15,24 @@ namespace TYPO3\CMS\Core\Database;
  */
 
 use Doctrine\DBAL\DBALException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
-use TYPO3\CMS\Core\Messaging\FlashMessageService;
+use TYPO3\CMS\Core\Messaging\FlashMessageRendererResolver;
 use TYPO3\CMS\Core\Registry;
-use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\Folder;
-use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\PathUtility;
+use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
 
 /**
  * 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 analyse whole database!)
+ * 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
@@ -41,40 +41,39 @@ use TYPO3\CMS\Core\Utility\PathUtility;
  * 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.
  */
-class ReferenceIndex
+class ReferenceIndex implements LoggerAwareInterface
 {
+    use LoggerAwareTrait;
+
     /**
-     * Definition of tables to exclude from searching for relations
+     * Definition of tables to exclude from the ReferenceIndex
      *
      * Only tables which do not contain any relations and never did so far since references also won't be deleted for
      * these. Since only tables with an entry in $GLOBALS['TCA] are handled by ReferenceIndex there is no need to add
      * *_mm-tables.
      *
-     * This is implemented as an array with fields as keys and booleans as values to be able to fast isset() instead of
-     * slow in_array() lookup.
+     * Implemented as array with fields as keys and booleans as values for fast isset() lookup instead of slow in_array()
      *
      * @var array
      * @see updateRefIndexTable()
-     * @todo #65461 Create configuration for tables to exclude from ReferenceIndex
+     * @see shouldExcludeTableFromReferenceIndex()
      */
-    protected static $nonRelationTables = [
+    protected static $excludedTables = [
         'sys_log' => true,
-        'sys_history' => true,
         'tx_extensionmanager_domain_model_extension' => true
     ];
 
     /**
-     * Definition of fields to exclude from searching for relations
+     * Definition of fields to exclude from ReferenceIndex in *every* table
      *
-     * This is implemented as an array with fields as keys and booleans as values to be able to fast isset() instead of
-     * slow in_array() lookup.
+     * Implemented as array with fields as keys and booleans as values for fast isset() lookup instead of slow in_array()
      *
      * @var array
      * @see getRelations()
      * @see fetchTableRelationFields()
-     * @todo #65460 Create configuration for fields to exclude from ReferenceIndex
+     * @see shouldExcludeTableColumnFromReferenceIndex()
      */
-    protected static $nonRelationFields = [
+    protected static $excludedColumns = [
         'uid' => true,
         'perms_userid' => true,
         'perms_groupid' => true,
@@ -96,7 +95,9 @@ class ReferenceIndex
      * This array holds the FlexForm references of a record
      *
      * @var array
-     * @see getRelations(),FlexFormTools::traverseFlexFormXMLData(),getRelations_flexFormCallBack()
+     * @see getRelations()
+     * @see FlexFormTools::traverseFlexFormXMLData()
+     * @see getRelations_flexFormCallBack()
      */
     public $temp_flexRelations = [];
 
@@ -113,11 +114,20 @@ class ReferenceIndex
      * An index of all found references of a single record created in createEntryData() and accumulated in generateRefIndexData()
      *
      * @var array
-     * @see createEntryData(),generateRefIndexData()
+     * @see createEntryData()
+     * @see generateRefIndexData()
      */
     public $relations = [];
 
     /**
+     * A cache to avoid that identical rows are refetched from the database
+     *
+     * @var array
+     * @see getRecordRawCached()
+     */
+    protected $recordCache = [];
+
+    /**
      * Number which we can increase if a change in the code means we will have to force a re-generation of the index.
      *
      * @var int
@@ -135,16 +145,22 @@ class ReferenceIndex
     /**
      * Runtime Cache to store and retrieve data computed for a single request
      *
-     * @var \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
+     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
+     */
+    protected $runtimeCache;
+
+    /**
+     * Enables $runtimeCache and $recordCache
+     * @var bool
      */
-    protected $runtimeCache = null;
+    protected $useRuntimeCache = false;
 
     /**
      * Constructor
      */
     public function __construct()
     {
-        $this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
+        $this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
     }
 
     /**
@@ -162,7 +178,8 @@ class ReferenceIndex
      * Gets the current workspace id
      *
      * @return int
-     * @see updateRefIndexTable(),createEntryData()
+     * @see updateRefIndexTable()
+     * @see createEntryData()
      */
     public function getWorkspaceId()
     {
@@ -183,7 +200,6 @@ class ReferenceIndex
      */
     public function updateRefIndexTable($tableName, $uid, $testOnly = false)
     {
-
         // First, secure that the index table is not updated with workspace tainted relations:
         $this->WSOL = false;
 
@@ -194,27 +210,33 @@ class ReferenceIndex
             'addedNodes' => 0
         ];
 
+        $uid = $uid ? (int)$uid : 0;
+        if (!$uid) {
+            return $result;
+        }
+
         // If this table cannot contain relations, skip it
-        if (isset(static::$nonRelationTables[$tableName])) {
+        if ($this->shouldExcludeTableFromReferenceIndex($tableName)) {
             return $result;
         }
 
         // Fetch tableRelationFields and save them in cache if not there yet
         $cacheId = static::$cachePrefixTableRelationFields . $tableName;
-        if (!$this->runtimeCache->has($cacheId)) {
+        if (!$this->useRuntimeCache || !$this->runtimeCache->has($cacheId)) {
             $tableRelationFields = $this->fetchTableRelationFields($tableName);
             $this->runtimeCache->set($cacheId, $tableRelationFields);
         } else {
             $tableRelationFields = $this->runtimeCache->get($cacheId);
         }
 
-        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_refindex');
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $connection = $connectionPool->getConnectionForTable('sys_refindex');
 
         // Get current index from Database with hash as index using $uidIndexField
         // no restrictions are needed, since sys_refindex is not a TCA table
         $queryBuilder = $connection->createQueryBuilder();
         $queryBuilder->getRestrictions()->removeAll();
-        $queryResult = $queryBuilder->select('*')->from('sys_refindex')->where(
+        $queryResult = $queryBuilder->select('hash')->from('sys_refindex')->where(
             $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)),
             $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
             $queryBuilder->expr()->eq(
@@ -222,16 +244,21 @@ class ReferenceIndex
                 $queryBuilder->createNamedParameter($this->getWorkspaceId(), \PDO::PARAM_INT)
             )
         )->execute();
-        $currentRelations = [];
+        $currentRelationHashes = [];
         while ($relation = $queryResult->fetch()) {
-            $currentRelations[$relation['hash']] = $currentRelations;
+            $currentRelationHashes[$relation['hash']] = true;
         }
 
         // If the table has fields which could contain relations and the record does exist (including deleted-flagged)
-        if ($tableRelationFields !== '' && BackendUtility::getRecordRaw($tableName, 'uid=' . (int)$uid, 'uid')) {
-            // Then, get relations:
-            $relations = $this->generateRefIndexData($tableName, $uid);
-            if (is_array($relations)) {
+        if ($tableRelationFields !== '') {
+            $existingRecord = $this->getRecordRawCached($tableName, $uid);
+            if ($existingRecord) {
+                // Table has relation fields and record exists - get relations
+                $this->relations = [];
+                $relations = $this->generateDataUsingRecord($tableName, $existingRecord);
+                if (!is_array($relations)) {
+                    return $result;
+                }
                 // Traverse the generated index:
                 foreach ($relations as &$relation) {
                     if (!is_array($relation)) {
@@ -239,8 +266,8 @@ class ReferenceIndex
                     }
                     $relation['hash'] = md5(implode('///', $relation) . '///' . $this->hashVersion);
                     // First, check if already indexed and if so, unset that row (so in the end we know which rows to remove!)
-                    if (isset($currentRelations[$relation['hash']])) {
-                        unset($currentRelations[$relation['hash']]);
+                    if (isset($currentRelationHashes[$relation['hash']])) {
+                        unset($currentRelationHashes[$relation['hash']]);
                         $result['keptNodes']++;
                         $relation['_ACTION'] = 'KEPT';
                     } else {
@@ -253,28 +280,32 @@ class ReferenceIndex
                     }
                 }
                 $result['relations'] = $relations;
-            } else {
-                return $result;
             }
         }
 
         // If any old are left, remove them:
-        if (!empty($currentRelations)) {
-            $hashList = array_keys($currentRelations);
+        if (!empty($currentRelationHashes)) {
+            $hashList = array_keys($currentRelationHashes);
             if (!empty($hashList)) {
                 $result['deletedNodes'] = count($hashList);
                 $result['deletedNodes_hashList'] = implode(',', $hashList);
                 if (!$testOnly) {
-                    $queryBuilder = $connection->createQueryBuilder();
-                    $queryBuilder
-                        ->delete('sys_refindex')
-                        ->where(
-                            $queryBuilder->expr()->in(
-                                'hash',
-                                $queryBuilder->createNamedParameter($hashList, Connection::PARAM_STR_ARRAY)
+                    $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
+                    foreach (array_chunk($hashList, $maxBindParameters - 10, true) as $chunk) {
+                        if (empty($chunk)) {
+                            continue;
+                        }
+                        $queryBuilder = $connection->createQueryBuilder();
+                        $queryBuilder
+                            ->delete('sys_refindex')
+                            ->where(
+                                $queryBuilder->expr()->in(
+                                    'hash',
+                                    $queryBuilder->createNamedParameter($chunk, Connection::PARAM_STR_ARRAY)
+                                )
                             )
-                        )
-                        ->execute();
+                            ->execute();
+                    }
                 }
             }
         }
@@ -288,7 +319,7 @@ class ReferenceIndex
      *
      * @param string $tableName Table name from $GLOBALS['TCA']
      * @param int $uid Record UID
-     * @return array|NULL Index Rows
+     * @return array|null Index Rows
      */
     public function generateRefIndexData($tableName, $uid)
     {
@@ -298,38 +329,61 @@ class ReferenceIndex
 
         $this->relations = [];
 
-        // Fetch tableRelationFields and save them in cache if not there yet
-        $cacheId = static::$cachePrefixTableRelationFields . $tableName;
-        if (!$this->runtimeCache->has($cacheId)) {
-            $tableRelationFields = $this->fetchTableRelationFields($tableName);
-            $this->runtimeCache->set($cacheId, $tableRelationFields);
-        } else {
-            $tableRelationFields = $this->runtimeCache->get($cacheId);
+        $record = null;
+        $uid = $uid ? (int)$uid : 0;
+        if ($uid) {
+            // Get raw record from DB
+            $record = $this->getRecordRawCached($tableName, $uid);
         }
 
-        // Return if there are no fields which could contain relations
-        if ($tableRelationFields === '') {
-            return $this->relations;
+        if (!is_array($record)) {
+            return null;
         }
 
-        $deleteField = $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
+        return $this->generateDataUsingRecord($tableName, $record);
+    }
 
-        if ($tableRelationFields === '*') {
-            // If one field of a record is of type flex, all fields have to be fetched
-            // to be passed to FlexFormTools->getDataStructureIdentifier()
-            $selectFields = '*';
-        } else {
-            // otherwise only fields that might contain relations are fetched
-            $selectFields = 'uid,' . $tableRelationFields . ($deleteField ? ',' . $deleteField : '');
-        }
+    /**
+     * Returns the amount of references for the given record
+     *
+     * @param string $tableName
+     * @param int $uid
+     * @return int
+     */
+    public function getNumberOfReferencedRecords(string $tableName, int $uid): int
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
+        return (int)$queryBuilder
+            ->count('*')->from('sys_refindex')
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'ref_table',
+                    $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
+                ),
+                $queryBuilder->expr()->eq(
+                    'ref_uid',
+                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->eq(
+                    'deleted',
+                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                )
+            )->execute()->fetchColumn(0);
+    }
 
-        // Get raw record from DB
-        $record = BackendUtility::getRecordRaw($tableName, 'uid=' . (int)$uid, $selectFields);
-        if (!is_array($record)) {
-            return null;
-        }
+    /**
+     * Calculate the relations for a record of a given table
+     *
+     * @param string $tableName Table being processed
+     * @param array $record Record from $tableName
+     * @return array
+     */
+    protected function generateDataUsingRecord(string $tableName, array $record): array
+    {
+        $this->relations = [];
+        $deleteField = $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
 
-        // Deleted:
+        // Is the record deleted?
         $deleted = $deleteField && $record[$deleteField] ? 1 : 0;
 
         // Get all relations from record:
@@ -339,43 +393,31 @@ class ReferenceIndex
             // Based on type
             switch ((string)$fieldRelations['type']) {
                 case 'db':
-                    $this->createEntryData_dbRels($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['itemArray']);
-                    break;
-                case 'file_reference':
-                    // not used (see getRelations()), but fallback to file
-                case 'file':
-                    $this->createEntryData_fileRels($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['newValueFiles']);
+                    $this->createEntryDataForDatabaseRelationsUsingRecord($tableName, $record, $fieldName, '', $deleted, $fieldRelations['itemArray']);
                     break;
                 case 'flex':
                     // DB references in FlexForms
                     if (is_array($fieldRelations['flexFormRels']['db'])) {
                         foreach ($fieldRelations['flexFormRels']['db'] as $flexPointer => $subList) {
-                            $this->createEntryData_dbRels($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList);
-                        }
-                    }
-                    // File references in FlexForms
-                    // @todo #65463 Test correct handling of file references in FlexForms
-                    if (is_array($fieldRelations['flexFormRels']['file'])) {
-                        foreach ($fieldRelations['flexFormRels']['file'] as $flexPointer => $subList) {
-                            $this->createEntryData_fileRels($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList);
+                            $this->createEntryDataForDatabaseRelationsUsingRecord($tableName, $record, $fieldName, $flexPointer, $deleted, $subList);
                         }
                     }
                     // Soft references in FlexForms
                     // @todo #65464 Test correct handling of soft references in FlexForms
                     if (is_array($fieldRelations['flexFormRels']['softrefs'])) {
                         foreach ($fieldRelations['flexFormRels']['softrefs'] as $flexPointer => $subList) {
-                            $this->createEntryData_softreferences($tableName, $uid, $fieldName, $flexPointer, $deleted, $subList['keys']);
+                            $this->createEntryDataForSoftReferencesUsingRecord($tableName, $record, $fieldName, $flexPointer, $deleted, $subList['keys']);
                         }
                     }
                     break;
             }
             // Soft references in the field
             if (is_array($fieldRelations['softrefs'])) {
-                $this->createEntryData_softreferences($tableName, $uid, $fieldName, '', $deleted, $fieldRelations['softrefs']['keys']);
+                $this->createEntryDataForSoftReferencesUsingRecord($tableName, $record, $fieldName, '', $deleted, $fieldRelations['softrefs']['keys']);
             }
         }
 
-        return $this->relations;
+        return array_filter($this->relations);
     }
 
     /**
@@ -387,37 +429,74 @@ class ReferenceIndex
      * @param string $field Fieldname of source record (where reference is located)
      * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
      * @param int $deleted Whether record is deleted-flagged or not
-     * @param string $ref_table For database references; the tablename the reference points to. Special keyword "_FILE" indicates that "ref_string" is a file reference either absolute or relative to PATH_site. Special keyword "_STRING" indicates some special usage (typ. softreference) where "ref_string" is used for the value.
-     * @param int $ref_uid For database references; The UID of the record (zero "ref_table" is "_FILE" or "_STRING")
-     * @param string $ref_string For "_FILE" or "_STRING" references: The filepath (relative to PATH_site or absolute) or other string.
+     * @param string $ref_table For database references; the tablename the reference points to. Special keyword "_STRING" indicates some special usage (typ. softreference) where "ref_string" is used for the value.
+     * @param int $ref_uid For database references; The UID of the record (zero "ref_table" is "_STRING")
+     * @param string $ref_string For "_STRING" references: The string.
      * @param int $sort The sorting order of references if many (the "group" or "select" TCA types). -1 if no sorting order is specified.
      * @param string $softref_key If the reference is a soft reference, this is the soft reference parser key. Otherwise empty.
      * @param string $softref_id Soft reference ID for key. Might be useful for replace operations.
-     * @return array Array record to insert into table.
+     * @return array|null Array record to insert into table.
      */
     public function createEntryData($table, $uid, $field, $flexPointer, $deleted, $ref_table, $ref_uid, $ref_string = '', $sort = -1, $softref_key = '', $softref_id = '')
     {
-        if (BackendUtility::isTableWorkspaceEnabled($table)) {
-            $element = BackendUtility::getRecord($table, $uid, 't3ver_wsid');
-            if ($element !== null && isset($element['t3ver_wsid']) && (int)$element['t3ver_wsid'] !== $this->getWorkspaceId()) {
-                //The given Element is ws-enabled but doesn't live in the selected workspace
-                // => don't add index as it's not actually there
+        $uid = $uid ? (int)$uid : 0;
+        if (!$uid) {
+            return null;
+        }
+        return $this->createEntryDataUsingRecord(
+            (string)$table,
+            $this->getRecordRawCached($table, $uid),
+            (string)$field,
+            (string)$flexPointer,
+            $deleted ? (int)$deleted : 0,
+            (string)$ref_table,
+            $ref_uid ? (int)$ref_uid : 0,
+            (string)$ref_string,
+            $sort ? (int)$sort : 0,
+            (string)$softref_key,
+            (string)$softref_id
+        );
+    }
+
+    /**
+     * Create array with field/value pairs ready to insert in database
+     *
+     * @param string $tableName Tablename of source record (where reference is located)
+     * @param array $record Record from $table
+     * @param string $fieldName Fieldname of source record (where reference is located)
+     * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [$field]
+     * @param int $deleted Whether record is deleted-flagged or not
+     * @param string $referencedTable In database references the tablename the reference points to. Keyword "_STRING" indicates special usage (typ. SoftReference) in $referenceString
+     * @param int $referencedUid In database references the UID of the record (zero $referencedTable is "_STRING")
+     * @param string $referenceString For "_STRING" references: The string.
+     * @param int $sort The sorting order of references if many (the "group" or "select" TCA types). -1 if no sorting order is specified.
+     * @param string $softReferenceKey If the reference is a soft reference, this is the soft reference parser key. Otherwise empty.
+     * @param string $softReferenceId Soft reference ID for key. Might be useful for replace operations.
+     * @return array|bool Array to insert in DB or false if record should not be processed
+     */
+    protected function createEntryDataUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, int $deleted, string $referencedTable, int $referencedUid, string $referenceString = '', int $sort = -1, string $softReferenceKey = '', string $softReferenceId = '')
+    {
+        $workspaceId = 0;
+        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
                 return false;
             }
         }
         return [
-            'tablename' => $table,
-            'recuid' => $uid,
-            'field' => $field,
+            'tablename' => $tableName,
+            'recuid' => $record['uid'],
+            'field' => $fieldName,
             'flexpointer' => $flexPointer,
-            'softref_key' => $softref_key,
-            'softref_id' => $softref_id,
+            'softref_key' => $softReferenceKey,
+            'softref_id' => $softReferenceId,
             'sorting' => $sort,
-            'deleted' => $deleted,
-            'workspace' => $this->getWorkspaceId(),
-            'ref_table' => $ref_table,
-            'ref_uid' => $ref_uid,
-            'ref_string' => $ref_string
+            'deleted' => (int)$deleted,
+            'workspace' => $workspaceId,
+            'ref_table' => $referencedTable,
+            'ref_uid' => $referencedUid,
+            'ref_string' => mb_substr($referenceString, 0, 1024)
         ];
     }
 
@@ -430,34 +509,37 @@ class ReferenceIndex
      * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
      * @param int $deleted Whether record is deleted-flagged or not
      * @param array $items Data array with database relations (table/id)
-     * @return void
      */
     public function createEntryData_dbRels($table, $uid, $fieldName, $flexPointer, $deleted, $items)
     {
-        foreach ($items as $sort => $i) {
-            $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, $i['table'], $i['id'], '', $sort);
+        $uid = $uid ? (int)$uid : 0;
+        if (!$uid) {
+            return;
         }
+        $this->createEntryDataForDatabaseRelationsUsingRecord(
+            (string)$table,
+            $this->getRecordRawCached($table, $uid),
+            (string)$fieldName,
+            (string)$flexPointer,
+            $deleted ? (int)$deleted : 0,
+            (array)$items
+        );
     }
 
     /**
-     * Enter file references to ->relations array
+     * Add database references to ->relations array based on fetched record
      *
-     * @param string $table Tablename of source record (where reference is located)
-     * @param int $uid UID of source record (where reference is located)
+     * @param string $tableName Tablename of source record (where reference is located)
+     * @param array $record Record from $tableName
      * @param string $fieldName Fieldname of source record (where reference is located)
-     * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in [field]
+     * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in $fieldName
      * @param int $deleted Whether record is deleted-flagged or not
-     * @param array $items Data array with file relations
-     * @return void
+     * @param array $items Data array with database relations (table/id)
      */
-    public function createEntryData_fileRels($table, $uid, $fieldName, $flexPointer, $deleted, $items)
+    protected function createEntryDataForDatabaseRelationsUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, int $deleted, array $items)
     {
         foreach ($items as $sort => $i) {
-            $filePath = $i['ID_absFile'];
-            if (GeneralUtility::isFirstPartOfStr($filePath, PATH_site)) {
-                $filePath = PathUtility::stripPathSitePrefix($filePath);
-            }
-            $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_FILE', 0, $filePath, $sort);
+            $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, $deleted, $i['table'], (int)$i['id'], '', $sort);
         }
     }
 
@@ -470,29 +552,47 @@ class ReferenceIndex
      * @param string $flexPointer Pointer to location inside FlexForm structure
      * @param int $deleted
      * @param array $keys Data array with soft reference keys
-     * @return void
      */
     public function createEntryData_softreferences($table, $uid, $fieldName, $flexPointer, $deleted, $keys)
     {
-        if (is_array($keys)) {
-            foreach ($keys as $spKey => $elements) {
-                if (is_array($elements)) {
-                    foreach ($elements as $subKey => $el) {
-                        if (is_array($el['subst'])) {
-                            switch ((string)$el['subst']['type']) {
-                                case 'db':
-                                    list($tableName, $recordId) = explode(':', $el['subst']['recordRef']);
-                                    $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, $tableName, $recordId, '', -1, $spKey, $subKey);
-                                    break;
-                                case 'file_reference':
-                                    // not used (see getRelations()), but fallback to file
-                                case 'file':
-                                    $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_FILE', 0, $el['subst']['relFileName'], -1, $spKey, $subKey);
-                                    break;
-                                case 'string':
-                                    $this->relations[] = $this->createEntryData($table, $uid, $fieldName, $flexPointer, $deleted, '_STRING', 0, $el['subst']['tokenValue'], -1, $spKey, $subKey);
-                                    break;
-                            }
+        $uid = $uid ? (int)$uid : 0;
+        if (!$uid || !is_array($keys)) {
+            return;
+        }
+        $this->createEntryDataForSoftReferencesUsingRecord(
+            (string)$table,
+            $this->getRecordRawCached($table, $uid),
+            (string)$fieldName,
+            (string)$flexPointer,
+            $deleted ? (int)$deleted : 0,
+            (array)$keys
+        );
+    }
+
+    /**
+     * Add SoftReference references to ->relations array based on fetched record
+     *
+     * @param string $tableName Tablename of source record (where reference is located)
+     * @param array $record Record from $tableName
+     * @param string $fieldName Fieldname of source record (where reference is located)
+     * @param string $flexPointer Pointer to location inside FlexForm structure where reference is located in $fieldName
+     * @param int $deleted Whether record is deleted-flagged or not
+     * @param array $keys Data array with soft reference keys
+     */
+    protected function createEntryDataForSoftReferencesUsingRecord(string $tableName, array $record, string $fieldName, string $flexPointer, int $deleted, array $keys)
+    {
+        foreach ($keys as $spKey => $elements) {
+            if (is_array($elements)) {
+                foreach ($elements as $subKey => $el) {
+                    if (is_array($el['subst'])) {
+                        switch ((string)$el['subst']['type']) {
+                            case 'db':
+                                list($referencedTable, $referencedUid) = explode(':', $el['subst']['recordRef']);
+                                $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, $deleted, $referencedTable, (int)$referencedUid, '', -1, $spKey, $subKey);
+                                break;
+                            case 'string':
+                                $this->relations[] = $this->createEntryDataUsingRecord($tableName, $record, $fieldName, $flexPointer, $deleted, '_STRING', 0, $el['subst']['tokenValue'], -1, $spKey, $subKey);
+                                break;
                         }
                     }
                 }
@@ -509,7 +609,7 @@ class ReferenceIndex
     /**
      * Returns relation information for a $table/$row-array
      * Traverses all fields in input row which are configured in TCA/columns
-     * It looks for hard relations to files and records in the TCA types "select" and "group"
+     * It looks for hard relations to records in the TCA types "select" and "group"
      *
      * @param string $table Table name
      * @param array $row Row from table
@@ -523,44 +623,14 @@ class ReferenceIndex
         $uid = $row['uid'];
         $outRow = [];
         foreach ($row as $field => $value) {
-            if (!isset(static::$nonRelationFields[$field]) && is_array($GLOBALS['TCA'][$table]['columns'][$field]) && (!$onlyField || $onlyField === $field)) {
+            if ($this->shouldExcludeTableColumnFromReferenceIndex($table, $field, $onlyField) === false) {
                 $conf = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
-                // Add files
-                $resultsFromFiles = $this->getRelations_procFiles($value, $conf, $uid);
-                if (!empty($resultsFromFiles)) {
-                    // We have to fill different arrays here depending on the result.
-                    // internal_type file is still a relation of type file and
-                    // since http://forge.typo3.org/issues/49538 internal_type file_reference
-                    // is a database relation to a sys_file record
-                    $fileResultsFromFiles = [];
-                    $dbResultsFromFiles = [];
-                    foreach ($resultsFromFiles as $resultFromFiles) {
-                        if (isset($resultFromFiles['table']) && $resultFromFiles['table'] === 'sys_file') {
-                            $dbResultsFromFiles[] = $resultFromFiles;
-                        } else {
-                            // Creates an entry for the field with all the files:
-                            $fileResultsFromFiles[] = $resultFromFiles;
-                        }
-                    }
-                    if (!empty($fileResultsFromFiles)) {
-                        $outRow[$field] = [
-                            'type' => 'file',
-                            'newValueFiles' => $fileResultsFromFiles
-                        ];
-                    }
-                    if (!empty($dbResultsFromFiles)) {
-                        $outRow[$field] = [
-                            'type' => 'db',
-                            'itemArray' => $dbResultsFromFiles
-                        ];
-                    }
-                }
                 // Add a softref definition for link fields if the TCA does not specify one already
-                if ($conf['type'] === 'input' && isset($conf['wizards']['link']) && empty($conf['softref'])) {
+                if ($conf['type'] === 'input' && $conf['renderType'] === 'inputLink' && empty($conf['softref'])) {
                     $conf['softref'] = 'typolink';
                 }
                 // Add DB:
-                $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table, $field);
+                $resultsFromDatabase = $this->getRelations_procDB($value, $conf, $uid, $table);
                 if (!empty($resultsFromDatabase)) {
                     // Create an entry for the field with all DB relations:
                     $outRow[$field] = [
@@ -568,17 +638,16 @@ class ReferenceIndex
                         'itemArray' => $resultsFromDatabase
                     ];
                 }
-                // For "flex" fieldtypes we need to traverse the structure looking for file and db references of course!
+                // For "flex" fieldtypes we need to traverse the structure looking for db references of course!
                 if ($conf['type'] === 'flex') {
                     // Get current value array:
                     // NOTICE: failure to resolve Data Structures can lead to integrity problems with the reference index. Please look up
                     // the note in the JavaDoc documentation for the function FlexFormTools->getDataStructureIdentifier()
                     $currentValueArray = GeneralUtility::xml2array($value);
-                    // Traversing the XML structure, processing files:
+                    // Traversing the XML structure, processing:
                     if (is_array($currentValueArray)) {
                         $this->temp_flexRelations = [
                             'db' => [],
-                            'file' => [],
                             'softrefs' => []
                         ];
                         // Create and call iterator object:
@@ -594,8 +663,8 @@ class ReferenceIndex
                 // Soft References:
                 if ((string)$value !== '') {
                     $softRefValue = $value;
-                    $softRefs = BackendUtility::explodeSoftRefParserList($conf['softref']);
-                    if ($softRefs !== false) {
+                    if (!empty($conf['softref'])) {
+                        $softRefs = BackendUtility::explodeSoftRefParserList($conf['softref']);
                         foreach ($softRefs as $spKey => $spParams) {
                             $softRefObj = BackendUtility::softRefParserObj($spKey);
                             if (is_object($softRefObj)) {
@@ -619,17 +688,16 @@ class ReferenceIndex
     }
 
     /**
-     * Callback function for traversing the FlexForm structure in relation to finding file and DB references!
+     * Callback function for traversing the FlexForm structure in relation to finding DB references!
      *
      * @param array $dsArr Data structure for the current value
      * @param mixed $dataValue Current value
      * @param array $PA Additional configuration used in calling function
      * @param string $structurePath Path of value in DS structure
-     * @param object $parentObject Object reference to caller (unused)
-     * @return void
-     * @see DataHandler::checkValue_flex_procInData_travDS(),FlexFormTools::traverseFlexFormXMLData()
+     * @see DataHandler::checkValue_flex_procInData_travDS()
+     * @see FlexFormTools::traverseFlexFormXMLData()
      */
-    public function getRelations_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath, $parentObject)
+    public function getRelations_flexFormCallBack($dsArr, $dataValue, $PA, $structurePath)
     {
         // Removing "data/" in the beginning of path (which points to location in data array)
         $structurePath = substr($structurePath, 5) . '/';
@@ -640,35 +708,12 @@ class ReferenceIndex
             $PA['uid'],
             $PA['field']
         ];
-        // Add files
-        $resultsFromFiles = $this->getRelations_procFiles($dataValue, $dsConf, $uid);
-        if (!empty($resultsFromFiles)) {
-            // We have to fill different arrays here depending on the result.
-            // internal_type file is still a relation of type file and
-            // since http://forge.typo3.org/issues/49538 internal_type file_reference
-            // is a database relation to a sys_file record
-            $fileResultsFromFiles = [];
-            $dbResultsFromFiles = [];
-            foreach ($resultsFromFiles as $resultFromFiles) {
-                if (isset($resultFromFiles['table']) && $resultFromFiles['table'] === 'sys_file') {
-                    $dbResultsFromFiles[] = $resultFromFiles;
-                } else {
-                    $fileResultsFromFiles[] = $resultFromFiles;
-                }
-            }
-            if (!empty($fileResultsFromFiles)) {
-                $this->temp_flexRelations['file'][$structurePath] = $fileResultsFromFiles;
-            }
-            if (!empty($dbResultsFromFiles)) {
-                $this->temp_flexRelations['db'][$structurePath] = $dbResultsFromFiles;
-            }
-        }
         // Add a softref definition for link fields if the TCA does not specify one already
-        if ($dsConf['type'] === 'input' && isset($dsConf['wizards']['link']) && empty($dsConf['softref'])) {
+        if ($dsConf['type'] === 'input' && $dsConf['renderType'] === 'inputLink' && empty($dsConf['softref'])) {
             $dsConf['softref'] = 'typolink';
         }
         // Add DB:
-        $resultsFromDatabase = $this->getRelations_procDB($dataValue, $dsConf, $uid, $table, $field);
+        $resultsFromDatabase = $this->getRelations_procDB($dataValue, $dsConf, $uid, $table);
         if (!empty($resultsFromDatabase)) {
             // Create an entry for the field with all DB relations:
             $this->temp_flexRelations['db'][$structurePath] = $resultsFromDatabase;
@@ -698,87 +743,28 @@ class ReferenceIndex
     }
 
     /**
-     * Check field configuration if it is a file relation field and extract file relations if any
-     *
-     * @param string $value Field value
-     * @param array $conf Field configuration array of type "TCA/columns
-     * @param int $uid Field uid
-     * @return bool|array If field type is OK it will return an array with the files inside. Else FALSE
-     */
-    public function getRelations_procFiles($value, $conf, $uid)
-    {
-        if ($conf['type'] !== 'group' || ($conf['internal_type'] !== 'file' && $conf['internal_type'] !== 'file_reference')) {
-            return false;
-        }
-
-        // Collect file values in array:
-        if ($conf['MM']) {
-            $theFileValues = [];
-            $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
-            $dbAnalysis->start('', 'files', $conf['MM'], $uid);
-            foreach ($dbAnalysis->itemArray as $someval) {
-                if ($someval['id']) {
-                    $theFileValues[] = $someval['id'];
-                }
-            }
-        } else {
-            $theFileValues = explode(',', $value);
-        }
-        // Traverse the files and add them:
-        $uploadFolder = $conf['internal_type'] === 'file' ? $conf['uploadfolder'] : '';
-        $destinationFolder = $this->destPathFromUploadFolder($uploadFolder);
-        $newValueFiles = [];
-        foreach ($theFileValues as $file) {
-            if (trim($file)) {
-                $realFile = $destinationFolder . '/' . trim($file);
-                $newValueFile = [
-                    'filename' => basename($file),
-                    'ID' => md5($realFile),
-                    'ID_absFile' => $realFile
-                ];
-                // Set sys_file and id for referenced files
-                if ($conf['internal_type'] === 'file_reference') {
-                    try {
-                        $file = ResourceFactory::getInstance()->retrieveFileOrFolderObject($file);
-                        if ($file instanceof File || $file instanceof Folder) {
-                            // For setting this as sys_file relation later, the keys filename, ID and ID_absFile
-                            // have not to be included, because the are not evaluated for db relations.
-                            $newValueFile = [
-                                'table' => 'sys_file',
-                                'id' => $file->getUid()
-                            ];
-                        }
-                    } catch (\Exception $e) {
-                    }
-                }
-                $newValueFiles[] = $newValueFile;
-            }
-        }
-        return $newValueFiles;
-    }
-
-    /**
      * Check field configuration if it is a DB relation field and extract DB relations if any
      *
      * @param string $value Field value
      * @param array $conf Field configuration array of type "TCA/columns
      * @param int $uid Field uid
      * @param string $table Table name
-     * @param string $field Field name
-     * @return array If field type is OK it will return an array with the database relations. Else FALSE
+     * @return array|bool If field type is OK it will return an array with the database relations. Else FALSE
      */
-    public function getRelations_procDB($value, $conf, $uid, $table = '', $field = '')
+    public function getRelations_procDB($value, $conf, $uid, $table = '')
     {
         // Get IRRE relations
         if (empty($conf)) {
             return false;
-        } elseif ($conf['type'] === 'inline' && !empty($conf['foreign_table']) && empty($conf['MM'])) {
+        }
+        if ($conf['type'] === 'inline' && !empty($conf['foreign_table']) && empty($conf['MM'])) {
             $dbAnalysis = GeneralUtility::makeInstance(RelationHandler::class);
             $dbAnalysis->setUseLiveReferenceIds(false);
             $dbAnalysis->start($value, $conf['foreign_table'], '', $uid, $table, $conf);
             return $dbAnalysis->itemArray;
             // DB record lists:
-        } elseif ($this->isDbReferenceField($conf)) {
+        }
+        if ($this->isDbReferenceField($conf)) {
             $allowedTables = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
             if ($conf['MM_opposite_field']) {
                 return [];
@@ -873,14 +859,6 @@ class ReferenceIndex
                                 return $error;
                             }
                             break;
-                        case 'file_reference':
-                            // not used (see getRelations()), but fallback to file
-                        case 'file':
-                            $error = $this->setReferenceValue_fileRels($referenceRecord, $fieldRelation['newValueFiles'], $newValue, $dataArray);
-                            if ($error) {
-                                return $error;
-                            }
-                            break;
                         case 'flex':
                             // DB references in FlexForms
                             if (is_array($fieldRelation['flexFormRels']['db'][$referenceRecord['flexpointer']])) {
@@ -889,13 +867,6 @@ class ReferenceIndex
                                     return $error;
                                 }
                             }
-                            // File references in FlexForms
-                            if (is_array($fieldRelation['flexFormRels']['file'][$referenceRecord['flexpointer']])) {
-                                $error = $this->setReferenceValue_fileRels($referenceRecord, $fieldRelation['flexFormRels']['file'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']);
-                                if ($error) {
-                                    return $error;
-                                }
-                            }
                             // Soft references in FlexForms
                             if ($referenceRecord['softref_key'] && is_array($fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']]['keys'][$referenceRecord['softref_key']])) {
                                 $error = $this->setReferenceValue_softreferences($referenceRecord, $fieldRelation['flexFormRels']['softrefs'][$referenceRecord['flexpointer']], $newValue, $dataArray, $referenceRecord['flexpointer']);
@@ -915,21 +886,19 @@ class ReferenceIndex
                     // Data Array, now ready to be sent to DataHandler
                     if ($returnDataArray) {
                         return $dataArray;
-                    } else {
-                        // Execute CMD array:
-                        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
-                        $dataHandler->dontProcessTransformations = true;
-                        $dataHandler->bypassWorkspaceRestrictions = true;
-                        $dataHandler->bypassFileHandling = true;
-                        // Otherwise this cannot update things in deleted records...
-                        $dataHandler->bypassAccessCheckForRecords = true;
-                        // Check has been done previously that there is a backend user which is Admin and also in live workspace
-                        $dataHandler->start($dataArray, []);
-                        $dataHandler->process_datamap();
-                        // Return errors if any:
-                        if (!empty($dataHandler->errorLog)) {
-                            return LF . 'DataHandler:' . implode((LF . 'DataHandler:'), $dataHandler->errorLog);
-                        }
+                    }
+                    // Execute CMD array:
+                    $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
+                    $dataHandler->dontProcessTransformations = true;
+                    $dataHandler->bypassWorkspaceRestrictions = true;
+                    // Otherwise this cannot update things in deleted records...
+                    $dataHandler->bypassAccessCheckForRecords = true;
+                    // Check has been done previously that there is a backend user which is Admin and also in live workspace
+                    $dataHandler->start($dataArray, []);
+                    $dataHandler->process_datamap();
+                    // Return errors if any:
+                    if (!empty($dataHandler->errorLog)) {
+                        return LF . 'DataHandler:' . implode(LF . 'DataHandler:', $dataHandler->errorLog);
                     }
                 }
             }
@@ -981,51 +950,10 @@ class ReferenceIndex
     }
 
     /**
-     * Setting a value for a reference for a FILE field:
-     *
-     * @param array $refRec sys_refindex record
-     * @param array $itemArray Array of references from that field
-     * @param string $newValue Value to substitute current value with (or NULL to unset it)
-     * @param array $dataArray Data array in which the new value is set (passed by reference)
-     * @param string $flexPointer Flexform pointer, if in a flex form field.
-     * @return string Error message if any, otherwise FALSE = OK
-     */
-    public function setReferenceValue_fileRels($refRec, $itemArray, $newValue, &$dataArray, $flexPointer = '')
-    {
-        $ID_absFile = PathUtility::stripPathSitePrefix($itemArray[$refRec['sorting']]['ID_absFile']);
-        if ($ID_absFile === (string)$refRec['ref_string'] && $refRec['ref_table'] === '_FILE') {
-            // Setting or removing value:
-            // Remove value:
-            if ($newValue === null) {
-                unset($itemArray[$refRec['sorting']]);
-            } else {
-                $itemArray[$refRec['sorting']]['filename'] = $newValue;
-            }
-            // Traverse and compile new list of records:
-            $saveValue = [];
-            foreach ($itemArray as $fileInfo) {
-                $saveValue[] = $fileInfo['filename'];
-            }
-            // Set in data array:
-            if ($flexPointer) {
-                $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-                $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = [];
-                $flexFormTools->setArrayValueByPath(substr($flexPointer, 0, -1), $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'], implode(',', $saveValue));
-            } else {
-                $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']] = implode(',', $saveValue);
-            }
-        } else {
-            return 'ERROR: either "' . $refRec['ref_table'] . '" was not "_FILE" or file PATH_site+"' . $refRec['ref_string'] . '" did not match that of the record ("' . $itemArray[$refRec['sorting']]['ID_absFile'] . '") in sorting index "' . $refRec['sorting'] . '"';
-        }
-
-        return false;
-    }
-
-    /**
      * Setting a value for a soft reference token
      *
      * @param array $refRec sys_refindex record
-     * @param array $softref Array of soft reference occurencies
+     * @param array $softref Array of soft reference occurrences
      * @param string $newValue Value to substitute current value with
      * @param array $dataArray Data array in which the new value is set (passed by reference)
      * @param string $flexPointer Flexform pointer, if in a flex form field.
@@ -1046,7 +974,7 @@ class ReferenceIndex
             }
         }
         // Set in data array:
-        if (!strstr($softref['tokenizedContent'], '{softref:')) {
+        if (strpos($softref['tokenizedContent'], '{softref:') === false) {
             if ($flexPointer) {
                 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
                 $dataArray[$refRec['tablename']][$refRec['recuid']][$refRec['field']]['data'] = [];
@@ -1095,9 +1023,7 @@ class ReferenceIndex
         return
             $this->isDbReferenceField($configuration)
             ||
-            ($configuration['type'] === 'group' && ($configuration['internal_type'] === 'file' || $configuration['internal_type'] === 'file_reference')) // getRelations_procFiles
-            ||
-            ($configuration['type'] === 'input' && isset($configuration['wizards']['link'])) // getRelations_procDB
+            ($configuration['type'] === 'input' && $configuration['renderType'] === 'inputLink') // getRelations_procDB
             ||
             $configuration['type'] === 'flex'
             ||
@@ -1138,20 +1064,6 @@ class ReferenceIndex
     }
 
     /**
-     * Returns destination path to an upload folder given by $folder
-     *
-     * @param string $folder Folder relative to PATH_site
-     * @return string Input folder prefixed with PATH_site. No checking for existence is done. Output must be a folder without trailing slash.
-     */
-    public function destPathFromUploadFolder($folder)
-    {
-        if (!$folder) {
-            return substr(PATH_site, 0, -1);
-        }
-        return PATH_site . $folder;
-    }
-
-    /**
      * Updating Index (External API)
      *
      * @param bool $testOnly If set, only a test
@@ -1170,10 +1082,18 @@ class ReferenceIndex
         }
         // Traverse all tables:
         $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'];
+
         foreach ($GLOBALS['TCA'] as $tableName => $cfg) {
-            if (isset(static::$nonRelationTables[$tableName])) {
+            if ($this->shouldExcludeTableFromReferenceIndex($tableName)) {
                 continue;
             }
+            $tableConnectionName = empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])
+                ? ConnectionPool::DEFAULT_CONNECTION_NAME
+                : $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
+
             $fields = ['uid'];
             if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
                 $fields[] = 't3ver_wsid';
@@ -1187,22 +1107,22 @@ class ReferenceIndex
                     ->from($tableName)
                     ->execute();
             } catch (DBALException $e) {
-                // Table exists in $TCA but does not exist in the database
-                // @todo: improve / change message and add actual sql error?
-                GeneralUtility::sysLog(sprintf('Table "%s" exists in $TCA but does not exist in the database. You should run the Database Analyzer in the Install Tool to fix this.', $tableName), 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                // Table exists in TCA but does not exist in the database
+                $msg = 'Table "' .
+                        $tableName .
+                        '" exists in TCA but does not exist in the database. You should run the Database Analyzer in the Install Tool to fix this.';
+                $this->logger->error($msg, ['exception' => $e]);
                 continue;
             }
 
             $tableNames[] = $tableName;
             $tableCount++;
-            $uidList = [0];
             while ($record = $queryResult->fetch()) {
                 $refIndexObj = GeneralUtility::makeInstance(self::class);
                 if (isset($record['t3ver_wsid'])) {
                     $refIndexObj->setWorkspaceId($record['t3ver_wsid']);
                 }
                 $result = $refIndexObj->updateRefIndexTable($tableName, $record['uid'], $testOnly);
-                $uidList[] = $record['uid'];
                 $recCount++;
                 if ($result['addedNodes'] || $result['deletedNodes']) {
                     $error = 'Record ' . $tableName . ':' . $record['uid'] . ' had ' . $result['addedNodes'] . ' added indexes and ' . $result['deletedNodes'] . ' deleted indexes';
@@ -1213,7 +1133,27 @@ class ReferenceIndex
                 }
             }
 
+            // Subselect based queries only work on the same connection
+            if ($refIndexConnectionName !== $tableConnectionName) {
+                $this->logger->error('Not checking table "' . $tableName . '" for lost indexes, "sys_refindex" table uses a different connection');
+                continue;
+            }
+
             // Searching for lost indexes for this table
+            // Build sub-query to find lost records
+            $subQueryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
+            $subQueryBuilder->getRestrictions()->removeAll();
+            $subQueryBuilder
+                ->select('uid')
+                ->from($tableName, 'sub_' . $tableName)
+                ->where(
+                    $subQueryBuilder->expr()->eq(
+                        'sub_' . $tableName . '.uid',
+                        $queryBuilder->quoteIdentifier('sys_refindex.recuid')
+                    )
+                );
+
+            // Main query to find lost records
             $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_refindex');
             $queryBuilder->getRestrictions()->removeAll();
             $lostIndexes = $queryBuilder
@@ -1224,10 +1164,7 @@ class ReferenceIndex
                         'tablename',
                         $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
                     ),
-                    $queryBuilder->expr()->notIn(
-                        'recuid',
-                        $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
-                    )
+                    'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
                 )
                 ->execute()
                 ->fetchColumn(0);
@@ -1246,11 +1183,9 @@ class ReferenceIndex
                                 'tablename',
                                 $queryBuilder->createNamedParameter($tableName, \PDO::PARAM_STR)
                             ),
-                            $queryBuilder->expr()->notIn(
-                                'recuid',
-                                $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
-                            )
-                        )->execute();
+                            'NOT EXISTS (' . $subQueryBuilder->getSQL() . ')'
+                        )
+                        ->execute();
                 }
             }
         }
@@ -1294,12 +1229,9 @@ class ReferenceIndex
             $recordsCheckedString,
             $errorCount ? FlashMessage::ERROR : FlashMessage::OK
         );
-        /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
-        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
-        /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
-        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
-        $defaultFlashMessageQueue->enqueue($flashMessage);
-        $bodyContent = $defaultFlashMessageQueue->renderFlashMessages();
+
+        $flashMessageRenderer = GeneralUtility::makeInstance(FlashMessageRendererResolver::class)->resolve();
+        $bodyContent = $flashMessageRenderer->render([$flashMessage]);
         if ($cli_echo) {
             echo $recordsCheckedString . ($errorCount ? 'Updates: ' . $errorCount : 'Index Integrity was perfect!') . LF;
         }
@@ -1311,6 +1243,144 @@ class ReferenceIndex
     }
 
     /**
+     * Gets one record from database and stores it in an internal cache (which expires along with object lifecycle) for faster retrieval
+     *
+     * Assumption:
+     *
+     * - This method is only used from within delegate methods and so only caches queries generated based on the record being indexed; the query
+     *   to select origin side record is uncached
+     * - Origin side records do not change in database while updating the reference index
+     * - Origin record does not get removed while updating index
+     * - Relations may change during indexing, which is why only the origin record is cached and all relations are re-process even when repeating
+     *   indexing of the same origin record
+     *
+     * Please note that the cache is disabled by default but can be enabled using $this->enableRuntimeCaches()
+     * due to possible side-effects while handling references that were changed during one single
+     * request.
+     *
+     * @param string $tableName
+     * @param int $uid
+     * @return array|false
+     */
+    protected function getRecordRawCached(string $tableName, int $uid)
+    {
+        $recordCacheId = $tableName . ':' . $uid;
+        if (!$this->useRuntimeCache || !isset($this->recordCache[$recordCacheId])) {
+
+            // Fetch fields of the table which might contain relations
+            $cacheId = static::$cachePrefixTableRelationFields . $tableName;
+            if (!$this->useRuntimeCache || !$this->runtimeCache->has($cacheId)) {
+                $tableRelationFields = $this->fetchTableRelationFields($tableName);
+                if ($this->useRuntimeCache) {
+                    $this->runtimeCache->set($cacheId, $tableRelationFields);
+                }
+            } else {
+                $tableRelationFields = $this->runtimeCache->get($cacheId);
+            }
+
+            // Return if there are no fields which could contain relations
+            if ($tableRelationFields === '') {
+                return $this->relations;
+            }
+
+            if ($tableRelationFields === '*') {
+                // If one field of a record is of type flex, all fields have to be fetched to be passed to FlexFormTools->getDataStructureIdentifier()
+                $selectFields = '*';
+            } else {
+                // otherwise only fields that might contain relations are fetched
+                $selectFields = 'uid,' . $tableRelationFields;
+                $deleteField = $GLOBALS['TCA'][$tableName]['ctrl']['delete'];
+                if ($deleteField) {
+                    $selectFields .= ',' . $deleteField;
+                }
+                if (BackendUtility::isTableWorkspaceEnabled($tableName)) {
+                    $selectFields .= ',t3ver_wsid';
+                }
+            }
+
+            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable($tableName);
+            $queryBuilder->getRestrictions()->removeAll();
+            $row = $queryBuilder
+                ->select(...GeneralUtility::trimExplode(',', $selectFields, true))
+                ->from($tableName)
+                ->where(
+                    $queryBuilder->expr()->eq(
+                        'uid',
+                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
+                    )
+                )
+                ->execute()
+                ->fetch();
+
+            $this->recordCache[$recordCacheId] = $row;
+        }
+        return $this->recordCache[$recordCacheId];
+    }
+
+    /**
+     * Checks if a given table should be excluded from ReferenceIndex
+     *
+     * @param string $tableName Name of the table
+     * @return bool true if it should be excluded
+     */
+    protected function shouldExcludeTableFromReferenceIndex($tableName)
+    {
+        if (isset(static::$excludedTables[$tableName])) {
+            return static::$excludedTables[$tableName];
+        }
+
+        // Only exclude tables from ReferenceIndex which do not contain any relations and never did since existing references won't be deleted!
+        // There is no need to add tables without a definition in $GLOBALS['TCA] since ReferenceIndex only handles those.
+        $excludeTable = false;
+        $signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
+        $signalSlotDispatcher->dispatch(__CLASS__, 'shouldExcludeTableFromReferenceIndex', [$tableName, &$excludeTable]);
+
+        static::$excludedTables[$tableName] = $excludeTable;
+
+        return static::$excludedTables[$tableName];
+    }
+
+    /**
+     * Checks if a given column in a given table should be excluded in the ReferenceIndex process
+     *
+     * @param string $tableName Name of the table
+     * @param string $column Name of the column
+     * @param string $onlyColumn Name of a specific column to fetch
+     * @return bool true if it should be excluded
+     */
+    protected function shouldExcludeTableColumnFromReferenceIndex($tableName, $column, $onlyColumn)
+    {
+        if (isset(static::$excludedColumns[$column])) {
+            return true;
+        }
+
+        if (is_array($GLOBALS['TCA'][$tableName]['columns'][$column]) && (!$onlyColumn || $onlyColumn === $column)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Enables the runtime-based caches
+     * Could lead to side effects, depending if the reference index instance is run multiple times
+     * while records would be changed.
+     */
+    public function enableRuntimeCache()
+    {
+        $this->useRuntimeCache = true;
+    }
+
+    /**
+     * Disables the runtime-based cache
+     */
+    public function disableRuntimeCache()
+    {
+        $this->useRuntimeCache = false;
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication