Skip to content

Commit ec70b92

Browse files
committed
prepare DB reflection for multi-DB analysis
1 parent b563915 commit ec70b92

File tree

7 files changed

+123
-60
lines changed

7 files changed

+123
-60
lines changed

phpunit.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<env name="testdb_password" value="root" />
1818
<!-- Make sure the database is used exclusively for purpose of these tests, data in it could be lost! -->
1919
<env name="testdb_dbname" value="mariastan_test" />
20+
<env name="testdb_dbname_2" value="mariastan_test2" />
2021
</php>
2122

2223
<testsuites>

src/DbReflection/DbReflection.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010
interface DbReflection
1111
{
12+
/** @phpstan-pure */
13+
public function getDefaultDatabase(): string;
14+
1215
/** @throws DbReflectionException */
13-
public function findTableSchema(string $table): Table;
16+
public function findTableSchema(string $table, ?string $database = null): Table;
1417

1518
/** @throws DbReflectionException */
1619
public function findViewDefinition(string $view, ?string $database = null): string;

src/DbReflection/MariaDbFileDbReflection.php

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
use MariaStan\Schema\Table;
1212
use MariaStan\Util\MariaDbErrorCodes;
1313

14+
use function array_column;
15+
use function array_fill_keys;
1416
use function array_map;
1517
use function file_get_contents;
1618
use function hash;
1719
use function is_array;
20+
use function is_string;
1821
use function serialize;
1922
use function unserialize;
2023

@@ -76,10 +79,15 @@ public function __construct(
7679
$this->schemaDump = $dump;
7780
}
7881

82+
public function getDefaultDatabase(): string
83+
{
84+
return $this->defaultDatabase;
85+
}
86+
7987
/** @throws DbReflectionException */
80-
public function findTableSchema(string $table): Table
88+
public function findTableSchema(string $table, ?string $database = null): Table
8189
{
82-
$tableDump = $this->schemaDump['databases'][$this->defaultDatabase]['tables'][$table] ?? [];
90+
$tableDump = $this->schemaDump['databases'][$database ?? $this->defaultDatabase]['tables'][$table] ?? [];
8391

8492
if (! is_array($tableDump)) {
8593
throw new UnexpectedValueException(
@@ -136,59 +144,67 @@ public function getHash(): string
136144
return hash('xxh128', serialize($this->schemaDump));
137145
}
138146

139-
public static function dumpSchema(\mysqli $db, string $database): string
147+
/** @param string|array<string>|null $databases null = all */
148+
public static function dumpSchema(\mysqli $db, string|array|null $databases): string
140149
{
150+
if (is_string($databases)) {
151+
$databases = [$databases];
152+
} elseif ($databases === null) {
153+
$result = $db->query('SELECT SCHEMA_NAME FROM information_schema.SCHEMATA');
154+
$databases = array_column($result->fetch_all(), 0);
155+
}
156+
141157
$result = [
142158
'__version' => self::DUMP_VERSION,
143-
'databases' => [
144-
$database => [
145-
'tables' => [],
146-
'views' => [],
147-
],
148-
],
159+
'databases' => array_fill_keys($databases, [
160+
'tables' => [],
161+
'views' => [],
162+
]),
149163
];
150164

151-
$stmt = $db->prepare('
152-
SELECT * FROM information_schema.COLUMNS
153-
WHERE TABLE_SCHEMA = ?
154-
ORDER BY TABLE_NAME, ORDINAL_POSITION
155-
');
156-
$stmt->execute([$database]);
157-
$columns = $stmt->get_result()->fetch_all(\MYSQLI_ASSOC);
158-
159-
/** @var array<string, scalar|null> $col */
160-
foreach ($columns as $col) {
161-
$result['databases'][$database]['tables'][$col['TABLE_NAME']]['columns'][] = $col;
162-
}
165+
foreach ($databases as $database) {
166+
$stmt = $db->prepare('
167+
SELECT * FROM information_schema.COLUMNS
168+
WHERE TABLE_SCHEMA = ?
169+
ORDER BY TABLE_NAME, ORDINAL_POSITION
170+
');
171+
$stmt->execute([$database]);
172+
$columns = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
173+
174+
/** @var array<string, scalar|null> $col */
175+
foreach ($columns as $col) {
176+
$result['databases'][$database]['tables'][$col['TABLE_NAME']]['columns'][] = $col;
177+
}
163178

164-
$stmt = $db->prepare('
165-
SELECT * FROM information_schema.TABLE_CONSTRAINTS tc
166-
JOIN information_schema.KEY_COLUMN_USAGE kcu
167-
USING (CONSTRAINT_SCHEMA, CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME)
168-
WHERE TABLE_SCHEMA = ? AND tc.CONSTRAINT_TYPE = "FOREIGN KEY"
169-
/* This happens when there is a FK and UNIQUE with same name. */
170-
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
171-
ORDER BY TABLE_NAME, CONSTRAINT_NAME, kcu.ORDINAL_POSITION
172-
');
173-
$stmt->execute([$database]);
174-
$foreignKeys = $stmt->get_result()->fetch_all(\MYSQLI_ASSOC);
175-
176-
/** @var array<string, scalar|null> $fk */
177-
foreach ($foreignKeys as $fk) {
178-
$result['databases'][$database]['tables'][$fk['TABLE_NAME']]['foreign_keys'][] = $fk;
179-
}
179+
$stmt = $db->prepare('
180+
SELECT * FROM information_schema.TABLE_CONSTRAINTS tc
181+
JOIN information_schema.KEY_COLUMN_USAGE kcu
182+
USING (CONSTRAINT_SCHEMA, CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME)
183+
WHERE TABLE_SCHEMA = ? AND tc.CONSTRAINT_TYPE = "FOREIGN KEY"
184+
/* This happens when there is a FK and UNIQUE with same name. */
185+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
186+
ORDER BY TABLE_NAME, CONSTRAINT_NAME, kcu.ORDINAL_POSITION
187+
');
188+
$stmt->execute([$database]);
189+
$foreignKeys = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
190+
191+
/** @var array<string, scalar|null> $fk */
192+
foreach ($foreignKeys as $fk) {
193+
$result['databases'][$database]['tables'][$fk['TABLE_NAME']]['foreign_keys'][] = $fk;
194+
}
180195

181-
$stmt = $db->prepare('
182-
SELECT TABLE_SCHEMA, TABLE_NAME, VIEW_DEFINITION
183-
FROM information_schema.VIEWS
184-
WHERE TABLE_SCHEMA = ?
185-
');
186-
$stmt->execute([$database]);
187-
$views = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
188-
189-
foreach ($views as $view) {
190-
$result['databases'][$view['TABLE_SCHEMA']]['views'][$view['TABLE_NAME']]
191-
= ['definition' => $view['VIEW_DEFINITION']];
196+
$stmt = $db->prepare('
197+
SELECT TABLE_SCHEMA, TABLE_NAME, VIEW_DEFINITION
198+
FROM information_schema.VIEWS
199+
WHERE TABLE_SCHEMA = ?
200+
');
201+
$stmt->execute([$database]);
202+
$views = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
203+
204+
foreach ($views as $view) {
205+
$result['databases'][$view['TABLE_SCHEMA']]['views'][$view['TABLE_NAME']]
206+
= ['definition' => $view['VIEW_DEFINITION']];
207+
}
192208
}
193209

194210
return serialize($result);

src/DbReflection/MariaDbOnlineDbReflection.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
class MariaDbOnlineDbReflection implements DbReflection
2121
{
22-
/** @var array<string, Table> table name => schema */
22+
/** @var array<string, array<string, Table>> database => table name => schema */
2323
private array $parsedSchemas = [];
2424

2525
public function __construct(
@@ -29,11 +29,18 @@ public function __construct(
2929
) {
3030
}
3131

32+
public function getDefaultDatabase(): string
33+
{
34+
return $this->defaultDatabase;
35+
}
36+
3237
/** @throws DbReflectionException */
33-
public function findTableSchema(string $table): Table
38+
public function findTableSchema(string $table, ?string $database = null): Table
3439
{
35-
if (isset($this->parsedSchemas[$table])) {
36-
return $this->parsedSchemas[$table];
40+
$database ??= $this->defaultDatabase;
41+
42+
if (isset($this->parsedSchemas[$database][$table])) {
43+
return $this->parsedSchemas[$database][$table];
3744
}
3845

3946
try {
@@ -42,7 +49,7 @@ public function findTableSchema(string $table): Table
4249
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
4350
ORDER BY ORDINAL_POSITION
4451
');
45-
$stmt->execute([$this->defaultDatabase, $table]);
52+
$stmt->execute([$database, $table]);
4653
/** @var array<array<string, scalar|null>> $tableCols */
4754
$tableCols = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
4855

@@ -55,14 +62,14 @@ public function findTableSchema(string $table): Table
5562
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
5663
ORDER BY CONSTRAINT_NAME, kcu.ORDINAL_POSITION
5764
');
58-
$stmt->execute([$this->defaultDatabase, $table]);
65+
$stmt->execute([$database, $table]);
5966
/** @var array<array<string, scalar|null>> $foreignKeys */
6067
$foreignKeys = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
6168
} catch (mysqli_sql_exception $e) {
6269
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
6370
}
6471

65-
return $this->parsedSchemas[$table] = new Table(
72+
return $this->parsedSchemas[$database][$table] = new Table(
6673
$table,
6774
$this->schemaParser->parseTableColumns($table, $tableCols),
6875
$this->schemaParser->parseTableForeignKeys($foreignKeys),
@@ -98,6 +105,6 @@ public function getViewDefinitions(): array
98105

99106
public function getHash(): string
100107
{
101-
return hash('xxh128', MariaDbFileDbReflection::dumpSchema($this->mysqli, $this->defaultDatabase));
108+
return hash('xxh128', MariaDbFileDbReflection::dumpSchema($this->mysqli, null));
102109
}
103110
}

tests/DbReflection/DbReflectionTest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,19 @@ private static function initDb(): void
132132
SELECT * FROM db_reflection_test;
133133
');
134134

135+
$secondDbName = TestCaseHelper::getSecondDbName();
136+
$secondDbNameQuoted = MysqliUtil::quoteIdentifier($secondDbName);
137+
$db->query("
138+
CREATE OR REPLACE TABLE {$secondDbNameQuoted}.db_reflection_test (
139+
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT
140+
);
141+
");
142+
135143
self::$dumpFile = tmpfile() ?: throw new RuntimeException('tmpfile() failed!');
136-
fwrite(self::$dumpFile, MariaDbFileDbReflection::dumpSchema($db, MysqliUtil::getDatabaseName($db)));
144+
fwrite(
145+
self::$dumpFile,
146+
MariaDbFileDbReflection::dumpSchema($db, [TestCaseHelper::getDefaultDbName(), $secondDbName]),
147+
);
137148
}
138149

139150
/** @return iterable<string, array<mixed>> */
@@ -199,6 +210,8 @@ public function test(DbReflection $reflection): void
199210
'val_binary' => new Column('val_binary', new VarcharType(), false),
200211
'val_varbinary' => new Column('val_varbinary', new VarcharType(), false),
201212
], $schema->columns);
213+
$fqnSchema = $reflection->findTableSchema($tableName, TestCaseHelper::getDefaultDbName());
214+
$this->assertEquals($schema, $fqnSchema, 'Schemas with and without DB name differ.');
202215
}
203216

204217
/** @dataProvider provideDbReflections */
@@ -223,6 +236,17 @@ public function testDefaultValues(DbReflection $reflection): void
223236
$this->assertFnCallDefaultValue('ROUND', $schema->columns['fn_call_default']);
224237
}
225238

239+
/** @dataProvider provideDbReflections */
240+
public function testSeconDb(DbReflection $reflection): void
241+
{
242+
$tableName = 'db_reflection_test';
243+
$schema = $reflection->findTableSchema($tableName, TestCaseHelper::getSecondDbName());
244+
$this->assertSame($tableName, $schema->name);
245+
$this->assertEquals($schema->columns, [
246+
'id' => new Column('id', new IntType(), false, null, true),
247+
]);
248+
}
249+
226250
/** @return iterable<string, array<mixed>> */
227251
public static function provideForeignKeyDbReflections(): iterable
228252
{

tests/DbReflection/data/file-reflection.v3.bin

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/TestCaseHelper.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,24 @@
2121

2222
abstract class TestCaseHelper
2323
{
24+
private const DEFAULT_CONFIG_PREFIX = 'testdb_';
25+
2426
/** @var array<string, mysqli> */
2527
private static array $connections = [];
2628

2729
public static function getDefaultSharedConnection(): mysqli
2830
{
29-
return self::getSharedConnection('testdb_');
31+
return self::getSharedConnection(self::DEFAULT_CONFIG_PREFIX);
32+
}
33+
34+
public static function getDefaultDbName(): string
35+
{
36+
return self::getConfigValue(self::DEFAULT_CONFIG_PREFIX, 'dbname');
37+
}
38+
39+
public static function getSecondDbName(): string
40+
{
41+
return self::getConfigValue(self::DEFAULT_CONFIG_PREFIX, 'dbname_2');
3042
}
3143

3244
public static function getSharedConnection(string $prefix): mysqli

0 commit comments

Comments
 (0)