diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 414f24da..f23b5375 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -78,7 +78,16 @@ To provide a change to the testing framework, you have to ./Build/Scripts/runTests.sh -s functional - and confirming that the test runs have the same result as without the + Run the framework's own functional regression tests from that Core + checkout as well. They test the snapshot export and import of + ``DatabaseAccessor`` and run on every database, SQLite included: + + .. code-block:: bash + + ./Build/Scripts/runTests.sh -s functional -d sqlite -- \ + vendor/typo3/testing-framework/Tests/Functional/ + + Confirm that the Core test runs have the same result as without the changes, 4. push the change to your fork by diff --git a/Classes/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessor.php b/Classes/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessor.php index f6729e85..11d00266 100644 --- a/Classes/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessor.php +++ b/Classes/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessor.php @@ -118,6 +118,10 @@ function (string $columnName) use ($table) { // @todo Simplify this after Doctine DBAL v3 support can be dropped. $type = $table->getColumn($columnName)->getType(); $bindingType = $type->getBindingType(); + // Snapshot strings already contain database values, including encoded JSON. + if ($bindingType === ParameterType::STRING) { + return Types::STRING; + } return (enum_exists(ParameterType::class)) ? Type::lookupName($type) : $type->getBindingType(); diff --git a/Tests/Functional/DatabaseAccessorTest.php b/Tests/Functional/DatabaseAccessorTest.php new file mode 100644 index 00000000..52934726 --- /dev/null +++ b/Tests/Functional/DatabaseAccessorTest.php @@ -0,0 +1,83 @@ +get(ConnectionPool::class)->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME); + $schemaManager = $connection->createSchemaManager(); + $table = new Table('tx_testingframework_snapshot'); + $table->addColumn('uid', Types::INTEGER, ['autoincrement' => true]); + $table->addColumn('payload', Types::JSON, ['notnull' => false]); + $table->addColumn('created_at', Types::DATETIME_MUTABLE); + $table->addColumn('enabled', Types::BOOLEAN); + $table->setPrimaryKey(['uid']); + $schemaManager->createTable($table); + + try { + $jsonValues = [ + '{"name":"Alice","nested":{"items":[1,true,null]}}', + '[{"name":"Alice"},{"name":"Bob"}]', + '"quoted \\"text\\""', + '42', + '1.5', + 'true', + 'false', + 'null', + '{}', + '[]', + null, + ]; + foreach ($jsonValues as $index => $json) { + $connection->insert($table->getName(), [ + 'uid' => $index + 1, + 'payload' => $json, + 'created_at' => '2026-11-14 12:34:56', + 'enabled' => $index % 2 === 0, + ], [ + 'uid' => ParameterType::INTEGER, + 'payload' => $json === null ? ParameterType::NULL : ParameterType::STRING, + 'created_at' => ParameterType::STRING, + 'enabled' => ParameterType::BOOLEAN, + ]); + } + $expected = $connection->select(['*'], $table->getName(), [], [], ['uid' => 'ASC'])->fetchAllAssociative(); + self::assertCount(count($jsonValues), $expected); + $subject = new DatabaseAccessor($connection); + $snapshot = json_decode(json_encode($subject->export(), JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR); + $connection->executeStatement('DELETE FROM ' . $connection->quoteIdentifier($table->getName())); + + $subject->import($snapshot); + + self::assertSame($expected, $connection->select(['*'], $table->getName(), [], [], ['uid' => 'ASC'])->fetchAllAssociative()); + } finally { + $schemaManager->dropTable($table->getName()); + } + } +} diff --git a/Tests/Unit/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessorTest.php b/Tests/Unit/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessorTest.php new file mode 100644 index 00000000..138a0a35 --- /dev/null +++ b/Tests/Unit/Core/Functional/Framework/DataHandling/Snapshot/DatabaseAccessorTest.php @@ -0,0 +1,65 @@ + [Types::JSON, '{"value":[1,true,null]}', Types::STRING]; + yield 'database datetime' => [Types::DATETIME_MUTABLE, '2026-11-14 12:34:56', Types::STRING]; + yield 'plain text' => [Types::TEXT, 'unchanged', Types::STRING]; + yield 'integer keeps its type' => [Types::INTEGER, '42', Types::INTEGER]; + } + + #[Test] + #[DataProvider('databaseValuesDataProvider')] + public function exportPreservesValuesAndNormalizesStringBoundTypes(string $type, string $value, string $expectedType): void + { + $table = new Table('tx_testingframework_snapshot'); + $table->addColumn('payload', $type); + $schemaManager = self::createStub(AbstractSchemaManager::class); + $schemaManager->method('listTables')->willReturn([$table]); + $result = self::createStub(Result::class); + $result->method('fetchAllAssociative')->willReturn([['payload' => $value]]); + $queryBuilder = self::createStub(QueryBuilder::class); + $queryBuilder->method('getRestrictions')->willReturn(self::createStub(DefaultRestrictionContainer::class)); + $queryBuilder->method('select')->willReturnSelf(); + $queryBuilder->method('from')->willReturnSelf(); + $queryBuilder->method('executeQuery')->willReturn($result); + $connection = self::createStub(Connection::class); + $connection->method('createSchemaManager')->willReturn($schemaManager); + $connection->method('createQueryBuilder')->willReturn($queryBuilder); + + $export = (new DatabaseAccessor($connection))->export(); + self::assertSame(['payload' => $expectedType], $export[0]['columns']); + self::assertSame([[$value]], $export[0]['items']); + } +}