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