diff --git a/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php
index 7c5babf68f1c906e11d1c21d7726863e9c9a68af..9a600be35095eed2f2811faa422b37b4f0af3614 100644
--- a/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php
+++ b/typo3/sysext/backend/Classes/Preview/FluidBasedContentPreviewRenderer.php
@@ -24,6 +24,7 @@ use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
 use TYPO3\CMS\Core\Attribute\AsEventListener;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Domain\RecordFactory;
 use TYPO3\CMS\Core\Service\FlexFormService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
@@ -42,7 +43,8 @@ final class FluidBasedContentPreviewRenderer implements LoggerAwareInterface
     use LoggerAwareTrait;
 
     public function __construct(
-        protected readonly FlexFormService $flexFormService
+        protected readonly FlexFormService $flexFormService,
+        protected readonly RecordFactory $recordFactory,
     ) {}
 
     #[AsEventListener('typo3-backend/fluid-preview/content')]
@@ -89,6 +91,7 @@ final class FluidBasedContentPreviewRenderer implements LoggerAwareInterface
             if ($table === 'tt_content' && !empty($row['pi_flexform'])) {
                 $view->assign('pi_flexform_transformed', $this->flexFormService->convertFlexFormContentToArray($row['pi_flexform']));
             }
+            $view->assign('record', $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $row));
             return $view->render();
         } catch (\Exception $e) {
             $this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [
diff --git a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php
index bed7e3c04181ab767fd062612ef570be5d2443a3..9338a6022dd1011b787357e252a5ceb111852e38 100644
--- a/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php
+++ b/typo3/sysext/backend/Classes/Preview/StandardContentPreviewRenderer.php
@@ -23,9 +23,16 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Domain\RawRecord;
+use TYPO3\CMS\Core\Domain\Record;
+use TYPO3\CMS\Core\Domain\RecordFactory;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Imaging\IconSize;
+use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\ProcessedFile;
+use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -39,6 +46,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * by changing this TCA configuration.
  *
  * See also PreviewRendererInterface documentation.
+ *
+ * @todo Evaluate class and streamline to properly use DI
  */
 class StandardContentPreviewRenderer implements PreviewRendererInterface, LoggerAwareInterface
 {
@@ -79,14 +88,15 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
 
     public function renderPageModulePreviewContent(GridColumnItem $item): string
     {
-        $recordType = $item->getRecordType();
         $languageService = $this->getLanguageService();
         $table = $item->getTable();
         $record = $item->getRecord();
+        $recordObj = GeneralUtility::makeInstance(RecordFactory::class)->createResolvedRecordFromDatabaseRow($table, $record);
+        $recordType = $recordObj->getRecordType();
         $out = '';
 
         // If record type is unknown, render warning message.
-        if ($item->getTypeColumn() !== '' && !is_array($GLOBALS['TCA'][$table]['types'][$recordType] ?? null)) {
+        if (!GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($recordObj->getMainType())->hasSubSchema($recordType)) {
             $message = sprintf(
                 $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
                 $recordType
@@ -105,29 +115,25 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
             case 'header':
                 break;
             case 'uploads':
-                if ($record['media']) {
-                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'media'), $record);
+                if ($recordObj['media'] ?? false) {
+                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['media']), $record);
                 }
                 break;
             case 'shortcut':
-                if (!empty($record['records'])) {
+                if (!empty($recordObj['records'])) {
                     $shortcutContent = '';
-                    $recordList = explode(',', $record['records']);
-                    foreach ($recordList as $recordIdentifier) {
-                        $split = BackendUtility::splitTable_Uid($recordIdentifier);
-                        $shortcutTableName = empty($split[0]) ? $table : $split[0];
-                        $shortcutRecord = BackendUtility::getRecord($shortcutTableName, $split[1]);
-                        if (is_array($shortcutRecord)) {
-                            $shortcutRecord = $this->translateShortcutRecord($record, $shortcutRecord, $shortcutTableName, (int)$split[1]);
-                            $icon = $this->getIconFactory()->getIconForRecord($shortcutTableName, $shortcutRecord, IconSize::SMALL)->render();
-                            $icon = BackendUtility::wrapClickMenuOnIcon(
-                                $icon,
-                                $shortcutTableName,
-                                $shortcutRecord['uid'],
-                                '1'
-                            );
-                            $shortcutContent .= '<li class="list-group-item">' . $icon . ' ' . htmlspecialchars(BackendUtility::getRecordTitle($shortcutTableName, $shortcutRecord)) . '</li>';
-                        }
+                    $shortcutRecords = $recordObj['records'] instanceof \Traversable ? $recordObj['records'] : [$recordObj['records']];
+                    foreach ($shortcutRecords as $shortcutRecord) {
+                        $shortcutTableName = $shortcutRecord->getMainType();
+                        $shortcutRecord = $this->translateShortcutRecord($recordObj, $shortcutRecord, $shortcutTableName);
+                        $icon = $this->getIconFactory()->getIconForRecord($shortcutTableName, $shortcutRecord->toArray(), IconSize::SMALL)->render();
+                        $icon = BackendUtility::wrapClickMenuOnIcon(
+                            $icon,
+                            $shortcutTableName,
+                            $shortcutRecord->getUid(),
+                            '1'
+                        );
+                        $shortcutContent .= '<li class="list-group-item">' . $icon . ' ' . htmlspecialchars(BackendUtility::getRecordTitle($shortcutTableName, $shortcutRecord->toArray())) . '</li>';
                     }
                     $out .= $shortcutContent ? '<ul class="list-group">' . $shortcutContent . '</ul>' : '';
                 }
@@ -162,17 +168,17 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
                 }
                 break;
             default:
-                if ($record['bodytext']) {
+                if ($recordObj['bodytext'] ?? false) {
                     $out .= $this->linkEditContent($this->renderText($record['bodytext']), $record);
                 }
-                if ($record['image']) {
-                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'image'), $record);
+                if ($recordObj['image'] ?? false) {
+                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['image']), $record);
                 }
-                if ($record['media']) {
-                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'media'), $record);
+                if ($recordObj['media'] ?? false) {
+                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['media']), $record);
                 }
-                if ($record['assets']) {
-                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($record, $table, 'assets'), $record);
+                if ($recordObj['assets'] ?? false) {
+                    $out .= $this->linkEditContent($this->getThumbCodeUnlinked($recordObj['assets']), $record);
                 }
         }
 
@@ -232,26 +238,21 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
         return $preview;
     }
 
-    protected function translateShortcutRecord(array $targetRecord, array $shortcutRecord, string $tableName, int $uid): array
+    protected function translateShortcutRecord(Record $targetRecord, Record $shortcutRecord, string $tableName): RawRecord
     {
-        $targetLanguage = (int)($targetRecord['sys_language_uid'] ?? 0);
-        if ($targetLanguage === 0 || !BackendUtility::isTableLocalizable($tableName)) {
-            return $shortcutRecord;
-        }
-
-        $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
-        $shortcutLanguage = (int)($shortcutRecord[$languageField] ?? 0);
-        if ($targetLanguage === $shortcutLanguage) {
-            return $shortcutRecord;
+        $targetLanguage = ($targetRecord->getLanguageId() ?? 0);
+        if ($targetLanguage === 0
+            || !GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($tableName)->isLanguageAware()
+            || $targetLanguage === ($shortcutRecord->getLanguageId() ?? 0)
+        ) {
+            return $shortcutRecord->getRawRecord();
         }
 
         // record is localized - fetch the shortcut record translation, if available
-        $shortcutRecordLocalization = BackendUtility::getRecordLocalization($tableName, $uid, $targetLanguage);
-        if (is_array($shortcutRecordLocalization) && !empty($shortcutRecordLocalization)) {
-            $shortcutRecord = $shortcutRecordLocalization[0];
-        }
-
-        return $shortcutRecord;
+        $shortcutRecordLocalization = BackendUtility::getRecordLocalization($tableName, $shortcutRecord->getUid(), $targetLanguage);
+        return is_array($shortcutRecordLocalization) && !empty($shortcutRecordLocalization)
+            ? GeneralUtility::makeInstance(RecordFactory::class)->createRawRecord($tableName, $shortcutRecordLocalization[0])
+            : $shortcutRecord->getRawRecord();
     }
 
     protected function getProcessedValue(GridColumnItem $item, string|array $fieldList, array &$info): void
@@ -268,17 +269,60 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
         }
     }
 
