Skip to content

Commit 6afc0ff

Browse files
pl-githubtemp
authored andcommitted
feat: Support mysql based schema tests
1 parent 754afd7 commit 6afc0ff

9 files changed

+426
-11
lines changed

src/Schema/CreateApplySchema.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public function __invoke(Connection $connection): ApplySchema
2323
return new SqliteFileBasedApplySchema();
2424
}
2525

26+
if ($this->isMysql($params)) {
27+
return new MysqlBasedApplySchema();
28+
}
29+
2630
throw NoApplySchemaStrategyFound::forConnectionParameters($params);
2731
}
2832

@@ -46,4 +50,12 @@ private function isSqLiteFile(array $params): bool
4650

4751
return $path !== '' && (str_contains($url, 'sqlite:') || str_ends_with($driver, 'sqlite'));
4852
}
53+
54+
/** @param mixed[] $params */
55+
private function isMysql(array $params): bool
56+
{
57+
$driver = (string) ($params['driver'] ?? '');
58+
59+
return str_ends_with($driver, 'mysql');
60+
}
4961
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brainbits\FunctionalTestHelpers\Schema;
6+
7+
use ArrayObject;
8+
use Doctrine\DBAL\Connection;
9+
10+
use function count;
11+
use function sprintf;
12+
13+
final class MysqlBasedApplySchema implements ApplySchema
14+
{
15+
public static ArrayObject $executedStatements;
16+
17+
public function __construct(bool $resetExecutedStatements = false)
18+
{
19+
if ($resetExecutedStatements) {
20+
self::$executedStatements = new ArrayObject();
21+
}
22+
23+
self::$executedStatements ??= new ArrayObject();
24+
}
25+
26+
public function __invoke(SchemaBuilder $schemaBuilder, Connection $connection): void
27+
{
28+
$platform = $connection->getDatabasePlatform();
29+
30+
$existingTables = $connection->executeQuery($platform->getListTablesSQL())->fetchFirstColumn();
31+
32+
if (count($existingTables) > 0) {
33+
if (count(self::$executedStatements) === 0) {
34+
$this->dropTables($existingTables, $connection);
35+
36+
self::$executedStatements = new ArrayObject();
37+
} else {
38+
$this->deleteData($existingTables, $connection);
39+
}
40+
}
41+
42+
foreach ($schemaBuilder->getSchema()->toSql($platform) as $sql) {
43+
if (self::$executedStatements->offsetExists($sql)) {
44+
continue;
45+
}
46+
47+
self::$executedStatements[$sql] = $connection->executeStatement($sql);
48+
}
49+
}
50+
51+
/** @param list<string> $tables */
52+
private function dropTables(array $tables, Connection $connection): void
53+
{
54+
$platform = $connection->getDatabasePlatform();
55+
56+
try {
57+
$connection->executeStatement('SET foreign_key_checks = 0');
58+
59+
foreach ($tables as $table) {
60+
$listForeignKeysSql = $platform->getListTableForeignKeysSQL($table);
61+
$foreignKeys = $connection->executeQuery($listForeignKeysSql)->fetchFirstColumn();
62+
63+
foreach ($foreignKeys as $foreignKey) {
64+
$dropForeignKeySQL = $platform->getDropForeignKeySQL($foreignKey, $table);
65+
$connection->executeStatement($dropForeignKeySQL);
66+
}
67+
68+
$dropTableSQL = $platform->getDropTableSQL($table);
69+
$connection->executeStatement($dropTableSQL);
70+
}
71+
} finally {
72+
$connection->executeStatement('SET foreign_key_checks = 1');
73+
}
74+
}
75+
76+
/** @param list<string> $tables */
77+
private function deleteData(array $tables, Connection $connection): void
78+
{
79+
try {
80+
$connection->executeStatement('SET foreign_key_checks = 0');
81+
82+
foreach ($tables as $table) {
83+
$quotedTableName = $connection->quoteIdentifier($table);
84+
85+
$connection->executeStatement(sprintf('DELETE FROM %s', $quotedTableName));
86+
$connection->executeStatement(sprintf('ALTER TABLE %s AUTO_INCREMENT = 0', $quotedTableName));
87+
}
88+
} finally {
89+
$connection->executeStatement('SET foreign_key_checks = 1');
90+
}
91+
}
92+
}

