packages/ztd-query-postgres/tests/Unit/PgSqlSessionFactoryTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Config\ZtdConfig;
11use ZtdQuery\Connection\ConnectionInterface;
12use ZtdQuery\Connection\StatementInterface;
13use ZtdQuery\Platform\Postgres\Connection\Copy\PgSqlCopySupport;
14use ZtdQuery\Platform\Postgres\Connection\Parameter\PgSqlPdoParameterBindingCompiler;
15use ZtdQuery\Platform\Postgres\Connection\Parameter\PgSqlPdoPlaceholderEscaper;
16use ZtdQuery\Platform\Postgres\Connection\Result\PgSqlPdoResultColumnTypeResolver;
17use ZtdQuery\Platform\Postgres\PgSqlSessionFactory;
18use ZtdQuery\Platform\Postgres\Rewrite\PgSqlQueryGuard;
19use ZtdQuery\Platform\Postgres\Rewrite\PgSqlRewriter;
20use ZtdQuery\Platform\Postgres\Rewrite\Transformer\DeleteTransformer;
21use ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertTransformer;
22use ZtdQuery\Platform\Postgres\Rewrite\Transformer\PgSqlTransformer;
23use ZtdQuery\Platform\Postgres\Rewrite\Transformer\SelectTransformer;
24use ZtdQuery\Platform\Postgres\Rewrite\Transformer\UpdateTransformer;
25use ZtdQuery\Platform\Postgres\Schema\Partition\PgSqlPartitionReflector;
26use ZtdQuery\Platform\Postgres\Schema\PgSqlSchemaParser;
27use ZtdQuery\Platform\Postgres\Schema\PgSqlSchemaReflector;
28use ZtdQuery\Platform\Postgres\Shadow\PgSqlMutationResolver;
29use ZtdQuery\Platform\Postgres\Sql\Partition\PgSqlPartitionParser;
30use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
31use ZtdQuery\Platform\Postgres\Sql\PgSqlParser;
32use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer;
33
34#[CoversClass(PgSqlSessionFactory::class)]
35#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\PgSqlColumnTypeMapper::class)]
36#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\PgSqlForeignKeyDefinitionParser::class)]
37#[UsesClass(PgSqlParser::class)]
38#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\PgSqlConflictTarget::class)]
39#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PostgreSqlLexicalMasker::class)]
40#[UsesClass(PgSqlSchemaParser::class)]
41#[UsesClass(PgSqlPartitionParser::class)]
42#[UsesClass(PgSqlPartitionReflector::class)]
43#[UsesClass(PgSqlQueryGuard::class)]
44#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Diagnostic\PgSqlReadOnlyDiagnosticStatement::class)]
45#[UsesClass(PgSqlRewriter::class)]
46#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\PgSqlSelectRelationParser::class)]
47#[UsesClass(PgSqlMutationResolver::class)]
48#[UsesClass(PgSqlTransformer::class)]
49#[UsesClass(PgSqlSchemaReflector::class)]
50#[UsesClass(PgSqlCastRenderer::class)]
51#[UsesClass(PgSqlCopySupport::class)]
52#[UsesClass(PgSqlPdoParameterBindingCompiler::class)]
53#[UsesClass(PgSqlPdoPlaceholderEscaper::class)]
54#[UsesClass(PgSqlPdoResultColumnTypeResolver::class)]
55#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\PgSqlValueRenderer::class)]
56#[UsesClass(PgSqlIdentifierQuoter::class)]
57#[UsesClass(SelectTransformer::class)]
58#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSampleParser::class)]
59#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\PgSqlTableSampleRewriter::class)]
60#[UsesClass(InsertTransformer::class)]
61#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertRowRenderer::class)]
62#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertSelectRenderer::class)]
63#[UsesClass(UpdateTransformer::class)]
64#[UsesClass(DeleteTransformer::class)]
65#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\PgSqlCteShadowComposer::class)]
66#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\PgSqlNativeUpsertProjector::class)]
67#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\View\PgSqlViewDefinitionParser::class)]
68#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\View\PgSqlViewShadowRenderer::class)]
69#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\GeneratedColumn\PgSqlGeneratedColumnProjector::class)]
70#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Partition\PgSqlPartitionPredicateRenderer::class)]
71#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Returning\PgSqlReturningProjectionParser::class)]
72#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\PgSqlUpsertExpressionParser::class)]
73#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::class)]
74#[CoversClass(\ZtdQuery\Platform\Postgres\Session\SchemaInitializer::class)]
75#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Copy\TargetColumns::class)]
76#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Copy\TargetSql::class)]
77#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Copy\TextFields::class)]
78#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Parameter\EscapeCursor::class)]
79#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Parameter\OperandInput::class)]
80#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Parameter\QuotedInput::class)]
81#[UsesClass(\ZtdQuery\Platform\Postgres\Connection\Parameter\TokenBoundary::class)]
82#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Table\TableMutationResolver::class)]
83#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Row\RowMutationResolver::class)]
84#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\ColumnSet::class)]
85#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Cte\HeaderParser::class)]
86#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Cte\IdentifierReferences::class)]
87#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\PrefixMerge::class)]
88#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\ShadowDependencies::class)]
89#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Diagnostic\KeywordSearch::class)]
90#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ComparisonOperator::class)]
91#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ExpressionCursor::class)]
92#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ExpressionLexeme::class)]
93#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\PrecedenceParser::class)]
94#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\PrimaryParser::class)]
95#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\ActionClause::class)]
96#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\BranchTokens::class)]
97#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\RelationTarget::class)]
98#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\StatementParts::class)]
99#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\FromClause::class)]
100#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\RelationReference::class)]
101#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Returning\ProjectionItem::class)]
102#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\SampleClause::class)]
103#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\SampleTokens::class)]
104#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\StatementClassifier::class)]
105#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::class)]
106#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\IdentifierDecoder::class)]
107#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\TargetTableParser::class)]
108#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Update\UpdateClauseParser::class)]
109#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Delete\DeleteClauseParser::class)]
110#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Insert\InsertClauseParser::class)]
111#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Insert\InsertClauseParser::class)]
112#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\SelectColumns::class)]
113#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\TableDefinitionClauses::class)]
114#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Transaction\KeywordForm::class)]
115#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeActionKind::class)]
116#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeClause::class)]
117#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeMatchKind::class)]
118#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeParser::class)]
119#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeStatement::class)]
120#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSample::class)]
121#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSampleMethod::class)]
122#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Transaction\PgSqlTransactionStatementParser::class)]
123#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Catalog\PartitionKeys::class)]
124#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Catalog\TableQueries::class)]
125#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Column\ColumnDefinitionSql::class)]
126#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Column\NativeTypeSql::class)]
127#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\ForeignKeyRow::class)]
128#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\ForeignKeys::class)]
129#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\IndexDefinitions::class)]
130#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\IndexRows::class)]
131#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\PrimaryColumns::class)]
132#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Reflection\Key\UniqueIndexes::class)]
133#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\SampleProjection::class)]
134#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\TableColumns::class)]
135#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\ConflictPredicate::class)]
136#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\ExpressionBinder::class)]
137#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\ColumnDefinition::class)]
138#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\ColumnTypeDeclaration::class)]
139#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableBody::class)]
140#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableConstraint::class)]
141#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableFields::class)]
142#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\DefinitionEntry::class)]
143#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\DefinitionTokens::class)]
144#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\BoundPredicate::class)]
145#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\ClauseTokens::class)]
146#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Partition\StorageTable::class)]
147#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Context\TableContext::class)]
148#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\StatementRewriter::class)]
149#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\CastTypeMapper::class)]
150#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\CommentSpan::class)]
151#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\QuotedSpan::class)]
152#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\NativeCastTarget::class)]
153#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\BinaryStream::class)]
154#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\LiteralText::class)]
155#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Cte\RowSourceRenderer::class)]
156#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\ExpressionCast::class)]
157#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\OrderedExpressions::class)]
158#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\SelectProjection::class)]
159#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\UpsertProjection::class)]
160#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\ValueProjection::class)]
161#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\MergeTransformer::class)]
162#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Merge\MatchConditions::class)]
163#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Merge\RowActions::class)]
164#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Update\ColumnProjection::class)]
165final class PgSqlSessionFactoryTest extends TestCase
166{
167 public function testCreateRegistersReflectedPartitionMetadata(): void
168 {
169 $tables = self::createStub(StatementInterface::class);
170 $tables->method('fetchAll')->willReturn([['table_name' => 'logs'], ['table_name' => 'logs_2024']]);
171 $columns = self::createStub(StatementInterface::class);
172 $columns->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'character_maximum_length' => null, 'numeric_precision' => 32, 'numeric_scale' => 0, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'log_date', 'data_type' => 'date', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'date']]);
173 $primaryKey = self::createStub(StatementInterface::class);
174 $primaryKey->method('fetchAll')->willReturn([['column_name' => 'id'], ['column_name' => 'log_date']]);
175 $empty = self::createStub(StatementInterface::class);
176 $empty->method('fetchAll')->willReturn([]);
177 $partitionKeys = self::createStub(StatementInterface::class);
178 $partitionKeys->method('fetchAll')->willReturn([['table_name' => 'logs', 'partition_key' => 'RANGE (log_date)']]);
179 $relations = self::createStub(StatementInterface::class);
180 $relations->method('fetchAll')->willReturn([['child_table' => 'logs_2024', 'parent_table' => 'logs', 'predicate' => "log_date >= '2024-01-01'::date AND log_date < '2025-01-01'::date"]]);
181 $connection = self::createStub(ConnectionInterface::class);
182 $connection->method('query')->willReturnCallback(static function (string $sql) use ($tables, $columns, $primaryKey, $empty, $partitionKeys, $relations) {
183 return match (true) {
184 str_contains($sql, 'information_schema.tables') => $tables,
185 str_contains($sql, 'information_schema.columns') => $columns,
186 str_contains($sql, "constraint_type = 'PRIMARY KEY'") => $primaryKey,
187 str_contains($sql, 'SELECT c.relname AS table_name') => $partitionKeys,
188 str_contains($sql, 'SELECT child.relname AS child_table') => $relations,
189 default => $empty,
190 };
191 });
192 $session = (new PgSqlSessionFactory())->create($connection, ZtdConfig::default());
193 $sql = $session->rewrite('SELECT * FROM logs_2024')->sql();
194 self::assertStringContainsString('"logs" AS MATERIALIZED', $sql);
195 self::assertStringContainsString('"logs_2024" AS MATERIALIZED (SELECT * FROM "logs" WHERE log_date >=', $sql);
196 $create = $session->rewrite("CREATE TABLE logs_2025 PARTITION OF logs FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')");
197 self::assertNotNull($create->mutation());
198 }
199 public function testCreateRegistersReflectedViews(): void
200 {
201 $empty = self::createStub(StatementInterface::class);
202 $empty->method('fetchAll')->willReturn([]);
203 $views = self::createStub(StatementInterface::class);
204 $views->method('fetchAll')->willReturn([['viewname' => 'active_users', 'definition' => 'SELECT 1 AS id']]);
205 $connection = self::createStub(ConnectionInterface::class);
206 $connection->method('query')->willReturnCallback(static fn (string $sql): StatementInterface => str_contains($sql, 'pg_views') ? $views : $empty);
207 $session = (new PgSqlSessionFactory())->create($connection, ZtdConfig::default());
208 self::assertSame("WITH \"active_users\" AS MATERIALIZED (SELECT 1 AS id)\nSELECT * FROM active_users", $session->rewrite('SELECT * FROM active_users')->sql());
209 }
210 public function testCreateReturnsSession(): void
211 {
212 $connection = static::createStub(ConnectionInterface::class);
213 $tablesStmt = static::createStub(StatementInterface::class);
214 $tablesStmt->method('fetchAll')->willReturn([]);
215 $connection->method('query')->willReturn($tablesStmt);
216 $factory = new PgSqlSessionFactory();
217 $session = $factory->create($connection, ZtdConfig::default());
218 self::assertTrue($session->isEnabled());
219 self::assertInstanceOf(PgSqlCopySupport::class, $session->copySupport());
220 self::assertInstanceOf(PgSqlPdoParameterBindingCompiler::class, $session->parameterBindingCompiler());
221 self::assertInstanceOf(PgSqlPdoResultColumnTypeResolver::class, $session->resultColumnTypeResolver());
222 }
223 public function testCreatedSessionIsEnabledByDefault(): void
224 {
225 $connection = static::createStub(ConnectionInterface::class);
226 $tablesStmt = static::createStub(StatementInterface::class);
227 $tablesStmt->method('fetchAll')->willReturn([]);
228 $connection->method('query')->willReturn($tablesStmt);
229 $factory = new PgSqlSessionFactory();
230 $session = $factory->create($connection, ZtdConfig::default());
231 self::assertTrue($session->isEnabled());
232 }
233 public function testCreateWithTablesReflectsSchema(): void
234 {
235 $connection = static::createStub(ConnectionInterface::class);
236 $tablesStmt = static::createStub(StatementInterface::class);
237 $tablesStmt->method('fetchAll')->willReturn([['table_name' => 'users']]);
238 $columnsStmt = static::createStub(StatementInterface::class);
239 $columnsStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'INTEGER', 'character_maximum_length' => null, 'numeric_precision' => 32, 'numeric_scale' => 0, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'name', 'data_type' => 'TEXT', 'character_maximum_length' => null, 'numeric_precision' => null, 'numeric_scale' => null, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'text']]);
240 $pkStmt = static::createStub(StatementInterface::class);
241 $pkStmt->method('fetchAll')->willReturn([['column_name' => 'id']]);
242 $uniqueStmt = static::createStub(StatementInterface::class);
243 $uniqueStmt->method('fetchAll')->willReturn([]);
244 $connection->method('query')->willReturnCallback(function (string $sql) use ($tablesStmt, $columnsStmt, $pkStmt, $uniqueStmt) {
245 if (str_contains($sql, 'information_schema.tables')) {
246 return $tablesStmt;
247 }
248 if (str_contains($sql, 'information_schema.columns')) {
249 return $columnsStmt;
250 }
251 if (str_contains($sql, "constraint_type = 'PRIMARY KEY'")) {
252 return $pkStmt;
253 }
254 if (str_contains($sql, "constraint_type = 'UNIQUE'")) {
255 return $uniqueStmt;
256 }
257 return false;
258 });
259 $factory = new PgSqlSessionFactory();
260 $session = $factory->create($connection, ZtdConfig::default());
261 self::assertTrue($session->isEnabled());
262 }
263 public function testCreateWithEmptyDatabaseReturnsSession(): void
264 {
265 $connection = static::createStub(ConnectionInterface::class);
266 $connection->method('query')->willReturn(false);
267 $factory = new PgSqlSessionFactory();
268 $session = $factory->create($connection, ZtdConfig::default());
269 self::assertTrue($session->isEnabled());
270 }
271 public function testSessionCanBeEnabledAfterCreation(): void
272 {
273 $connection = static::createStub(ConnectionInterface::class);
274 $tablesStmt = static::createStub(StatementInterface::class);
275 $tablesStmt->method('fetchAll')->willReturn([]);
276 $connection->method('query')->willReturn($tablesStmt);
277 $factory = new PgSqlSessionFactory();
278 $session = $factory->create($connection, ZtdConfig::default());
279 $session->enable();
280 self::assertTrue($session->isEnabled());
281 }
282 public function testCreateWithTableRegistersSchemaInSession(): void
283 {
284 $connection = static::createStub(ConnectionInterface::class);
285 $tablesStmt = static::createStub(StatementInterface::class);
286 $tablesStmt->method('fetchAll')->willReturn([['table_name' => 'products']]);
287 $columnsStmt = static::createStub(StatementInterface::class);
288 $columnsStmt->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'INTEGER', 'character_maximum_length' => null, 'numeric_precision' => 32, 'numeric_scale' => 0, 'is_nullable' => 'NO', 'column_default' => null, 'udt_name' => 'int4'], ['column_name' => 'price', 'data_type' => 'NUMERIC', 'character_maximum_length' => null, 'numeric_precision' => 10, 'numeric_scale' => 2, 'is_nullable' => 'YES', 'column_default' => null, 'udt_name' => 'numeric']]);
289 $pkStmt = static::createStub(StatementInterface::class);
290 $pkStmt->method('fetchAll')->willReturn([['column_name' => 'id']]);
291 $uniqueStmt = static::createStub(StatementInterface::class);
292 $uniqueStmt->method('fetchAll')->willReturn([]);
293 $connection->method('query')->willReturnCallback(function (string $sql) use ($tablesStmt, $columnsStmt, $pkStmt, $uniqueStmt) {
294 if (str_contains($sql, 'information_schema.tables')) {
295 return $tablesStmt;
296 }
297 if (str_contains($sql, 'information_schema.columns')) {
298 return $columnsStmt;
299 }
300 if (str_contains($sql, "constraint_type = 'PRIMARY KEY'")) {
301 return $pkStmt;
302 }
303 if (str_contains($sql, "constraint_type = 'UNIQUE'")) {
304 return $uniqueStmt;
305 }
306 return false;
307 });
308 $factory = new PgSqlSessionFactory();
309 $session = $factory->create($connection, ZtdConfig::default());
310 $session->enable();
311 $plan = $session->rewrite('SELECT * FROM products');
312 self::assertStringContainsString('"products" AS MATERIALIZED', $plan->sql());
313 }
314 public function testCreateWithNullParseResultStillWorks(): void
315 {
316 $connection = static::createStub(ConnectionInterface::class);
317 $tablesStmt = static::createStub(StatementInterface::class);
318 $tablesStmt->method('fetchAll')->willReturn([['table_name' => 'bad_table']]);
319 $columnsStmt = static::createStub(StatementInterface::class);
320 $columnsStmt->method('fetchAll')->willReturn([]);
321 $connection->method('query')->willReturnCallback(function (string $sql) use ($tablesStmt, $columnsStmt) {
322 if (str_contains($sql, 'information_schema.tables')) {
323 return $tablesStmt;
324 }
325 return $columnsStmt;
326 });
327 $factory = new PgSqlSessionFactory();
328 $session = $factory->create($connection, ZtdConfig::default());
329 self::assertTrue($session->isEnabled());
330 }
331 public function testCreateRegistersReflectedPartialUniqueIndexes(): void
332 {
333 $tables = self::createStub(StatementInterface::class);
334 $tables->method('fetchAll')->willReturn([['table_name' => 'users']]);
335 $columns = self::createStub(StatementInterface::class);
336 $columns->method('fetchAll')->willReturn([['column_name' => 'id', 'data_type' => 'integer', 'is_nullable' => 'NO', 'udt_name' => 'int4'], ['column_name' => 'email', 'data_type' => 'text', 'is_nullable' => 'NO', 'udt_name' => 'text'], ['column_name' => 'status', 'data_type' => 'text', 'is_nullable' => 'NO', 'udt_name' => 'text']]);
337 $primaryKey = self::createStub(StatementInterface::class);
338 $primaryKey->method('fetchAll')->willReturn([['column_name' => 'id']]);
339 $unique = self::createStub(StatementInterface::class);
340 $unique->method('fetchAll')->willReturn([['constraint_name' => 'users_active_email', 'column_name' => 'email', 'predicate' => "status = 'active'"]]);
341 $empty = self::createStub(StatementInterface::class);
342 $empty->method('fetchAll')->willReturn([]);
343 $connection = self::createStub(ConnectionInterface::class);
344 $connection->method('query')->willReturnCallback(static function (string $sql) use ($tables, $columns, $primaryKey, $unique, $empty): StatementInterface {
345 return match (true) {
346 str_contains($sql, 'information_schema.tables') => $tables,
347 str_contains($sql, 'information_schema.columns') => $columns,
348 str_contains($sql, "constraint_type = 'PRIMARY KEY'") => $primaryKey,
349 str_contains($sql, 'pg_catalog.pg_index') => $unique,
350 default => $empty,
351 };
352 });
353 $session = (new PgSqlSessionFactory())->create($connection, ZtdConfig::default());
354 $plan = $session->rewrite("INSERT INTO users (id, email, status) VALUES (1, 'a@example.com', 'active') " . "ON CONFLICT (email) WHERE status = 'active' " . 'DO UPDATE SET status = EXCLUDED.status');
355 self::assertStringContainsString('"__ztd_existing"."email" = "__ztd_incoming"."email"', $plan->sql());
356 self::assertStringContainsString('"__ztd_existing"."status" = \'active\'', $plan->sql());
357 }
358}
359