packages/ztd-query-postgres/tests/Unit/Schema/PgSqlSchemaReflectorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Schema;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Connection\ConnectionInterface;
11use ZtdQuery\Connection\StatementInterface;
12use ZtdQuery\Platform\Postgres\Schema\PgSqlSchemaReflector;
13use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
14
15#[CoversClass(PgSqlSchemaReflector::class)]
16#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\PgSqlSelectRelationParser::class)]
17#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\View\PgSqlViewDefinitionParser::class)]
18#[UsesClass(PgSqlIdentifierQuoter::class)]
19#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::class)]
20#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Catalog\PartitionKeys::class)]
21#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Catalog\TableQueries::class)]
22#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Column\ColumnDefinitionSql::class)]
23#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Column\NativeTypeSql::class)]
24#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\ForeignKeyRow::class)]
25#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\ForeignKeys::class)]
26#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\IndexDefinitions::class)]
27#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\IndexRows::class)]
28#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\PrimaryColumns::class)]
29#[CoversClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\UniqueIndexes::class)]
30#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\FromClause::class)]
31#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\RelationReference::class)]
32#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\PgSqlPartitionParser::class)]
33#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\BoundPredicate::class)]
34#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\ClauseTokens::class)]
35final class PgSqlSchemaReflectorTest extends TestCase
36{
37    public function testReflectViewsReturnsEmptyWhenQueryFails(): void
38    {
39        $connection = self::createStub(ConnectionInterface::class);
40        $connection->method('query')->willReturn(false);
41        self::assertSame([], (new PgSqlSchemaReflector($connection))->reflectViews());
42    }
43    public function testReflectViewsSkipsMalformedRows(): void
44    {
45        $statement = self::createStub(StatementInterface::class);
46        $statement->method('fetchAll')->willReturn([['viewname' => null, 'definition' => 'SELECT 1'], ['viewname' => '', 'definition' => 'SELECT 1'], ['viewname' => 'missing_query'], ['viewname' => 'non_string', 'definition' => null], ['viewname' => 'blank', 'definition' => '   '], ['viewname' => 'active_users', 'definition' => 'SELECT * FROM public.users WHERE active'], ['viewname' => 'all_users', 'definition' => 'SELECT * FROM public.users']]);
47        $connection = self::createStub(ConnectionInterface::class);
48        $connection->method('query')->willReturn($statement);
49        $definitions = (new PgSqlSchemaReflector($connection))->reflectViews();
50        self::assertSame(['active_users', 'all_users'], array_keys($definitions));
51        self::assertSame(['users'], $definitions['active_users']->dependencies);
52    }
53    public function testGetCreateStatementReturnsNullWhenNoColumns(): void
54    {
55        $colStmt = static::createStub(StatementInterface::class);
56        $colStmt->method('fetchAll')->willReturn([]);
57        $connection = static::createStub(ConnectionInterface::class);
58        $connection->method('query')->willReturn($colStmt);
59        $reflector = new PgSqlSchemaReflector($connection);
60        self::assertNull($reflector->getCreateStatement('empty_table'));
61    }
62    public function testGetCreateStatementReturnsNullWhenQueryFails(): void
63    {
64        $connection = static::createStub(ConnectionInterface::class);
65        $connection->method('query')->willReturn(false);
66        $reflector = new PgSqlSchemaReflector($connection);
67        self::assertNull($reflector->getCreateStatement('nonexistent'));
68    }
69    public function testExactSqlForIntegerNotNull(): void
70    {
71        $statement = self::createStub(StatementInterface::class);
72        $statement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4']]);
73        $statement2 = self::createStub(StatementInterface::class);
74        $statement2->method('fetchAll')->willReturn([]);
75        $statement3 = self::createStub(StatementInterface::class);
76        $statement3->method('fetchAll')->willReturn([]);
77        $statementResults = [$statement, $statement2, $statement3];
78        $connection = self::createStub(ConnectionInterface::class);
79        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
80            return array_shift($statementResults) ?? false;
81        });
82        $r = new PgSqlSchemaReflector($connection);
83        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER NOT NULL\n)", $r->getCreateStatement('t'));
84    }
85    public function testExactSqlForSmallint(): void
86    {
87        $statement = self::createStub(StatementInterface::class);
88        $statement->method('fetchAll')->willReturn([['column_name' => 'v', 'data_type' => 'smallint', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int2']]);
89        $statement2 = self::createStub(StatementInterface::class);
90        $statement2->method('fetchAll')->willReturn([]);
91        $statement3 = self::createStub(StatementInterface::class);
92        $statement3->method('fetchAll')->willReturn([]);
93        $statementResults = [$statement, $statement2, $statement3];
94        $connection = self::createStub(ConnectionInterface::class);
95        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
96            return array_shift($statementResults) ?? false;
97        });
98        $r = new PgSqlSchemaReflector($connection);
99        self::assertSame("CREATE TABLE \"t\" (\n  \"v\" SMALLINT\n)", $r->getCreateStatement('t'));
100    }
101    public function testExactSqlForBigint(): void
102    {
103        $statement = self::createStub(StatementInterface::class);
104        $statement->method('fetchAll')->willReturn([['column_name' => 'b', 'data_type' => 'bigint', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int8']]);
105        $statement2 = self::createStub(StatementInterface::class);
106        $statement2->method('fetchAll')->willReturn([]);
107        $statement3 = self::createStub(StatementInterface::class);
108        $statement3->method('fetchAll')->willReturn([]);
109        $statementResults = [$statement, $statement2, $statement3];
110        $connection = self::createStub(ConnectionInterface::class);
111        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
112            return array_shift($statementResults) ?? false;
113        });
114        $r = new PgSqlSchemaReflector($connection);
115        self::assertSame("CREATE TABLE \"t\" (\n  \"b\" BIGINT\n)", $r->getCreateStatement('t'));
116    }
117    public function testExactSqlForReal(): void
118    {
119        $statement = self::createStub(StatementInterface::class);
120        $statement->method('fetchAll')->willReturn([['column_name' => 'f', 'data_type' => 'real', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'float4']]);
121        $statement2 = self::createStub(StatementInterface::class);
122        $statement2->method('fetchAll')->willReturn([]);
123        $statement3 = self::createStub(StatementInterface::class);
124        $statement3->method('fetchAll')->willReturn([]);
125        $statementResults = [$statement, $statement2, $statement3];
126        $connection = self::createStub(ConnectionInterface::class);
127        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
128            return array_shift($statementResults) ?? false;
129        });
130        $r = new PgSqlSchemaReflector($connection);
131        self::assertSame("CREATE TABLE \"t\" (\n  \"f\" REAL\n)", $r->getCreateStatement('t'));
132    }
133    public function testExactSqlForDoublePrecision(): void
134    {
135        $statement = self::createStub(StatementInterface::class);
136        $statement->method('fetchAll')->willReturn([['column_name' => 'd', 'data_type' => 'double precision', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'float8']]);
137        $statement2 = self::createStub(StatementInterface::class);
138        $statement2->method('fetchAll')->willReturn([]);
139        $statement3 = self::createStub(StatementInterface::class);
140        $statement3->method('fetchAll')->willReturn([]);
141        $statementResults = [$statement, $statement2, $statement3];
142        $connection = self::createStub(ConnectionInterface::class);
143        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
144            return array_shift($statementResults) ?? false;
145        });
146        $r = new PgSqlSchemaReflector($connection);
147        self::assertSame("CREATE TABLE \"t\" (\n  \"d\" DOUBLE PRECISION\n)", $r->getCreateStatement('t'));
148    }
149    public function testExactSqlForBoolean(): void
150    {
151        $statement = self::createStub(StatementInterface::class);
152        $statement->method('fetchAll')->willReturn([['column_name' => 'a', 'data_type' => 'boolean', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bool']]);
153        $statement2 = self::createStub(StatementInterface::class);
154        $statement2->method('fetchAll')->willReturn([]);
155        $statement3 = self::createStub(StatementInterface::class);
156        $statement3->method('fetchAll')->willReturn([]);
157        $statementResults = [$statement, $statement2, $statement3];
158        $connection = self::createStub(ConnectionInterface::class);
159        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
160            return array_shift($statementResults) ?? false;
161        });
162        $r = new PgSqlSchemaReflector($connection);
163        self::assertSame("CREATE TABLE \"t\" (\n  \"a\" BOOLEAN\n)", $r->getCreateStatement('t'));
164    }
165    public function testExactSqlForDate(): void
166    {
167        $statement = self::createStub(StatementInterface::class);
168        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'date', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'date']]);
169        $statement2 = self::createStub(StatementInterface::class);
170        $statement2->method('fetchAll')->willReturn([]);
171        $statement3 = self::createStub(StatementInterface::class);
172        $statement3->method('fetchAll')->willReturn([]);
173        $statementResults = [$statement, $statement2, $statement3];
174        $connection = self::createStub(ConnectionInterface::class);
175        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
176            return array_shift($statementResults) ?? false;
177        });
178        $r = new PgSqlSchemaReflector($connection);
179        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" DATE\n)", $r->getCreateStatement('t'));
180    }
181    public function testExactSqlForTimestamp(): void
182    {
183        $statement = self::createStub(StatementInterface::class);
184        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'timestamp without time zone', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'timestamp']]);
185        $statement2 = self::createStub(StatementInterface::class);
186        $statement2->method('fetchAll')->willReturn([]);
187        $statement3 = self::createStub(StatementInterface::class);
188        $statement3->method('fetchAll')->willReturn([]);
189        $statementResults = [$statement, $statement2, $statement3];
190        $connection = self::createStub(ConnectionInterface::class);
191        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
192            return array_shift($statementResults) ?? false;
193        });
194        $r = new PgSqlSchemaReflector($connection);
195        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TIMESTAMP\n)", $r->getCreateStatement('t'));
196    }
197    public function testExactSqlForTimestamptz(): void
198    {
199        $statement = self::createStub(StatementInterface::class);
200        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'timestamp with time zone', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'timestamptz']]);
201        $statement2 = self::createStub(StatementInterface::class);
202        $statement2->method('fetchAll')->willReturn([]);
203        $statement3 = self::createStub(StatementInterface::class);
204        $statement3->method('fetchAll')->willReturn([]);
205        $statementResults = [$statement, $statement2, $statement3];
206        $connection = self::createStub(ConnectionInterface::class);
207        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
208            return array_shift($statementResults) ?? false;
209        });
210        $r = new PgSqlSchemaReflector($connection);
211        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TIMESTAMPTZ\n)", $r->getCreateStatement('t'));
212    }
213    public function testExactSqlForTime(): void
214    {
215        $statement = self::createStub(StatementInterface::class);
216        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'time without time zone', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'time']]);
217        $statement2 = self::createStub(StatementInterface::class);
218        $statement2->method('fetchAll')->willReturn([]);
219        $statement3 = self::createStub(StatementInterface::class);
220        $statement3->method('fetchAll')->willReturn([]);
221        $statementResults = [$statement, $statement2, $statement3];
222        $connection = self::createStub(ConnectionInterface::class);
223        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
224            return array_shift($statementResults) ?? false;
225        });
226        $r = new PgSqlSchemaReflector($connection);
227        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TIME\n)", $r->getCreateStatement('t'));
228    }
229    public function testExactSqlForTimetz(): void
230    {
231        $statement = self::createStub(StatementInterface::class);
232        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'time with time zone', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'timetz']]);
233        $statement2 = self::createStub(StatementInterface::class);
234        $statement2->method('fetchAll')->willReturn([]);
235        $statement3 = self::createStub(StatementInterface::class);
236        $statement3->method('fetchAll')->willReturn([]);
237        $statementResults = [$statement, $statement2, $statement3];
238        $connection = self::createStub(ConnectionInterface::class);
239        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
240            return array_shift($statementResults) ?? false;
241        });
242        $r = new PgSqlSchemaReflector($connection);
243        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TIMETZ\n)", $r->getCreateStatement('t'));
244    }
245    public function testExactSqlForText(): void
246    {
247        $statement = self::createStub(StatementInterface::class);
248        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text']]);
249        $statement2 = self::createStub(StatementInterface::class);
250        $statement2->method('fetchAll')->willReturn([]);
251        $statement3 = self::createStub(StatementInterface::class);
252        $statement3->method('fetchAll')->willReturn([]);
253        $statementResults = [$statement, $statement2, $statement3];
254        $connection = self::createStub(ConnectionInterface::class);
255        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
256            return array_shift($statementResults) ?? false;
257        });
258        $r = new PgSqlSchemaReflector($connection);
259        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TEXT\n)", $r->getCreateStatement('t'));
260    }
261    public function testExactSqlForBytea(): void
262    {
263        $statement = self::createStub(StatementInterface::class);
264        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'bytea', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bytea']]);
265        $statement2 = self::createStub(StatementInterface::class);
266        $statement2->method('fetchAll')->willReturn([]);
267        $statement3 = self::createStub(StatementInterface::class);
268        $statement3->method('fetchAll')->willReturn([]);
269        $statementResults = [$statement, $statement2, $statement3];
270        $connection = self::createStub(ConnectionInterface::class);
271        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
272            return array_shift($statementResults) ?? false;
273        });
274        $r = new PgSqlSchemaReflector($connection);
275        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" BYTEA\n)", $r->getCreateStatement('t'));
276    }
277    public function testExactSqlForJson(): void
278    {
279        $statement = self::createStub(StatementInterface::class);
280        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'json', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'json']]);
281        $statement2 = self::createStub(StatementInterface::class);
282        $statement2->method('fetchAll')->willReturn([]);
283        $statement3 = self::createStub(StatementInterface::class);
284        $statement3->method('fetchAll')->willReturn([]);
285        $statementResults = [$statement, $statement2, $statement3];
286        $connection = self::createStub(ConnectionInterface::class);
287        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
288            return array_shift($statementResults) ?? false;
289        });
290        $r = new PgSqlSchemaReflector($connection);
291        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" JSON\n)", $r->getCreateStatement('t'));
292    }
293    public function testExactSqlForJsonb(): void
294    {
295        $statement = self::createStub(StatementInterface::class);
296        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'jsonb', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'jsonb']]);
297        $statement2 = self::createStub(StatementInterface::class);
298        $statement2->method('fetchAll')->willReturn([]);
299        $statement3 = self::createStub(StatementInterface::class);
300        $statement3->method('fetchAll')->willReturn([]);
301        $statementResults = [$statement, $statement2, $statement3];
302        $connection = self::createStub(ConnectionInterface::class);
303        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
304            return array_shift($statementResults) ?? false;
305        });
306        $r = new PgSqlSchemaReflector($connection);
307        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" JSONB\n)", $r->getCreateStatement('t'));
308    }
309    public function testExactSqlForUuid(): void
310    {
311        $statement = self::createStub(StatementInterface::class);
312        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'uuid', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'uuid']]);
313        $statement2 = self::createStub(StatementInterface::class);
314        $statement2->method('fetchAll')->willReturn([]);
315        $statement3 = self::createStub(StatementInterface::class);
316        $statement3->method('fetchAll')->willReturn([]);
317        $statementResults = [$statement, $statement2, $statement3];
318        $connection = self::createStub(ConnectionInterface::class);
319        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
320            return array_shift($statementResults) ?? false;
321        });
322        $r = new PgSqlSchemaReflector($connection);
323        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" UUID\n)", $r->getCreateStatement('t'));
324    }
325    public function testExactSqlForVarcharWithLen(): void
326    {
327        $statement = self::createStub(StatementInterface::class);
328        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character varying', 'character_maximum_length' => 50, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'varchar']]);
329        $statement2 = self::createStub(StatementInterface::class);
330        $statement2->method('fetchAll')->willReturn([]);
331        $statement3 = self::createStub(StatementInterface::class);
332        $statement3->method('fetchAll')->willReturn([]);
333        $statementResults = [$statement, $statement2, $statement3];
334        $connection = self::createStub(ConnectionInterface::class);
335        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
336            return array_shift($statementResults) ?? false;
337        });
338        $r = new PgSqlSchemaReflector($connection);
339        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" VARCHAR(50)\n)", $r->getCreateStatement('t'));
340    }
341    public function testExactSqlForVarcharNoLen(): void
342    {
343        $statement = self::createStub(StatementInterface::class);
344        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character varying', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'varchar']]);
345        $statement2 = self::createStub(StatementInterface::class);
346        $statement2->method('fetchAll')->willReturn([]);
347        $statement3 = self::createStub(StatementInterface::class);
348        $statement3->method('fetchAll')->willReturn([]);
349        $statementResults = [$statement, $statement2, $statement3];
350        $connection = self::createStub(ConnectionInterface::class);
351        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
352            return array_shift($statementResults) ?? false;
353        });
354        $r = new PgSqlSchemaReflector($connection);
355        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" VARCHAR\n)", $r->getCreateStatement('t'));
356    }
357    public function testExactSqlForCharWithLen(): void
358    {
359        $statement = self::createStub(StatementInterface::class);
360        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character', 'character_maximum_length' => 5, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bpchar']]);
361        $statement2 = self::createStub(StatementInterface::class);
362        $statement2->method('fetchAll')->willReturn([]);
363        $statement3 = self::createStub(StatementInterface::class);
364        $statement3->method('fetchAll')->willReturn([]);
365        $statementResults = [$statement, $statement2, $statement3];
366        $connection = self::createStub(ConnectionInterface::class);
367        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
368            return array_shift($statementResults) ?? false;
369        });
370        $r = new PgSqlSchemaReflector($connection);
371        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" CHAR(5)\n)", $r->getCreateStatement('t'));
372    }
373    public function testExactSqlForCharNoLen(): void
374    {
375        $statement = self::createStub(StatementInterface::class);
376        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bpchar']]);
377        $statement2 = self::createStub(StatementInterface::class);
378        $statement2->method('fetchAll')->willReturn([]);
379        $statement3 = self::createStub(StatementInterface::class);
380        $statement3->method('fetchAll')->willReturn([]);
381        $statementResults = [$statement, $statement2, $statement3];
382        $connection = self::createStub(ConnectionInterface::class);
383        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
384            return array_shift($statementResults) ?? false;
385        });
386        $r = new PgSqlSchemaReflector($connection);
387        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" CHAR(1)\n)", $r->getCreateStatement('t'));
388    }
389    public function testExactSqlPreservesBitWidths(): void
390    {
391        $statement = self::createStub(StatementInterface::class);
392        $statement->method('fetchAll')->willReturn([['column_name' => 'fixed', 'data_type' => 'bit', 'character_maximum_length' => 8, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bit'], ['column_name' => 'varying', 'data_type' => 'bit varying', 'character_maximum_length' => '16', 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'varbit']]);
393        $statement2 = self::createStub(StatementInterface::class);
394        $statement2->method('fetchAll')->willReturn([]);
395        $statement3 = self::createStub(StatementInterface::class);
396        $statement3->method('fetchAll')->willReturn([]);
397        $statementResults = [$statement, $statement2, $statement3];
398        $connection = self::createStub(ConnectionInterface::class);
399        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
400            return array_shift($statementResults) ?? false;
401        });
402        $r = new PgSqlSchemaReflector($connection);
403        self::assertSame("CREATE TABLE \"t\" (\n  \"fixed\" BIT(8),\n  \"varying\" BIT VARYING(16)\n)", $r->getCreateStatement('t'));
404    }
405    public function testExactSqlPreservesUnboundedBitTypes(): void
406    {
407        $statement = self::createStub(StatementInterface::class);
408        $statement->method('fetchAll')->willReturn([['column_name' => 'fixed', 'data_type' => 'bit', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bit'], ['column_name' => 'varying', 'data_type' => 'bit varying', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'varbit']]);
409        $statement2 = self::createStub(StatementInterface::class);
410        $statement2->method('fetchAll')->willReturn([]);
411        $statement3 = self::createStub(StatementInterface::class);
412        $statement3->method('fetchAll')->willReturn([]);
413        $statementResults = [$statement, $statement2, $statement3];
414        $connection = self::createStub(ConnectionInterface::class);
415        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
416            return array_shift($statementResults) ?? false;
417        });
418        $r = new PgSqlSchemaReflector($connection);
419        self::assertSame("CREATE TABLE \"t\" (\n  \"fixed\" BIT,\n  \"varying\" BIT VARYING\n)", $r->getCreateStatement('t'));
420    }
421    public function testExactSqlForNumericPrecisionScale(): void
422    {
423        $statement = self::createStub(StatementInterface::class);
424        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 10, 'numeric_scale' => 2, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
425        $statement2 = self::createStub(StatementInterface::class);
426        $statement2->method('fetchAll')->willReturn([]);
427        $statement3 = self::createStub(StatementInterface::class);
428        $statement3->method('fetchAll')->willReturn([]);
429        $statementResults = [$statement, $statement2, $statement3];
430        $connection = self::createStub(ConnectionInterface::class);
431        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
432            return array_shift($statementResults) ?? false;
433        });
434        $r = new PgSqlSchemaReflector($connection);
435        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(10,2)\n)", $r->getCreateStatement('t'));
436    }
437    public function testExactSqlForNumericPrecisionOnly(): void
438    {
439        $statement = self::createStub(StatementInterface::class);
440        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 18, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
441        $statement2 = self::createStub(StatementInterface::class);
442        $statement2->method('fetchAll')->willReturn([]);
443        $statement3 = self::createStub(StatementInterface::class);
444        $statement3->method('fetchAll')->willReturn([]);
445        $statementResults = [$statement, $statement2, $statement3];
446        $connection = self::createStub(ConnectionInterface::class);
447        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
448            return array_shift($statementResults) ?? false;
449        });
450        $r = new PgSqlSchemaReflector($connection);
451        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(18)\n)", $r->getCreateStatement('t'));
452    }
453    public function testExactSqlForNumericBare(): void
454    {
455        $statement = self::createStub(StatementInterface::class);
456        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
457        $statement2 = self::createStub(StatementInterface::class);
458        $statement2->method('fetchAll')->willReturn([]);
459        $statement3 = self::createStub(StatementInterface::class);
460        $statement3->method('fetchAll')->willReturn([]);
461        $statementResults = [$statement, $statement2, $statement3];
462        $connection = self::createStub(ConnectionInterface::class);
463        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
464            return array_shift($statementResults) ?? false;
465        });
466        $r = new PgSqlSchemaReflector($connection);
467        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC\n)", $r->getCreateStatement('t'));
468    }
469    public function testExactSqlForUserDefinedCitext(): void
470    {
471        $statement = self::createStub(StatementInterface::class);
472        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'USER-DEFINED', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'citext']]);
473        $statement2 = self::createStub(StatementInterface::class);
474        $statement2->method('fetchAll')->willReturn([]);
475        $statement3 = self::createStub(StatementInterface::class);
476        $statement3->method('fetchAll')->willReturn([]);
477        $statementResults = [$statement, $statement2, $statement3];
478        $connection = self::createStub(ConnectionInterface::class);
479        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
480            return array_shift($statementResults) ?? false;
481        });
482        $r = new PgSqlSchemaReflector($connection);
483        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" CITEXT\n)", $r->getCreateStatement('t'));
484    }
485    public function testExactSqlForUserDefinedHstore(): void
486    {
487        $statement = self::createStub(StatementInterface::class);
488        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'USER-DEFINED', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'hstore']]);
489        $statement2 = self::createStub(StatementInterface::class);
490        $statement2->method('fetchAll')->willReturn([]);
491        $statement3 = self::createStub(StatementInterface::class);
492        $statement3->method('fetchAll')->willReturn([]);
493        $statementResults = [$statement, $statement2, $statement3];
494        $connection = self::createStub(ConnectionInterface::class);
495        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
496            return array_shift($statementResults) ?? false;
497        });
498        $r = new PgSqlSchemaReflector($connection);
499        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" HSTORE\n)", $r->getCreateStatement('t'));
500    }
501    public function testExactSqlForUserDefinedLtree(): void
502    {
503        $statement = self::createStub(StatementInterface::class);
504        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'USER-DEFINED', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'ltree']]);
505        $statement2 = self::createStub(StatementInterface::class);
506        $statement2->method('fetchAll')->willReturn([]);
507        $statement3 = self::createStub(StatementInterface::class);
508        $statement3->method('fetchAll')->willReturn([]);
509        $statementResults = [$statement, $statement2, $statement3];
510        $connection = self::createStub(ConnectionInterface::class);
511        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
512            return array_shift($statementResults) ?? false;
513        });
514        $r = new PgSqlSchemaReflector($connection);
515        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" LTREE\n)", $r->getCreateStatement('t'));
516    }
517    public function testExactSqlForUserDefinedCustom(): void
518    {
519        $statement = self::createStub(StatementInterface::class);
520        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'USER-DEFINED', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'myenum']]);
521        $statement2 = self::createStub(StatementInterface::class);
522        $statement2->method('fetchAll')->willReturn([]);
523        $statement3 = self::createStub(StatementInterface::class);
524        $statement3->method('fetchAll')->willReturn([]);
525        $statementResults = [$statement, $statement2, $statement3];
526        $connection = self::createStub(ConnectionInterface::class);
527        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
528            return array_shift($statementResults) ?? false;
529        });
530        $r = new PgSqlSchemaReflector($connection);
531        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" MYENUM\n)", $r->getCreateStatement('t'));
532    }
533    public function testExactSqlForUserDefinedEmpty(): void
534    {
535        $statement = self::createStub(StatementInterface::class);
536        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'USER-DEFINED', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => '']]);
537        $statement2 = self::createStub(StatementInterface::class);
538        $statement2->method('fetchAll')->willReturn([]);
539        $statement3 = self::createStub(StatementInterface::class);
540        $statement3->method('fetchAll')->willReturn([]);
541        $statementResults = [$statement, $statement2, $statement3];
542        $connection = self::createStub(ConnectionInterface::class);
543        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
544            return array_shift($statementResults) ?? false;
545        });
546        $r = new PgSqlSchemaReflector($connection);
547        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TEXT\n)", $r->getCreateStatement('t'));
548    }
549    public function testExactSqlForArrayWithUnderscore(): void
550    {
551        $statement = self::createStub(StatementInterface::class);
552        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'ARRAY', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => '_int4']]);
553        $statement2 = self::createStub(StatementInterface::class);
554        $statement2->method('fetchAll')->willReturn([]);
555        $statement3 = self::createStub(StatementInterface::class);
556        $statement3->method('fetchAll')->willReturn([]);
557        $statementResults = [$statement, $statement2, $statement3];
558        $connection = self::createStub(ConnectionInterface::class);
559        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
560            return array_shift($statementResults) ?? false;
561        });
562        $r = new PgSqlSchemaReflector($connection);
563        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" INT4[]\n)", $r->getCreateStatement('t'));
564    }
565    public function testExactSqlForArrayNoUnderscore(): void
566    {
567        $statement = self::createStub(StatementInterface::class);
568        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'ARRAY', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'mytypes']]);
569        $statement2 = self::createStub(StatementInterface::class);
570        $statement2->method('fetchAll')->willReturn([]);
571        $statement3 = self::createStub(StatementInterface::class);
572        $statement3->method('fetchAll')->willReturn([]);
573        $statementResults = [$statement, $statement2, $statement3];
574        $connection = self::createStub(ConnectionInterface::class);
575        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
576            return array_shift($statementResults) ?? false;
577        });
578        $r = new PgSqlSchemaReflector($connection);
579        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" MYTYPES[]\n)", $r->getCreateStatement('t'));
580    }
581    public function testExactSqlWithDefault(): void
582    {
583        $statement = self::createStub(StatementInterface::class);
584        $statement->method('fetchAll')->willReturn([['column_name' => 's', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => "'active'::text", 'udt_name' => 'text']]);
585        $statement2 = self::createStub(StatementInterface::class);
586        $statement2->method('fetchAll')->willReturn([]);
587        $statement3 = self::createStub(StatementInterface::class);
588        $statement3->method('fetchAll')->willReturn([]);
589        $statementResults = [$statement, $statement2, $statement3];
590        $connection = self::createStub(ConnectionInterface::class);
591        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
592            return array_shift($statementResults) ?? false;
593        });
594        $r = new PgSqlSchemaReflector($connection);
595        self::assertSame("CREATE TABLE \"t\" (\n  \"s\" TEXT NOT NULL DEFAULT 'active'::text\n)", $r->getCreateStatement('t'));
596    }
597    public function testExactSqlWithPk(): void
598    {
599        $statement = self::createStub(StatementInterface::class);
600        $statement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4']]);
601        $statement2 = self::createStub(StatementInterface::class);
602        $statement2->method('fetchAll')->willReturn([['column_name' => 'id']]);
603        $statement3 = self::createStub(StatementInterface::class);
604        $statement3->method('fetchAll')->willReturn([]);
605        $statementResults = [$statement, $statement2, $statement3];
606        $connection = self::createStub(ConnectionInterface::class);
607        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
608            return array_shift($statementResults) ?? false;
609        });
610        $r = new PgSqlSchemaReflector($connection);
611        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER NOT NULL,\n  PRIMARY KEY (\"id\")\n)", $r->getCreateStatement('t'));
612    }
613    public function testExactSqlWithCompositePk(): void
614    {
615        $statement = self::createStub(StatementInterface::class);
616        $statement->method('fetchAll')->willReturn([['column_name' => 'a', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'b', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4']]);
617        $statement2 = self::createStub(StatementInterface::class);
618        $statement2->method('fetchAll')->willReturn([['column_name' => 'a'], ['column_name' => 'b']]);
619        $statement3 = self::createStub(StatementInterface::class);
620        $statement3->method('fetchAll')->willReturn([]);
621        $statementResults = [$statement, $statement2, $statement3];
622        $connection = self::createStub(ConnectionInterface::class);
623        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
624            return array_shift($statementResults) ?? false;
625        });
626        $r = new PgSqlSchemaReflector($connection);
627        self::assertSame("CREATE TABLE \"t\" (\n  \"a\" INTEGER NOT NULL,\n  \"b\" INTEGER NOT NULL,\n  PRIMARY KEY (\"a\", \"b\")\n)", $r->getCreateStatement('t'));
628    }
629    public function testExactSqlWithUnique(): void
630    {
631        $statement = self::createStub(StatementInterface::class);
632        $statement->method('fetchAll')->willReturn([['column_name' => 'e', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text']]);
633        $statement2 = self::createStub(StatementInterface::class);
634        $statement2->method('fetchAll')->willReturn([]);
635        $statement3 = self::createStub(StatementInterface::class);
636        $statement3->method('fetchAll')->willReturn([['constraint_name' => 'uq_e', 'column_name' => 'e']]);
637        $statementResults = [$statement, $statement2, $statement3];
638        $connection = self::createStub(ConnectionInterface::class);
639        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
640            return array_shift($statementResults) ?? false;
641        });
642        $r = new PgSqlSchemaReflector($connection);
643        self::assertSame("CREATE TABLE \"t\" (\n  \"e\" TEXT,\n  CONSTRAINT \"uq_e\" UNIQUE (\"e\")\n)", $r->getCreateStatement('t'));
644    }
645    public function testExactSqlWithCompositeUnique(): void
646    {
647        $statement = self::createStub(StatementInterface::class);
648        $statement->method('fetchAll')->willReturn([['column_name' => 'a', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'b', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
649        $statement2 = self::createStub(StatementInterface::class);
650        $statement2->method('fetchAll')->willReturn([]);
651        $statement3 = self::createStub(StatementInterface::class);
652        $statement3->method('fetchAll')->willReturn([['constraint_name' => 'uq_ab', 'column_name' => 'a'], ['constraint_name' => 'uq_ab', 'column_name' => 'b']]);
653        $statementResults = [$statement, $statement2, $statement3];
654        $connection = self::createStub(ConnectionInterface::class);
655        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
656            return array_shift($statementResults) ?? false;
657        });
658        $r = new PgSqlSchemaReflector($connection);
659        self::assertSame("CREATE TABLE \"t\" (\n  \"a\" INTEGER,\n  \"b\" INTEGER,\n  CONSTRAINT \"uq_ab\" UNIQUE (\"a\", \"b\")\n)", $r->getCreateStatement('t'));
660    }
661    public function testExactSqlWithPkAndUnique(): void
662    {
663        $statement = self::createStub(StatementInterface::class);
664        $statement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'email', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'text']]);
665        $statement2 = self::createStub(StatementInterface::class);
666        $statement2->method('fetchAll')->willReturn([['column_name' => 'id']]);
667        $statement3 = self::createStub(StatementInterface::class);
668        $statement3->method('fetchAll')->willReturn([['constraint_name' => 'uq_email', 'column_name' => 'email']]);
669        $statementResults = [$statement, $statement2, $statement3];
670        $connection = self::createStub(ConnectionInterface::class);
671        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
672            return array_shift($statementResults) ?? false;
673        });
674        $r = new PgSqlSchemaReflector($connection);
675        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER NOT NULL,\n  \"email\" TEXT NOT NULL,\n  PRIMARY KEY (\"id\"),\n  CONSTRAINT \"uq_email\" UNIQUE (\"email\")\n)", $r->getCreateStatement('t'));
676    }
677    public function testPartialUniqueIndexesReflectsPartialUniqueIndexesWithoutFlatteningTheirPredicate(): void
678    {
679        $statement = self::createStub(StatementInterface::class);
680        $statement->method('fetchAll')->willReturn([['column_name' => 'email', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text'], ['column_name' => 'status', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text']]);
681        $statement2 = self::createStub(StatementInterface::class);
682        $statement2->method('fetchAll')->willReturn([]);
683        $statement3 = self::createStub(StatementInterface::class);
684        $statement3->method('fetchAll')->willReturn([['constraint_name' => 'users_active_email', 'column_name' => 'email', 'predicate' => "status = 'active'::text"], ['constraint_name' => 'users_expression', 'column_name' => null, 'predicate' => 'lower(email) IS NOT NULL']]);
685        $statementResults = [$statement, $statement2, $statement3];
686        $connection = self::createStub(ConnectionInterface::class);
687        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
688            return array_shift($statementResults) ?? false;
689        });
690        $reflector = new PgSqlSchemaReflector($connection);
691        $createSql = $reflector->getCreateStatement('users');
692        $indexes = $reflector->partialUniqueIndexes();
693        self::assertSame("CREATE TABLE \"users\" (\n  \"email\" TEXT,\n  \"status\" TEXT\n)", $createSql);
694        self::assertSame(['users_active_email'], array_keys($indexes['users']));
695        self::assertSame(['email'], $indexes['users']['users_active_email']->columns);
696        self::assertSame("status = 'active'::text", $indexes['users']['users_active_email']->predicate);
697    }
698    public function testCollectsCompositePartialIndexesAndDiscardsMalformedMetadata(): void
699    {
700        $statement = self::createStub(StatementInterface::class);
701        $statement->method('fetchAll')->willReturn([['column_name' => 'email', 'data_type' => 'text', 'is_nullable' => 'YES', 'udt_name' => 'text'], ['column_name' => 'tenant_id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4'], ['column_name' => 'status', 'data_type' => 'text', 'is_nullable' => 'YES', 'udt_name' => 'text']]);
702        $statement2 = self::createStub(StatementInterface::class);
703        $statement2->method('fetchAll')->willReturn([]);
704        $statement3 = self::createStub(StatementInterface::class);
705        $statement3->method('fetchAll')->willReturn([['constraint_name' => null, 'column_name' => null, 'predicate' => null], ['constraint_name' => '', 'column_name' => 'email', 'predicate' => null], ['constraint_name' => 'users_active_email', 'column_name' => 'email', 'predicate' => "status = 'active'"], ['constraint_name' => 'users_active_email', 'column_name' => 'tenant_id', 'predicate' => "status = 'active'"], ['constraint_name' => 'users_broken', 'column_name' => null, 'predicate' => 'lower(email) IS NOT NULL'], ['constraint_name' => 'users_broken', 'column_name' => 'email', 'predicate' => 'lower(email) IS NOT NULL'], ['constraint_name' => 'users_status', 'column_name' => 'status', 'predicate' => '   '], ['constraint_name' => 'users_pending_email', 'column_name' => 'email', 'predicate' => "status = 'pending'"]]);
706        $statement4 = self::createStub(StatementInterface::class);
707        $statement4->method('fetchAll')->willReturn([]);
708        $statementResults = [$statement, $statement2, $statement3, $statement4];
709        $connection = self::createStub(ConnectionInterface::class);
710        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
711            return array_shift($statementResults) ?? false;
712        });
713        $reflector = new PgSqlSchemaReflector($connection);
714        $createSql = $reflector->getCreateStatement('users');
715        $indexes = $reflector->partialUniqueIndexes()['users'];
716        self::assertNotNull($createSql);
717        self::assertStringContainsString('CONSTRAINT "users_status" UNIQUE ("status")', $createSql);
718        self::assertStringNotContainsString('users_broken', $createSql);
719        self::assertSame(['users_active_email', 'users_pending_email'], array_keys($indexes));
720        self::assertSame(['email', 'tenant_id'], $indexes['users_active_email']->columns);
721        self::assertSame("status = 'active'", $indexes['users_active_email']->predicate);
722    }
723    public function testEscapesTableNameInEveryMetadataQuery(): void
724    {
725        $queries = [];
726        $columns = self::createStub(StatementInterface::class);
727        $columns->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4']]);
728        $empty = self::createStub(StatementInterface::class);
729        $empty->method('fetchAll')->willReturn([]);
730        $connection = self::createStub(ConnectionInterface::class);
731        $connection->method('query')->willReturnCallback(function (string $sql) use (&$queries, $columns, $empty): StatementInterface {
732            $queries[] = $sql;
733            return str_contains($sql, 'information_schema.columns') ? $columns : $empty;
734        });
735        self::assertNotNull((new PgSqlSchemaReflector($connection))->getCreateStatement("it's"));
736        self::assertCount(4, $queries);
737        self::assertStringContainsString("'it''s'", $queries[0]);
738        self::assertStringContainsString("'it''s'", $queries[1]);
739        self::assertStringContainsString("'it''s'", $queries[2]);
740        self::assertStringContainsString("'it''s'", $queries[3]);
741    }
742    public function testVerifyColumnsQueryExact(): void
743    {
744        $queries = [];
745        $colStmt = static::createStub(StatementInterface::class);
746        $colStmt->method('fetchAll')->willReturn([]);
747        $connection = static::createStub(ConnectionInterface::class);
748        $connection->method('query')->willReturnCallback(function (string $q) use (&$queries, $colStmt) {
749            $queries[] = $q;
750            return $colStmt;
751        });
752        $reflector = new PgSqlSchemaReflector($connection);
753        $reflector->getCreateStatement('users');
754        self::assertSame('SELECT column_name, data_type, character_maximum_length, ' . 'numeric_precision, numeric_scale, is_nullable, column_default, ' . 'udt_name, domain_schema, domain_name, is_identity, identity_generation, ' . 'is_generated, generation_expression ' . 'FROM information_schema.columns ' . "WHERE table_schema = current_schema() AND table_name = 'users' " . 'ORDER BY ordinal_position', $queries[0]);
755    }
756    public function testReflectsSchemaQualifiedDomainTypeInsteadOfItsBaseType(): void
757    {
758        $statement = self::createStub(StatementInterface::class);
759        $statement->method('fetchAll')->willReturn([['column_name' => 'amount', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 5, 'numeric_scale' => 2, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'numeric', 'domain_schema' => 'tenant "one"', 'domain_name' => 'Percentage']]);
760        $statement2 = self::createStub(StatementInterface::class);
761        $statement2->method('fetchAll')->willReturn([]);
762        $statement3 = self::createStub(StatementInterface::class);
763        $statement3->method('fetchAll')->willReturn([]);
764        $statementResults = [$statement, $statement2, $statement3];
765        $connection = self::createStub(ConnectionInterface::class);
766        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
767            return array_shift($statementResults) ?? false;
768        });
769        $reflector = new PgSqlSchemaReflector($connection);
770        self::assertSame("CREATE TABLE \"payments\" (\n  \"amount\" \"tenant \"\"one\"\"\".\"Percentage\" NOT NULL\n)", $reflector->getCreateStatement('payments'));
771    }
772    public function testReflectsDomainWithoutSchemaAsAQuotedType(): void
773    {
774        $statement = self::createStub(StatementInterface::class);
775        $statement->method('fetchAll')->willReturn([['column_name' => 'age', 'data_type' => 'integer', 'is_nullable' => 'YES', 'udt_name' => 'int4', 'domain_schema' => null, 'domain_name' => 'PositiveValue']]);
776        $statement2 = self::createStub(StatementInterface::class);
777        $statement2->method('fetchAll')->willReturn([]);
778        $statement3 = self::createStub(StatementInterface::class);
779        $statement3->method('fetchAll')->willReturn([]);
780        $statementResults = [$statement, $statement2, $statement3];
781        $connection = self::createStub(ConnectionInterface::class);
782        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
783            return array_shift($statementResults) ?? false;
784        });
785        $reflector = new PgSqlSchemaReflector($connection);
786        self::assertSame("CREATE TABLE \"contacts\" (\n  \"age\" \"PositiveValue\"\n)", $reflector->getCreateStatement('contacts'));
787    }
788    public function testReflectsGeneratedAndIdentityColumns(): void
789    {
790        $statement = self::createStub(StatementInterface::class);
791        $statement->method('fetchAll')->willReturn([['column_name' => 'total', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 10, 'numeric_scale' => 2, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric', 'is_identity' => 'NO', 'identity_generation' => null, 'is_generated' => 'ALWAYS', 'generation_expression' => 'qty * unit_price'], ['column_name' => 'id', 'data_type' => 'bigint', 'character_maximum_length' => null, 'numeric_precision' => 64, 'numeric_scale' => 0, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int8', 'is_identity' => 'YES', 'identity_generation' => 'ALWAYS', 'is_generated' => 'NEVER', 'generation_expression' => null], ['column_name' => 'ordinary', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => 32, 'numeric_scale' => 0, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4', 'is_identity' => 'NO', 'identity_generation' => null, 'is_generated' => 'NEVER', 'generation_expression' => 'qty + 1'], ['column_name' => 'blank', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => 32, 'numeric_scale' => 0, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4', 'is_identity' => 'NO', 'identity_generation' => null, 'is_generated' => 'ALWAYS', 'generation_expression' => '   ']]);
792        $statement2 = self::createStub(StatementInterface::class);
793        $statement2->method('fetchAll')->willReturn([]);
794        $statement3 = self::createStub(StatementInterface::class);
795        $statement3->method('fetchAll')->willReturn([]);
796        $statementResults = [$statement, $statement2, $statement3];
797        $connection = self::createStub(ConnectionInterface::class);
798        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
799            return array_shift($statementResults) ?? false;
800        });
801        $reflector = new PgSqlSchemaReflector($connection);
802        self::assertSame("CREATE TABLE \"orders\" (\n  \"total\" NUMERIC(10,2) GENERATED ALWAYS AS (qty * unit_price) STORED,\n  \"id\" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY,\n  \"ordinary\" INTEGER,\n  \"blank\" INTEGER\n)", $reflector->getCreateStatement('orders'));
803    }
804    public function testVerifyPkQueryExact(): void
805    {
806        $queries = [];
807        $colStmt = static::createStub(StatementInterface::class);
808        $colStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
809        $pkStmt = static::createStub(StatementInterface::class);
810        $pkStmt->method('fetchAll')->willReturn([]);
811        $uniqueStmt = static::createStub(StatementInterface::class);
812        $uniqueStmt->method('fetchAll')->willReturn([]);
813        $connection = static::createStub(ConnectionInterface::class);
814        $callCount = 0;
815        $connection->method('query')->willReturnCallback(function (string $q) use (&$callCount, &$queries, $colStmt, $pkStmt, $uniqueStmt) {
816            $callCount++;
817            $queries[] = $q;
818            return match ($callCount) {
819                1 => $colStmt,
820                2 => $pkStmt,
821                3 => $uniqueStmt,
822                default => false,
823            };
824        });
825        $reflector = new PgSqlSchemaReflector($connection);
826        $reflector->getCreateStatement('my_table');
827        self::assertSame('SELECT kcu.column_name ' . 'FROM information_schema.table_constraints tc ' . 'JOIN information_schema.key_column_usage kcu ' . '  ON tc.constraint_name = kcu.constraint_name ' . '  AND tc.table_schema = kcu.table_schema ' . 'WHERE tc.table_schema = current_schema() ' . "  AND tc.table_name = 'my_table' " . "  AND tc.constraint_type = 'PRIMARY KEY' " . 'ORDER BY kcu.ordinal_position', $queries[1]);
828        self::assertSame('SELECT index_relation.relname AS constraint_name, attribute.attname AS column_name, ' . 'pg_get_expr(index_metadata.indpred, index_metadata.indrelid) AS predicate ' . 'FROM pg_catalog.pg_class table_relation ' . 'JOIN pg_catalog.pg_namespace namespace ON namespace.oid = table_relation.relnamespace ' . 'JOIN pg_catalog.pg_index index_metadata ON index_metadata.indrelid = table_relation.oid ' . 'JOIN pg_catalog.pg_class index_relation ON index_relation.oid = index_metadata.indexrelid ' . 'JOIN LATERAL unnest(index_metadata.indkey) WITH ORDINALITY key_column(attnum, ordinality) ' . '  ON key_column.ordinality <= index_metadata.indnkeyatts ' . 'LEFT JOIN pg_catalog.pg_attribute attribute ' . '  ON attribute.attrelid = table_relation.oid AND attribute.attnum = key_column.attnum ' . 'WHERE namespace.nspname = current_schema() ' . "  AND table_relation.relname = 'my_table' " . '  AND index_metadata.indisunique ' . '  AND index_metadata.indisvalid ' . '  AND NOT index_metadata.indisprimary ' . 'ORDER BY index_relation.relname, key_column.ordinality', $queries[2]);
829    }
830    public function testVerifyReflectAllListQuery(): void
831    {
832        $queries = [];
833        $tableStmt = static::createStub(StatementInterface::class);
834        $tableStmt->method('fetchAll')->willReturn([]);
835        $connection = static::createStub(ConnectionInterface::class);
836        $connection->method('query')->willReturnCallback(function (string $q) use (&$queries, $tableStmt) {
837            $queries[] = $q;
838            return $tableStmt;
839        });
840        $reflector = new PgSqlSchemaReflector($connection);
841        $reflector->reflectAll();
842        self::assertSame('SELECT table_name FROM information_schema.tables ' . "WHERE table_schema = current_schema() AND table_type = 'BASE TABLE' " . 'ORDER BY table_name', $queries[0]);
843    }
844    public function testReflectAllExactResults(): void
845    {
846        $tableStmt = static::createStub(StatementInterface::class);
847        $tableStmt->method('fetchAll')->willReturn([['table_name' => 'items']]);
848        $colStmt = static::createStub(StatementInterface::class);
849        $colStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4']]);
850        $emptyStmt = static::createStub(StatementInterface::class);
851        $emptyStmt->method('fetchAll')->willReturn([]);
852        $connection = static::createStub(ConnectionInterface::class);
853        $callCount = 0;
854        $connection->method('query')->willReturnCallback(function () use (&$callCount, $tableStmt, $colStmt, $emptyStmt) {
855            $callCount++;
856            return match ($callCount) {
857                1 => $tableStmt,
858                2 => $colStmt,
859                3 => $emptyStmt,
860                4 => $emptyStmt,
861                default => false,
862            };
863        });
864        $reflector = new PgSqlSchemaReflector($connection);
865        $result = $reflector->reflectAll();
866        self::assertSame(['items' => "CREATE TABLE \"items\" (\n  \"id\" INTEGER NOT NULL\n)"], $result);
867    }
868    public function testReflectAllSkipsInvalidNames(): void
869    {
870        $tableStmt = static::createStub(StatementInterface::class);
871        $tableStmt->method('fetchAll')->willReturn([['table_name' => ''], ['table_name' => null], ['other' => 'x']]);
872        $connection = static::createStub(ConnectionInterface::class);
873        $connection->method('query')->willReturn($tableStmt);
874        $reflector = new PgSqlSchemaReflector($connection);
875        self::assertSame([], $reflector->reflectAll());
876    }
877    public function testReflectAllReturnsEmptyOnFailure(): void
878    {
879        $connection = static::createStub(ConnectionInterface::class);
880        $connection->method('query')->willReturn(false);
881        $reflector = new PgSqlSchemaReflector($connection);
882        self::assertSame([], $reflector->reflectAll());
883    }
884    public function testPkQueryReturnsFalseStillWorks(): void
885    {
886        $colStmt = static::createStub(StatementInterface::class);
887        $colStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
888        $uniqueStmt = static::createStub(StatementInterface::class);
889        $uniqueStmt->method('fetchAll')->willReturn([]);
890        $connection = static::createStub(ConnectionInterface::class);
891        $callCount = 0;
892        $connection->method('query')->willReturnCallback(function () use (&$callCount, $colStmt, $uniqueStmt) {
893            $callCount++;
894            return match ($callCount) {
895                1 => $colStmt,
896                2 => false,
897                3 => $uniqueStmt,
898                default => false,
899            };
900        });
901        $reflector = new PgSqlSchemaReflector($connection);
902        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER\n)", $reflector->getCreateStatement('t'));
903    }
904    public function testUniqueQueryReturnsFalseStillWorks(): void
905    {
906        $colStmt = static::createStub(StatementInterface::class);
907        $colStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
908        $pkStmt = static::createStub(StatementInterface::class);
909        $pkStmt->method('fetchAll')->willReturn([]);
910        $connection = static::createStub(ConnectionInterface::class);
911        $callCount = 0;
912        $connection->method('query')->willReturnCallback(function () use (&$callCount, $colStmt, $pkStmt) {
913            $callCount++;
914            return match ($callCount) {
915                1 => $colStmt,
916                2 => $pkStmt,
917                3 => false,
918                default => false,
919            };
920        });
921        $reflector = new PgSqlSchemaReflector($connection);
922        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER\n)", $reflector->getCreateStatement('t'));
923    }
924    public function testTableNameEscaping(): void
925    {
926        $queries = [];
927        $colStmt = static::createStub(StatementInterface::class);
928        $colStmt->method('fetchAll')->willReturn([]);
929        $connection = static::createStub(ConnectionInterface::class);
930        $connection->method('query')->willReturnCallback(function (string $q) use (&$queries, $colStmt) {
931            $queries[] = $q;
932            return $colStmt;
933        });
934        $reflector = new PgSqlSchemaReflector($connection);
935        $reflector->getCreateStatement("it's");
936        self::assertStringContainsString("table_name = 'it''s'", $queries[0]);
937    }
938    public function testNumericStringPrecisionScale(): void
939    {
940        $statement = self::createStub(StatementInterface::class);
941        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => '8', 'numeric_scale' => '3', 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
942        $statement2 = self::createStub(StatementInterface::class);
943        $statement2->method('fetchAll')->willReturn([]);
944        $statement3 = self::createStub(StatementInterface::class);
945        $statement3->method('fetchAll')->willReturn([]);
946        $statementResults = [$statement, $statement2, $statement3];
947        $connection = self::createStub(ConnectionInterface::class);
948        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
949            return array_shift($statementResults) ?? false;
950        });
951        $r = new PgSqlSchemaReflector($connection);
952        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(8,3)\n)", $r->getCreateStatement('t'));
953    }
954    public function testVarcharStringLength(): void
955    {
956        $statement = self::createStub(StatementInterface::class);
957        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character varying', 'character_maximum_length' => '100', 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'varchar']]);
958        $statement2 = self::createStub(StatementInterface::class);
959        $statement2->method('fetchAll')->willReturn([]);
960        $statement3 = self::createStub(StatementInterface::class);
961        $statement3->method('fetchAll')->willReturn([]);
962        $statementResults = [$statement, $statement2, $statement3];
963        $connection = self::createStub(ConnectionInterface::class);
964        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
965            return array_shift($statementResults) ?? false;
966        });
967        $r = new PgSqlSchemaReflector($connection);
968        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" VARCHAR(100)\n)", $r->getCreateStatement('t'));
969    }
970    public function testCharStringLength(): void
971    {
972        $statement = self::createStub(StatementInterface::class);
973        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'character', 'character_maximum_length' => '3', 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'bpchar']]);
974        $statement2 = self::createStub(StatementInterface::class);
975        $statement2->method('fetchAll')->willReturn([]);
976        $statement3 = self::createStub(StatementInterface::class);
977        $statement3->method('fetchAll')->willReturn([]);
978        $statementResults = [$statement, $statement2, $statement3];
979        $connection = self::createStub(ConnectionInterface::class);
980        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
981            return array_shift($statementResults) ?? false;
982        });
983        $r = new PgSqlSchemaReflector($connection);
984        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" CHAR(3)\n)", $r->getCreateStatement('t'));
985    }
986    public function testNumericStringPrecisionOnly(): void
987    {
988        $statement = self::createStub(StatementInterface::class);
989        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => '12', 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
990        $statement2 = self::createStub(StatementInterface::class);
991        $statement2->method('fetchAll')->willReturn([]);
992        $statement3 = self::createStub(StatementInterface::class);
993        $statement3->method('fetchAll')->willReturn([]);
994        $statementResults = [$statement, $statement2, $statement3];
995        $connection = self::createStub(ConnectionInterface::class);
996        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
997            return array_shift($statementResults) ?? false;
998        });
999        $r = new PgSqlSchemaReflector($connection);
1000        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(12)\n)", $r->getCreateStatement('t'));
1001    }
1002    public function testMissingColumnNameDefaults(): void
1003    {
1004        $statement = self::createStub(StatementInterface::class);
1005        $statement->method('fetchAll')->willReturn([['data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
1006        $statement2 = self::createStub(StatementInterface::class);
1007        $statement2->method('fetchAll')->willReturn([]);
1008        $statement3 = self::createStub(StatementInterface::class);
1009        $statement3->method('fetchAll')->willReturn([]);
1010        $statementResults = [$statement, $statement2, $statement3];
1011        $connection = self::createStub(ConnectionInterface::class);
1012        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1013            return array_shift($statementResults) ?? false;
1014        });
1015        $r = new PgSqlSchemaReflector($connection);
1016        self::assertSame("CREATE TABLE \"t\" (\n  \"\" INTEGER\n)", $r->getCreateStatement('t'));
1017    }
1018    public function testMissingDataTypeDefaultsToText(): void
1019    {
1020        $statement = self::createStub(StatementInterface::class);
1021        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => '']]);
1022        $statement2 = self::createStub(StatementInterface::class);
1023        $statement2->method('fetchAll')->willReturn([]);
1024        $statement3 = self::createStub(StatementInterface::class);
1025        $statement3->method('fetchAll')->willReturn([]);
1026        $statementResults = [$statement, $statement2, $statement3];
1027        $connection = self::createStub(ConnectionInterface::class);
1028        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1029            return array_shift($statementResults) ?? false;
1030        });
1031        $r = new PgSqlSchemaReflector($connection);
1032        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TEXT\n)", $r->getCreateStatement('t'));
1033    }
1034    public function testUnknownDataTypePassedThrough(): void
1035    {
1036        $statement = self::createStub(StatementInterface::class);
1037        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'xml', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'xml']]);
1038        $statement2 = self::createStub(StatementInterface::class);
1039        $statement2->method('fetchAll')->willReturn([]);
1040        $statement3 = self::createStub(StatementInterface::class);
1041        $statement3->method('fetchAll')->willReturn([]);
1042        $statementResults = [$statement, $statement2, $statement3];
1043        $connection = self::createStub(ConnectionInterface::class);
1044        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1045            return array_shift($statementResults) ?? false;
1046        });
1047        $r = new PgSqlSchemaReflector($connection);
1048        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" XML\n)", $r->getCreateStatement('t'));
1049    }
1050    public function testReflectAllMultipleTablesExactResults(): void
1051    {
1052        $tableStmt = static::createStub(StatementInterface::class);
1053        $tableStmt->method('fetchAll')->willReturn([['table_name' => 'alpha'], ['table_name' => 'beta']]);
1054        $colStmtA = static::createStub(StatementInterface::class);
1055        $colStmtA->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4']]);
1056        $colStmtB = static::createStub(StatementInterface::class);
1057        $colStmtB->method('fetchAll')->willReturn([['column_name' => 'val', 'data_type' => 'text', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text']]);
1058        $emptyStmt = static::createStub(StatementInterface::class);
1059        $emptyStmt->method('fetchAll')->willReturn([]);
1060        $connection = static::createStub(ConnectionInterface::class);
1061        $callCount = 0;
1062        $connection->method('query')->willReturnCallback(function () use (&$callCount, $tableStmt, $colStmtA, $colStmtB, $emptyStmt) {
1063            $callCount++;
1064            return match ($callCount) {
1065                1 => $tableStmt,
1066                2 => $colStmtA,
1067                3 => $emptyStmt,
1068                4 => $emptyStmt,
1069                5 => $emptyStmt,
1070                6 => $colStmtB,
1071                7 => $emptyStmt,
1072                8 => $emptyStmt,
1073                9 => $emptyStmt,
1074                default => false,
1075            };
1076        });
1077        $reflector = new PgSqlSchemaReflector($connection);
1078        $result = $reflector->reflectAll();
1079        self::assertCount(2, $result);
1080        self::assertArrayHasKey('alpha', $result);
1081        self::assertArrayHasKey('beta', $result);
1082        self::assertSame("CREATE TABLE \"alpha\" (\n  \"id\" INTEGER NOT NULL\n)", $result['alpha']);
1083        self::assertSame("CREATE TABLE \"beta\" (\n  \"val\" TEXT\n)", $result['beta']);
1084    }
1085    public function testReflectAllSkipsTablesWithNoColumns(): void
1086    {
1087        $tableStmt = static::createStub(StatementInterface::class);
1088        $tableStmt->method('fetchAll')->willReturn([['table_name' => 'good'], ['table_name' => 'empty_cols']]);
1089        $colStmtGood = static::createStub(StatementInterface::class);
1090        $colStmtGood->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
1091        $colStmtEmpty = static::createStub(StatementInterface::class);
1092        $colStmtEmpty->method('fetchAll')->willReturn([]);
1093        $emptyStmt = static::createStub(StatementInterface::class);
1094        $emptyStmt->method('fetchAll')->willReturn([]);
1095        $connection = static::createStub(ConnectionInterface::class);
1096        $callCount = 0;
1097        $connection->method('query')->willReturnCallback(function () use (&$callCount, $tableStmt, $colStmtGood, $colStmtEmpty, $emptyStmt) {
1098            $callCount++;
1099            return match ($callCount) {
1100                1 => $tableStmt,
1101                2 => $colStmtGood,
1102                3 => $emptyStmt,
1103                4 => $emptyStmt,
1104                5 => $emptyStmt,
1105                6 => $colStmtEmpty,
1106                default => false,
1107            };
1108        });
1109        $reflector = new PgSqlSchemaReflector($connection);
1110        $result = $reflector->reflectAll();
1111        self::assertCount(1, $result);
1112        self::assertArrayHasKey('good', $result);
1113    }
1114    public function testNumericNonIntPrecisionCastsToInt(): void
1115    {
1116        $statement = self::createStub(StatementInterface::class);
1117        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 'abc', 'numeric_scale' => 'xyz', 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
1118        $statement2 = self::createStub(StatementInterface::class);
1119        $statement2->method('fetchAll')->willReturn([]);
1120        $statement3 = self::createStub(StatementInterface::class);
1121        $statement3->method('fetchAll')->willReturn([]);
1122        $statementResults = [$statement, $statement2, $statement3];
1123        $connection = self::createStub(ConnectionInterface::class);
1124        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1125            return array_shift($statementResults) ?? false;
1126        });
1127        $r = new PgSqlSchemaReflector($connection);
1128        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(0,0)\n)", $r->getCreateStatement('t'));
1129    }
1130    public function testNumericIntPrecisionAndScaleExactOutput(): void
1131    {
1132        $statement = self::createStub(StatementInterface::class);
1133        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 5, 'numeric_scale' => 3, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
1134        $statement2 = self::createStub(StatementInterface::class);
1135        $statement2->method('fetchAll')->willReturn([]);
1136        $statement3 = self::createStub(StatementInterface::class);
1137        $statement3->method('fetchAll')->willReturn([]);
1138        $statementResults = [$statement, $statement2, $statement3];
1139        $connection = self::createStub(ConnectionInterface::class);
1140        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1141            return array_shift($statementResults) ?? false;
1142        });
1143        $r = new PgSqlSchemaReflector($connection);
1144        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(5,3)\n)", $r->getCreateStatement('t'));
1145    }
1146    public function testNumericPrecisionOnlyNonIntCastsToInt(): void
1147    {
1148        $statement = self::createStub(StatementInterface::class);
1149        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'numeric', 'character_maximum_length' => null, 'numeric_precision' => 'bad', 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
1150        $statement2 = self::createStub(StatementInterface::class);
1151        $statement2->method('fetchAll')->willReturn([]);
1152        $statement3 = self::createStub(StatementInterface::class);
1153        $statement3->method('fetchAll')->willReturn([]);
1154        $statementResults = [$statement, $statement2, $statement3];
1155        $connection = self::createStub(ConnectionInterface::class);
1156        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1157            return array_shift($statementResults) ?? false;
1158        });
1159        $r = new PgSqlSchemaReflector($connection);
1160        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" NUMERIC(0)\n)", $r->getCreateStatement('t'));
1161    }
1162    public function testArrayWithLowercaseUdtName(): void
1163    {
1164        $statement = self::createStub(StatementInterface::class);
1165        $statement->method('fetchAll')->willReturn([['column_name' => 'x', 'data_type' => 'ARRAY', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => '_text']]);
1166        $statement2 = self::createStub(StatementInterface::class);
1167        $statement2->method('fetchAll')->willReturn([]);
1168        $statement3 = self::createStub(StatementInterface::class);
1169        $statement3->method('fetchAll')->willReturn([]);
1170        $statementResults = [$statement, $statement2, $statement3];
1171        $connection = self::createStub(ConnectionInterface::class);
1172        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1173            return array_shift($statementResults) ?? false;
1174        });
1175        $r = new PgSqlSchemaReflector($connection);
1176        self::assertSame("CREATE TABLE \"t\" (\n  \"x\" TEXT[]\n)", $r->getCreateStatement('t'));
1177    }
1178    public function testTableNameWithSingleQuoteInReflectAll(): void
1179    {
1180        $tableStmt = static::createStub(StatementInterface::class);
1181        $tableStmt->method('fetchAll')->willReturn([['table_name' => "it's"]]);
1182        $colStmt = static::createStub(StatementInterface::class);
1183        $colStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
1184        $emptyStmt = static::createStub(StatementInterface::class);
1185        $emptyStmt->method('fetchAll')->willReturn([]);
1186        $connection = static::createStub(ConnectionInterface::class);
1187        $callCount = 0;
1188        $connection->method('query')->willReturnCallback(function (string $q) use (&$callCount, $tableStmt, $colStmt, $emptyStmt) {
1189            $callCount++;
1190            return match ($callCount) {
1191                1 => $tableStmt,
1192                2 => $colStmt,
1193                3 => $emptyStmt,
1194                4 => $emptyStmt,
1195                default => false,
1196            };
1197        });
1198        $reflector = new PgSqlSchemaReflector($connection);
1199        $result = $reflector->reflectAll();
1200        self::assertCount(1, $result);
1201        self::assertArrayHasKey("it's", $result);
1202    }
1203    public function testUniqueConstraintNonStringColumnsAreSkipped(): void
1204    {
1205        $statement = self::createStub(StatementInterface::class);
1206        $statement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
1207        $statement2 = self::createStub(StatementInterface::class);
1208        $statement2->method('fetchAll')->willReturn([]);
1209        $statement3 = self::createStub(StatementInterface::class);
1210        $statement3->method('fetchAll')->willReturn([['constraint_name' => null, 'column_name' => null]]);
1211        $statementResults = [$statement, $statement2, $statement3];
1212        $connection = self::createStub(ConnectionInterface::class);
1213        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1214            return array_shift($statementResults) ?? false;
1215        });
1216        $r = new PgSqlSchemaReflector($connection);
1217        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER\n)", $r->getCreateStatement('t'));
1218    }
1219    public function testPkNonStringColumnNameIsSkipped(): void
1220    {
1221        $statement = self::createStub(StatementInterface::class);
1222        $statement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'int4']]);
1223        $statement2 = self::createStub(StatementInterface::class);
1224        $statement2->method('fetchAll')->willReturn([['column_name' => null]]);
1225        $statement3 = self::createStub(StatementInterface::class);
1226        $statement3->method('fetchAll')->willReturn([]);
1227        $statementResults = [$statement, $statement2, $statement3];
1228        $connection = self::createStub(ConnectionInterface::class);
1229        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1230            return array_shift($statementResults) ?? false;
1231        });
1232        $r = new PgSqlSchemaReflector($connection);
1233        self::assertSame("CREATE TABLE \"t\" (\n  \"id\" INTEGER\n)", $r->getCreateStatement('t'));
1234    }
1235    public function testForeignKeysAreReconstructedWithCompositeColumnsAndActions(): void
1236    {
1237        $statement = self::createStub(StatementInterface::class);
1238        $statement->method('fetchAll')->willReturn([['column_name' => 'tenant_id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4'], ['column_name' => 'parent_id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4']]);
1239        $statement2 = self::createStub(StatementInterface::class);
1240        $statement2->method('fetchAll')->willReturn([]);
1241        $statement3 = self::createStub(StatementInterface::class);
1242        $statement3->method('fetchAll')->willReturn([]);
1243        $statement4 = self::createStub(StatementInterface::class);
1244        $statement4->method('fetchAll')->willReturn([['constraint_name' => 'fk_parent', 'column_name' => 'tenant_id', 'foreign_table_name' => 'parents', 'foreign_column_name' => 'tenant_id', 'update_rule' => 'CASCADE', 'delete_rule' => 'CASCADE'], ['constraint_name' => 'fk_parent', 'column_name' => 'parent_id', 'foreign_table_name' => 'parents', 'foreign_column_name' => 'id', 'update_rule' => 'CASCADE', 'delete_rule' => 'CASCADE']]);
1245        $statementResults = [$statement, $statement2, $statement3, $statement4];
1246        $connection = self::createStub(ConnectionInterface::class);
1247        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1248            return array_shift($statementResults) ?? false;
1249        });
1250        $r = new PgSqlSchemaReflector($connection);
1251        self::assertSame("CREATE TABLE \"children\" (\n" . "  \"tenant_id\" INTEGER NOT NULL,\n" . "  \"parent_id\" INTEGER NOT NULL,\n" . '  CONSTRAINT "fk_parent" FOREIGN KEY ("tenant_id", "parent_id") ' . "REFERENCES \"parents\" (\"tenant_id\", \"id\") ON UPDATE CASCADE ON DELETE CASCADE\n)", $r->getCreateStatement('children'));
1252    }
1253    public function testForeignKeyQueryIsExactAndEscapesTableName(): void
1254    {
1255        $queries = [];
1256        $columnStatement = static::createStub(StatementInterface::class);
1257        $columnStatement->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4']]);
1258        $emptyStatement = static::createStub(StatementInterface::class);
1259        $emptyStatement->method('fetchAll')->willReturn([]);
1260        $connection = static::createStub(ConnectionInterface::class);
1261        $callCount = 0;
1262        $connection->method('query')->willReturnCallback(function (string $query) use (&$callCount, &$queries, $columnStatement, $emptyStatement) {
1263            $queries[] = $query;
1264            $callCount++;
1265            return $callCount === 1 ? $columnStatement : $emptyStatement;
1266        });
1267        (new PgSqlSchemaReflector($connection))->getCreateStatement("child'ren");
1268        self::assertSame('SELECT fk.constraint_name, fk.column_name, ' . 'pk.table_name AS foreign_table_name, pk.column_name AS foreign_column_name, ' . 'rc.update_rule, rc.delete_rule ' . 'FROM information_schema.referential_constraints rc ' . 'JOIN information_schema.key_column_usage fk ' . '  ON fk.constraint_catalog = rc.constraint_catalog ' . '  AND fk.constraint_schema = rc.constraint_schema ' . '  AND fk.constraint_name = rc.constraint_name ' . 'JOIN information_schema.key_column_usage pk ' . '  ON pk.constraint_catalog = rc.unique_constraint_catalog ' . '  AND pk.constraint_schema = rc.unique_constraint_schema ' . '  AND pk.constraint_name = rc.unique_constraint_name ' . '  AND pk.ordinal_position = fk.position_in_unique_constraint ' . 'WHERE fk.table_schema = current_schema() ' . "  AND fk.table_name = 'child''ren' " . 'ORDER BY fk.constraint_name, fk.ordinal_position', $queries[3]);
1269    }
1270    public function testMalformedForeignKeyRowsAreSkippedIndependently(): void
1271    {
1272        $valid = ['constraint_name' => 'fk_parent', 'column_name' => 'parent_id', 'foreign_table_name' => 'parents', 'foreign_column_name' => 'id', 'update_rule' => 'CASCADE', 'delete_rule' => 'CASCADE'];
1273        $rows = [array_replace($valid, ['constraint_name' => null]), array_replace($valid, ['column_name' => null]), array_replace($valid, ['foreign_table_name' => null]), array_replace($valid, ['foreign_column_name' => null]), array_replace($valid, ['update_rule' => null]), array_replace($valid, ['delete_rule' => null]), $valid];
1274        $statement = self::createStub(StatementInterface::class);
1275        $statement->method('fetchAll')->willReturn([['column_name' => 'parent_id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4']]);
1276        $statement2 = self::createStub(StatementInterface::class);
1277        $statement2->method('fetchAll')->willReturn([]);
1278        $statement3 = self::createStub(StatementInterface::class);
1279        $statement3->method('fetchAll')->willReturn([]);
1280        $statement4 = self::createStub(StatementInterface::class);
1281        $statement4->method('fetchAll')->willReturn($rows);
1282        $statementResults = [$statement, $statement2, $statement3, $statement4];
1283        $connection = self::createStub(ConnectionInterface::class);
1284        $connection->method('query')->willReturnCallback(static function () use (&$statementResults): StatementInterface|false {
1285            return array_shift($statementResults) ?? false;
1286        });
1287        $r = new PgSqlSchemaReflector($connection);
1288        self::assertSame("CREATE TABLE \"children\" (\n" . "  \"parent_id\" INTEGER NOT NULL,\n" . '  CONSTRAINT "fk_parent" FOREIGN KEY ("parent_id") ' . "REFERENCES \"parents\" (\"id\") ON UPDATE CASCADE ON DELETE CASCADE\n)", $r->getCreateStatement('children'));
1289    }
1290}
1291