src/Schema/SchemaTrait.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,31 @@ private function applySchema(SchemaBuilder $schemaBuilder, Connection $connectio
5959
private function applyData(DataBuilder $dataBuilder, Connection $connection, bool $quoteDataTable = true): void
6060
{
6161
foreach ($dataBuilder->getData() as $table => $rows) {
62+
$table = $quoteDataTable ? $connection->quoteIdentifier($table) : $table;
63+
6264
foreach ($rows as $row) {
63-
$connection->insert($quoteDataTable ? $connection->quoteIdentifier($table) : $table, $row);
65+
$row = $quoteDataTable ? $this->quoteKeys($connection, $row) : $row;
66+
67+
$connection->insert($table, $row);
6468
}
6569
}
6670
}
71+
72+
/**
73+
* @param array<string, mixed> $row
74+
*
75+
* @return array<string, mixed>
76+
*
77+
* @interal
78+
*/
79+
private function quoteKeys(Connection $connection, mixed $row): array
80+
{
81+
$quotedRow = [];
82+
83+
foreach ($row as $key => $value) {
84+
$quotedRow[$connection->quoteIdentifier($key)] = $value;
85+
}
86+
87+
return $quotedRow;
88+
}
6789
}

tests/Schema/CreateApplySchemaTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Brainbits\FunctionalTestHelpers\Tests\Schema;
66

77
use Brainbits\FunctionalTestHelpers\Schema\CreateApplySchema;
8+
use Brainbits\FunctionalTestHelpers\Schema\MysqlBasedApplySchema;
89
use Brainbits\FunctionalTestHelpers\Schema\NoApplySchemaStrategyFound;
910
use Brainbits\FunctionalTestHelpers\Schema\SqliteFileBasedApplySchema;
1011
use Brainbits\FunctionalTestHelpers\Schema\SqliteMemoryBasedApplySchema;
@@ -62,6 +63,18 @@ public function testItUsesSqliteStrategyIfConfiguredByParams(): void
6263
$this->assertInstanceOf(SqliteFileBasedApplySchema::class, $applySchema);
6364
}
6465

