Skip to content

Commit 4530c30

Browse files
authored
perf(Schema): bulk table introspection (getSchemas) (#265)
Assisted-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
1 parent 8dcf9dc commit 4530c30

18 files changed

Lines changed: 1399 additions & 77 deletions

‎src/Database.php‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Cycle\Database;
1313

14+
use Cycle\Database\Driver\BulkSchemaProviderInterface;
1415
use Cycle\Database\Driver\Driver;
1516
use Cycle\Database\Driver\DriverInterface;
1617
use Cycle\Database\Driver\CursorInterface;
1718
use Cycle\Database\Driver\CursorOptions;
1819
use Cycle\Database\Exception\DriverException;
20+
use Cycle\Database\Schema\AbstractTable;
1921
use Cycle\Database\Query\DeleteQuery;
2022
use Cycle\Database\Query\InsertQuery;
2123
use Cycle\Database\Query\QueryParameters;
@@ -117,6 +119,43 @@ public function table(string $name): Table
117119
return new Table($this, $name);
118120
}
119121

122+
/**
123+
* Introspect several tables at once. When the driver supports batched introspection (currently
124+
* Postgres and SQL Server) this costs a constant number of queries instead of a full
125+
* introspection per table; otherwise it falls back to introspecting each table on its own. The
126+
* observable result is identical to calling {@see Table::getSchema()} for each table.
127+
*
128+
* @param non-empty-string[]|null $tables Table names WITHOUT the database prefix. When `null`,
129+
* every table of the database is introspected (names resolved via the driver, then fed
130+
* into the batched path — this is the explicit "whole database" entry point).
131+
*
132+
* @return array<non-empty-string, AbstractTable> Keyed by the table name.
133+
*/
134+
public function getSchemas(?array $tables = null): array
135+
{
136+
$handler = $this->getDriver(self::READ)->getSchemaHandler();
137+
138+
if ($tables === null) {
139+
$tables = [];
140+
foreach ($handler->getTableNames($this->prefix) as $table) {
141+
$tables[] = \str_contains($table, '.')
142+
? \str_replace('.' . $this->prefix, '.', $table)
143+
: \substr($table, \strlen($this->prefix));
144+
}
145+
}
146+
147+
if ($handler instanceof BulkSchemaProviderInterface) {
148+
return $handler->getSchemas($tables, $this->prefix);
149+
}
150+
151+
$result = [];
152+
foreach ($tables as $table) {
153+
$result[$table] = $handler->getSchema($table, $this->prefix);
154+
}
155+
156+
return $result;
157+
}
158+
120159
/**
121160
* @psalm-param non-empty-string $query
122161
*/
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/**
4+
* This file is part of Cycle ORM package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Cycle\Database\Driver;
13+
14+
use Cycle\Database\Schema\AbstractTable;
15+
16+
/**
17+
* Introspect a set of tables with a constant number of queries instead of one full introspection
18+
* per table.
19+
*
20+
* The list of tables is mandatory: there is deliberately no "no list means the whole database"
21+
* mode, so that the queries never read catalog rows of tables the caller did not ask for. The
22+
* "all tables of the database" scenario is an explicit path in
23+
* {@see \Cycle\Database\Database::getSchemas()} called with no list, which first resolves the names
24+
* via {@see HandlerInterface::getTableNames()} and then passes them here.
25+
*/
26+
interface BulkSchemaProviderInterface
27+
{
28+
/**
29+
* Introspect several tables at once.
30+
*
31+
* Existence is derived from presence in the batched result: a table that does not exist yet is
32+
* returned as an empty schema ({@see AbstractTable::exists()} is `false`), never skipped. For a
33+
* table that does exist the result is identical to {@see HandlerInterface::getSchema()}.
34+
*
35+
* @param non-empty-string[] $tables Table names WITHOUT the database prefix.
36+
* @param string|null $prefix Database specific table prefix applied to every table.
37+
*
38+
* @return array<non-empty-string, AbstractTable> Keyed by the input table name, in the input order.
39+
*/
40+
public function getSchemas(array $tables, ?string $prefix = null): array;
41+
}

‎src/Driver/Handler.php‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
use Cycle\Database\Schema\ComparatorInterface;
2323
use Cycle\Database\Schema\ElementInterface;
2424

25-
abstract class Handler implements HandlerInterface
25+
abstract class Handler implements HandlerInterface, BulkSchemaProviderInterface
2626
{
2727
protected ?DriverInterface $driver = null;
2828

@@ -34,6 +34,26 @@ public function withDriver(DriverInterface $driver): HandlerInterface
3434
return $handler;
3535
}
3636

37+
/**
38+
* Default implementation introspects each table on its own. Drivers whose catalog can be read
39+
* for a set of tables at once override this with a batched implementation; the observable result
40+
* must stay identical to {@see getSchema()}.
41+
*
42+
* @param non-empty-string[] $tables
43+
*
44+
* @return array<non-empty-string, AbstractTable>
45+
*/
46+
#[\Override]
47+
public function getSchemas(array $tables, ?string $prefix = null): array
48+
{
49+
$result = [];
50+
foreach ($tables as $table) {
51+
$result[$table] = $this->getSchema($table, $prefix);
52+
}
53+
54+
return $result;
55+
}
56+
3757
/**
3858
* Associated driver.
3959
*/

‎src/Driver/HandlerInterface.php‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,19 @@ public function withDriver(DriverInterface $driver): self;
6161
/**
6262
* Get all available table names.
6363
*
64-
* @param string|null $prefix
65-
*
64+
* @return array<non-empty-string>
6665
*/
6766
public function getTableNames(string $prefix = ''): array;
6867

6968
/**
7069
* Check if given table exists in database.
71-
*
7270
*/
7371
public function hasTable(string $table): bool;
7472

7573
/**
7674
* Get or create table schema.
7775
*
7876
* @throws HandlerException
79-
*
8077
*/
8178
public function getSchema(string $table, ?string $prefix = null): AbstractTable;
8279

0 commit comments

Comments
 (0)