-    /**
-     * Create thumbnail code for record/field but not linked
-     *
-     * @param mixed[] $row Record array
-     * @param string $table Table (record is from)
-     * @param string $field Field name for which thumbnail are to be rendered.
-     * @return string HTML for thumbnails, if any.
-     */
-    protected function getThumbCodeUnlinked(array $row, string $table, string $field): string
+    protected function getThumbCodeUnlinked(iterable|FileReference $fileReferences): string
     {
-        return BackendUtility::thumbCode(row: $row, table: $table, field: $field, linkInfoPopup: false);
+        $thumbData = '';
+        $fileReferences = $fileReferences instanceof FileReference ? [$fileReferences] : $fileReferences;
+        foreach ($fileReferences as $fileReferenceObject) {
+            // Do not show previews of hidden references
+            if ($fileReferenceObject->getProperty('hidden')) {
+                continue;
+            }
+            $fileObject = $fileReferenceObject->getOriginalFile();
+            if ($fileObject->isMissing()) {
+                $missingFileIcon = $this->getIconFactory()
+                    ->getIcon('mimetypes-other-other', IconSize::MEDIUM, 'overlay-missing')
+                    ->setTitle(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') . ' ' . $fileObject->getName())
+                    ->render();
+                $thumbData .= '<div class="preview-thumbnails-element"><div class="preview-thumbnails-element-image">' . $missingFileIcon . '</div></div>';
+                continue;
+            }
+
+            // Preview web image or media elements
+            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
+                && ($fileReferenceObject->getOriginalFile()->isImage() || $fileReferenceObject->getOriginalFile()->isMediaFile())
+            ) {
+                $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
+                $cropArea = $cropVariantCollection->getCropArea();
+                $taskType = ProcessedFile::CONTEXT_IMAGEPREVIEW;
+                $processingConfiguration = [
+                    'width' => 64,
+                    'height' => 64,
+                ];
+                if (!$cropArea->isEmpty()) {
+                    $taskType = ProcessedFile::CONTEXT_IMAGECROPSCALEMASK;
+                    $processingConfiguration = [
+                        'maxWidth' => 64,
+                        'maxHeight' => 64,
+                        'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
+                    ];
+                }
+                $processedImage = $fileObject->process($taskType, $processingConfiguration);
+                $attributes = [
+                    'src' => $processedImage->getPublicUrl() ?? '',
+                    'width' => $processedImage->getProperty('width'),
+                    'height' => $processedImage->getProperty('height'),
+                    'alt' => $fileReferenceObject->getAlternative() ?: $fileReferenceObject->getName(),
+                    'loading' => 'lazy',
+                ];
+                $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . '/>';
+            } else {
+                $imgTag = $this->getIconFactory()->getIconForResource($fileObject)->setTitle($fileObject->getName())->render();
+            }
+            $thumbData .= '<div class="preview-thumbnails-element"><div class="preview-thumbnails-element-image">' . $imgTag . '</div></div>';
+        }
+
+        return $thumbData ? '<div class="preview-thumbnails" style="--preview-thumbnails-size: 64px">' . $thumbData . '</div>' : '';
     }
 
     /**
@@ -324,12 +368,14 @@ class StandardContentPreviewRenderer implements PreviewRendererInterface, Logger
     }
 
     /**
-     * Will create a link on the input string and possibly a big button after the string which links to editing in the RTE.
-     * Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor" button
+     * Will create a link on the input string and possibly a big button after the string which links to editing in the
+     * RTE. Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor"
+     * button
      *
      * @param string $linkText String to link. Must be prepared for HTML output.
      * @param array $row The row.
-     * @return string If the whole thing was editable and $linkText is not empty $linkText is returned with link around. Otherwise just $linkText.
+     * @return string If the whole thing was editable and $linkText is not empty $linkText is returned with link
+     *                around. Otherwise just $linkText.
      */
     protected function linkEditContent(string $linkText, array $row, string $table = 'tt_content'): string
     {
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index 8c8399ccd0b5d23faca28f2b2635b5f6511a0753..7efab50781d3fd535b3f2bcd2d81759879ae54d2 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -1051,6 +1051,7 @@ class BackendUtility
      * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
      * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
      * @return string Thumbnail image tag.
+     * @todo Unused in Core deprecate it!
      */
     public static function thumbCode(
         $row,
diff --git a/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php
index 7ae6c081843c58a4083d6fff7897b47056559c55..0dae4431c1636ff3551560817501ba42580eec66 100644
--- a/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php
+++ b/typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php
@@ -74,7 +74,7 @@ class BackendLayoutRenderer
                         // @todo: ideally we hand in the record object into the GridColumnItem in the future
                         if (!$recordIdentityMap->hasIdentifier('tt_content', (int)($contentRecord['uid'] ?? null))) {
                             try {
-                                $recordObject = $this->recordFactory->createFromDatabaseRow('tt_content', $contentRecord);
+                                $recordObject = $this->recordFactory->createResolvedRecordFromDatabaseRow('tt_content', $contentRecord);
                                 $recordIdentityMap->add($recordObject);
                             } catch (UndefinedSchemaException) {
                             }
diff --git a/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php b/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php
new file mode 100644
index 0000000000000000000000000000000000000000..c60e1ae259e0c1685cd5979ca4846f2479e458b5
--- /dev/null
+++ b/typo3/sysext/core/Classes/Collection/LazyRecordCollection.php
@@ -0,0 +1,98 @@
+<?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\Collection;
+
+use TYPO3\CMS\Core\Domain\RecordInterface;
+
+/**
+ * When first accessed, this class will initialize itself and find the relations
+ * for this record field.
+ *
+ * This class acts as a "Value holder", as it only fetches the related records
+ * when needed.
+ *
+ * @todo: Evaluate if we should use a Ghost object instead.
+ *
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+class LazyRecordCollection implements \IteratorAggregate, \ArrayAccess, \Countable
+{
+    /**
+     * @var RecordInterface[]|\Closure
+     */
+    private array|\Closure $items;
+
+    public function __construct(
+        private readonly mixed $fieldValue,
+        \Closure $initialization
+    ) {
+        $this->items = $initialization;
+    }
+
+    public function count(): int
+    {
+        $this->initialize();
+        return count($this->items);
+    }
+
+    private function initialize(): void
+    {
+        if ($this->items instanceof \Closure) {
+            $this->items = ($this->items)();
+        }
+    }
+
+    public function getIterator(): \Iterator
+    {
+        $this->initialize();
+        return new \ArrayIterator($this->items);
+    }
+
+    public function __toString(): string
+    {
+        return (string)$this->fieldValue;
+    }
+
+    public function offsetExists(mixed $offset): bool
+    {
+        $this->initialize();
+        return isset($this->items[$offset]);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        $this->initialize();
+        return $this->items[$offset] ?? null;
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void
+    {
+        if ($value instanceof RecordInterface === false) {
+            throw new \InvalidArgumentException(
+                'Modifying the record collection is only allowed by setting a value of type RecordInterface.',
+                1723188315
+            );
+        }
+        $this->items[$offset] = $value;
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        throw new \RuntimeException('Removing items from the record collection is not implemented.', 1723188316);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php b/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php
index 13ea356e7a0e1743efb72ac432174e05ffbbf49e..9e477bad0a2f94514a3dbdf7fd34240fc6a071e9 100644
--- a/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php
+++ b/typo3/sysext/core/Classes/Configuration/Tca/TcaPreparation.php
@@ -47,6 +47,7 @@ readonly class TcaPreparation
         $tca = $this->configureEmailSoftReferences($tca);
         $tca = $this->configureLinkSoftReferences($tca);
         $tca = $this->configureSelectSingle($tca);
+        $tca = $this->configureRelationshipToOne($tca);
         return $tca;
     }
 
@@ -330,7 +331,12 @@ readonly class TcaPreparation
     }
 
     /**
-     * Add "'maxitems' => 1" to all "'type' => 'select'" column fields with "'renderType' => 'selectSingle'".
+     * Add "'relationship' for TCA type "select" fields, having "selectSingle" set as renderType and are
+     * pointing to a "foreign_table". Depending on further configuration, this will set the "relationship"
+     * to either "manyToMany" (in case "MM" is set) or to "manyToOne".
+     * Already defined "relationship" is not overwritten!
+     *
+     * This is mainly done to prevent checks on the renderType, which should be avoided.
      */
     protected function configureSelectSingle(array $tca): array
     {
@@ -339,11 +345,40 @@ readonly class TcaPreparation
                 continue;
             }
             foreach ($tableDefinition['columns'] as &$fieldConfig) {
-                if (($fieldConfig['config']['type'] ?? null) === 'select' && ($fieldConfig['config']['renderType'] ?? null) === 'selectSingle') {
-                    // Hard set/override: 'maxitems' to 1, since selectSingle - as the name suggests - only
-                    // allows a single item to be selected. Setting this config option allows to prevent
-                    // further checks for the renderType, which should be avoided.
-                    // @todo This should be solved by using a dedicated TCA type for selectSingle instead.
+                if (($fieldConfig['config']['type'] ?? null) !== 'select'
+                    || ($fieldConfig['config']['renderType'] ?? null) !== 'selectSingle'
+                    || !isset($fieldConfig['config']['foreign_table'])
+                    || isset($fieldConfig['config']['relationship'])
+                ) {
+                    continue;
+                }
+
+                if (isset($fieldConfig['config']['MM'])) {
+                    $fieldConfig['config']['relationship'] = 'manyToMany';
+                } else {
+                    $fieldConfig['config']['relationship'] = 'manyToOne';
+                }
+            }
+        }
+        return $tca;
+    }
+
+    /**
+     * Add "'maxitems' => 1" to all relation type column fields with 'relationship' set to 'oneToOne' or 'manyToOne'.
+     */
+    protected function configureRelationshipToOne(array $tca): array
+    {
+        foreach ($tca as &$tableDefinition) {
+            if (!is_array($tableDefinition['columns'] ?? null)) {
+                continue;
+            }
+            foreach ($tableDefinition['columns'] as &$fieldConfig) {
+                $type = $fieldConfig['config']['type'] ?? null;
+                if (in_array($type, ['select', 'inline', 'group', 'folder', 'file'], true)
+                    && in_array($fieldConfig['config']['relationship'] ?? null, ['oneToOne', 'manyToOne'], true)
+                ) {
+                    // Hard set/override: 'maxitems' to 1, since relationship [x]ToOne - as the name suggests -
+                    // only allows a single item to be selected.
                     $fieldConfig['config']['maxitems'] = 1;
                 }
             }
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index ee5da9a59ec84a5b0647699bea02c82799a0f8d6..6a7e569ca3d1bec0b0c4760a60733676914149f9 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -68,7 +68,6 @@ use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
 use TYPO3\CMS\Core\Schema\Field\FieldTranslationBehaviour;
 use TYPO3\CMS\Core\Schema\Field\FileFieldType;
 use TYPO3\CMS\Core\Schema\Field\InlineFieldType;
-use TYPO3\CMS\Core\Schema\RelationshipType;
 use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
 use TYPO3\CMS\Core\Service\OpcodeCacheService;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
@@ -6101,7 +6100,7 @@ class DataHandler
                 ) {
                     continue;
                 }
-                if (in_array($fieldType->getRelationshipType(), [RelationshipType::List, RelationshipType::ForeignField], true)) {
+                if ($fieldType->getRelationshipType()->isSingularRelationship()) {
                     $dbAnalysis = $this->createRelationHandlerInstance();
                     $dbAnalysis->start($value, $fieldConfig['foreign_table'], '', (int)$record['uid'], $table, $fieldConfig);
                     $dbAnalysis->undeleteRecord = true;
diff --git a/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php
new file mode 100644
index 0000000000000000000000000000000000000000..607e595acb3c68671a50526501ac309ec7023ce4
--- /dev/null
+++ b/typo3/sysext/core/Classes/DataHandling/RecordFieldTransformer.php
@@ -0,0 +1,248 @@
+<?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\DataHandling;
+
+use Doctrine\DBAL\Types\Type;
+use TYPO3\CMS\Core\Collection\LazyRecordCollection;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
+use TYPO3\CMS\Core\Domain\RawRecord;
+use TYPO3\CMS\Core\Domain\RecordFactory;
+use TYPO3\CMS\Core\Domain\RecordInterface;
+use TYPO3\CMS\Core\Domain\RecordPropertyClosure;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
+use TYPO3\CMS\Core\Resource\Collection\LazyFileReferenceCollection;
+use TYPO3\CMS\Core\Resource\Collection\LazyFolderCollection;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
+use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
+use TYPO3\CMS\Core\Schema\Field\FileFieldType;
+use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
+use TYPO3\CMS\Core\Schema\Field\RelationalFieldTypeInterface;
+use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
+use TYPO3\CMS\Core\Schema\FlexFormSchemaFactory;
+use TYPO3\CMS\Core\Schema\RelationMap;
+use TYPO3\CMS\Core\Service\FlexFormService;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Frontend\Typolink\TypolinkParameter;
+
+/**
+ * This generic mapper takes a field value of a record, and maps the value
+ * of a field with a specific type (TCA column type) to an expanded property
+ * (e.g. a type "file" field to a collection of FileReference objects).
+ *
+ * Common examples are \DateTimeImmutable objects for TCA fields of type=datetime.
+ *
+ * In general, this class is very inflexible, and not configurable,
+ * but we hope to extend this further in the future to add custom mappings.
+ *
+ * This class also calls the RelationResolver for any kind of resolved relations,
+ * but tries to handle most of the logic on its own when no lazy-loading is needed.
+ *
+ * About lazy-laading: For all relation types, which do not have a "toOne" relationship,
+ * a lazy collection is used. The relations in this collection are only resolved once
+ * they are accessed. For the "toOne" relations, a RecordPropertyClosure is used, which
+ * also initializes the corresponding record only when accessed. While a collection could
+ * be empty after being resolved, a single record might resolve to NULL, in case of an
+ * invalid relation value.
+ *
+ * @internal This class is not part of the TYPO3 Core API. It might get moved or changed.
+ */
+readonly class RecordFieldTransformer
+{
+    public function __construct(
+        protected RelationResolver $relationResolver,
+        protected ResourceFactory $resourceFactory,
+        protected FlexFormService $flexFormService,
+        protected FlexFormSchemaFactory $flexFormSchemaFactory,
+        protected LinkService $linkService,
+        protected TypoLinkCodecService $typoLinkCodecService,
+        protected ConnectionPool $connectionPool,
+        protected RecordIdentityMap $recordIdentityMap,
+    ) {}
+
+    public function transformField(FieldTypeInterface $fieldInformation, RawRecord $rawRecord, Context $context): mixed
+    {
+        $fieldValue = $rawRecord[$fieldInformation->getName()];
+
+        // type=file needs to be handled before RelationalFieldTypeInterface
+        if ($fieldInformation instanceof FileFieldType) {
+            if ($fieldInformation->getRelationshipType()->isToOne()) {
+                return new RecordPropertyClosure(
+                    function () use ($rawRecord, $fieldInformation, $context): ?FileReference {
+                        $fileReference = $this->relationResolver->resolveFileReferences($rawRecord, $fieldInformation, $context)[0] ?? null;
+                        if ($fileReference === null) {
+                            return null;
+                        }
+                        return new FileReference($fileReference->getProperties());
+                    }
+                );
+            }
+            return new LazyFileReferenceCollection($fieldValue, function () use ($rawRecord, $fieldInformation, $context): array {
+                return $this->relationResolver->resolveFileReferences($rawRecord, $fieldInformation, $context);
+            });
+        }
+
+        if ($fieldInformation instanceof RelationalFieldTypeInterface) {
+            /** @var RecordFactory $recordFactory */
+            // @todo This method is called by RecordFactory -> instantiating the factory here again shows, that those classes should actually be somehow belong together.
+            $recordFactory = GeneralUtility::makeInstance(RecordFactory::class);
+            if ($fieldInformation->getRelationshipType()->isToOne()) {
+                return new RecordPropertyClosure(
+                    function () use ($rawRecord, $fieldInformation, $context, $recordFactory): ?RecordInterface {
+                        $recordData = $this->relationResolver->resolve($rawRecord, $fieldInformation, $context)[0] ?? null;
+                        if ($recordData === null) {
+                            return null;
+                        }
+                        $dbTable = $recordData['table'];
+                        $row = $recordData['row'];
+                        // check RecordIdentityMap for already loaded records
+                        if ($this->recordIdentityMap->hasIdentifier($dbTable, (int)$row['uid'])) {
+                            $record = $this->recordIdentityMap->findByIdentifier($dbTable, (int)$row['uid']);
+                        } else {
+                            $record = $recordFactory->createResolvedRecordFromDatabaseRow($dbTable, $row, $context);
+                            $this->recordIdentityMap->add($record);
+                        }
+                        return $record;
+                    }
+                );
+            }
+            return new LazyRecordCollection(
+                $fieldValue,
+                function () use ($rawRecord, $fieldInformation, $context, $recordFactory): array {
+                    $relationalRecords = [];
+                    $recordData = $this->relationResolver->resolve($rawRecord, $fieldInformation, $context);
+                    foreach ($recordData as $singleRecordData) {
+                        $dbTable = $singleRecordData['table'];
+                        $row = $singleRecordData['row'];
+                        // check RecordIdentityMap for already loaded records
+                        if ($this->recordIdentityMap->hasIdentifier($dbTable, (int)$row['uid'])) {
+                            $relationalRecords[] = $this->recordIdentityMap->findByIdentifier($dbTable, (int)$row['uid']);
+                        } else {
+                            $record = $recordFactory->createResolvedRecordFromDatabaseRow($dbTable, $row, $context);
+                            $this->recordIdentityMap->add($record);
+                            $relationalRecords[] = $record;
+                        }
+                    }
+                    return $relationalRecords;
+                }
+            );
+        }
+
+        if ($fieldInformation->isType(TableColumnType::FOLDER)) {
+            if (in_array((string)($fieldInformation->getConfiguration()['relationship'] ?? ''), ['oneToOne', 'manyToOne'], true)) {
+                return new RecordPropertyClosure(
+                    function () use ($fieldValue): ?Folder {
+                        $folder = $this->resolveFoldersRecursive(GeneralUtility::trimExplode(',', (string)$fieldValue, true, 1))[0] ?? null;
+                        if ($folder === null) {
+                            return null;
+                        }
+                        return new Folder($folder->getStorage(), $folder->getIdentifier(), $folder->getName());
+                    }
+                );
+            }
+            return new LazyFolderCollection($fieldValue, function () use ($fieldValue): array {
+                return $this->resolveFoldersRecursive(GeneralUtility::trimExplode(',', (string)$fieldValue, true));
+            });
+        }
+
+        // Static select lists is transformed into an array of values
+        if ($fieldInformation instanceof StaticSelectFieldType) {
+            $selectForcedToSingle = (string)($fieldInformation->getConfiguration()['renderType'] ?? '') === 'selectSingle';
+            return $selectForcedToSingle ? $fieldValue : GeneralUtility::trimExplode(',', (string)$fieldValue, true);
+        }
+        if ($fieldInformation->isType(TableColumnType::FLEX)) {
+            /** @var FlexFormFieldType $fieldInformation */
+            return $this->processFlexForm($rawRecord, $fieldInformation, (string)$fieldValue, $context);
+        }
+        if ($fieldInformation->isType(TableColumnType::JSON)) {
+            return Type::getType('json')->convertToPHPValue((string)$fieldValue, $this->connectionPool->getConnectionForTable($rawRecord->getMainType())->getDatabasePlatform());
+        }
+        if ($fieldInformation instanceof DateTimeFieldType) {
+            return $fieldValue === null || (!$fieldInformation->isNullable() && $fieldValue === 0)
+                ? null
+                : new \DateTimeImmutable((MathUtility::canBeInterpretedAsInteger($fieldValue) ? '@' : '') . $fieldValue);
+        }
+        if ($fieldInformation->isType(TableColumnType::LINK)) {
+            return TypolinkParameter::createFromTypolinkParts($this->typoLinkCodecService->decode($fieldValue));
+        }
+        return $fieldValue;
+    }
+
+    /**
+     * @return Folder[]
+     */
+    protected function resolveFoldersRecursive(array $folders): array
+    {
+        $foldersRecursive = [];
+        foreach ($folders as $singleFolder) {
+            if ($singleFolder instanceof Folder === false) {
+                $singleFolder = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($singleFolder);
+            }
+            $foldersRecursive[] = $singleFolder;
+            array_push($foldersRecursive, ...$this->resolveFoldersRecursive($singleFolder->getSubfolders()));
+        }
+        return $foldersRecursive;
+    }
+
+    /**
+     * This method creates an array which contains all information which is valid from the
+     * selected Schema. Ideally, this should be "FlexRecord" objects, and also keep the original values.
+     * This functionality will likely change in the future.
+     */
+    protected function processFlexForm(RawRecord $record, FlexFormFieldType $fieldInformation, mixed $fieldValue, Context $context): array
+    {
+        $plainValues = $this->flexFormService->convertFlexFormContentToArray((string)$fieldValue);
+        $usedSchema = $this->flexFormSchemaFactory->getSchemaForRecord(
+            $record,
+            $fieldInformation,
+            // @todo: RelationMap does not work in FlexForm currently, as we do not have this information persisted somewhere
+            new RelationMap()
+        );
+
+        if ($usedSchema !== null) {
+            $resolvedValues = [];
+            // Flatten keys (because we receive settings[mysetting] and we want settings.mysetting)
+            $plainValues = ArrayUtility::flattenPlain($plainValues);
+            $recordFactory = GeneralUtility::makeInstance(RecordFactory::class);
+            foreach ($plainValues as $fieldName => $plainFieldValue) {
+                // That's a "fun" workaround: In order to allow to process e.g. "sDEF/header", we need
+                // to add this to the "rawRecord" (thus, we clone it), so it is within the array
+                // and then set "sDEF/header" even though this is not a DB field. Then we keep it in "$fieldName"
+                // which actually is the plain field name (in this case "header")
+                $fieldInformationOfFlexField = $usedSchema->getField($fieldName);
+                // No field given, we just skip the value, as it is not properly defined
+                if ($fieldInformationOfFlexField === null) {
+                    continue;
+                }
+                $rawRecordValues = array_replace($record->toArray(), [$fieldInformationOfFlexField->getName() => $plainFieldValue]);
+                $fakeRawRecordWithFlexField = $recordFactory->createRawRecord($record->getMainType(), $rawRecordValues);
+                $transformedValue = $this->transformField($fieldInformationOfFlexField, $fakeRawRecordWithFlexField, $context);
+                $resolvedValues[$fieldName] = $transformedValue;
+            }
+            return ArrayUtility::unflatten($resolvedValues);
+        }
+        return $plainValues;
+    }
+}
diff --git a/typo3/sysext/core/Classes/DataHandling/RelationResolver.php b/typo3/sysext/core/Classes/DataHandling/RelationResolver.php
new file mode 100644
index 0000000000000000000000000000000000000000..72fd390e6f41ff29fb53fb3881c3aed184951b21
--- /dev/null
+++ b/typo3/sysext/core/Classes/DataHandling/RelationResolver.php
@@ -0,0 +1,148 @@
+<?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\DataHandling;
+
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\Domain\Persistence\GreedyDatabaseBackend;
+use TYPO3\CMS\Core\Domain\RawRecord;
+use TYPO3\CMS\Core\Domain\Record;
+use TYPO3\CMS\Core\Domain\RecordInterface;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Finds relations for a RelationalFieldType field such as:
+ * - inline
+ * - select with foreign_table or MM
+ * - group with allowed or MM
+ * - category
+ * - file
+ *
+ * Files are handled differently with specific File / FileReference objects
+ * instead of general Record objects. Therefore, resolveFileReferences() is used.
+ *
+ * What it does to the outside world:
+ * - You have a record and a field with relations and you get a collection of the related raw DB records.
+ *
+ * What it hides:
+ * - How the DB queries are made.
+ *
+ * The result is usually wrapped in a Closure, so it is only called when needed,
+ * however this piece of code does not care about how it is used.
+ *
+ * @internal not part of public API, as this needs to be streamlined and proven.
+ */
+readonly class RelationResolver
+{
+    public function __construct(
+        #[Autowire(service: 'cache.runtime')]
+        protected FrontendInterface $runtimeCache,
+        protected ResourceFactory $resourceFactory,
+        protected GreedyDatabaseBackend $greedyDatabaseBackend
+    ) {}
+
+    /**
+     * This method currently returns an array with "table" and "row" pairs,
+     * but will probably return something else in the future.
+     *
+     * @return list<array{table: string, row: array<string, mixed>}>
+     */
+    public function resolve(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array
+    {
+        $sortedAndGroupedIds = $this->getGroupedRelationIds($record, $fieldInformation, $context);
+
+        $groupedByTable = [];
+        // group sorted items by table
+        foreach ($sortedAndGroupedIds as $item) {
+            $groupedByTable[$item['table']][] = (int)$item['id'];
+        }
+        $unorderedRows = $this->getRelationalRows($groupedByTable, $context);
+        $sortedRows = [];
+        // Sort the relation rows based on the field value
+        foreach ($sortedAndGroupedIds as $item) {
+            if (isset($unorderedRows[$item['table']][(int)$item['id']])) {
+                $sortedRows[] = $unorderedRows[$item['table']][(int)$item['id']];
+            }
+        }
+        return $sortedRows;
+    }
+
+    /**
+     * @return FileReference[]
+     */
+    public function resolveFileReferences(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array
+    {
+        $sortedAndGroupedIds = $this->getGroupedRelationIds($record, $fieldInformation, $context);
+        $sortedFileReferenceIds = array_map(static fn(array $item) => (int)$item['id'], $sortedAndGroupedIds);
+        $rows = $this->greedyDatabaseBackend->getRows('sys_file_reference', $sortedFileReferenceIds, $context);
+        $fileReferenceObjects = [];
+        foreach ($rows as $row) {
+            $fileReferenceObjects[] = $this->resourceFactory->createFileReferenceObject($row);
+        }
+        return $fileReferenceObjects;
+    }
+
+    /**
+     * We currently use the RelationHandler to resolve all records attached to a given field.
+     * @todo This will be replaced by querying the RefIndex directly in the future.
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    protected function getGroupedRelationIds(RecordInterface $record, FieldTypeInterface $fieldInformation, Context $context): array
+    {
+        $rawRecord = $record instanceof Record ? $record->getRawRecord() : $record;
+        $recordData = $rawRecord->toArray();
+        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
+        $relationHandler->setWorkspaceId($context->getPropertyFromAspect('workspace', 'id', 0));
+        if ($rawRecord instanceof RawRecord && $rawRecord->getComputedProperties()->getLocalizedUid() > 0) {
+            $relationHandler->initializeForField($record->getMainType(), $fieldInformation, $rawRecord->getComputedProperties()->getLocalizedUid(), $recordData[$fieldInformation->getName()] ?? null);
+        } else {
+            $relationHandler->initializeForField($record->getMainType(), $fieldInformation, $recordData, $recordData[$fieldInformation->getName()] ?? null);
+        }
+        $relationHandler->processDeletePlaceholder();
+        return $relationHandler->itemArray;
+    }
+
+    /**
+     * Find the relations relevant for this field. This could be multiple tables!
+     *
+     * Note: While $necessaryRelationsOfRequestedField is sorted, the result will be the plain unsorted database rows.
+     *
+     * @return array<string, array<int, array{table: string, row: array<string, mixed>}>>
+     */
+    protected function getRelationalRows(array $necessaryRelationsOfRequestedField, Context $context): array
+    {
+        $finalRows = [];
+        foreach ($necessaryRelationsOfRequestedField as $dbTable => $uids) {
+            // Let's loop over all tables, and fetch all records of the PIDs of the given UIDs in a greedy way
+            $rows = $this->greedyDatabaseBackend->getRows($dbTable, $uids, $context);
+            foreach ($rows as $row) {
+                $finalRows[$dbTable][(int)$row['uid']] = [
+                    'table' => $dbTable,
+                    'row' => $row,
+                ];
+            }
+        }
+        return $finalRows;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/ReferenceIndex.php b/typo3/sysext/core/Classes/Database/ReferenceIndex.php
index 6bc6ea034157813d2706eea1c8c3cd606fe1e46a..96be31409c71d978f19a87241e64a346120eb2b3 100644
--- a/typo3/sysext/core/Classes/Database/ReferenceIndex.php
+++ b/typo3/sysext/core/Classes/Database/ReferenceIndex.php
@@ -578,7 +578,7 @@ class ReferenceIndex
                 $itemArray = $fieldRelations['itemArray'];
                 if ($field->isType(TableColumnType::INLINE, TableColumnType::FILE)
                     && $field instanceof RelationalFieldTypeInterface
-                    && ($field->getRelationshipType() === RelationshipType::List || $field->getRelationshipType() === RelationshipType::ForeignField)
+                    && $field->getRelationshipType()->isSingularRelationship()
                 ) {
                     // RelationHandler does not return info on hidden, starttime, endtime for inline non-MM, yet. Add this now.
                     // @todo: Refactor RelationHandler / PlainDataResolver to (optionally?) return full child row record
diff --git a/typo3/sysext/core/Classes/Database/RelationHandler.php b/typo3/sysext/core/Classes/Database/RelationHandler.php
index 5f554a253d0faa37067aec9649175298001a0734..8a6837b2127209244c26f043a90ef30931959f5d 100644
--- a/typo3/sysext/core/Classes/Database/RelationHandler.php
+++ b/typo3/sysext/core/Classes/Database/RelationHandler.php
@@ -187,7 +187,7 @@ class RelationHandler
         string $tableName,
         array|FieldTypeInterface $fieldConfiguration,
         array|string|int|null $baseRecordOrUid,
-        string|array|null $currentValue = null
+        string|int|float|array|null $currentValue = null
     ): void {
         if ($fieldConfiguration instanceof FieldTypeInterface) {
             $fieldConfiguration = $fieldConfiguration->getConfiguration();
diff --git a/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php b/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8f8ad2aa440693958298c1f164aa6d1d54f9154
--- /dev/null
+++ b/typo3/sysext/core/Classes/Domain/Persistence/GreedyDatabaseBackend.php
@@ -0,0 +1,163 @@
+<?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\Domain\Persistence;
+
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\DateTimeAspect;
+use TYPO3\CMS\Core\Context\LanguageAspect;
+use TYPO3\CMS\Core\Context\UserAspect;
+use TYPO3\CMS\Core\Context\VisibilityAspect;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
+use TYPO3\CMS\Core\Domain\Repository\PageRepository;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Fetches all records of a single table of one / multiple PID(s)
+ * in one DB query, and stores them in a runtime cache.
+ *
+ * The records are neither grouped nor do we care about
+ * - sorting
+ * - limit / offset
+ * - or BE User permissions.
+ *
+ * It is "greedy" because it is meant to do simple query
+ * in a fast way, to reduce overlays on a "per record" basis.
+ *
+ * The records are fetched depending on
+ * - fe_group permissions
+ * - language (incl. overlays etc)
+ * - workspace
+ * - visibility restrictions (starttime / endtime / hidden / deleted)
+ *
+ * The class returns an array and does not handle objects,
+ * as it is very "lowlevel" and thus: internal.
+ *
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+readonly class GreedyDatabaseBackend
+{
+    public function __construct(
+        #[Autowire(service: 'cache.runtime')]
+        protected FrontendInterface $runtimeCache,
+        protected RecordAccessVoter $recordAccessVoter,
+        protected ConnectionPool $connectionPool
+    ) {}
+
+    public function getRows(string $tableName, array $uids, Context $context): array
+    {
+        // Check runtime cache first
+        // @todo: the runtime cache needs to be much more sophisticated
+        //        as it needs to understand that a DB query for ID 4,5 has been made, because
+        //        they have been fetched with IDs 2,3 in the call before.
+        $cacheIdentifier = $this->createRuntimeCacheIdentifier($tableName, $uids, $context);
+        if ($this->runtimeCache->has($cacheIdentifier)) {
+            $allRows = $this->runtimeCache->get($cacheIdentifier);
+        } else {
+            $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
+            $queryBuilder
+                ->select('*')
+                ->from($tableName);
+            // @todo: consider a context-based query restriction container here!
+
+            // Subselect is doing: give me the PID of the given UIDs
+            // So we can get a greedy query for all records of these PIDs
+            $queryBuilderForSubselect = $queryBuilder->getConnection()->createQueryBuilder();
+            $queryBuilderForSubselect
+                ->select('pid')
+                ->from($tableName)
+                ->where(
+                    $queryBuilderForSubselect->expr()->in(
+                        'uid',
+                        $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)
+                    )
+                );
+
+            // Inject the subselect in the WHERE part of the main query
+            $queryBuilder->where(
+                $queryBuilder->expr()->comparison(
+                    $queryBuilder->quoteIdentifier('pid'),
+                    'IN',
+                    '(' . $queryBuilderForSubselect->getSQL() . ')'
+                )
+            );
+
+            $allRows = $queryBuilder->executeQuery()->fetchAllAssociative();
+            $this->runtimeCache->set($cacheIdentifier, $allRows);
+        }
+
+        return $this->handleOverlays(
+            // Only use the records from the given UIDs
+            array_filter($allRows, static fn(array $row) => in_array((int)$row['uid'], $uids, true)),
+            $tableName,
+            $context
+        );
+    }
+
+    protected function handleOverlays(array $rows, string $dbTable, Context $context): array
+    {
+        $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
+        $finalRows = [];
+        foreach ($rows as $row) {
+            $pageRepository->versionOL($dbTable, $row);
+            if ($row === false) {
+                continue;
+            }
+            $row = $pageRepository->getLanguageOverlay($dbTable, $row);
+            if ($row === null) {
+                continue;
+            }
+            // Check fe_group, hidden, starttime, endtime etc.
+            if (!$this->recordAccessVoter->accessGranted($dbTable, $row, $context)) {
+                continue;
+            }
+            $finalRows[] = $row;
+        }
+        return $finalRows;
+    }
+
+    protected function createRuntimeCacheIdentifier(string $tableName, array $uids, Context $context): string
+    {
+        sort($uids);
+        $cacheIdentifier = 'greedy_database_backend_' . $tableName . '-' . md5(implode('_', $uids)) . '-';
+        $cacheIdentifier .= $context->getAspect('workspace')->getId() . '-';
+        /** @var LanguageAspect $languageAspect */
+        $languageAspect = $context->getAspect('language');
+        $cacheIdentifier .= $languageAspect->getId() . '-' . $languageAspect->getOverlayType() . '-' . md5(implode('_', $languageAspect->getFallbackChain())) . '-';
+        /** @var VisibilityAspect $visibilityAspect */
+        $visibilityAspect = $context->getAspect('visibility');
+        $cacheIdentifier .= $visibilityAspect->includeHiddenPages() ? '1' : '0';
+        $cacheIdentifier .= $visibilityAspect->includeHiddenContent() ? '1' : '0';
+        $cacheIdentifier .= $visibilityAspect->includeScheduledRecords() ? '1' : '0';
+        $cacheIdentifier .= $visibilityAspect->includeDeletedRecords() ? '1' : '0';
+
+        /** @var DateTimeAspect $dateAspect */
+        $dateAspect = $context->getAspect('date');
+        $cacheIdentifier .= '-' . $dateAspect->get('timestamp');
+
+        /** @var UserAspect $userAspect */
+        $userAspect = $context->getAspect('frontend.user');
+        $groupIds = $userAspect->getGroupIds();
+        $cacheIdentifier .= '-' . implode('_', $groupIds);
+
+        return $cacheIdentifier;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Domain/Record.php b/typo3/sysext/core/Classes/Domain/Record.php
index 8ea67f0b8d532022225afc70027a824955cd743e..d0f2ba385372f4fce026688217b2baba91978a1d 100644
--- a/typo3/sysext/core/Classes/Domain/Record.php
+++ b/typo3/sysext/core/Classes/Domain/Record.php
@@ -27,12 +27,12 @@ use TYPO3\CMS\Core\Domain\Record\VersionInfo;
  *
  * @internal not part of public API, as this needs to be streamlined and proven
  */
-readonly class Record implements \ArrayAccess, RecordInterface
+class Record implements \ArrayAccess, RecordInterface
 {
     public function __construct(
-        protected RawRecord $rawRecord,
+        protected readonly RawRecord $rawRecord,
         protected array $properties,
-        protected ?SystemProperties $systemProperties
+        protected readonly ?SystemProperties $systemProperties
     ) {}
 
     public function getUid(): int
@@ -62,6 +62,11 @@ readonly class Record implements \ArrayAccess, RecordInterface
 
     public function toArray(bool $includeSpecialProperties = false): array
     {
+        foreach ($this->properties as $key => $property) {
+            if ($property instanceof RecordPropertyClosure) {
+                $this->properties[$key] = $property->instantiate();
+            }
+        }
         if ($includeSpecialProperties) {
             return ['uid' => $this->getUid(), 'pid' => $this->getPid()] + $this->properties + ($this->systemProperties?->toArray() ?? []);
         }
@@ -86,7 +91,17 @@ readonly class Record implements \ArrayAccess, RecordInterface
     public function offsetGet(mixed $offset): mixed
     {
         if (isset($this->properties[$offset])) {
-            return $this->properties[$offset];
+            $property = $this->properties[$offset];
+            if ($property instanceof RecordPropertyClosure) {
+                $property = $property->instantiate();
+                $this->properties[$offset] = $property;
+            }
+            return $property;
+        }
+
+        if (in_array($offset, ['uid', 'pid'], true)) {
+            // Enable access of uid and pid via array access
+            return $this->rawRecord[$offset];
         }
 
         if ($this->getRecordType() === null && isset($this->rawRecord[$offset])) {
diff --git a/typo3/sysext/core/Classes/Domain/RecordFactory.php b/typo3/sysext/core/Classes/Domain/RecordFactory.php
index 8ae7404502b4345cfa7f24e6e4866c012ec5aefd..60c3e8b65f2d9f06ac5245f6459c7d3597b638a5 100644
--- a/typo3/sysext/core/Classes/Domain/RecordFactory.php
+++ b/typo3/sysext/core/Classes/Domain/RecordFactory.php
@@ -18,6 +18,8 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Domain;
 
 use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer;
 use TYPO3\CMS\Core\Domain\Record\ComputedProperties;
 use TYPO3\CMS\Core\Domain\Record\LanguageInfo;
 use TYPO3\CMS\Core\Domain\Record\SystemProperties;
@@ -32,10 +34,18 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
 
 /**
- * Creates record objects out of TCA-based database rows,
- * by evaluating the TCA columns, and splits everything which is not a declared column
- * for a TCA type. This is usually the case when a TCA table has a 'typeField' defined,
- * such as "pages", "be_users" and "tt_content".
+ * Creates record objects out of TCA-based database rows by evaluating the TCA columns and splitting
+ * everything which is not a declared column for a TCA type. This is usually the case when a TCA table
+ * has a 'typeField' defined, such as "pages", "be_users" and "tt_content".
+ *
+ * In addition, the RecordFactory can create "Resolved records" by utilizing the RecordFieldTransformer.
+ * A "Resolved record" is checked for the actual type (TCA field column type) and is then resolved to
+ * - a relation (records, files or folders - wrapped in collections)
+ * - an exploded list (e.g. static select)
+ * - a FlexForm field
+ * - a DateTime field.
+ *
+ * This means that the field value of a "Resolved Record" is expanded to the actual types (Date objects etc.)
  *
  * @internal not part of TYPO3 Core API yet.
  */
@@ -43,14 +53,77 @@ use TYPO3\CMS\Core\Versioning\VersionState;
 readonly class RecordFactory
 {
     public function __construct(
-        protected TcaSchemaFactory $schemaFactory
+        protected TcaSchemaFactory $schemaFactory,
+        protected RecordFieldTransformer $fieldTransformer,
     ) {}
 
     /**
-     * Takes a full database record (the whole row), and creates a Record object out of it, based on the type
-     * of the record.
+     * Takes a full database record (the whole row), and creates a Record object out of it,
+     * based on the type of the record.
+     *
+     * This method does not handle special expansion of fields.
+     * @todo Now unused - we might want to remove this again
      */
     public function createFromDatabaseRow(string $table, array $record): Record
+    {
+        $rawRecord = $this->createRawRecord($table, $record);
+        $schema = $this->schemaFactory->get($table);
+        $subSchema = null;
+        if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
+            $subSchema = $schema->getSubSchema($rawRecord->getRecordType());
+        }
+
+        // Only use the fields that are defined in the schema
+        $properties = [];
+        foreach ($record as $fieldName => $fieldValue) {
+            if ($subSchema && !$subSchema->hasField($fieldName)) {
+                continue;
+            }
+            $properties[$fieldName] = $fieldValue;
+        }
+        return $this->createRecord($rawRecord, $properties);
+    }
+
+    /**
+     * Create a "resolved" record. Resolved means that the fields will have
+     * their values resolved and extended. A typical use-case is resolving
+     * of related records, or using \DateTimeImmutable objects for datetime fields.
+     */
+    public function createResolvedRecordFromDatabaseRow(string $table, array $record, ?Context $context = null): Record
+    {
+        $context = $context ?? GeneralUtility::makeInstance(Context::class);
+        $properties = [];
+        $rawRecord = $this->createRawRecord($table, $record);
+        $schema = $this->schemaFactory->get($table);
+        $subSchema = null;
+        if ($schema->hasSubSchema($rawRecord->getRecordType() ?? '')) {
+            $subSchema = $schema->getSubSchema($rawRecord->getRecordType());
+        }
+
+        // Only use the fields that are defined in the schema
+        foreach ($record as $fieldName => $fieldValue) {
+            if ($subSchema) {
+                if (!$subSchema->hasField($fieldName)) {
+                    continue;
+                }
+                $schema = $subSchema;
+            } elseif (!$schema->hasField($fieldName)) {
+                continue;
+            }
+            $fieldInformation = $schema->getField($fieldName);
+            $properties[$fieldName] = $this->fieldTransformer->transformField(
+                $fieldInformation,
+                $rawRecord,
+                $context
+            );
+        }
+        return $this->createRecord($rawRecord, $properties);
+    }
+
+    /**
+     * Creates a raw record object from a table and a record array.
+     */
+    public function createRawRecord(string $table, array $record): RawRecord
     {
         if (!$this->schemaFactory->has($table)) {
             throw new \InvalidArgumentException(
@@ -60,30 +133,28 @@ readonly class RecordFactory
         }
         $schema = $this->schemaFactory->get($table);
         $fullType = $table;
-        $properties = [];
-        $subSchema = null;
-        $typeFieldDefinition = $schema->getSubSchemaDivisorField();
-        if ($typeFieldDefinition !== null) {
-            if (!isset($record[$typeFieldDefinition->getName()])) {
+        $subSchemaDivisorField = $schema->getSubSchemaDivisorField();
+        if ($subSchemaDivisorField !== null) {
+            $subSchemaDivisorFieldName = $subSchemaDivisorField->getName();
+            if (!isset($record[$subSchemaDivisorFieldName])) {
                 throw new \InvalidArgumentException(
-                    'Missing typeField "' . $typeFieldDefinition->getName() . '" in record of requested table "' . $table . '".',
+                    'Missing typeField "' . $subSchemaDivisorFieldName . '" in record of requested table "' . $table . '".',
                     1715267513,
                 );
             }
-            $recordType = (string)$record[$typeFieldDefinition->getName()];
+            $recordType = (string)$record[$subSchemaDivisorFieldName];
             $fullType .= '.' . $recordType;
-            $subSchema = $schema->getSubSchema($recordType);
         }
         $computedProperties = $this->extractComputedProperties($record);
-        $rawRecord = new RawRecord((int)$record['uid'], (int)$record['pid'], $record, $computedProperties, $fullType);
+        return new RawRecord((int)$record['uid'], (int)$record['pid'], $record, $computedProperties, $fullType);
+    }
 
-        // Only use the fields that are defined in the schema
-        foreach ($record as $fieldName => $fieldValue) {
-            if ($subSchema && !$subSchema->hasField($fieldName)) {
-                continue;
-            }
-            $properties[$fieldName] = $fieldValue;
-        }
+    /**
+     * Quick helper function in order to avoid duplicate code.
+     */
+    protected function createRecord(RawRecord $rawRecord, array $properties): Record
+    {
+        $schema = $this->schemaFactory->get($rawRecord->getMainType());
         [$properties, $systemProperties] = $this->extractSystemInformation(
             $schema,
             $rawRecord,
diff --git a/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php b/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d019bc17a5d3e178daa1744a5a80863ba0e3b1f
--- /dev/null
+++ b/typo3/sysext/core/Classes/Domain/RecordPropertyClosure.php
@@ -0,0 +1,35 @@
+<?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\Domain;
+
+/**
+ * Used to initialize a record once a property is accessed
+ *
+ * @internal
+ */
+final readonly class RecordPropertyClosure
+{
+    public function __construct(
+        private \Closure $instantiator
+    ) {}
+
+    public function instantiate(): ?object
+    {
+        return ($this->instantiator)();
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php b/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ebb724d246d08fb3215d262089897818de7f9ff
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Collection/LazyFileReferenceCollection.php
@@ -0,0 +1,96 @@
+<?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\Resource\Collection;
+
+use TYPO3\CMS\Core\Resource\FileReference;
+
+/**
+ * When first accessed, this class will initialize itself and find the file references
+ * for this record field.
+ *
+ * This class acts as a "Value holder", as it only fetches the related file references
+ * when needed.
+ *
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+class LazyFileReferenceCollection implements \IteratorAggregate, \ArrayAccess, \Countable
+{
+    /**
+     * @var FileReference[]|\Closure
+     */
+    private array|\Closure $items;
+
+    public function __construct(
+        private readonly mixed $fieldValue,
+        \Closure $initialization
+    ) {
+        $this->items = $initialization;
+    }
+
+    public function count(): int
+    {
+        $this->initialize();
+        return count($this->items);
+    }
+
+    private function initialize(): void
+    {
+        if ($this->items instanceof \Closure) {
+            $this->items = ($this->items)();
+        }
+    }
+
+    public function getIterator(): \Iterator
+    {
+        $this->initialize();
+        return new \ArrayIterator($this->items);
+    }
+
+    public function __toString(): string
+    {
+        return (string)$this->fieldValue;
+    }
+
+    public function offsetExists(mixed $offset): bool
+    {
+        $this->initialize();
+        return isset($this->items[$offset]);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        $this->initialize();
+        return $this->items[$offset] ?? null;
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void
+    {
+        if ($value instanceof FileReference === false) {
+            throw new \InvalidArgumentException(
+                'Modifying the file reference collection is only allowed by setting a value of type FileReference.',
+                1723188317
+            );
+        }
+        $this->items[$offset] = $value;
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        throw new \RuntimeException('Removing items from the file reference collection is not implemented.', 1723188318);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php b/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php
new file mode 100644
index 0000000000000000000000000000000000000000..6a56aa7d8b98f7b10e142192a170e01c9d3eb4a9
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Collection/LazyFolderCollection.php
@@ -0,0 +1,96 @@
+<?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\Resource\Collection;
+
+use TYPO3\CMS\Core\Resource\Folder;
+
+/**
+ * When first accessed, this class will initialize itself and find the folders
+ * for this record field.
+ *
+ * This class acts as a "Value holder", as it only fetches the related folders
+ * when needed.
+ *
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+class LazyFolderCollection implements \IteratorAggregate, \ArrayAccess, \Countable
+{
+    /**
+     * @var Folder[]|\Closure
+     */
+    private array|\Closure $items;
+
+    public function __construct(
+        private readonly mixed $fieldValue,
+        \Closure $initialization
+    ) {
+        $this->items = $initialization;
+    }
+
+    public function count(): int
+    {
+        $this->initialize();
+        return count($this->items);
+    }
+
+    private function initialize(): void
+    {
+        if ($this->items instanceof \Closure) {
+            $this->items = ($this->items)();
+        }
+    }
+
+    public function getIterator(): \Iterator
+    {
+        $this->initialize();
+        return new \ArrayIterator($this->items);
+    }
+
+    public function __toString(): string
+    {
+        return (string)$this->fieldValue;
+    }
+
+    public function offsetExists(mixed $offset): bool
+    {
+        $this->initialize();
+        return isset($this->items[$offset]);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        $this->initialize();
+        return $this->items[$offset] ?? null;
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void
+    {
+        if ($value instanceof Folder === false) {
+            throw new \InvalidArgumentException(
+                'Modifying the folder collection is only allowed by setting a value of type Folder.',
+                1724136133
+            );
+        }
+        $this->items[$offset] = $value;
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        throw new \RuntimeException('Removing items from the folder collection is not implemented.', 1724136134);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php
index 7c8e2f00bd87f3cf356dcb5a27d1e8fede42f492..e20639f638f52156a4529e59c89029cc6fd801a4 100644
--- a/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php
+++ b/typo3/sysext/core/Classes/Schema/Field/DateTimeFieldType.php
@@ -37,6 +37,11 @@ final readonly class DateTimeFieldType extends AbstractFieldType implements Fiel
         return $this->configuration['dbtype'] ?? null;
     }
 
+    public function isNullable(): bool
+    {
+        return (bool)($this->configuration['nullable'] ?? false);
+    }
+
     public static function __set_state(array $state): self
     {
         return new self(...$state);
diff --git a/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php b/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php
index d6b927de128fc96f95d850684f9294e2573a1c76..de796efa460f5d9b50c30c2cdfc4a34f6773d06f 100644
--- a/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php
+++ b/typo3/sysext/core/Classes/Schema/FieldTypeFactory.php
@@ -104,15 +104,16 @@ class FieldTypeFactory
                 // Build all schemata first
                 return $this->createFlexFormField($parentSchemaName ?? $schemaName, $fieldName, $configuration, $relationMap, $parentSchemaName ? $schemaName : null);
             case 'select':
-                if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::Static) {
-                    return $this->createFromTca(SelectRelationFieldType::class, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName, $parentFieldName ? $fieldName : null));
+                // In case type "select" is used without any relationship information, it's a static list
+                if (RelationshipType::fromTcaConfiguration($configuration) === RelationshipType::Undefined) {
+                    return $this->createFromTca(StaticSelectFieldType::class, $fieldName, $configuration);
                 }
-                return $this->createFromTca(StaticSelectFieldType::class, $fieldName, $configuration);
+                return $this->createFromTca(SelectRelationFieldType::class, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName));
             default:
                 if ($this->hasFieldType($fieldType)) {
                     $fieldTypeClass = $this->availableFieldTypes[$fieldType];
                     if (is_a($fieldTypeClass, RelationalFieldTypeInterface::class, true)) {
-                        return $this->createFromTca($fieldTypeClass, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName, $parentFieldName ? $fieldName : null));
+                        return $this->createFromTca($fieldTypeClass, $fieldName, $configuration, $relationMap->getActiveRelations($parentSchemaName ?? $schemaName, $parentFieldName ?? $fieldName));
                     }
                     return $this->createFromTca($fieldTypeClass, $fieldName, $configuration);
 
diff --git a/typo3/sysext/core/Classes/Schema/FlexFormSchema.php b/typo3/sysext/core/Classes/Schema/FlexFormSchema.php
index f176c1bee66f4b5bcfc582c3da7d47eb9d47159c..5c5a3268d3f82509669f476f21463eef6aebcf69 100644
--- a/typo3/sysext/core/Classes/Schema/FlexFormSchema.php
+++ b/typo3/sysext/core/Classes/Schema/FlexFormSchema.php
@@ -17,6 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Schema;
 
+use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
 use TYPO3\CMS\Core\Schema\Struct\FlexSheet;
 
 /**
@@ -40,6 +41,23 @@ final readonly class FlexFormSchema implements SchemaInterface
         return $this->structIdentifier;
     }
 
+    public function getField(string $fieldName, string $sheetName = 'sDEF'): ?FieldTypeInterface
+    {
+        if (!isset($this->sheets[$sheetName])) {
+            return null;
+        }
+        if ($this->sheets[$sheetName]->hasField($sheetName . '/' . $fieldName)) {
+            return $this->sheets[$sheetName]->getField($sheetName . '/' . $fieldName);
+        }
+        // Look for any kind of field that has the same name, regardless of the sheet name
+        foreach ($this->sheets as $sheetName => $sheet) {
+            if ($sheet->hasField($sheetName . '/' . $fieldName)) {
+                return $sheet->getField($sheetName . '/' . $fieldName);
+            }
+        }
+        return null;
+    }
+
     public static function __set_state(array $state): self
     {
         return new self(...$state);
diff --git a/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php b/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php
index 6c6fffe86d2a764a7f599d9c35eea6576b55c080..f1172295a90b65ac758314e4091d4153e5662929 100644
--- a/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php
+++ b/typo3/sysext/core/Classes/Schema/FlexFormSchemaFactory.php
@@ -19,12 +19,15 @@ namespace TYPO3\CMS\Core\Schema;
 
 use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Domain\RawRecord;
 use TYPO3\CMS\Core\Schema\Field\FieldCollection;
+use TYPO3\CMS\Core\Schema\Field\FlexFormFieldType;
 use TYPO3\CMS\Core\Schema\Struct\FlexSectionContainer;
 use TYPO3\CMS\Core\Schema\Struct\FlexSheet;
 
 /**
- * Parses all possibles schemas of all sheets of a field
+ * Parses all possibles schemas of all sheets of a field.
+ *
  * @internal This is an experimental implementation and might change until TYPO3 v13 LTS
  */
 #[Autoconfigure(public: true, shared: true)]
@@ -35,10 +38,44 @@ final readonly class FlexFormSchemaFactory
         protected FieldTypeFactory $fieldTypeFactory
     ) {}
 
+    /**
+     * Currently this mixes Schema and Record Information, and could be handled in a cleaner way, especially
+     * with the list_type resolving. This method signature will most likely change.
+     */
+    public function getSchemaForRecord(RawRecord $record, FlexFormFieldType $field, RelationMap $relationMap): ?FlexFormSchema
+    {
+        $allSchemata = $this->createSchemataForFlexField(
+            $field->getConfiguration(),
+            $record->getMainType(),
+            $field->getName(),
+            $relationMap
+        );
+
+        $structIdentifierParts = [
+            $record->getMainType(),
+            $field->getName(),
+        ];
+        $alternativeIdentifierParts = $structIdentifierParts;
+        if ($record->getRecordType()) {
+            $structIdentifierParts[] = $record->getRecordType();
+            $alternativeIdentifierParts[] = '*,' . $record->getRecordType();
+        }
+
+        $structIdentifier = implode('/', $structIdentifierParts);
+        $alternativeIdentifier = implode('/', $alternativeIdentifierParts);
+
+        foreach ($allSchemata as $schema) {
+            if ($schema->getName() === $structIdentifier || $schema->getName() === $alternativeIdentifier) {
+                return $schema;
+            }
+        }
+        return null;
+    }
+
     /**
      * @return FlexFormSchema[]
      */
-    public function createSchemataForFlexField(array $tcaConfig, string $tableName, string $fieldName, RelationMap $relationMap): array
+    protected function createSchemataForFlexField(array $tcaConfig, string $tableName, string $fieldName, RelationMap $relationMap): array
     {
         // Create schema for each possibility we have
         $flexSchemas = [];
diff --git a/typo3/sysext/core/Classes/Schema/RelationMap.php b/typo3/sysext/core/Classes/Schema/RelationMap.php
index 91c6146fe3234a1317ff7f6267a6a0278c85e265..bd094d2cb588712bb4ca3fb481250dd8e7b73f46 100644
--- a/typo3/sysext/core/Classes/Schema/RelationMap.php
+++ b/typo3/sysext/core/Classes/Schema/RelationMap.php
@@ -44,7 +44,7 @@ final class RelationMap
                         $fromFieldName,
                         $toTable,
                         $fieldConfig['MM'],
-                        $fieldConfig['MM_opposite'] ?? null,
+                        $fieldConfig['MM_opposite_field'] ?? null,
                         $flexPointer
                     );
                 } else {
@@ -58,10 +58,10 @@ final class RelationMap
                     $fromFieldName,
                     $fieldConfig['foreign_table'],
                     $fieldConfig['MM'],
-                    $fieldConfig['MM_opposite'] ?? null,
+                    $fieldConfig['MM_opposite_field'] ?? null,
                     $flexPointer
                 );
-            } elseif (isset($fieldConfig['foreign_table']) && isset($fieldConfig['foreign_field'])) {
+            } elseif (isset($fieldConfig['foreign_table'], $fieldConfig['foreign_field'])) {
                 $this->addActiveRelationWithField(
                     $fromTable,
                     $fromFieldName,
@@ -81,12 +81,12 @@ final class RelationMap
         }
     }
 
-    protected function addMMRelation(string $fromTable, string $fromField, string $toTable, string $mm, ?string $mmOpposite = null, ?string $flexPointer = null): void
+    protected function addMMRelation(string $fromTable, string $fromField, string $toTable, string $mm, ?string $mmOppositeField = null, ?string $flexPointer = null): void
     {
         $this->relations[$fromTable][$fromField][] = [
             'target' => $toTable,
             'mm' => $mm,
-            'mmOpposite' => $mmOpposite,
+            'mmOppositeField' => $mmOppositeField,
             'flexPointer' => $flexPointer,
         ];
     }
@@ -111,21 +111,14 @@ final class RelationMap
     /**
      * @return ActiveRelation[]
      */
-    public function getActiveRelations(string $tableName, ?string $fieldName = null, ?string $flexPointer = null): array
+    public function getActiveRelations(string $tableName, string $fieldName): array
     {
-        if ($fieldName === null) {
-            $relations = [];
-            foreach ($this->relations[$tableName] as $fieldRelations) {
-                $relations = array_merge($relations, $fieldRelations);
-            }
-            return array_map([$this, 'makeActiveRelation'], $relations);
-        }
         return array_map([$this, 'makeActiveRelation'], $this->relations[$tableName][$fieldName] ?? []);
     }
 
     protected function makeActiveRelation(array $relation): ActiveRelation
     {
-        return new ActiveRelation($relation['target'], $relation['targetField'] ?? null);
+        return new ActiveRelation($relation['mm'] ?? $relation['target'], $relation['mmOppositeField'] ?? $relation['targetField'] ?? null);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Schema/RelationshipType.php b/typo3/sysext/core/Classes/Schema/RelationshipType.php
index 604dc654154551d85155695d3120c747cc61db20..541112fe4273c7a76bcca9debd04d4e64c4b878d 100644
--- a/typo3/sysext/core/Classes/Schema/RelationshipType.php
+++ b/typo3/sysext/core/Classes/Schema/RelationshipType.php
@@ -22,9 +22,15 @@ namespace TYPO3\CMS\Core\Schema;
  */
 enum RelationshipType: string
 {
-    // The reference to the parent is stored in a pointer field in the child record
-    // Typically used when 'foreign_field' is set
-    case ForeignField = 'field';
+    // A direct relation, e.g. sys_file.metadata => sys_file_metadata
+    case OneToOne = '1:1';
+
+    // A record with active relations, e.g. inline elements blog_article.comments => comment. The reference
+    // to the left side is stored in a pointer field in the right side. Typically used when 'foreign_field' is set.
+    case OneToMany = '1:n';
+
+    // One item is selected on the active site, e.g. be_users.avatar => file, while file can be selected by any user
+    case ManyToOne = 'n:1';
 
     // Regular MM intermediate table is used to store data
     case ManyToMany = 'mm';
@@ -32,10 +38,7 @@ enum RelationshipType: string
     // An item list (separated by comma) is stored (like select type is doing)
     case List = 'list';
 
-    // Do we really need this?
-    // @todo check if we need this
-    case Static = 'static';
-
+    // Type can not be defined
     case Undefined = '';
 
     public static function fromTcaConfiguration(array $configuration): self
@@ -46,18 +49,36 @@ enum RelationshipType: string
         if (isset($configuration['MM'])) {
             return self::ManyToMany;
         }
+        if (isset($configuration['relationship'])) {
+            return match ($configuration['relationship']) {
+                'oneToOne' => self::OneToOne,
+                'oneToMany' => self::OneToMany,
+                'manyToOne' => self::ManyToOne,
+                default => throw new \UnexpectedValueException('Invalid relationship type: ' . $configuration['relationship'], 1724661829),
+            };
+        }
         if (isset($configuration['foreign_field'])) {
-            return self::ForeignField;
+            return self::OneToMany;
         }
         if (isset($configuration['foreign_table'])) {
+            // ManyToOne (as with `renderType=selectSingle`) is
+            // handled by `relationship` configuration above.
+            // See `TcaPreparation::configureSelectSingle()`.
             return self::List;
         }
         if (($configuration['type'] ?? '') === 'group') {
             return self::List;
         }
-        if (($configuration['type'] ?? '') === 'select') {
-            return self::Static;
-        }
         return self::Undefined;
     }
+
+    public function isToOne(): bool
+    {
+        return in_array($this, [self::OneToOne, self::ManyToOne], true);
+    }
+
+    public function isSingularRelationship(): bool
+    {
+        return in_array($this, [self::OneToOne, self::ManyToOne, self::OneToMany, self::List], true);
+    }
 }
diff --git a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php
index 68ae369174b4672b523efec1f19c29dc114d2501..e827e84629240681342f85b5f2636217656a70e0 100644
--- a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php
+++ b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_metadata.php
@@ -10,6 +10,6 @@ $GLOBALS['TCA']['sys_file_metadata']['columns']['l10n_parent']['config'] = [
     'type' => 'group',
     'allowed' => 'sys_file_metadata',
     'size' => 1,
-    'maxitems' => 1,
+    'relationship' => 'manyToOne',
     'default' => 0,
 ];
diff --git a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php
index fc11ef456ebeec413575d66bee652710cdee083a..3cc87e81ba2b6b61d391728a1398077ae114178b 100644
--- a/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php
+++ b/typo3/sysext/core/Configuration/TCA/Overrides/sys_file_reference.php
@@ -10,6 +10,6 @@ $GLOBALS['TCA']['sys_file_reference']['columns']['l10n_parent']['config'] = [
     'type' => 'group',
     'allowed' => 'sys_file_reference',
     'size' => 1,
-    'maxitems' => 1,
+    'relationship' => 'manyToOne',
     'default' => 0,
 ];
diff --git a/typo3/sysext/core/Configuration/TCA/be_users.php b/typo3/sysext/core/Configuration/TCA/be_users.php
index 8600f260a055020d7ec6cb91a4870db729ed0c69..e0589c14ef607164122bca0c3c099f570d144012 100644
--- a/typo3/sysext/core/Configuration/TCA/be_users.php
+++ b/typo3/sysext/core/Configuration/TCA/be_users.php
@@ -92,7 +92,7 @@ return [
             'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.avatar',
             'config' => [
                 'type' => 'file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'allowed' => 'common-image-types',
             ],
         ],
diff --git a/typo3/sysext/core/Configuration/TCA/pages.php b/typo3/sysext/core/Configuration/TCA/pages.php
index dc1214977b3fece5be9a3021917c79805f519177..32d4683a721e2f2f69206cb4cf3cd0b6f36131fc 100644
--- a/typo3/sysext/core/Configuration/TCA/pages.php
+++ b/typo3/sysext/core/Configuration/TCA/pages.php
@@ -392,7 +392,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'suggestOptions' => [
                     'default' => [
                         'additionalSearchFields' => 'nav_title, url',
@@ -442,7 +442,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'suggestOptions' => [
                     'default' => [
                         'additionalSearchFields' => 'nav_title, url',
@@ -462,7 +462,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'default' => 0,
             ],
         ],
diff --git a/typo3/sysext/core/Configuration/TCA/sys_file.php b/typo3/sysext/core/Configuration/TCA/sys_file.php
index 4389aa75cf2f3b1ea3b9397cfa00c37ba8ca697c..6f12651232c8e241f18dcd4f949b1983178f2375 100644
--- a/typo3/sysext/core/Configuration/TCA/sys_file.php
+++ b/typo3/sysext/core/Configuration/TCA/sys_file.php
@@ -118,7 +118,7 @@ return [
                 'foreign_field' => 'file',
                 'size' => 1,
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'oneToOne',
             ],
         ],
     ],
diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_collection.php b/typo3/sysext/core/Configuration/TCA/sys_file_collection.php
index 2157667599f440be69c52c351b4e372460d77ecb..bec526409de52ee1914b0beca5d48f3665a8f1d7 100644
--- a/typo3/sysext/core/Configuration/TCA/sys_file_collection.php
+++ b/typo3/sysext/core/Configuration/TCA/sys_file_collection.php
@@ -60,7 +60,7 @@ return [
             'config' => [
                 'type' => 'folder',
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'size' => 1,
             ],
         ],
diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php
index d54b4f1b8a1ae3089b303b9cab746f6394a5c396..5f494738b3f52080775b0c9b94da9a42a09912e3 100644
--- a/typo3/sysext/core/Configuration/TCA/sys_file_reference.php
+++ b/typo3/sysext/core/Configuration/TCA/sys_file_reference.php
@@ -32,7 +32,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'allowed' => 'sys_file',
                 'hideSuggest' => true,
             ],
diff --git a/typo3/sysext/core/Configuration/TCA/sys_filemounts.php b/typo3/sysext/core/Configuration/TCA/sys_filemounts.php
index e3d256de0469ff68df672eadf03ed006be700753..d2077482d1b3a0c80a5e93827601ddb04a758806 100644
--- a/typo3/sysext/core/Configuration/TCA/sys_filemounts.php
+++ b/typo3/sysext/core/Configuration/TCA/sys_filemounts.php
@@ -38,7 +38,7 @@ return [
             'config' => [
                 'type' => 'folder',
                 'required' => true,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'size' => 1,
             ],
         ],
diff --git a/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6ecb8d0861e9059c95e23fff7168bd2bbe6ed334
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-103581-AutomaticallyTransformTCAFieldValuesForRecordObjects.rst
@@ -0,0 +1,159 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-103581-1723209131:
+
+==============================================================================
+Feature: #103581 - Automatically transform TCA field values for Record objects
+==============================================================================
+
+See :issue:`103581`
+
+Description
+===========
+
+With :issue:`103783` the new :php:`Record` object has been introduced. It's an
+object representing a raw database record, based on TCA and is usually used in
+the frontend (via Fluid Templates), when fetching records with the
+:php:`RecordTransformationDataProcessor` (:typoscript:`record-transformation`)
+or by collecting content elements with the :php:`PageContentFetchingProcessor`
+(:typoscript:`page-content`).
+
+The Records API - introduced together with the Schema API in :issue:`104002` -
+now expands the :php:`Records`'s values for most common field types (known
+from the TCA Schema) from their raw database value into "rich-flavored" values,
+which might be :php:`Record`, :php:`FileReference`, :php:`Folder` or
+:php:`\DateTimeImmutable` objects.
+
+This works for the following "relation" TCA types:
+
+* :php:`category`
+* :php:`file`
+* :php:`folder`
+* :php:`group`
+* :php:`inline`
+* :php:`select` with :php:`MM` and :php:`foreign_table`
+
+In addition, the values of following TCA types are also resolved and
+expanded automatically:
+
+* :php:`datetime`
+* :php:`flex`
+* :php:`json`
+* :php:`link`
+* :php:`select` with a static list of entries
+
+Each of the fields receive a full-fledged resolved value, based on the field
+configuration from TCA.
+
+In case of relations (:php:`category`, :php:`group`, :php:`inline`,
+:php:`select` with :php:`MM` and :php:`foreign_table`), a collection
+(:php:`LazyRecordCollection`) of new :php:`Record` objects is attached as
+value. In case of :php:`file`, a collection (:php:`LazyFileReferenceCollection`)
+of :php:`FileReference` objects and in case of type :php:`folder`, a collection
+(:php:`LazyFolderCollection`) of :php:`Folder` objects are attached.
+
+.. note::
+
+    The relations are only resolved once they are accessed - also known as
+    "lazy loading". This allows for recursion and circular dependencies to be
+    managed automatically. It's therefore also possible that the collection
+    is actually empty.
+
+
+Example
+=======
+
+..  code-block:: html
+
+    <f:for each="{myContent.main.records}" as="record">
+        <f:for each="{record.image}" as="image">
+            <f:image image="{image}" />
+        </f:for>
+    </f:for>
+
+New TCA option `relationship`
+=============================
+
+In order to define cardinality on TCA level, the option :php:`relationship` is
+introduced for all "relation" TCA types listed above. If this option is set to
+:php:`oneToOne` or :php:`manyToOne`, then relations are resolved directly
+without being wrapped into collection objects. In case the relation can
+not be resolved, :php:`NULL` is returned.
+
+..  code-block:: php
+
+    'image' => [
+        'config' => [
+            'type' => 'file',
+            'relationship' => 'manyToOne',
+        ]
+    ]
+
+..  code-block:: html
+
+    <f:for each="{myContent.main.records}" as="record">
+        <f:image image="{record.image}" />
+    </f:for>
+
+.. note::
+
+    The TCA option :php:`maxitems` does not influence this behavior. This means
+    it is possible to have a :php:`oneToMany` relation with maximum one value
+    allowed. This way, overrides of this value will not break functionality.
+
+Field expansion
+===============
+
+For TCA type :php:`flex`, the corresponding FlexForm is resolved and therefore
+all values within this FlexForm are processed and expanded as well.
+
+Fields of TCA type :php:`datetime` will be transformed into a full
+:php:`\DateTimeInterface` object.
+
+Fields of TCA type :php:`json` will provide the decoded JSON value.
+
+Fields of TCA type :php:`link` will provide the :php:`TypolinkParameter` object,
+which is a object oriented representation of the corresponding TypoLink
+:typoscript:`parameter` configuration.
+
+Fields of TCA type :php:`select` without a :php:`relationship` will always provide
+an array of static values.
+
+.. note::
+
+    TYPO3 tries to automatically resolve the :php:`relationship` for type
+    :php:`select` fields, which use :php:`renderType=selectSingle` and
+    having a :php:`foreign_table` set. This means, in case no
+    :php:`relationship` has been defined yet, it is set to either :php:`manyToOne`
+    as the default or :php:`manyToMany` for fields with option :php:`MM`.
+
+Impact
+======
+
+When using :php:`Record` objects through the :php:`RecordFactory` API, e.g. via
+:php:`RecordTransformationDataProcessor` (:typoscript:`record-transformation`)
+or :php:`PageContentFetchingProcessor` (`page-content`), the corresponding
+:php:`Record` objects are now automatically processed and enriched.
+
+Those can not only be used in the frontend but also for Backend Previews in
+the page module. This is possible by configuring a Fluid Template via Page
+TSconfig to be used for the page preview rendering:
+
+
+..  code-block:: typoscript
+
+    mod.web_layout.tt_content.preview {
+        textmedia = EXT:site/Resources/Private/Templates/Preview/Textmedia.html
+    }
+
+In such template the newly available variable :html:`{record}` can be used to
+access the resolved field values. It is advised to migrate existing preview
+templates to this new object, as the former values will probably vanish in the
+next major version.
+
+By utilizing the new API for fetching records and content elements, the need
+for further data processors, e.g. :php:`FilesProcessor` (:typoscript:`files`),
+becomes superfluous since all relations are resolved automatically when
+requested.
+
+.. index:: Backend, FlexForm, Frontend, TCA, ext:core
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv
new file mode 100644
index 0000000000000000000000000000000000000000..6617cca53ba9cc4ebcb92410d467e7b5570faf69
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many.csv
@@ -0,0 +1,18 @@
+"sys_category"
+,"uid","pid","title","t3ver_oid","t3ver_wsid"
+,2,1,"Category 1",0,0
+,11,1,"Category 2",0,0
+,12,1,"Category 1 ws",2,1
+,13,1,"Category 2 ws",11,1
+"sys_category_record_mm",
+,"uid_local","uid_foreign","tablenames","fieldname","sorting"
+,2,260,"tt_content","typo3tests_contentelementb_categories_mm",1
+,11,260,"tt_content","typo3tests_contentelementb_categories_mm",0
+,12,263,"tt_content","typo3tests_contentelementb_categories_mm",2
+,13,263,"tt_content","typo3tests_contentelementb_categories_mm",1
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting"
+,"2c5724d09ec962d1e2f91f95b8c09aca","sys_category",2,"items",1,0,"tt_content",260,"typo3tests_contentelementb_categories_mm",1
+,"92c238672948a46154e0d92e554863d8","sys_category",11,"items",0,0,"tt_content",260,"typo3tests_contentelementb_categories_mm",2
+,"9639b2368a6e342fb05b6973bf0d4e9b","sys_category",12,"items",0,1,"tt_content",263,"typo3tests_contentelementb_categories_mm",2
+,"aca02beed00d6e3f32efe78945bc9d63","sys_category",13,"items",0,1,"tt_content",263,"typo3tests_contentelementb_categories_mm",1
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv
new file mode 100644
index 0000000000000000000000000000000000000000..066ff033877f43b0ffc3df4f746a82a49806e530
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_many_to_many_localized.csv
@@ -0,0 +1,18 @@
+"sys_category"
+,"uid","pid","title","sys_language_uid","l10n_parent"
+,2,1,"Category 1",0,0
+,11,1,"Category 2",0,0
+,12,1,"Category 1 translated",1,2
+,13,1,"Category 2 translated",1,11
+"sys_category_record_mm"
+,"uid_local","uid_foreign","tablenames","fieldname"
+,2,260,"tt_content","typo3tests_contentelementb_categories_mm"
+,11,260,"tt_content","typo3tests_contentelementb_categories_mm"
+,12,381,"tt_content","typo3tests_contentelementb_categories_mm"
+"tt_content"
+,"uid","pid","typo3tests_contentelementb_categories_mm","sys_language_uid","l18n_parent"
+,260,1,2,0,0
+,381,1,1,1,260
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting"
+,"f25cbb70441772d669dfc0052fc047f8","sys_category",2,"items",0,0,"tt_content",381,"typo3tests_contentelementb_categories_mm",1
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv
new file mode 100644
index 0000000000000000000000000000000000000000..64c2edbdcf6e3bc0361e8256dd046b135e9e2779
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_many.csv
@@ -0,0 +1,8 @@
+"sys_category"
+,"uid","pid","title"
+,2,1,"Category 1"
+,11,1,"Category 2"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting"
+,"41d028c84304886e4e2e2e8800297890","tt_content",260,"typo3tests_contentelementb_categories_1m",0,0,"sys_category",2,"",0
+,"448bd2548af7b53f1ce3da31153fae47","tt_content",260,"typo3tests_contentelementb_categories_1m",1,0,"sys_category",11,"",0
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv
new file mode 100644
index 0000000000000000000000000000000000000000..288cd9b72e02567be3f3d753faf2962d9eee151f
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/category_one_to_one.csv
@@ -0,0 +1,7 @@
+"sys_category"
+,"uid","pid","title"
+,2,1,"Category 1"
+,3,1,"Category 2"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid","ref_field","ref_sorting"
+,"1b0c3b8f46a1290b073881cd58cd12b1","tt_content",260,"typo3tests_contentelementb_categories_11",0,0,"sys_category",2,"",0
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv
new file mode 100644
index 0000000000000000000000000000000000000000..04a3306fe25a2b609d653592f3344a92b1f9f49e
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/circular_relation.csv
@@ -0,0 +1,6 @@
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_circular_relation","CType"
+,260,1,0,0,"260","typo3tests_contentelementb"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"83f48fc0812d5b6a2f26df9a21617edd","tt_content",260,"typo3tests_contentelementb_circular_relation",1,0,"tt_content",260
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv
new file mode 100644
index 0000000000000000000000000000000000000000..2f08c6e7427de0b7762e18f25d00ddc6f4407e0e
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections.csv
@@ -0,0 +1,16 @@
+"typo3tests_contentelementb_collection"
+,"uid","pid","fieldA","foreign_table_parent_uid","t3ver_oid","t3ver_wsid"
+,1,1,"lorem foo bar",260,0,0
+,2,1,"lorem foo bar 2",260,0,0
+,3,1,"lorem foo bar WS",260,1,1
+,4,1,"lorem foo bar 2 WS",260,2,1
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_collection","CType"
+,260,1,0,0,2,"typo3tests_contentelementb"
+,261,1,1,1,2,"typo3tests_contentelementb"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"4a0b3d6b5dbe434c6d0c4571a08a9ae4","tt_content",260,"typo3tests_contentelementb_collection",0,0,"typo3tests_contentelementb_collection",1
+,"92c238672948a46154e0d92e554863d8","tt_content",260,"typo3tests_contentelementb_collection",1,0,"typo3tests_contentelementb_collection",2
+,"5cff181b1eb5b95ef7d6459477d59b0f","tt_content",261,"typo3tests_contentelementb_collection",0,1,"typo3tests_contentelementb_collection",3
+,"0739f14b25a651517c31d1da0c41b751","tt_content",261,"typo3tests_contentelementb_collection",1,1,"typo3tests_contentelementb_collection",4
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv
new file mode 100644
index 0000000000000000000000000000000000000000..2d4fce240805d373cd0fb9aa39c5e563416573c4
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/collections_recursive.csv
@@ -0,0 +1,17 @@
+"typo3tests_contentelementb_collection_recursive"
+,"uid","pid","fieldA","collection_inner","foreign_table_parent_uid"
+,1,1,"lorem foo bar A",2,260
+,2,1,"lorem foo bar A2",0,260
+"collection_inner"
+,"uid","pid","fieldB","foreign_table_parent_uid"
+,1,1,"lorem foo bar B",1
+,2,1,"lorem foo bar B2",1
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_collection_recursive","CType"
+,1,1,0,0,2,"typo3tests_contentelementb"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"bf519eea10aaf2c85976a6622a7ed88b","tt_content",260,"typo3tests_contentelementb_collection_recursive",0,0,"typo3tests_contentelementb_collection_recursive",1
+,"e8e5539ae8691d1045d5beca3ca7b480","tt_content",260,"typo3tests_contentelementb_collection_recursive",1,0,"typo3tests_contentelementb_collection_recursive",2
+,"7142cce84a4d688bca9a30075a7aea7d","typo3tests_contentelementb_collection_recursive",1,"collection_inner",0,0,"collection_inner",1
+,"b40eeb513311e4f465d9455466cfd410","typo3tests_contentelementb_collection_recursive",1,"collection_inner",1,0,"collection_inner",2
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv
new file mode 100644
index 0000000000000000000000000000000000000000..f6877ae6819c9693a349b720faf39660b93f0ad2
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_reference_hidden.csv
@@ -0,0 +1,10 @@
+"pages"
+,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden"
+,1,1,"Page 1",0,0
+,2,1,"Page 2",0,0
+,3,1,"Page 1 ws",1,1
+,4,1,"Page 2 ws",2,1
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relation"
+,1,1,0,0,"1,2"
+,2,1,1,1,"1,2"
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv
new file mode 100644
index 0000000000000000000000000000000000000000..2c570abd555ca116d38987a4e9cbc69cb5c46ccd
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation.csv
@@ -0,0 +1,9 @@
+"pages"
+,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden"
+,1906,1,"Page 1",0,0,0
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relation"
+,260,1,0,0,"1"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"367a388427279ea5b203c37fbfb6b71c","tt_content",260,"typo3tests_contentelementb_pages_relation",0,0,"pages",1906
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv
new file mode 100644
index 0000000000000000000000000000000000000000..79f20322ba2c8a3f84689ba726e520c3928d9c71
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_mm.csv
@@ -0,0 +1,12 @@
+"pages"
+,"uid","pid","title"
+,3207,1,"Page 1"
+,3208,1,"Page 2"
+"block_pages_mm"
+,"uid_local","uid_foreign","sorting"
+,263,3207,1
+,263,3208,2
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"6b6b81550679e1783f44989f4fb19193","tt_content",263,"typo3tests_contentelementb_pages_mm",1,0,"pages",3208
+,"f59806ad6a2c2d46ebe07df3a49e384b","tt_content",263,"typo3tests_contentelementb_pages_mm",0,0,"pages",3207
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv
new file mode 100644
index 0000000000000000000000000000000000000000..a1c61ec87eaea35227bb399f3489222fff668efe
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_multiple.csv
@@ -0,0 +1,8 @@
+"pages"
+,"uid","pid","title"
+,1,1,"Page 1"
+,2,1,"Page 2"
+"tt_content"
+,"uid","pid","header"
+,1,1,"Content 1"
+,2,1,"Content 2"
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv
new file mode 100644
index 0000000000000000000000000000000000000000..1c0d5acea47efec602b73db6e25a23ccede37e9f
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relation_recursive.csv
@@ -0,0 +1,14 @@
+"test_record"
+,"uid","pid","title","record_collection"
+,1,1,"Record 1",1
+,2,1,"Record 2",1
+"record_collection"
+,"uid","pid","text","foreign_table_parent_uid"
+,3,1,"Collection 1",1
+,2,1,"Collection 2",2
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"c0900336e432959e26c567c08ee7c4a1","tt_content",260,"typo3tests_contentelementb_record_relation_recursive",0,0,"test_record",1
+,"978725a067d15a001f9770b19a70669d","tt_content",260,"typo3tests_contentelementb_record_relation_recursive",1,0,"test_record",2
+,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",3
+,"e933eac5cddb5176f87431f03c78e18c","test_record",2,"record_collection",0,0,"record_collection",2
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv
new file mode 100644
index 0000000000000000000000000000000000000000..db282d0b8e251a3eebd7bee217d4f6de085cd344
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/db_relations.csv
@@ -0,0 +1,17 @@
+"pages"
+,"uid","pid","title","t3ver_oid","t3ver_wsid","hidden"
+,1906,1,"Page 1",0,0,0
+,3389,1,"Page 2",0,0,0
+,3391,1,"Page 1 ws",1906,1,0
+,3393,1,"Page 2 ws",3389,1,0
+,5,1,"Page 3",0,0,1
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_pages_relations"
+,260,1,0,0,"1,2"
+,261,1,1,1,"1,2"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"367a388427279ea5b203c37fbfb6b71c","tt_content",260,"typo3tests_contentelementb_pages_relations",0,0,"pages",1906
+,"978725a067d15a001f9770b19a70669d","tt_content",260,"typo3tests_contentelementb_pages_relations",1,0,"pages",3389
+,"0ea740965dd56db56d18acf0bdb0bc15","tt_content",261,"typo3tests_contentelementb_pages_relations",0,1,"pages",1906
+,"85acf9c1c591795353d8faf79544faf5","tt_content",261,"typo3tests_contentelementb_pages_relations",1,1,"pages",3389
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv
new file mode 100644
index 0000000000000000000000000000000000000000..a16ed5251dc11b6f759fd89a6c3d7adb66961d28
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_reference.csv
@@ -0,0 +1,9 @@
+"sys_file_reference"
+,"uid","pid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid"
+,2161,1,1,260,"tt_content","image",1,0,0,0,0
+"sys_file",,,,,,,,,,,,,,,,,,,
+,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed"
+,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"01df57e9d30506cb7ec9cd2724b985eb","tt_content",260,"image",0,0,"sys_file_reference",2161
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv
new file mode 100644
index 0000000000000000000000000000000000000000..d8f56fab41317e30c1305a72536fe46f4a9ad368
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/file_references.csv
@@ -0,0 +1,17 @@
+"sys_file_reference"
+,"uid","pid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid"
+,2161,1,1,260,"tt_content","image",1,0,0,0,0
+,2162,1,1,260,"tt_content","image",2,0,0,0,0
+,2163,1,1,260,"tt_content","media",1,0,0,0,0
+,2164,1,1,260,"tt_content","media",2,0,0,0,0
+,2165,1,1,260,"tt_content","assets",1,0,0,0,0
+"sys_file",,,,,,,,,,,,,,,,,,,
+,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed"
+,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"01df57e9d30506cb7ec9cd2724b985eb","tt_content",260,"image",0,0,"sys_file_reference",2161
+,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"image",0,0,"sys_file_reference",2162
+,"bd99dcda6d449ad66d1be20499f31056","tt_content",260,"media",0,0,"sys_file_reference",2163
+,"3032602f676415aee093600bb17a3fd6","tt_content",260,"media",0,0,"sys_file_reference",2164
+,"2fe787ec710ceeb6470cb3da40c66f81","tt_content",260,"assets",0,0,"sys_file_reference",2165
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv
new file mode 100644
index 0000000000000000000000000000000000000000..7c90beb26910d8749d6539f14de51870a2437768
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/folder_files.csv
@@ -0,0 +1,3 @@
+"sys_file",,,,,,,,,,,,,,,,,,,
+,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed"
+,1,0,2,1,"/kasper-skarhoj1.jpg","jpg","image/jpeg","kasper-skarhoj1.jpg","05d8c6dda534a0b9e7023c3031e60e4b49c3da40",39037,1677330452,1658395918,0,0,"98576a90d58d51fcfe91c41f4c571fd600e1190e","42099b4af021e53fd8fd4e056c2568d7c2e3ffa8",0
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv
new file mode 100644
index 0000000000000000000000000000000000000000..e886ab33f337dd86d05598030d120fa741ffa706
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/foreign_table_select_multiple.csv
@@ -0,0 +1,14 @@
+"pages"
+,"uid","pid","title","doktype"
+,2,1,"Record Storage",254
+"test_record"
+,"uid","pid","title","record_collection"
+,1,2,"Record 1",1
+"record_collection"
+,"uid","pid","text","foreign_table_parent_uid",
+,1,2,"Collection 1",1
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",0,0,"test_record",1
+,"493dc67b404141c0ca3aa1b43409647c","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",1,0,"test_record",1
+,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",1
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv
new file mode 100644
index 0000000000000000000000000000000000000000..ffba73da9d395891bde60a276a89db0aba748ca0
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign.csv
@@ -0,0 +1,9 @@
+"test_record"
+,"uid","pid","title","record_collection"
+,1,1,"Record 1",0
+,2,1,"Record 2",0
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_foreign",0,0,"test_record",1
+,"1cac14244432b984e356dd3a7a1e6cbf","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",0,0,"test_record",1
+,"5e6ed22d795e28d598703907f1d7574f","tt_content",260,"typo3tests_contentelementb_select_foreign_multiple",1,0,"test_record",2
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv
new file mode 100644
index 0000000000000000000000000000000000000000..78bcc25383e5426b9a6d60558554beb82439eb51
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_native.csv
@@ -0,0 +1,4 @@
+"native_record"
+,"uid","pid","title",
+,1,1,"Record 1",0
+,2,1,"Record 2",0
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv
new file mode 100644
index 0000000000000000000000000000000000000000..fba12fb4178e3dc403faa4ae9bc381c3a9333f74
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_foreign_recursive.csv
@@ -0,0 +1,13 @@
+"test_record"
+,"uid","pid","title","record_collection"
+,1,1,"Record 1",1
+,2,1,"Record 2",1
+"record_collection"
+,"uid","pid","text","foreign_table_parent_uid",
+,3,1,"Collection 1",1
+,2,1,"Collection 2",2,
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_foreign",0,0,"test_record",1
+,"90e9e109fbcd06ef38430673574ca426","test_record",1,"record_collection",0,0,"record_collection",3
+,"e933eac5cddb5176f87431f03c78e18c","test_record",2,"record_collection",0,0,"record_collection",2
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv
new file mode 100644
index 0000000000000000000000000000000000000000..26bce422c6e821855e94a1ec775a70a3db4c95ea
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/DataSet/select_one_to_one.csv
@@ -0,0 +1,9 @@
+"test_record"
+,"uid","pid","title"
+,1,1,"Record 1"
+"tt_content"
+,"uid","pid","t3ver_oid","t3ver_wsid","typo3tests_contentelementb_select_one_to_one"
+,260,1,0,0,"1"
+"sys_refindex"
+,"hash","tablename","recuid","field","sorting","workspace","ref_table","ref_uid"
+,"f36643910656b91d77a0640a92dec881","tt_content",260,"typo3tests_contentelementb_select_one_to_one",0,0,"test_record",1
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/file.jpg b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/file.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/sub/subfile.jpg b/typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/sub/subfile.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..75f11ff40db7f1168369f120f9c7cf272a1ad7ad
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/RecordFieldTransformerTest.php
@@ -0,0 +1,1210 @@
+<?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\DataHandling;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Core\Collection\LazyRecordCollection;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\LanguageAspect;
+use TYPO3\CMS\Core\Context\WorkspaceAspect;
+use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer;
+use TYPO3\CMS\Core\Domain\Record;
+use TYPO3\CMS\Core\Domain\RecordFactory;
+use TYPO3\CMS\Core\Domain\RecordPropertyClosure;
+use TYPO3\CMS\Core\Resource\Collection\LazyFileReferenceCollection;
+use TYPO3\CMS\Core\Resource\Collection\LazyFolderCollection;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class RecordFieldTransformerTest extends FunctionalTestCase
+{
+    protected array $coreExtensionsToLoad = [
+        'workspaces',
+    ];
+
+    protected array $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver',
+    ];
+
+    protected array $pathsToProvideInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/DataHandling/Fixtures/TestFolder/' => 'fileadmin/',
+    ];
+
+    #[Test]
+    public function canResolveFileReference(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_reference.csv');
+        $dummyRecord = $this->createTestRecordObject(['image' => 1]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('image');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+
+        self::assertInstanceOf(FileReference::class, $result);
+        self::assertEquals('/kasper-skarhoj1.jpg', $result->getIdentifier());
+        self::assertIsArray($result->getProperties());
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(FileReference::class, $resolvedRecord['image']);
+        self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['image']->getIdentifier());
+    }
+
+    #[Test]
+    public function canHandleInvalidFileReference(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_reference.csv');
+        $dummyRecord = $this->createTestRecordObject(['uid' => 261, 'image' => 1]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('image');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertNull($result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertNull($resolvedRecord['image']);
+    }
+
+    #[Test]
+    public function canResolveFileReferences(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_references.csv');
+        $dummyRecord = $this->createTestRecordObject(['media' => 2]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('media');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        foreach ($result as $fileReference) {
+            self::assertInstanceOf(FileReference::class, $fileReference);
+            self::assertEquals('/kasper-skarhoj1.jpg', $fileReference->getIdentifier());
+        }
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyFileReferenceCollection::class, $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyFileReferenceCollection::class, $resolvedRecord['media']);
+        self::assertInstanceOf(FileReference::class, $resolvedRecord['media'][0]);
+        self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['media'][0]->getIdentifier());
+    }
+
+    #[Test]
+    public function resolvesSingleFileReferenceWithoutMaxItems(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/file_references.csv');
+        $dummyRecord = $this->createTestRecordObject(['assets' => 1]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('assets');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        foreach ($result as $fileReference) {
+            self::assertInstanceOf(FileReference::class, $fileReference);
+            self::assertEquals('/kasper-skarhoj1.jpg', $fileReference->getIdentifier());
+        }
+
+        self::assertCount(1, $result);
+        self::assertInstanceOf(LazyFileReferenceCollection::class, $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertCount(1, $resolvedRecord['assets']);
+        self::assertInstanceOf(LazyFileReferenceCollection::class, $resolvedRecord['assets']);
+        self::assertInstanceOf(FileReference::class, $resolvedRecord['assets'][0]);
+        self::assertEquals('/kasper-skarhoj1.jpg', $resolvedRecord['assets'][0]->getIdentifier());
+    }
+
+    #[Test]
+    public function canResolveFilesFromFolder(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv');
+        $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder' => '1:/']);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertInstanceOf(Folder::class, $result);
+        self::assertEquals('/', $result->getIdentifier());
+        self::assertEquals('1:/', $result->getCombinedIdentifier());
+        self::assertCount(1, $result->getFiles());
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(Folder::class, $resolvedRecord['typo3tests_contentelementb_folder']);
+        self::assertEquals('/', $resolvedRecord['typo3tests_contentelementb_folder']->getIdentifier());
+        self::assertEquals('1:/', $resolvedRecord['typo3tests_contentelementb_folder']->getCombinedIdentifier());
+        self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_folder']->getFiles());
+    }
+
+    #[Test]
+    public function canHandleInvalidFolder(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv');
+        $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder' => '']);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertNull($result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertNull($resolvedRecord['typo3tests_contentelementb_folder']);
+    }
+
+    #[Test]
+    public function canResolveFilesFromFolders(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/folder_files.csv');
+        $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_folder_recursive' => '1:/']);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_folder_recursive');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class),
+        );
+
+        foreach ($result as $folder) {
+            self::assertInstanceOf(Folder::class, $folder);
+            self::assertEquals(1, $folder->getStorage()->getUid());
+            self::assertCount(1, $folder->getFiles());
+        }
+
+        self::assertInstanceOf(LazyFolderCollection::class, $result);
+        self::assertCount(2, $result);
+        self::assertInstanceOf(Folder::class, $result[0]);
+        self::assertEquals('/sub/', $result[1]->getIdentifier());
+        self::assertEquals('1:/sub/', $result[1]->getCombinedIdentifier());
+        self::assertCount(1, $result[1]->getFiles());
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyFolderCollection::class, $resolvedRecord['typo3tests_contentelementb_folder_recursive']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_folder_recursive']);
+        self::assertInstanceOf(Folder::class, $resolvedRecord['typo3tests_contentelementb_folder_recursive'][0]);
+        self::assertEquals('/sub/', $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getIdentifier());
+        self::assertEquals('1:/sub/', $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getCombinedIdentifier());
+        self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_folder_recursive'][1]->getFiles());
+    }
+
+    #[Test]
+    public function canResolveCollections(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections.csv');
+        $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_collection' => 2]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('lorem foo bar', $result[0]['fieldA']);
+        self::assertSame('lorem foo bar 2', $result[1]['fieldA']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection']);
+        self::assertSame('lorem foo bar', $resolvedRecord['typo3tests_contentelementb_collection'][0]['fieldA']);
+        self::assertSame('lorem foo bar 2', $resolvedRecord['typo3tests_contentelementb_collection'][1]['fieldA']);
+    }
+
+    #[Test]
+    public function canResolveCollectionsRecursively(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections_recursive.csv');
+        $dummyRecord = $this->createTestRecordObject(['typo3tests_contentelementb_collection_recursive' => 2]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection_recursive');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('lorem foo bar A', $result[0]['fieldA']);
+        self::assertSame('lorem foo bar A2', $result[1]['fieldA']);
+        self::assertCount(2, $result[0]['collection_inner']);
+        self::assertSame('lorem foo bar B', $result[0]['collection_inner'][0]['fieldB']);
+        self::assertSame('lorem foo bar B2', $result[0]['collection_inner'][1]['fieldB']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection_recursive']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection_recursive']);
+        self::assertSame('lorem foo bar A', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['fieldA']);
+        self::assertSame('lorem foo bar A2', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][1]['fieldA']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner']);
+        self::assertSame('lorem foo bar B', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner'][0]['fieldB']);
+        self::assertSame('lorem foo bar B2', $resolvedRecord['typo3tests_contentelementb_collection_recursive'][0]['collection_inner'][1]['fieldB']);
+    }
+
+    #[Test]
+    public function canResolveCollectionsInWorkspaces(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/collections.csv');
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
+        $this->setUpBackendUser(1);
+        $this->setWorkspaceId(1);
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_collection' => 2,
+            't3ver_oid' => 260,
+            't3ver_wsid' => 1,
+            '_ORIG_uid' => 261,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_collection');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('lorem foo bar WS', $result[0]['fieldA']);
+        self::assertSame('lorem foo bar 2 WS', $result[1]['fieldA']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_collection']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_collection']);
+        self::assertSame('lorem foo bar WS', $resolvedRecord['typo3tests_contentelementb_collection'][0]['fieldA']);
+        self::assertSame('lorem foo bar 2 WS', $resolvedRecord['typo3tests_contentelementb_collection'][1]['fieldA']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesManyToMany(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many.csv');
+        $dummyRecord = $this->createTestRecordObject();
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertSame('Category 1', $result[0]['title']);
+        self::assertSame('Category 2', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']);
+        self::assertSame('Category 2', $resolvedRecord['typo3tests_contentelementb_categories_mm'][1]['title']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesManyToManyInWorkspaces(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many.csv');
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
+        $this->setUpBackendUser(1);
+        $this->setWorkspaceId(1);
+
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_categories_mm' => 2,
+            'sys_language_uid' => 1,
+            't3ver_oid' => 260,
+            't3ver_wsid' => 1,
+            '_ORIG_uid' => 261,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        // @todo: this should be the other way around, but currently RelationResolver cannot handle different sorting in WS
+        self::assertSame('Category 2 ws', $result[1]['title']);
+        self::assertSame('Category 1 ws', $result[0]['title']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesManyToManyLocalizedOverlaysOff(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many_localized.csv');
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
+        $context = $this->get(Context::class);
+        $context->setAspect('language', new LanguageAspect(1, 1, LanguageAspect::OVERLAYS_OFF));
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_categories_mm' => 2,
+            'sys_language_uid' => 1,
+            'l18n_parent' => 260,
+            '_LOCALIZED_UID' => 381,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $context
+        );
+
+        self::assertCount(1, $result);
+        self::assertSame('Category 1 translated', $result[0]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', array_replace($dummyRecord->toArray(), ['uid' => 381]), $context);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertSame('Category 1 translated', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesManyToManyLocalizedOverlaysOn(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_many_to_many_localized.csv');
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
+        $context = $this->get(Context::class);
+        $context->setAspect('language', new LanguageAspect(1, 1, LanguageAspect::OVERLAYS_ON));
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_categories_mm' => 2,
+            'sys_language_uid' => 1,
+            'l18n_parent' => 260,
+            '_LOCALIZED_UID' => 381,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_mm');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $context
+        );
+
+        self::assertCount(1, $result);
+        self::assertSame('Category 1 translated', $result[0]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', array_replace($dummyRecord->toArray(), ['uid' => 381]), $context);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertCount(1, $resolvedRecord['typo3tests_contentelementb_categories_mm']);
+        self::assertSame('Category 1 translated', $resolvedRecord['typo3tests_contentelementb_categories_mm'][0]['title']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesOneToOne(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_one_to_one.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_categories_11' => 2,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_11');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertInstanceOf(Record::class, $result);
+        self::assertSame(2, $result->getUid());
+        self::assertSame('Category 1', $result['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertInstanceOf(Record::class, $resolvedRecord['typo3tests_contentelementb_categories_11']);
+        self::assertSame(2, $resolvedRecord['typo3tests_contentelementb_categories_11']['uid']);
+        self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_11']['title']);
+    }
+
+    #[Test]
+    public function canResolveCategoriesOneToMany(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/category_one_to_many.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_categories_1m' => '2,11',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_categories_1m');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('Category 1', $result[0]['title']);
+        self::assertSame('Category 2', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertCount(2, $resolvedRecord['typo3tests_contentelementb_categories_1m']);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRecord['typo3tests_contentelementb_categories_1m']);
+        self::assertSame('Category 1', $resolvedRecord['typo3tests_contentelementb_categories_1m'][0]['title']);
+        self::assertSame('Category 2', $resolvedRecord['typo3tests_contentelementb_categories_1m'][1]['title']);
+    }
+
+    #[Test]
+    public function canResolveDbRelation(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_pages_relation' => '1906',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_pages_relation');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertInstanceOf(Record::class, $result);
+        self::assertSame(1906, $result->getUid());
+        self::assertSame(1906, $result['uid']);
+        self::assertSame('Page 1', $result['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relation'];
+        self::assertInstanceOf(Record::class, $resolvedRelation);
+        self::assertSame(1906, $resolvedRelation->getUid());
+        self::assertSame(1906, $resolvedRelation['uid']);
+        self::assertSame('Page 1', $resolvedRelation['title']);
+    }
+
+    #[Test]
+    public function canResolveDbRelations(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relations.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_pages_relations' => '1906,3389',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_relations');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('Page 1', $result[0]['title']);
+        self::assertSame('Page 2', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relations'];
+        self::assertCount(2, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertSame(1906, $resolvedRelation[0]->getUid());
+        self::assertSame(1906, $resolvedRelation[0]['uid']);
+        self::assertSame('Page 1', $resolvedRelation[0]['title']);
+        self::assertSame('Page 2', $resolvedRelation[1]['title']);
+    }
+
+    #[Test]
+    public function canResolveCircularRelation(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/circular_relation.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_circular_relation' => '260',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_circular_relation');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(1, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame(260, $result[0]->getUid());
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_circular_relation'];
+        self::assertCount(1, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame(260, $resolvedRelation[0]->getUid());
+        self::assertSame(260, $resolvedRelation[0]['uid']);
+    }
+
+    #[Test]
+    public function canResolveDbRelationRecursive(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_recursive.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_record_relation_recursive' => '1,2',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_record_relation_recursive');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertCount(2, $result);
+        self::assertSame('Record 1', $result[0]['title']);
+        self::assertSame('Record 2', $result[1]['title']);
+        self::assertCount(1, $result[0]['record_collection']);
+        self::assertCount(1, $result[1]['record_collection']);
+        self::assertSame('Collection 1', $result[0]['record_collection'][0]['text']);
+        self::assertSame('Collection 2', $result[1]['record_collection'][0]['text']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_record_relation_recursive'];
+        self::assertCount(2, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertSame('Record 1', $resolvedRelation[0]['title']);
+        self::assertSame('Record 2', $resolvedRelation[1]['title']);
+        self::assertCount(1, $resolvedRelation[0]['record_collection']);
+        self::assertCount(1, $resolvedRelation[1]['record_collection']);
+        self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']);
+        self::assertSame('Collection 2', $resolvedRelation[1]['record_collection'][0]['text']);
+    }
+
+    #[Test]
+    public function canResolveDbRelationsInWorkspaces(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relations.csv');
+        $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
+        $this->setUpBackendUser(1);
+        $this->setWorkspaceId(1);
+        $dummyRecord = $this->createTestRecordObject([
+            'uid' => 260,
+            't3ver_oid' => 260,
+            't3ver_wsid' => 1,
+            '_ORIG_uid' => 261,
+            'typo3tests_contentelementb_pages_relations' => '1906,3389',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_relations');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('Page 1 ws', $result[0]['title']);
+        self::assertSame('Page 2 ws', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_relations'];
+        self::assertCount(2, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertSame('Page 1 ws', $resolvedRelation[0]['title']);
+        self::assertSame('Page 2 ws', $resolvedRelation[1]['title']);
+    }
+
+    #[Test]
+    public function canResolveMultipleDbRelations(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_multiple.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_pages_content_relation' => 'pages_1,pages_2,tt_content_1,tt_content_2',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_content_relation');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+        self::assertCount(4, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('Page 1', $result[0]['title']);
+        self::assertSame('Page 2', $result[1]['title']);
+        self::assertSame('Content 1', $result[2]['header']);
+        self::assertSame('Content 2', $result[3]['header']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_content_relation'];
+        self::assertCount(4, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertSame('Page 1', $resolvedRelation[0]['title']);
+        self::assertSame('Page 2', $resolvedRelation[1]['title']);
+        self::assertSame('Content 1', $resolvedRelation[2]['header']);
+        self::assertSame('Content 2', $resolvedRelation[3]['header']);
+    }
+
+    #[Test]
+    public function canResolveDbRelationsMM(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/db_relation_mm.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'uid' => 263,
+            'typo3tests_contentelementb_pages_mm' => 2,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_pages_mm');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertSame('Page 1', $result[0]['title']);
+        self::assertSame('Page 2', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_pages_mm'];
+        self::assertCount(2, $resolvedRelation);
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertSame('Page 1', $resolvedRelation[0]['title']);
+        self::assertSame('Page 2', $resolvedRelation[1]['title']);
+    }
+
+    public static function multipleItemsAsArrayConversionDataProvider(): \Generator
+    {
+        yield 'selectCheckboxFormat' => [
+            'fieldName' => 'typo3tests_contentelementb_select_checkbox',
+            'input' => '1,2,3',
+            'expected' => ['1', '2', '3'],
+        ];
+        yield 'selectSingleBoxCommaList' => [
+            'fieldName' => 'typo3tests_contentelementb_select_single_box',
+            'input' => '1,2,3',
+            'expected' => ['1', '2', '3'],
+        ];
+        yield 'selectMultipleSideBySideCommaList' => [
+            'fieldName' => 'typo3tests_contentelementb_select_multiple',
+            'input' => '1,2,3',
+            'expected' => ['1', '2', '3'],
+        ];
+        yield 'selectMultipleSideBySideWithOneValue' => [
+            'fieldName' => 'typo3tests_contentelementb_select_multiple',
+            'input' => '1',
+            'expected' => ['1'],
+        ];
+        yield 'selectMultipleSideBySideWithEmptyOneValue' => [
+            'fieldName' => 'typo3tests_contentelementb_select_multiple',
+            'input' => '',
+            'expected' => [],
+        ];
+        yield 'canResolveJson' => [
+            'fieldName' => 'typo3tests_contentelementb_json',
+            'input' => '{"foo": "bar"}',
+            'expected' => ['foo' => 'bar'],
+        ];
+    }
+
+    #[Test]
+    #[DataProvider('multipleItemsAsArrayConversionDataProvider')]
+    public function multipleItemsAsArrayConversionConvertedToArray(string $fieldName, string|int $input, array $expected): void
+    {
+        $dummyRecord = $this->createTestRecordObject([
+            $fieldName => $input,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField($fieldName);
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertSame($expected, $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertSame($expected, $resolvedRecord[$fieldName]);
+    }
+
+    public static function canConvertDateTimeDataProvider(): \Generator
+    {
+        yield 'canResolveDatetime' => [
+            'fieldName' => 'typo3tests_contentelementb_datetime',
+            'input' => 30,
+            'expected' => '1970-01-01T00:00:30+00:00',
+        ];
+        yield 'canResolveDatetimeZero' => [
+            'fieldName' => 'typo3tests_contentelementb_datetime',
+            'input' => 0,
+            'expected' => null,
+        ];
+        yield 'canResolveDatetimeNull' => [
+            'fieldName' => 'typo3tests_contentelementb_datetime_nullable',
+            'input' => 30,
+            'expected' => '1970-01-01T00:00:30+00:00',
+        ];
+        yield 'canResolveDatetimeNullZero' => [
+            'fieldName' => 'typo3tests_contentelementb_datetime_nullable',
+            'input' => 0,
+            'expected' => '1970-01-01T00:00:00+00:00',
+        ];
+        yield 'canResolveDatetimeNullNull' => [
+            'fieldName' => 'typo3tests_contentelementb_datetime_nullable',
+            'input' => null,
+            'expected' => null,
+        ];
+    }
+
+    #[Test]
+    #[DataProvider('canConvertDateTimeDataProvider')]
+    public function canConvertDateTime(string $fieldName, ?int $input, ?string $expected): void
+    {
+        $dummyRecord = $this->createTestRecordObject([
+            $fieldName => $input,
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField($fieldName);
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertSame($expected, $result?->format('c'));
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertSame($expected, $resolvedRecord[$fieldName]?->format('c'));
+    }
+
+    #[Test]
+    public function canResolveSelectSingle(): void
+    {
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_single' => '1',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_single');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertSame('1', $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertSame('1', $resolvedRecord['typo3tests_contentelementb_select_single']);
+    }
+
+    #[Test]
+    public function canResolveSelectRelationOneToOne(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_one_to_one.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_one_to_one' => '1',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_one_to_one');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertInstanceOf(Record::class, $result);
+        self::assertSame('Record 1', $result['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_one_to_one'];
+        self::assertInstanceOf(Record::class, $resolvedRelation);
+        self::assertSame('Record 1', $resolvedRelation['title']);
+    }
+
+    /**
+     * Special case where NO Collection is returned, since the field has relationship="oneToOne"
+     */
+    #[Test]
+    public function canResolveSelectForeignTableSingle(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign_native' => '1',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_foreign_native');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertInstanceOf(Record::class, $result);
+        self::assertSame('Record 1', $result['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedField = $resolvedRecord['typo3tests_contentelementb_select_foreign_native'];
+        self::assertInstanceOf(Record::class, $resolvedField);
+        self::assertSame('Record 1', $resolvedField['title']);
+    }
+
+    /**
+     * Special case where null is returned since the relation is invalid and the field has relationship="oneToOne"
+     */
+    #[Test]
+    public function resolveSelectForeignTableSingleToNullRecord(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign_native' => '123',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getSubSchema('typo3tests_contentelementb')->getField('typo3tests_contentelementb_select_foreign_native');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $propertyClosure = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(RecordPropertyClosure::class, $propertyClosure);
+        $result = $propertyClosure->instantiate();
+        self::assertNull($result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        self::assertNull($resolvedRecord['typo3tests_contentelementb_select_foreign_native']);
+    }
+
+    /**
+     * Special case where a an empty Collection is returned, since the relation is invalid
+     */
+    #[Test]
+    public function resolveSelectForeignTableToEmptyCollection(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign' => '123',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertCount(0, $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign'];
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertCount(0, $resolvedRelation);
+    }
+
+    /**
+     * Special case where an empty Collection is returned, since the relation is invalid
+     */
+    #[Test]
+    public function resolveSelectForeignTableMultipleToEmptyCollection(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/foreign_table_select_multiple.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign_multiple' => '123',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        self::assertCount(0, $result);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple'];
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertCount(0, $resolvedRelation);
+    }
+
+    #[Test]
+    public function canResolveSelectForeignTableMultiple(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign_multiple' => '1,2',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertSame('Record 1', $result[0]['title']);
+        self::assertSame('Record 2', $result[1]['title']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple'];
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertCount(2, $resolvedRelation);
+        self::assertSame('Record 1', $resolvedRelation[0]['title']);
+        self::assertSame('Record 2', $resolvedRelation[1]['title']);
+    }
+
+    #[Test]
+    public function canResolveSelectForeignTableMultipleAndSame(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/foreign_table_select_multiple.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign_multiple' => '1,1',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign_multiple');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(2, $result);
+        self::assertSame('Record 1', $result[0]['title']);
+        self::assertSame('Collection 1', $result[0]['record_collection'][0]['text']);
+        self::assertSame('Record 1', $result[1]['title']);
+        self::assertSame('Collection 1', $result[1]['record_collection'][0]['text']);
+        self::assertSame($result[0], $result[1]);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign_multiple'];
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertCount(2, $resolvedRelation);
+        self::assertSame('Record 1', $resolvedRelation[0]['title']);
+        self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']);
+        self::assertSame('Record 1', $resolvedRelation[1]['title']);
+        self::assertSame('Collection 1', $resolvedRelation[1]['record_collection'][0]['text']);
+        self::assertSame($resolvedRelation[0], $resolvedRelation[1]);
+    }
+
+    #[Test]
+    public function canResolveSelectForeignTableRecursive(): void
+    {
+        $this->importCSVDataSet(__DIR__ . '/Fixtures/DataSet/select_foreign_recursive.csv');
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_select_foreign' => '1',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_select_foreign');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertCount(1, $result);
+        self::assertInstanceOf(LazyRecordCollection::class, $result);
+        $result = $result[0];
+        self::assertSame('Record 1', $result['title']);
+        self::assertCount(1, $result['record_collection']);
+        self::assertSame('Collection 1', $result['record_collection'][0]['text']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_select_foreign'];
+        self::assertInstanceOf(LazyRecordCollection::class, $resolvedRelation);
+        self::assertCount(1, $resolvedRelation);
+        self::assertSame('Record 1', $resolvedRelation[0]['title']);
+        self::assertCount(1, $resolvedRelation[0]['record_collection']);
+        self::assertSame('Collection 1', $resolvedRelation[0]['record_collection'][0]['text']);
+    }
+
+    #[Test]
+    public function canResolveFlexForm(): void
+    {
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_flexfield' => '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<T3FlexForms>
+    <data>
+        <sheet index="sDEF">
+            <language index="lDEF">
+                <field index="header">
+                    <value index="vDEF">Header in Flex</value>
+                </field>
+                <field index="textarea">
+                    <value index="vDEF">Text in Flex</value>
+                </field>
+            </language>
+        </sheet>
+    </data>
+</T3FlexForms>',
+        ]);
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_flexfield');
+
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertIsArray($result);
+        self::assertSame('Header in Flex', $result['header']);
+        self::assertSame('Text in Flex', $result['textarea']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_flexfield'];
+        self::assertIsArray($resolvedRelation);
+        self::assertSame('Header in Flex', $resolvedRelation['header']);
+        self::assertSame('Text in Flex', $resolvedRelation['textarea']);
+    }
+
+    #[Test]
+    public function canResolveFlexFormWithSheetsOtherThanDefault(): void
+    {
+        $dummyRecord = $this->createTestRecordObject([
+            'typo3tests_contentelementb_flexfield' => '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<T3FlexForms>
+    <data>
+        <sheet index="sheet1">
+            <language index="lDEF">
+                <field index="header">
+                    <value index="vDEF">Header in Flex</value>
+                </field>
+                <field index="textarea">
+                    <value index="vDEF">Text in Flex</value>
+                </field>
+            </language>
+        </sheet>
+        <sheet index="sheet2">
+            <language index="lDEF">
+                <field index="link">
+                    <value index="vDEF">t3://page?uid=13</value>
+                </field>
+                <field index="number">
+                    <value index="vDEF">12</value>
+                </field>
+            </language>
+        </sheet>
+    </data>
+</T3FlexForms>',
+        ]);
+
+        $fieldInformation = $this->get(TcaSchemaFactory::class)->get('tt_content')->getField('typo3tests_contentelementb_flexfield');
+        $subject = $this->get(RecordFieldTransformer::class);
+        $result = $subject->transformField(
+            $fieldInformation,
+            $dummyRecord->getRawRecord(),
+            $this->get(Context::class)
+        );
+
+        self::assertIsArray($result);
+        self::assertSame('Header in Flex', $result['header']);
+        self::assertSame('Text in Flex', $result['textarea']);
+        self::assertSame('t3://page?uid=13', $result['link']->url);
+        self::assertSame('12', $result['number']);
+
+        $resolvedRecord = $this->get(RecordFactory::class)->createResolvedRecordFromDatabaseRow('tt_content', $dummyRecord->toArray());
+        $resolvedRelation = $resolvedRecord['typo3tests_contentelementb_flexfield'];
+        self::assertIsArray($resolvedRelation);
+        self::assertSame('Header in Flex', $resolvedRelation['header']);
+        self::assertSame('Text in Flex', $resolvedRelation['textarea']);
+        self::assertSame('t3://page?uid=13', $resolvedRelation['link']->url);
+        self::assertSame('12', $resolvedRelation['number']);
+    }
+
+    protected function setWorkspaceId(int $workspaceId): void
+    {
+        $GLOBALS['BE_USER']->workspace = $workspaceId;
+        GeneralUtility::makeInstance(Context::class)->setAspect('workspace', new WorkspaceAspect($workspaceId));
+    }
+
+    protected function getTestRecord(): array
+    {
+        return [
+            'uid' => 260,
+            'pid' => 1,
+            'sys_language_uid' => 0,
+            'l18n_parent' => 0,
+            'hidden' => 0,
+            'starttime' => 0,
+            'endtime' => 0,
+            'fe_group' => '',
+            'editlock' => 0,
+            'rowDescription' => '',
+            'CType' => 'typo3tests_contentelementb',
+            'colPos' => 0,
+            'image' => 0,
+            'typo3tests_contentelementb_collection' => 0,
+            'typo3tests_contentelementb_collection2' => 0,
+            'typo3tests_contentelementb_collection_external' => 0,
+            'typo3tests_contentelementb_collection_recursive' => 0,
+            'typo3tests_contentelementb_categories_mm' => 0,
+            'typo3tests_contentelementb_categories_11' => 0,
+            'typo3tests_contentelementb_categories_1m' => 0,
+            'typo3tests_contentelementb_pages_relation' => 0,
+            'typo3tests_contentelementb_circular_relation' => 0,
+            'typo3tests_contentelementb_record_relation_recursive' => 0,
+            'typo3tests_contentelementb_pages_content_relation' => '',
+            'typo3tests_contentelementb_pages_mm' => 0,
+            'typo3tests_contentelementb_folder' => 0,
+            'typo3tests_contentelementb_folder_recursive' => 0,
+            'typo3tests_contentelementb_select_single' => '',
+            'typo3tests_contentelementb_select_checkbox' => '',
+            'typo3tests_contentelementb_select_single_box' => '',
+            'typo3tests_contentelementb_select_multiple' => '',
+            'typo3tests_contentelementb_select_foreign_multiple' => '',
+            'typo3tests_contentelementb_flexfield' => '',
+            'typo3tests_contentelementb_json' => '',
+            'typo3tests_contentelementb_datetime' => 0,
+            'typo3tests_contentelementb_datetime_nullable' => null,
+            'typo3tests_contentelementb_select_foreign' => '',
+        ];
+    }
+
+    protected function createTestRecordObject(array $overriddenValues = []): Record
+    {
+        $dummyRecordData = $this->getTestRecord();
+        $dummyRecordData = array_replace($dummyRecordData, $overriddenValues);
+        return $this->get(RecordFactory::class)->createFromDatabaseRow('tt_content', $dummyRecordData);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php
index 22f691104fd58fa9c11911ceb7cc7c71d0b19434..4cae894c2edf1070d4291c89bbee6a784cff05d7 100644
--- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_classic_content/Configuration/TCA/test_content_carousel_item.php
@@ -51,7 +51,7 @@ return [
             'label' => 'Image',
             'config' => [
                 'type' => 'file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ],
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php
new file mode 100644
index 0000000000000000000000000000000000000000..48a03e88c20a4f98be4199e6371d6e52be52a51d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/Overrides/tt_content.php
@@ -0,0 +1,461 @@
+<?php
+
+$overrides = [
+    'palettes' => [
+        'typo3tests_contentelementb_palette' => [
+            'showitem' => 'typo3tests_contentelementb_select_single,typo3tests_contentelementb_select_one_to_one,typo3tests_contentelementb_select_checkbox,typo3tests_contentelementb_select_single_box,typo3tests_contentelementb_select_multiple,typo3tests_contentelementb_select_foreign,typo3tests_contentelementb_select_foreign_native,typo3tests_contentelementb_select_foreign_multiple',
+        ],
+    ],
+    'columns' => [
+        'typo3tests_contentelementb_collection' => [
+            'config' => [
+                'type' => 'inline',
+                'foreign_table' => 'typo3tests_contentelementb_collection',
+                'foreign_field' => 'foreign_table_parent_uid',
+            ],
+            'exclude' => true,
+            'label' => 'collection',
+        ],
+        'typo3tests_contentelementb_collection_recursive' => [
+            'config' => [
+                'type' => 'inline',
+                'foreign_table' => 'typo3tests_contentelementb_collection_recursive',
+                'foreign_field' => 'foreign_table_parent_uid',
+            ],
+            'exclude' => true,
+            'label' => 'collection_recursive',
+        ],
+        'typo3tests_contentelementb_categories_mm' => [
+            'config' => [
+                'type' => 'category',
+            ],
+            'exclude' => true,
+            'label' => 'categories_mm',
+        ],
+        'typo3tests_contentelementb_categories_11' => [
+            'config' => [
+                'type' => 'category',
+                'relationship' => 'oneToOne',
+            ],
+            'exclude' => true,
+            'label' => 'categories_11',
+        ],
+        'typo3tests_contentelementb_categories_1m' => [
+            'config' => [
+                'type' => 'category',
+                'relationship' => 'oneToMany',
+            ],
+            'exclude' => true,
+            'label' => 'categories_1m',
+        ],
+        'typo3tests_contentelementb_pages_relation' => [
+            'config' => [
+                'type' => 'group',
+                'allowed' => 'pages',
+            ],
+            'exclude' => true,
+            'label' => 'pages_relation',
+        ],
+        'typo3tests_contentelementb_pages_relations' => [
+            'config' => [
+                'type' => 'group',
+                'allowed' => 'pages',
+            ],
+            'exclude' => true,
+            'label' => 'pages_relations',
+        ],
+        'typo3tests_contentelementb_circular_relation' => [
+            'config' => [
+                'type' => 'group',
+                'allowed' => 'tt_content',
+            ],
+            'exclude' => true,
+            'label' => 'circular_relation',
+        ],
+        'typo3tests_contentelementb_record_relation_recursive' => [
+            'config' => [
+                'type' => 'group',
+                'allowed' => 'test_record',
+            ],
+            'exclude' => true,
+            'label' => 'record_relation_recursive',
+        ],
+        'typo3tests_contentelementb_pages_content_relation' => [
+            'config' => [
+                'type' => 'group',
+                'allowed' => 'pages,tt_content',
+            ],
+            'exclude' => true,
+            'label' => 'pages_content_relation',
+        ],
+        'typo3tests_contentelementb_pages_mm' => [
+            'config' => [
+                'type' => 'group',
+                'MM' => 'block_pages_mm',
+                'allowed' => 'pages',
+            ],
+            'exclude' => true,
+            'label' => 'pages_mm',
+        ],
+        'typo3tests_contentelementb_folder' => [
+            'config' => [
+                'type' => 'folder',
+                'relationship' => 'manyToOne',
+            ],
+            'exclude' => true,
+            'label' => 'folder',
+        ],
+        'typo3tests_contentelementb_folder_recursive' => [
+            'config' => [
+                'type' => 'folder',
+            ],
+            'exclude' => true,
+            'label' => 'folder_recursive',
+        ],
+        'typo3tests_contentelementb_select_checkbox' => [
+            'config' => [
+                'type' => 'select',
+            ],
+            'exclude' => true,
+            'label' => 'select_checkbox',
+        ],
+        'typo3tests_contentelementb_select_single_box' => [
+            'config' => [
+                'type' => 'select',
+            ],
+            'exclude' => true,
+            'label' => 'select_single_box',
+        ],
+        'typo3tests_contentelementb_select_single' => [
+            'config' => [
+                'type' => 'select',
+                'renderType' => 'selectSingle',
+            ],
+            'exclude' => true,
+            'label' => 'select_single',
+        ],
+        'typo3tests_contentelementb_select_one_to_one' => [
+            'config' => [
+                'type' => 'select',
+                'relationship' => 'oneToOne',
+                'foreign_table' => 'test_record',
+            ],
+            'exclude' => true,
+            'label' => 'select_one_to_one',
+        ],
+        'typo3tests_contentelementb_select_multiple' => [
+            'config' => [
+                'type' => 'select',
+            ],
+            'exclude' => true,
+            'label' => 'select_multiple',
+        ],
+        'typo3tests_contentelementb_select_foreign' => [
+            'config' => [
+                'type' => 'select',
+                'foreign_table' => 'test_record',
+            ],
+            'exclude' => true,
+        ],
+        'typo3tests_contentelementb_select_foreign_native' => [
+            'config' => [
+                'type' => 'select',
+                'foreign_table' => 'test_record',
+            ],
+            'exclude' => true,
+            'label' => 'select_foreign_native',
+        ],
+        'typo3tests_contentelementb_select_foreign_multiple' => [
+            'config' => [
+                'type' => 'select',
+                'foreign_table' => 'test_record',
+            ],
+            'exclude' => true,
+            'label' => 'select_foreign_multiple',
+        ],
+        'typo3tests_contentelementb_flexfield' => [
+            'config' => [
+                'type' => 'flex',
+                'ds' => [
+                    'typo3tests_contentelementb' => '<T3FlexForms>' . "\n"
+                        . '    <sheets type="array">' . "\n"
+                        . '        <sDEF type="array">' . "\n"
+                        . '            <ROOT type="array">' . "\n"
+                        . '                <type>array</type>' . "\n"
+                        . '                <el type="array">' . "\n"
+                        . '                    <field index="header" type="array">' . "\n"
+                        . '                        <label>header</label>' . "\n"
+                        . '                        <config>' . "\n"
+                        . '                            <type>input</type>' . "\n"
+                        . '                        </config>' . "\n"
+                        . '                    </field>' . "\n"
+                        . '                    <field index="textarea" type="array">' . "\n"
+                        . '                        <label>textarea</label>' . "\n"
+                        . '                        <config>' . "\n"
+                        . '                            <type>text</type>' . "\n"
+                        . '                        </config>' . "\n"
+                        . '                    </field>' . "\n"
+                        . '                </el>' . "\n"
+                        . '            </ROOT>' . "\n"
+                        . '        </sDEF>' . "\n"
+                        . '        <sheet2 type="array">' . "\n"
+                        . '            <ROOT type="array">' . "\n"
+                        . '                <type>array</type>' . "\n"
+                        . '                <el>' . "\n"
+                        . '                    <link>' . "\n"
+                        . '                        <label>header</label>' . "\n"
+                        . '                        <config>' . "\n"
+                        . '                            <type>link</type>' . "\n"
+                        . '                        </config>' . "\n"
+                        . '                    </link>' . "\n"
+                        . '                    <number>' . "\n"
+                        . '                        <label>number</label>' . "\n"
+                        . '                        <config>' . "\n"
+                        . '                            <type>number</type>' . "\n"
+                        . '                        </config>' . "\n"
+                        . '                    </number>' . "\n"
+                        . '                </el>' . "\n"
+                        . '            </ROOT>' . "\n"
+                        . '        </sheet2>' . "\n"
+                        . '    </sheets>' . "\n"
+                        . '</T3FlexForms>',
+                    'default' => '<T3DataStructure>' . "\n"
+                        . '  <ROOT>' . "\n"
+                        . '    <type>array</type>' . "\n"
+                        . '    <el>' . "\n"
+                        . '      <xmlTitle>' . "\n"
+                        . '        <label>The Title:</label>' . "\n"
+                        . '        <config>' . "\n"
+                        . '            <type>input</type>' . "\n"
+                        . '            <size>48</size>' . "\n"
+                        . '        </config>' . "\n"
+                        . '      </xmlTitle>' . "\n"
+                        . '    </el>' . "\n"
+                        . '  </ROOT>' . "\n"
+                        . '</T3DataStructure>',
+                ],
+                'ds_pointerField' => 'CType',
+            ],
+            'exclude' => true,
+            'label' => 'flexfield',
+        ],
+        'typo3tests_contentelementb_json' => [
+            'config' => [
+                'type' => 'json',
+            ],
+            'exclude' => true,
+            'label' => 'json',
+        ],
+        'typo3tests_contentelementb_datetime' => [
+            'config' => [
+                'type' => 'datetime',
+            ],
+            'exclude' => true,
+            'label' => 'datetime',
+        ],
+        'typo3tests_contentelementb_datetime_nullable' => [
+            'config' => [
+                'type' => 'datetime',
+                'nullable' => true,
+            ],
+            'exclude' => true,
+            'label' => 'datetime nullable',
+        ],
+    ],
+    'types' => [
+        'typo3tests_contentelementb' => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,--palette--;;general,image,media,assets,typo3tests_contentelementb_collection,typo3tests_contentelementb_collection_recursive,typo3tests_contentelementb_categories_mm,typo3tests_contentelementb_categories_11,typo3tests_contentelementb_categories_1m,typo3tests_contentelementb_pages_relation,typo3tests_contentelementb_circular_relation,typo3tests_contentelementb_record_relation_recursive,typo3tests_contentelementb_pages_content_relation,typo3tests_contentelementb_pages_relations,typo3tests_contentelementb_pages_mm,typo3tests_contentelementb_folder,typo3tests_contentelementb_folder_recursive,--palette--;;typo3tests_contentelementb_palette,typo3tests_contentelementb_flexfield,typo3tests_contentelementb_json,typo3tests_contentelementb_datetime,typo3tests_contentelementb_datetime_nullable,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes,rowDescription',
+            'columnsOverrides' => [
+                'image' => [
+                    'config' => [
+                        'relationship' => 'manyToOne',
+                    ],
+                ],
+                'typo3tests_contentelementb_collection' => [
+                    'label' => 'collection',
+                    'config' => [
+                        'appearance' => [
+                            'useSortable' => true,
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_collection_recursive' => [
+                    'label' => 'collection_recursive',
+                    'config' => [
+                        'appearance' => [
+                            'useSortable' => true,
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_categories_mm' => [
+                    'label' => 'categories_mm',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_categories_11' => [
+                    'label' => 'categories_11',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_categories_1m' => [
+                    'label' => 'categories_1m',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_pages_relation' => [
+                    'label' => 'pages_relation',
+                    'config' => [
+                        'relationship' => 'manyToOne',
+                    ],
+                ],
+                'typo3tests_contentelementb_pages_relations' => [
+                    'label' => 'pages_relations',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_circular_relation' => [
+                    'label' => 'circular_relation',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_record_relation_recursive' => [
+                    'label' => 'record_relation_recursive',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_pages_content_relation' => [
+                    'label' => 'pages_content_relation',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_pages_mm' => [
+                    'label' => 'pages_mm',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_folder' => [
+                    'label' => 'folder',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_folder_recursive' => [
+                    'label' => 'folder_recursive',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_select_single' => [
+                    'config' => [
+                        'items' => [
+                            [
+                                'label' => 'Foo 1',
+                                'value' => '1',
+                            ],
+                            [
+                                'label' => 'Foo 2',
+                                'value' => '2',
+                            ],
+                            [
+                                'label' => 'Foo 3',
+                                'value' => '3',
+                            ],
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_checkbox' => [
+                    'label' => 'select_checkbox',
+                    'config' => [
+                        'renderType' => 'selectCheckBox',
+                        'items' => [
+                            [
+                                'label' => 'Foo 1',
+                                'value' => '1',
+                            ],
+                            [
+                                'label' => 'Foo 2',
+                                'value' => '2',
+                            ],
+                            [
+                                'label' => 'Foo 3',
+                                'value' => '3',
+                            ],
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_single_box' => [
+                    'label' => 'select_single_box',
+                    'config' => [
+                        'renderType' => 'selectSingleBox',
+                        'items' => [
+                            [
+                                'label' => 'Foo 1',
+                                'value' => '1',
+                            ],
+                            [
+                                'label' => 'Foo 2',
+                                'value' => '2',
+                            ],
+                            [
+                                'label' => 'Foo 3',
+                                'value' => '3',
+                            ],
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_multiple' => [
+                    'label' => 'select_multiple',
+                    'config' => [
+                        'renderType' => 'selectMultipleSideBySide',
+                        'items' => [
+                            [
+                                'label' => 'Foo 1',
+                                'value' => '1',
+                            ],
+                            [
+                                'label' => 'Foo 2',
+                                'value' => '2',
+                            ],
+                            [
+                                'label' => 'Foo 3',
+                                'value' => '3',
+                            ],
+                        ],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_foreign' => [
+                    'label' => 'select_foreign',
+                    'config' => [
+                        'items' => [],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_foreign_multiple' => [
+                    'label' => 'select_foreign_multiple',
+                    'config' => [
+                        'renderType' => 'selectMultipleSideBySide',
+                        'items' => [],
+                    ],
+                ],
+                'typo3tests_contentelementb_select_foreign_native' => [
+                    'label' => 'select_foreign_native',
+                    'config' => [
+                        'relationship' => 'manyToOne',
+                        'items' => [],
+                    ],
+                ],
+                'typo3tests_contentelementb_flexfield' => [
+                    'label' => 'flexfield',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_json' => [
+                    'label' => 'json',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_datetime' => [
+                    'label' => 'json',
+                    'config' => [],
+                ],
+                'typo3tests_contentelementb_datetime_nullable' => [
+                    'label' => 'json',
+                    'config' => [],
+                ],
+            ],
+        ],
+    ],
+    'ctrl' => [
+        'typeicon_classes' => [
+            'typo3tests_contentelementb' => 'tt_content-typo3tests_contentelementb-175ef6f',
+        ],
+        'searchFields' => 'header,header_link,subheader,bodytext,pi_flexform,typo3tests_contentelementb_flexfield,typo3tests_contentelementb_json',
+    ],
+];
+
+$GLOBALS['TCA']['tt_content'] = array_replace_recursive($GLOBALS['TCA']['tt_content'], $overrides);
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php
new file mode 100644
index 0000000000000000000000000000000000000000..1346b7c4c64b9bf6c7c9b66ba2aab5bf2209a6bc
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/collection_inner.php
@@ -0,0 +1,64 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'collection_inner',
+        'label' => 'fieldB',
+        'hideTable' => true,
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+        'editlock' => 'editlock',
+        'delete' => 'deleted',
+        'crdate' => 'crdate',
+        'tstamp' => 'tstamp',
+        'versioningWS' => true,
+        'sortby' => 'sorting',
+        'security' => [
+            'ignorePageTypeRestriction' => true,
+        ],
+        'transOrigPointerField' => 'l10n_parent',
+        'translationSource' => 'l10n_source',
+        'transOrigDiffSourceField' => 'l10n_diffsource',
+        'languageField' => 'sys_language_uid',
+        'typeicon_classes' => [
+            'default' => 'collection_inner-1-116cf86',
+        ],
+        'searchFields' => 'fieldB',
+    ],
+    'palettes' => [
+        'language' => [
+            'showitem' => 'sys_language_uid,l10n_parent',
+        ],
+        'hidden' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
+            'showitem' => 'hidden',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access',
+            'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock',
+        ],
+    ],
+    'columns' => [
+        'foreign_table_parent_uid' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'fieldB' => [
+            'label' => 'fieldB',
+            'exclude' => true,
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+    ],
+    'types' => [
+        1 => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldB,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access',
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php
new file mode 100644
index 0000000000000000000000000000000000000000..a93669824eb521f2f338d707edba7280edc1f6ce
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/record_collection.php
@@ -0,0 +1,61 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'record_collection',
+        'label' => 'text',
+        'hideTable' => true,
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+        'editlock' => 'editlock',
+        'delete' => 'deleted',
+        'crdate' => 'crdate',
+        'tstamp' => 'tstamp',
+        'versioningWS' => true,
+        'sortby' => 'sorting',
+        'transOrigPointerField' => 'l10n_parent',
+        'translationSource' => 'l10n_source',
+        'transOrigDiffSourceField' => 'l10n_diffsource',
+        'languageField' => 'sys_language_uid',
+        'typeicon_classes' => [
+            'default' => 'record_collection-1-cc2849f',
+        ],
+        'searchFields' => 'text',
+    ],
+    'palettes' => [
+        'language' => [
+            'showitem' => 'sys_language_uid,l10n_parent',
+        ],
+        'hidden' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
+            'showitem' => 'hidden',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access',
+            'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock',
+        ],
+    ],
+    'columns' => [
+        'foreign_table_parent_uid' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'text' => [
+            'label' => 'text',
+            'exclude' => true,
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+    ],
+    'types' => [
+        1 => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,text,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access',
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php
new file mode 100644
index 0000000000000000000000000000000000000000..3d116835040bac0f71d0b9013447fc7cd67a62ff
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/test_record.php
@@ -0,0 +1,77 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'typo3tests/test-record',
+        'label' => 'title',
+        'hideTable' => true,
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+        'origUid' => 't3_origuid',
+        'editlock' => 'editlock',
+        'delete' => 'deleted',
+        'crdate' => 'crdate',
+        'tstamp' => 'tstamp',
+        'versioningWS' => true,
+        'sortby' => 'sorting',
+        'security' => [
+            'ignorePageTypeRestriction' => true,
+        ],
+        'typeicon_classes' => [
+            'default' => 'test_record-typo3tests_testrecord-cc2849f',
+        ],
+        'searchFields' => 'title',
+    ],
+    'palettes' => [
+        'hidden' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
+            'showitem' => 'hidden',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access',
+            'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock',
+        ],
+    ],
+    'columns' => [
+        'foreign_table_parent_uid' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'tablenames' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'fieldname' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'title' => [
+            'label' => 'title',
+            'exclude' => true,
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+        'record_collection' => [
+            'label' => 'record_collection',
+            'exclude' => true,
+            'config' => [
+                'type' => 'inline',
+                'foreign_table' => 'record_collection',
+                'foreign_field' => 'foreign_table_parent_uid',
+            ],
+        ],
+    ],
+    'types' => [
+        1 => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,title,record_collection,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access',
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php
new file mode 100644
index 0000000000000000000000000000000000000000..913df5530cb35fa890d8a018e0aa514196eaceb4
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection.php
@@ -0,0 +1,64 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'collection',
+        'label' => 'fieldA',
+        'hideTable' => true,
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+        'editlock' => 'editlock',
+        'delete' => 'deleted',
+        'crdate' => 'crdate',
+        'tstamp' => 'tstamp',
+        'versioningWS' => true,
+        'sortby' => 'sorting',
+        'security' => [
+            'ignorePageTypeRestriction' => true,
+        ],
+        'transOrigPointerField' => 'l10n_parent',
+        'translationSource' => 'l10n_source',
+        'transOrigDiffSourceField' => 'l10n_diffsource',
+        'languageField' => 'sys_language_uid',
+        'typeicon_classes' => [
+            'default' => 'typo3tests_contentelementb_collection-1-116cf86',
+        ],
+        'searchFields' => 'fieldA',
+    ],
+    'palettes' => [
+        'language' => [
+            'showitem' => 'sys_language_uid,l10n_parent',
+        ],
+        'hidden' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
+            'showitem' => 'hidden',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access',
+            'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock',
+        ],
+    ],
+    'columns' => [
+        'foreign_table_parent_uid' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'fieldA' => [
+            'label' => 'fieldA',
+            'exclude' => true,
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+    ],
+    'types' => [
+        1 => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldA,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access',
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php
new file mode 100644
index 0000000000000000000000000000000000000000..8786b31bf6334867ab898850e8bd5c01906eedfa
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/Configuration/TCA/typo3tests_contentelementb_collection_recursive.php
@@ -0,0 +1,73 @@
+<?php
+
+return [
+    'ctrl' => [
+        'title' => 'collection_recursive',
+        'label' => 'fieldA',
+        'hideTable' => true,
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+        'editlock' => 'editlock',
+        'delete' => 'deleted',
+        'crdate' => 'crdate',
+        'tstamp' => 'tstamp',
+        'versioningWS' => true,
+        'sortby' => 'sorting',
+        'security' => [
+            'ignorePageTypeRestriction' => true,
+        ],
+        'transOrigPointerField' => 'l10n_parent',
+        'translationSource' => 'l10n_source',
+        'transOrigDiffSourceField' => 'l10n_diffsource',
+        'languageField' => 'sys_language_uid',
+        'typeicon_classes' => [
+            'default' => 'typo3tests_contentelementb_collection_recursive-1-116cf86',
+        ],
+        'searchFields' => 'fieldA',
+    ],
+    'palettes' => [
+        'language' => [
+            'showitem' => 'sys_language_uid,l10n_parent',
+        ],
+        'hidden' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.visibility',
+            'showitem' => 'hidden',
+        ],
+        'access' => [
+            'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.access',
+            'showitem' => 'starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel,endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel,--linebreak--,fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabel,--linebreak--,editlock',
+        ],
+    ],
+    'columns' => [
+        'foreign_table_parent_uid' => [
+            'config' => [
+                'type' => 'passthrough',
+            ],
+        ],
+        'fieldA' => [
+            'label' => 'fieldA',
+            'exclude' => true,
+            'config' => [
+                'type' => 'input',
+            ],
+        ],
+        'collection_inner' => [
+            'label' => 'collection_inner',
+            'exclude' => true,
+            'config' => [
+                'type' => 'inline',
+                'foreign_table' => 'collection_inner',
+                'foreign_field' => 'foreign_table_parent_uid',
+            ],
+        ],
+    ],
+    'types' => [
+        1 => [
+            'showitem' => '--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,fieldA,collection_inner,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,--palette--;;language,--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,--palette--;;hidden,--palette--;;access',
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..cda791ae4a3a4a43404ad6b7c5cc4f83d32bf13b
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/composer.json
@@ -0,0 +1,14 @@
+{
+	"name": "typo3tests/fixture-relation-resolver-test",
+	"type": "typo3-cms-extension",
+	"description": "FluidTemplateContentObject Test",
+	"license": "GPL-2.0-or-later",
+	"require": {
+		"typo3/cms-core": "13.3.*@dev"
+	},
+	"extra": {
+		"typo3/cms": {
+			"extension-key": "test_relation_resolver"
+		}
+	}
+}
diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php
new file mode 100644
index 0000000000000000000000000000000000000000..638ba2ada49f75efda5685f35242bf9acbf8d852
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_relation_resolver/ext_emconf.php
@@ -0,0 +1,18 @@
+<?php
+
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'Relation Resolver Test',
+    'description' => 'Relation Resolver Test',
+    'category' => 'example',
+    'version' => '13.3.0',
+    'state' => 'beta',
+    'author' => 'Nikita Hovratov',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '13.3.0',
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php b/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php
index 2e5383733995be82a22710d93f0683806f47c348..42099440a0b66bc7226dea923713f0da2efb80ee 100644
--- a/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php
+++ b/typo3/sysext/core/Tests/Unit/Configuration/Tca/TcaPreparationTest.php
@@ -757,10 +757,61 @@ final class TcaPreparationTest extends UnitTestCase
         self::assertEquals('jpg,png,gif', $subjectMethodReflection->invoke($subject, ['common-image-types,jpg,gif']));
     }
 
+    public static function prepareSelectSingleAddsRelationshipDataProvider(): iterable
+    {
+        yield [
+            [
+                'MM' => 'select_table_mm',
+            ],
+            'manyToMany',
+        ];
+        yield [
+            [],
+            'manyToOne',
+        ];
+    }
+
+    #[DataProvider('prepareSelectSingleAddsRelationshipDataProvider')]
+    #[Test]
+    public function prepareSelectSingleAddsRelationship(array $configuration, $expectedRelation): void
+    {
+        $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => array_merge(['type' => 'select', 'renderType' => 'selectSingle', 'foreign_table' => 'tx_myextension_bar'], $configuration)]]]]);
+        self::assertEquals($expectedRelation, $subject['foo']['columns']['select']['config']['relationship']);
+    }
+
     #[Test]
-    public function prepareSelectSingleAddsMaxItems(): void
+    public function prepareSelectSingleDoesNotOverwriteRelationship(): void
+    {
+        $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => ['type' => 'select', 'renderType' => 'selectSingle', 'foreign_table' => 'tx_myextension_bar', 'relationship' => 'oneToOne']]]]]);
+        self::assertEquals('oneToOne', $subject['foo']['columns']['select']['config']['relationship']);
+    }
+
+    #[Test]
+    public function prepareSelectSingleDoesNotAddRelationshipOnMissingForeignTable(): void
     {
         $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['select' => ['config' => ['type' => 'select', 'renderType' => 'selectSingle']]]]]);
-        self::assertEquals(1, $subject['foo']['columns']['select']['config']['maxitems']);
+        self::assertNull($subject['foo']['columns']['select']['config']['relationship'] ?? null);
+    }
+
+    public static function prepareRelationshipToOneAddsMaxItemsDataProvider(): iterable
+    {
+        yield ['select'];
+        yield ['inline'];
+        yield ['file'];
+        yield ['folder'];
+        yield ['group'];
+        yield ['input', 0];
+    }
+
+    #[DataProvider('prepareRelationshipToOneAddsMaxItemsDataProvider')]
+    #[Test]
+    public function prepareRelationshipToOneAddsMaxItems(string $type, int $maxitems = 1): void
+    {
+        $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'oneToOne']]]]]);
+        self::assertEquals($maxitems, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0);
+        $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'manyToOne']]]]]);
+        self::assertEquals($maxitems, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0);
+        $subject = (new TcaPreparation())->prepare(['foo' => ['columns' => ['relation' => ['config' => ['type' => $type, 'relationship' => 'manyToMany']]]]]);
+        self::assertEquals(0, $subject['foo']['columns']['relation']['config']['maxitems'] ?? 0);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php b/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php
index b2be73e33df2c29a768344c89a779c0711ed5a9e..938ed168a9d6305cd9590a45f676b582bb021be9 100644
--- a/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php
+++ b/typo3/sysext/core/Tests/Unit/Domain/RecordFactoryTest.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\Domain;
 
 use PHPUnit\Framework\Attributes\Test;
 use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\DataHandling\RecordFieldTransformer;
 use TYPO3\CMS\Core\Domain\RecordFactory;
 use TYPO3\CMS\Core\Schema\FieldTypeFactory;
 use TYPO3\CMS\Core\Schema\RelationMapBuilder;
@@ -40,7 +41,7 @@ final class RecordFactoryTest extends UnitTestCase
             $cacheMock
         );
         $schemaFactory->load(['existing_schema' => ['ctrl' => [], 'columns' => []]]);
-        $subject = new RecordFactory($schemaFactory);
+        $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class));
         $subject->createFromDatabaseRow('foo', ['foo' => 1]);
     }
 
@@ -62,7 +63,7 @@ final class RecordFactoryTest extends UnitTestCase
                 'types' => ['bar' => ['showitem' => 'type']],
             ],
         ]);
-        $subject = new RecordFactory($schemaFactory);
+        $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class));
         $recordObject = $subject->createFromDatabaseRow('foo', ['uid' => 1, 'pid' => 2, 'type' => 'bar']);
         self::assertEquals('bar', $recordObject->toArray()['type']);
     }
@@ -85,7 +86,7 @@ final class RecordFactoryTest extends UnitTestCase
                 'types' => ['foo' => ['showitem' => 'foo']],
             ],
         ]);
-        $subject = new RecordFactory($schemaFactory);
+        $subject = new RecordFactory($schemaFactory, $this->createMock(RecordFieldTransformer::class));
         $recordObject = $subject->createFromDatabaseRow('foo', ['uid' => 1, 'pid' => 2, 'type' => 'foo', 'foo' => 'fooValue', 'bar' => 'barValue']);
         self::assertFalse($recordObject->offsetExists('bar'));
         self::assertTrue($recordObject->offsetExists('foo'));
diff --git a/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php b/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php
index e60bad9e88c99260836dbad42af9eae271e56bc9..721cdef8c3a3d4741ccd1adf3606abbacbb91f63 100644
--- a/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php
+++ b/typo3/sysext/core/Tests/Unit/Schema/RelationshipTypeTest.php
@@ -34,6 +34,10 @@ final class RelationshipTypeTest extends UnitTestCase
             ['config' => []],
             RelationshipType::Undefined,
         ];
+        yield 'detect select as static list - this is no relation => undefined' => [
+            ['type' => 'select'],
+            RelationshipType::Undefined,
+        ];
         yield 'use MM if given' => [
             ['type' => 'text', 'config' => ['type' => 'text', 'MM' => 1]],
             RelationshipType::ManyToMany,
@@ -46,10 +50,6 @@ final class RelationshipTypeTest extends UnitTestCase
             ['type' => 'group'],
             RelationshipType::List,
         ];
-        yield 'detect select as static list' => [
-            ['type' => 'select'],
-            RelationshipType::Static,
-        ];
         yield 'detect select with MM' => [
             ['type' => 'select', 'MM' => true],
             RelationshipType::ManyToMany,
@@ -64,7 +64,15 @@ final class RelationshipTypeTest extends UnitTestCase
         ];
         yield 'detect inline with foreign field' => [
             ['type' => 'inline', 'foreign_table' => 'sys_file_reference', 'foreign_field' => 'uid_foreign'],
-            RelationshipType::ForeignField,
+            RelationshipType::OneToMany,
+        ];
+        yield 'detect relationship set to "oneToOne"' => [
+            ['type' => 'select', 'foreign_table' => 'sys_file_metadata', 'relationship' => 'oneToOne'],
+            RelationshipType::OneToOne,
+        ];
+        yield 'detect relationship set to "manyToOne"' => [
+            ['type' => 'group', 'allowed' => 'pages', 'relationship' => 'manyToOne'],
+            RelationshipType::ManyToOne,
         ];
         yield 'subarray key overloads main level key' => [
             ['type' => 'group', 'config' => ['type' => 'text']],
diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
index 364ef14b8f351bd3c5e8c8855e5f515984542831..d84884cc1e5f4f12d39bc3538dbe333bf319e637 100644
--- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
+++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
@@ -101,7 +101,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'tx_blogexample_domain_model_person',
                 'foreign_table' => 'tx_blogexample_domain_model_person',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'fieldControl' => [
                     'editPopup' => [
                         'disabled' => false,
@@ -195,7 +195,7 @@ return [
             'config' => [
                 'type' => 'inline', // this will store the info uid in the additional_name field (CSV)
                 'foreign_table' => 'tx_blogexample_domain_model_info',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'default' => 0,
             ],
         ],
@@ -206,7 +206,7 @@ return [
                 'type' => 'inline', // this will store the post uid in the post field of the info table
                 'foreign_table' => 'tx_blogexample_domain_model_info',
                 'foreign_field' => 'post',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'default' => 0,
             ],
         ],
diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php
index 356ba159a1ef039cc9957cc387d3c2b7d8703b6e..c3a777c8f8d23b6d1506acd0e0cca9a2b80fff07 100644
--- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php
+++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/parent_child_translation/Configuration/TCA/tx_parentchildtranslation_domain_model_main.php
@@ -54,7 +54,7 @@ return [
                 'foreign_table' => 'tx_parentchildtranslation_domain_model_squeeze',
                 'foreign_table_where' => 'AND {#tx_parentchildtranslation_domain_model_squeeze}.{#sys_language_uid} IN (0,-1)',
                 'foreign_field' => 'parent',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'default' => 0,
             ],
         ],
diff --git a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php
index 9c6d7b5ad08aa86617913991a37c0187030f0567..1cf6528465cc834ff5f96930aafd14ba81a88a0e 100644
--- a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php
+++ b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_groups.php
@@ -12,7 +12,7 @@ call_user_func(static function () {
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ];
diff --git a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php
index 1d0aae5b765796c3e930f3030d779a16b8e46679..38e043632637ae113ac67c35a1bb2fb68f088496 100644
--- a/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php
+++ b/typo3/sysext/felogin/Configuration/TCA/Overrides/fe_users.php
@@ -12,7 +12,7 @@ call_user_func(static function () {
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         'felogin_forgotHash' => [
diff --git a/typo3/sysext/frontend/Classes/Content/RecordCollector.php b/typo3/sysext/frontend/Classes/Content/RecordCollector.php
index 2592e5292578319faa8575a5e74965f280912ff9..50aa6802211795c866fd6ee2af96f184c3e6b910 100644
--- a/typo3/sysext/frontend/Classes/Content/RecordCollector.php
+++ b/typo3/sysext/frontend/Classes/Content/RecordCollector.php
@@ -26,14 +26,16 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
  */
 readonly class RecordCollector
 {
-    public function __construct(protected RecordFactory $recordFactory) {}
+    public function __construct(
+        protected RecordFactory $recordFactory,
+        protected RecordIdentityMap $recordIdentityMap
+    ) {}
 
     public function collect(
         string $table,
         array $select,
         ContentSlideMode $slideMode,
-        ContentObjectRenderer $contentObjectRenderer,
-        ?RecordIdentityMap $recordIdentityMap = null,
+        ContentObjectRenderer $contentObjectRenderer
     ): array {
         $slideCollectReverse = false;
         $collect = false;
@@ -59,12 +61,12 @@ readonly class RecordCollector
         do {
             $recordsOnPid = $contentObjectRenderer->getRecords($table, $select);
             $recordsOnPid = array_map(
-                function ($record) use ($recordIdentityMap, $table) {
-                    if ($recordIdentityMap !== null && $recordIdentityMap->hasIdentifier($table, (int)$record['uid'])) {
-                        return $recordIdentityMap->findByIdentifier($table, (int)$record['uid']);
+                function ($record) use ($table) {
+                    if ($this->recordIdentityMap->hasIdentifier($table, (int)$record['uid'])) {
+                        return $this->recordIdentityMap->findByIdentifier($table, (int)$record['uid']);
                     }
-                    $obj = $this->recordFactory->createFromDatabaseRow($table, $record);
-                    $recordIdentityMap?->add($obj);
+                    $obj = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record);
+                    $this->recordIdentityMap->add($obj);
                     return $obj;
                 },
                 $recordsOnPid
diff --git a/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php
index a0cb540f1804eed6c517c1f07f003bf93fafab8c..b2b4b698e1ee172df135a371315f6b0aab0c544a 100644
--- a/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php
+++ b/typo3/sysext/frontend/Classes/DataProcessing/PageContentFetchingProcessor.php
@@ -17,7 +17,6 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Frontend\DataProcessing;
 
-use TYPO3\CMS\Core\Domain\Persistence\RecordIdentityMap;
 use TYPO3\CMS\Core\Page\PageLayoutResolver;
 use TYPO3\CMS\Frontend\Content\ContentSlideMode;
 use TYPO3\CMS\Frontend\Content\RecordCollector;
@@ -53,7 +52,6 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface
     public function __construct(
         protected RecordCollector $recordCollector,
         protected PageLayoutResolver $pageLayoutResolver,
-        protected RecordIdentityMap $recordIdentityMap,
     ) {}
 
     public function process(
@@ -105,8 +103,7 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface
                     'orderBy' => 'colPos, sorting',
                 ],
                 ContentSlideMode::None,
-                $cObj,
-                $this->recordIdentityMap
+                $cObj
             );
             // 1b. Sort the records into the contentArea they belong to
             foreach ($flatRecords as $recordToSort) {
@@ -125,8 +122,7 @@ readonly class PageContentFetchingProcessor implements DataProcessorInterface
                     'orderBy' => 'sorting',
                 ],
                 ContentSlideMode::tryFrom($contentAreaData['slideMode'] ?? null),
-                $cObj,
-                $this->recordIdentityMap
+                $cObj
             );
             $contentAreaData['records'] = $records;
             $contentAreaName = $contentAreaData['identifier'];
diff --git a/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php
index f43d8be9c6a5138cec1bd56311b4160bed28ea0d..ba9a5d60e64a2067785ad94735308dd545cad612 100644
--- a/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php
+++ b/typo3/sysext/frontend/Classes/DataProcessing/RecordTransformationProcessor.php
@@ -96,11 +96,11 @@ readonly class RecordTransformationProcessor implements DataProcessorInterface
         $output = [];
         if (array_is_list($input)) {
             foreach ($input as $record) {
-                $output[] = $this->recordFactory->createFromDatabaseRow($table, $record);
+                $output[] = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $record);
             }
             $defaultTargetVariableName = 'records';
         } else {
-            $output = $this->recordFactory->createFromDatabaseRow($table, $input);
+            $output = $this->recordFactory->createResolvedRecordFromDatabaseRow($table, $input);
             $defaultTargetVariableName = 'record';
         }
         $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, $defaultTargetVariableName);
diff --git a/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php b/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php
index cbb69e4d1fe4e1f23b72efdaeca33969a07a9cdc..4790297968fc02fe493f5f379d70ecbe63ddec38 100644
--- a/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php
+++ b/typo3/sysext/frontend/Classes/Typolink/TypolinkParameter.php
@@ -20,7 +20,7 @@ namespace TYPO3\CMS\Frontend\Typolink;
 /**
  * This class represents an object containing the resolved parameters of a typolink
  */
-readonly class TypolinkParameter implements \JsonSerializable
+final readonly class TypolinkParameter implements \JsonSerializable
 {
     public function __construct(
         public string $url = '',
diff --git a/typo3/sysext/frontend/Configuration/TCA/backend_layout.php b/typo3/sysext/frontend/Configuration/TCA/backend_layout.php
index 6fbd3b1ba266500654ae07da08d340ac8d0d3735..6c7b5ab257455372df3db7aa4afc6b2b46613fae 100644
--- a/typo3/sysext/frontend/Configuration/TCA/backend_layout.php
+++ b/typo3/sysext/frontend/Configuration/TCA/backend_layout.php
@@ -45,7 +45,7 @@ return [
             'config' => [
                 'type' => 'file',
                 'allowed' => 'common-image-types',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'appearance' => [
                     'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference',
                 ],
diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html
index c4b93052867ffc4cea4d5cd2fe1763aa78552d11..d87365195e585b36a9a6be53b165b989de8eb98c 100644
--- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html
+++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/PageContentProcessor/Partials/SingleContent.html
@@ -6,5 +6,8 @@
     <f:case value="test_carousel">
         <h3>{record.header}</h3>
         <p>Carousel Items will show up: {record.carousel_items}</p>
+        <f:for each="{record.carousel_items}" as="carouselItem">
+            <h4>{carouselItem.header}</h4>
+        </f:for>
     </f:case>
 </f:switch>
diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html
index c4b93052867ffc4cea4d5cd2fe1763aa78552d11..d87365195e585b36a9a6be53b165b989de8eb98c 100644
--- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html
+++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/Fixtures/RecordTransform/Partials/SingleContent.html
@@ -6,5 +6,8 @@
     <f:case value="test_carousel">
         <h3>{record.header}</h3>
         <p>Carousel Items will show up: {record.carousel_items}</p>
+        <f:for each="{record.carousel_items}" as="carouselItem">
+            <h4>{carouselItem.header}</h4>
+        </f:for>
     </f:case>
 </f:switch>
diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php b/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php
index a57486f9c8bdce25c334f76abd438ac8baca0700..4bdc0e7fbbe94b9432999643a4fcfa93227179b8 100644
--- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php
+++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/PageContentFetchingProcessorTest.php
@@ -81,6 +81,7 @@ final class PageContentFetchingProcessorTest extends FunctionalTestCase
         $body = (string)$response->getBody();
         self::assertStringContainsString('Welcome to ACME guitars', $body);
         self::assertStringContainsString('Carousel Items will show up: 2', $body);
+        self::assertStringContainsString('Meet us at Guitar Brussels in 2035', $body);
         self::assertStringContainsString('Great to see you here', $body);
         self::assertStringContainsString('If you read this you are at the end.', $body);
     }
diff --git a/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php b/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php
index 1f3ac93adf97bddf25863bea5068829dd680f931..5ae16c2a70d5f2457e76314a4709ecafed3f81bd 100644
--- a/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php
+++ b/typo3/sysext/frontend/Tests/Functional/DataProcessing/RecordTransformationProcessorTest.php
@@ -58,7 +58,7 @@ final class RecordTransformationProcessorTest extends FunctionalTestCase
             $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
             $writer = DataHandlerWriter::withBackendUser($backendUser);
             $writer->invokeFactory($factory);
-            static::failIfArrayIsNotEmpty($writer->getErrors());
+            self::failIfArrayIsNotEmpty($writer->getErrors());
             $connection = $this->get(ConnectionPool::class)->getConnectionForTable('pages');
 
             $pageLayoutFileContents[] = file_get_contents(__DIR__ . '/Fixtures/PageLayouts/Default.tsconfig');
@@ -80,6 +80,8 @@ final class RecordTransformationProcessorTest extends FunctionalTestCase
         $response = $this->executeFrontendSubRequest((new InternalRequest('https://acme.com/'))->withPageId(1000));
         $body = (string)$response->getBody();
         self::assertStringContainsString('Welcome to ACME guitars', $body);
+        self::assertStringContainsString('Carousel Items will show up: 2', $body);
+        self::assertStringContainsString('Meet us at Guitar Brussels in 2035', $body);
         self::assertStringContainsString('Great to see you here', $body);
         self::assertStringContainsString('If you read this you are at the end.', $body);
     }
diff --git a/typo3/sysext/indexed_search/Configuration/TCA/index_config.php b/typo3/sysext/indexed_search/Configuration/TCA/index_config.php
index 8f5254742ec3c869a9f3f2a409c7af79822a7093..5d94678c5fc195938c2551f8799ff680c9a54b40 100644
--- a/typo3/sysext/indexed_search/Configuration/TCA/index_config.php
+++ b/typo3/sysext/indexed_search/Configuration/TCA/index_config.php
@@ -77,7 +77,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         'indexcfgs' => [
diff --git a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
index c6a734a8d88c45cf5d9efff51fd39f8687fb3444..f23cd8cf6e41f3695b1a2fe0c11f2bd038b76e68 100644
--- a/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
+++ b/typo3/sysext/reactions/Configuration/TCA/Overrides/sys_reaction_create_record.php
@@ -10,7 +10,7 @@
                 'type' => 'group',
                 'allowed' => 'pages',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         'fields' => [
diff --git a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php
index d5780e23bcd7ccddb8f371b43a110d3f476cbdc3..0c041492ac7ced8e0856e0afd1870d37f584d3d9 100644
--- a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php
+++ b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php
@@ -107,7 +107,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'be_users',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         // "table_name" is not referenced in this TCA but needs to be defined here to ensure extensions can
diff --git a/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php b/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php
index 15a868466eb13a41efb8867f0b71b62258292947..ece9ce5c694e695a1428cef4dd6cd37a0195802d 100644
--- a/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php
+++ b/typo3/sysext/styleguide/Classes/TcaDataGenerator/FieldGenerator/TypeInlineFalSelectSingle12Foreign.php
@@ -46,7 +46,7 @@ final class TypeInlineFalSelectSingle12Foreign extends AbstractFieldGenerator im
             'label' => 'fal_1 selicon_field',
             'config' => [
                 'type' => 'file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ];
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php
index f573991d39b19d49c78fe0e0ef9bda784e4c81f7..a74ed274522a610b6470663a132d693cfe6d6c91 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_folder.php
@@ -39,11 +39,11 @@ return [
             ],
         ],
         'folder_3' => [
-            'label' => 'folder_3 maxitems=1',
+            'label' => 'folder_3 relationship=manyToOne',
             'description' => 'field description',
             'config' => [
                 'type' => 'folder',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'size' => 1,
             ],
         ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php
index 118ee7e4daaa3344ebb9dbe68cb8332024a59255..6105553f8b209ff501dfa481f16fdede0eb28b77 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_group.php
@@ -103,7 +103,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'tx_styleguide_staticdata',
                 'size' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         'group_db_5' => [
@@ -127,7 +127,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'allowed' => 'pages',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'size' => 1,
                 'suggestOptions' => [
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php
index a6899d804918b00170d811064706056ed2bf9076..9ce6a677bd50e9439205e967bd8c2614f0e7a2d1 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_imagemanipulation.php
@@ -28,7 +28,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'allowed' => 'sys_file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'size' => 1,
             ],
@@ -38,7 +38,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'allowed' => 'sys_file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'size' => 1,
             ],
@@ -48,7 +48,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'allowed' => 'sys_file',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'size' => 1,
             ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php
index 326ec9c7fce0befb3d13060f9f82d77618ae8714..faecbf74203b5c2f1f412026cf892fb3ed4faa48 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select.php
@@ -386,11 +386,11 @@ return [
             ],
         ],
         'select_checkbox_2' => [
-            'label' => 'select_checkbox_2, maxitems=1',
+            'label' => 'select_checkbox_2, relationship=manyToOne',
             'config' => [
                 'type' => 'select',
                 'renderType' => 'selectCheckBox',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'items' => [
                     ['label' => 'foo 1', 'value' => 1],
                     ['label' => 'foo 2', 'value' => 2],
@@ -609,11 +609,11 @@ return [
             ],
         ],
         'select_multiplesidebyside_9' => [
-            'label' => 'select_multiplesidebyside_9 maxitems=1',
+            'label' => 'select_multiplesidebyside_9 relationship=manyToOne',
             'config' => [
                 'type' => 'select',
                 'renderType' => 'selectMultipleSideBySide',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'items' => [
                     ['label' => 'foo 1', 'value' => 1],
                     ['label' => 'foo 2', 'value' => 2],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php
index 7f364169742cfc20d67007d8db6595eff05469f9..7d25c080667853242cbbb78bd84a49246249cb0a 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_elements_select_single_12_foreign.php
@@ -28,7 +28,7 @@ return [
             'config' => [
                 'type' => 'file',
                 'allowed' => 'common-media-types',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php
index 2951922fddf80f358a5289d1338ebe7cebd70abb..42ff03caa7aaf083b91563d29f2dcd72b1490bb9 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_11.php
@@ -33,7 +33,7 @@ return [
                     'showAllLocalizationLink' => true,
                     'showPossibleLocalizationRecords' => true,
                 ],
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php
index e436bfbe7c4aa3cff93beefb0481547effdda842..59e0bc05a39d4106ff0de21a366ebdd4eccb99c8 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_1nnol10n.php
@@ -32,7 +32,7 @@ return [
                 'foreign_field' => 'parentid',
                 'foreign_table_field' => 'parenttable',
             ],
-            'maxitems' => 1,
+            'relationship' => 'manyToOne',
         ],
     ],
 
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php
index 0a74d112bb858b74309679bd3729027754402d7d..9431a2da14eb054c65645350c49d451b38fab7ba 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mngroup_mm.php
@@ -27,8 +27,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'size' => 1,
-                'eval' => 'int',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'allowed' => 'tx_styleguide_inline_mngroup',
                 'hideSuggest' => true,
@@ -44,8 +43,7 @@ return [
             'config' => [
                 'type' => 'group',
                 'size' => 1,
-                'eval' => 'int',
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'minitems' => 0,
                 'allowed' => 'tx_styleguide_inline_mngroup_child',
                 'hideSuggest' => true,
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php
index 1b873d23d63e47b042cbfd4cc28a0d92a8ba8122..c3414d201f07a3f2fe87d2ecdb3c926913ab1524 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_mnsymmetricgroup_mm.php
@@ -28,7 +28,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'tx_styleguide_inline_mnsymmetricgroup',
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'size' => 1,
             ],
         ],
@@ -38,7 +38,7 @@ return [
                 'type' => 'group',
                 'allowed' => 'tx_styleguide_inline_mnsymmetricgroup',
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
                 'size' => 1,
             ],
         ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php
index e1e52e5d663120aabbe2e77587431d40951fce92..ec2f7bc2f1ec89cb61672a4e52b816ec729363f1 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_inline_usecombinationgroup_mm.php
@@ -37,7 +37,7 @@ return [
                 'allowed' => 'tx_styleguide_inline_usecombinationgroup_child',
                 'size' => 1,
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
     ],
diff --git a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php
index b1b747d4ffce3c82bb9670f8edfc49ac3a694f1b..b3d42da4346fb356776e42fdf8e7d8ac7d138ce4 100644
--- a/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php
+++ b/typo3/sysext/styleguide/Configuration/TCA/tx_styleguide_required.php
@@ -157,13 +157,13 @@ return [
             ],
         ],
         'group_2' => [
-            'label' => 'group_2 db, minitems = 1, maxitems=1, size=1',
+            'label' => 'group_2 db, minitems = 1, relationship=manyToOne, size=1',
             'config' => [
                 'type' => 'group',
                 'allowed' => 'tx_styleguide_staticdata',
                 'size' => 1,
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
 
@@ -188,14 +188,14 @@ return [
         ],
 
         'inline_1' => [
-            'label' => 'inline_1 minitems=1, maxitems=1',
+            'label' => 'inline_1 minitems=1, relationship=manyToOne',
             'config' => [
                 'type' => 'inline',
                 'foreign_table' => 'tx_styleguide_required_inline_1_child',
                 'foreign_field' => 'parentid',
                 'foreign_table_field' => 'parenttable',
                 'minitems' => 1,
-                'maxitems' => 1,
+                'relationship' => 'manyToOne',
             ],
         ],
         'inline_2' => [
diff --git a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
index 2addcdf4c78961a99f605413effb897d80d5bab7..41536f8050be31714be5d227d74d94535c24d240 100644
--- a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
+++ b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
@@ -453,7 +453,7 @@ class DataHandlerHook
         if ($fieldInformation->isType(TableColumnType::INLINE) && !$fieldInformation->isMovingChildrenEnabled()) {
             return;
         }
-        if ($fieldInformation->getRelationshipType() !== RelationshipType::ForeignField && $fieldInformation->getRelationshipType() !== RelationshipType::List) {
+        if (!$fieldInformation->getRelationshipType()->isSingularRelationship()) {
             return;
         }
         $configuration = $fieldInformation->getConfiguration();
@@ -892,7 +892,7 @@ class DataHandlerHook
      */
     protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
     {
-        if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::ForeignField) {
+        if (RelationshipType::fromTcaConfiguration($configuration) !== RelationshipType::OneToMany) {
             return;
         }
         $foreignTable = $configuration['foreign_table'];