66+
public function testItUsesSqliteStrategyIfDriverIsSelected(): void
67+
{
68+
$connection = $this->createMock(Connection::class);
69+
$connection->expects($this->once())
70+
->method('getParams')
71+
->willReturn(['driver' => 'pdo_mysql']);
72+
73+
$applySchema = (new CreateApplySchema())($connection);
74+
75+
$this->assertInstanceOf(MysqlBasedApplySchema::class, $applySchema);
76+
}
77+
6578
public function testItThrowsException(): void
6679
{
6780
$this->expectException(NoApplySchemaStrategyFound::class);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brainbits\FunctionalTestHelpers\Tests\Schema;
6+
7+
use ArrayObject;
8+
use Brainbits\FunctionalTestHelpers\Schema\MysqlBasedApplySchema;
9+
use Brainbits\FunctionalTestHelpers\Schema\SchemaBuilder;
10+
use Brainbits\FunctionalTestHelpers\Snapshot\SnapshotTrait;
11+
use Doctrine\DBAL\Cache\ArrayResult;
12+
use Doctrine\DBAL\Connection;
13+
use Doctrine\DBAL\Platforms\MySQLPlatform;
14+
use Doctrine\DBAL\Result;
15+
use Doctrine\DBAL\Schema\Schema;
16+
use PHPUnit\Framework\TestCase;
17+
18+
use function func_get_arg;
19+
use function Safe\preg_match;
20+
use function str_starts_with;
21+
22+
/** @covers \Brainbits\FunctionalTestHelpers\Schema\MysqlBasedApplySchema */
23+
final class MysqlBasedApplySchemaTest extends TestCase
24+
{
25+
use SnapshotTrait;
26+
27+
private MySQLPlatform $platform;
28+
private SchemaBuilder $schemaBuilder;
29+
30+
protected function setUp(): void
31+
{
32+
$this->platform = new MySQLPlatform();
33+
34+
$this->schemaBuilder = $this->createSchemaBuilder();
35+
$this->schemaBuilder->foo();
36+
}
37+
38+
public function testApplySchema(): void
39+
{
40+
/** @phpstan-var ArrayObject<string, mixed[]> $queryLog */
41+
$queryLog = new ArrayObject();
42+
43+
$connection = $this->createMock(Connection::class);
44+
$connection->expects($this->any())
45+
->method('quoteIdentifier')
46+
->willReturnCallback($this->platform->quoteIdentifier(...));
47+
$connection->expects($this->any())
48+
->method('getParams')
49+
->willReturn(['driver' => 'pdo_mysql']);
50+
$connection->expects($this->any())
51+
->method('getDatabasePlatform')
52+
->willReturn($this->platform);
53+
$connection->expects($this->any())
54+
->method('executeStatement')
55+
->willReturnCallback(
56+
static function () use ($queryLog): void {
57+
$queryLog[] = ['statement' => func_get_arg(0)];
58+
},
59+
);
60+
$connection->expects($this->any())
61+
->method('executeQuery')
62+
->willReturnCallback(
63+
static function () use ($queryLog, $connection) {
64+
$query = func_get_arg(0);
65+
$result = [];
66+
67+
$queryLog[] = ['query' => $query, 'result' => $result];
68+
69+
return new Result(new ArrayResult($result), $connection);
70+
},
71+
);
72+
73+
$applySchema = new MysqlBasedApplySchema(resetExecutedStatements: true);
74+
$applySchema($this->schemaBuilder, $connection);
75+
76+
$this->assertMatchesArraySnapshot($queryLog->getArrayCopy());
77+
}
78+
79+
public function testExistingTablesAreDroppedBeforeCreatingFreshSchema(): void
80+
{
81+
/** @phpstan-var ArrayObject<string, mixed[]> $queryLog */
82+
$queryLog = new ArrayObject();
83+
84+
$connection = $this->createMock(Connection::class);
85+
$connection->expects($this->any())
86+
->method('quoteIdentifier')
87+
->willReturnCallback($this->platform->quoteIdentifier(...));
88+
$connection->expects($this->any())
89+
->method('getParams')
90+
->willReturn(['driver' => 'pdo_mysql']);
91+
$connection->expects($this->any())
92+
->method('getDatabasePlatform')
93+
->willReturn($this->platform);
94+
$connection->expects($this->any())
95+
->method('executeStatement')
96+
->willReturnCallback(
97+
static function () use ($queryLog): void {
98+
$queryLog[] = ['statement' => func_get_arg(0)];
99+
},
100+
);
101+
$connection->expects($this->any())
102+
->method('executeQuery')
103+
->willReturnCallback(
104+
static function () use ($queryLog, $connection) {
105+
$query = func_get_arg(0);
106+
107+
$result = [];
108+
if (str_starts_with($query, 'SHOW FULL TABLES WHERE Table_type = \'BASE TABLE\'')) {
109+
// two old tables exists
110+
$result = [['name' => 'old_table_1'], ['name' => 'old_table_2']];
111+
} elseif (preg_match('/SELECT.*CONSTRAINT_NAME.*old_table_1/', $query)) {
112+
// "old_table_1" has two constraints
113+
$result = [['name' => 'constraint_1'], ['name' => 'constraint_2']];
114+
}
115+
116+
$queryLog[] = ['query' => $query, 'result' => $result];
117+
118+
return new Result(new ArrayResult($result), $connection);
119+
},
120+
);
121+
122+
$applySchema = new MysqlBasedApplySchema(resetExecutedStatements: true);
123+
$applySchema($this->schemaBuilder, $connection);
124+
125+
$applySchema = new MysqlBasedApplySchema(resetExecutedStatements: false);
126+
127+
$this->assertMatchesArraySnapshot($queryLog->getArrayCopy());
128+
}
129+
130+
public function testSchemaIsReadFromCacheIfDatabaseAndCacheExists(): void
131+
{
132+
/** @phpstan-var ArrayObject<string, mixed[]> $queryLog */
133+
$queryLog = new ArrayObject();
134+
135+
$connection = $this->createMock(Connection::class);
136+
$connection->expects($this->any())
137+
->method('quoteIdentifier')
138+
->willReturnCallback($this->platform->quoteIdentifier(...));
139+
$connection->expects($this->any())
140+
->method('getParams')
141+
->willReturn(['driver' => 'pdo_mysql']);
142+
$connection->expects($this->any())
143+
->method('getDatabasePlatform')
144+
->willReturn(new MySQLPlatform());
145+
$connection->expects($this->any())
146+
->method('executeStatement')
147+
->willReturnCallback(
148+
static function () use ($queryLog): void {
149+
$queryLog[] = ['statement' => func_get_arg(0)];
150+
},
151+
);
152+
$connection->expects($this->any())
153+
->method('executeQuery')
154+
->willReturnCallback(
155+
static function () use ($queryLog, $connection) {
156+
$query = func_get_arg(0);
157+
$result = [];
158+
159+
$queryLog[] = ['query' => $query, 'result' => $result];
160+
161+
return new Result(new ArrayResult($result), $connection);
162+
},
163+
);
164+
165+
$applySchema = new MysqlBasedApplySchema(resetExecutedStatements: true);
166+
$applySchema($this->schemaBuilder, $connection);
167+
168+
$applySchema = new MysqlBasedApplySchema(resetExecutedStatements: false);
169+
$applySchema($this->schemaBuilder, $connection);
170+
171+
$this->assertMatchesArraySnapshot($queryLog->getArrayCopy());
172+
}
173+
174+
private function createSchemaBuilder(): SchemaBuilder
175+
{
176+
return new class implements SchemaBuilder {
177+
private Schema $schema;
178+
179+
public function __construct()
180+
{
181+
$this->schema = new Schema();
182+
}
183+
184+
public static function create(): SchemaBuilder
185+
{
186+
return new self();
187+
}
188+
189+
public function getSchema(): Schema
190+
{
191+
return $this->schema;
192+
}
193+
194+
public function foo(): void
195+
{
196+
$table = $this->schema->createTable('foo');
197+
$table->addColumn('bar', 'string');
198+
}
199+
};
200+
}
201+
202+
private function snapshotPath(): string
203+
{
204+
return __DIR__;
205+
}
206+
}

0 commit comments

Comments
 (0)