packages/ztd-query-postgres/tests/Unit/Rewrite/PgSqlRewriterTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use ZtdQuery\Exception\UnknownSchemaException;
10use ZtdQuery\Exception\UnsupportedSqlException;
11use ZtdQuery\Platform\Postgres\Rewrite\PgSqlQueryGuard;
12use ZtdQuery\Platform\Postgres\Rewrite\PgSqlRewriter;
13use ZtdQuery\Platform\Postgres\Rewrite\Returning\PgSqlReturningProjectionParser;
14use ZtdQuery\Platform\Postgres\Rewrite\Transformer\DeleteTransformer;
15use ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertTransformer;
16use ZtdQuery\Platform\Postgres\Rewrite\Transformer\MergeTransformer;
17use ZtdQuery\Platform\Postgres\Rewrite\Transformer\PgSqlTransformer;
18use ZtdQuery\Platform\Postgres\Rewrite\Transformer\SelectTransformer;
19use ZtdQuery\Platform\Postgres\Rewrite\Transformer\UpdateTransformer;
20use ZtdQuery\Platform\Postgres\Schema\PgSqlSchemaParser;
21use ZtdQuery\Platform\Postgres\Schema\View\PgSqlViewDefinitionParser;
22use ZtdQuery\Platform\Postgres\Shadow\PgSqlMutationResolver;
23use ZtdQuery\Platform\Postgres\Sql\Partition\PgSqlPartitionParser;
24use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
25use ZtdQuery\Platform\Postgres\Sql\PgSqlParser;
26use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer;
27use ZtdQuery\Rewrite\AffectedRowsMode;
28use ZtdQuery\Rewrite\QueryKind;
29use ZtdQuery\Schema\ColumnDeclaration;
30use ZtdQuery\Schema\ColumnTypeFamily;
31use ZtdQuery\Schema\Partition\TablePartitionRelation;
32use ZtdQuery\Schema\TableDefinition;
33use ZtdQuery\Schema\TableDefinitionRegistry;
34use ZtdQuery\Schema\ViewDefinitionSet;
35use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
36use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
37use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
38use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
39use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
40use ZtdQuery\Shadow\Mutation\Table\SynchronizeMutation;
41use ZtdQuery\Shadow\Mutation\Table\TruncateMutation;
42use ZtdQuery\Shadow\ShadowStore;
43use ZtdQuery\Shadow\ShadowTableState;
44
45#[CoversClass(PgSqlRewriter::class)]
46#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\PgSqlColumnTypeMapper::class)]
47#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\PgSqlForeignKeyDefinitionParser::class)]
48#[UsesClass(PgSqlParser::class)]
49#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\PgSqlUpsertExpressionParser::class)]
50#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\PgSqlConflictTarget::class)]
51#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PostgreSqlLexicalMasker::class)]
52#[UsesClass(PgSqlSchemaParser::class)]
53#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\PgSqlSelectRelationParser::class)]
54#[UsesClass(PgSqlPartitionParser::class)]
55#[UsesClass(PgSqlQueryGuard::class)]
56#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Diagnostic\PgSqlReadOnlyDiagnosticStatement::class)]
57#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Transaction\PgSqlTransactionStatementParser::class)]
58#[UsesClass(PgSqlReturningProjectionParser::class)]
59#[UsesClass(PgSqlMutationResolver::class)]
60#[UsesClass(PgSqlTransformer::class)]
61#[UsesClass(SelectTransformer::class)]
62#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSampleParser::class)]
63#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\PgSqlTableSampleRewriter::class)]
64#[UsesClass(InsertTransformer::class)]
65#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertRowRenderer::class)]
66#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\InsertSelectRenderer::class)]
67#[UsesClass(MergeTransformer::class)]
68#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeParser::class)]
69#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeStatement::class)]
70#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeClause::class)]
71#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeMatchKind::class)]
72#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\PgSqlMergeActionKind::class)]
73#[UsesClass(UpdateTransformer::class)]
74#[UsesClass(DeleteTransformer::class)]
75#[UsesClass(PgSqlCastRenderer::class)]
76#[UsesClass(PgSqlIdentifierQuoter::class)]
77#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\PgSqlValueRenderer::class)]
78#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\PgSqlCteShadowComposer::class)]
79#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\PgSqlNativeUpsertProjector::class)]
80#[UsesClass(PgSqlViewDefinitionParser::class)]
81#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\View\PgSqlViewShadowRenderer::class)]
82#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\GeneratedColumn\PgSqlGeneratedColumnProjector::class)]
83#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Partition\PgSqlPartitionPredicateRenderer::class)]
84#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\PgSqlLexerProfile::class)]
85#[CoversClass(\ZtdQuery\Platform\Postgres\Rewrite\Partition\StorageTable::class)]
86#[CoversClass(\ZtdQuery\Platform\Postgres\Rewrite\Context\TableContext::class)]
87#[CoversClass(\ZtdQuery\Platform\Postgres\Rewrite\StatementRewriter::class)]
88#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Table\TableMutationResolver::class)]
89#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Row\RowMutationResolver::class)]
90#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Conflict\ColumnSet::class)]
91#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Cte\HeaderParser::class)]
92#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Cte\IdentifierReferences::class)]
93#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\PrefixMerge::class)]
94#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Cte\ShadowDependencies::class)]
95#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Diagnostic\KeywordSearch::class)]
96#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ComparisonOperator::class)]
97#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ExpressionCursor::class)]
98#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\ExpressionLexeme::class)]
99#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\PrecedenceParser::class)]
100#[UsesClass(\ZtdQuery\Platform\Postgres\Shadow\Mutation\Upsert\Expression\PrimaryParser::class)]
101#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\ActionClause::class)]
102#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\BranchTokens::class)]
103#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\RelationTarget::class)]
104#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Merge\StatementParts::class)]
105#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\FromClause::class)]
106#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Relation\RelationReference::class)]
107#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Returning\ProjectionItem::class)]
108#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\SampleClause::class)]
109#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\SampleTokens::class)]
110#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\StatementClassifier::class)]
111#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\ConflictClause::class)]
112#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\IdentifierDecoder::class)]
113#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\TargetTableParser::class)]
114#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Update\UpdateClauseParser::class)]
115#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Delete\DeleteClauseParser::class)]
116#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Insert\InsertClauseParser::class)]
117#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Dml\Insert\InsertClauseParser::class)]
118#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\SelectColumns::class)]
119#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Statement\TableDefinitionClauses::class)]
120#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Transaction\KeywordForm::class)]
121#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSample::class)]
122#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Sampling\PgSqlTableSampleMethod::class)]
123#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\SampleProjection::class)]
124#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Sampling\TableColumns::class)]
125#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\ConflictPredicate::class)]
126#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Upsert\ExpressionBinder::class)]
127#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\ColumnDefinition::class)]
128#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\ColumnTypeDeclaration::class)]
129#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableBody::class)]
130#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableConstraint::class)]
131#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Definition\TableFields::class)]
132#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\DefinitionEntry::class)]
133#[UsesClass(\ZtdQuery\Platform\Postgres\Schema\Key\DefinitionTokens::class)]
134#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\BoundPredicate::class)]
135#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Partition\ClauseTokens::class)]
136#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\CastTypeMapper::class)]
137#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\CommentSpan::class)]
138#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Lexing\QuotedSpan::class)]
139#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\NativeCastTarget::class)]
140#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\BinaryStream::class)]
141#[UsesClass(\ZtdQuery\Platform\Postgres\Sql\Value\LiteralText::class)]
142#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Cte\RowSourceRenderer::class)]
143#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\ExpressionCast::class)]
144#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\OrderedExpressions::class)]
145#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\SelectProjection::class)]
146#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\UpsertProjection::class)]
147#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Insert\ValueProjection::class)]
148#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Merge\MatchConditions::class)]
149#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Merge\RowActions::class)]
150#[UsesClass(\ZtdQuery\Platform\Postgres\Rewrite\Transformer\Update\ColumnProjection::class)]
151final class PgSqlRewriterTest extends \PHPUnit\Framework\TestCase
152{
153    public function testSelectReturnsReadKind(): void
154    {
155        $shadowStore = new ShadowStore();
156        $registry = new TableDefinitionRegistry();
157        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
158        $parser = new PgSqlParser();
159        $guard = new PgSqlQueryGuard($parser);
160        $selectTransformer = new SelectTransformer();
161        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
162        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
163        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
164        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
165        $schemaParser = new PgSqlSchemaParser();
166        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
167        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
168        $plan = $rewriter->rewrite('SELECT * FROM users');
169        self::assertSame(QueryKind::READ, $plan->kind());
170        self::assertNull($plan->mutation());
171    }
172
173    public function testInsertReturnsWriteSimulatedWithMutation(): void
174    {
175        $shadowStore = new ShadowStore();
176        $registry = new TableDefinitionRegistry();
177        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
178        $parser = new PgSqlParser();
179        $guard = new PgSqlQueryGuard($parser);
180        $selectTransformer = new SelectTransformer();
181        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
182        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
183        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
184        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
185        $schemaParser = new PgSqlSchemaParser();
186        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
187        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
188        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
189        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
190        self::assertNotNull($plan->mutation());
191        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
192    }
193
194    public function testUpdateReturnsWriteSimulatedWithMutation(): void
195    {
196        $shadowStore = new ShadowStore();
197        $registry = new TableDefinitionRegistry();
198        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
199        $parser = new PgSqlParser();
200        $guard = new PgSqlQueryGuard($parser);
201        $selectTransformer = new SelectTransformer();
202        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
203        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
204        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
205        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
206        $schemaParser = new PgSqlSchemaParser();
207        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
208        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
209        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
210        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
211        self::assertNotNull($plan->mutation());
212        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
213        self::assertStringContainsString('"users"."id" AS "__ztd_original_id"', $plan->sql());
214    }
215
216    public function testDeleteReturnsWriteSimulatedWithMutation(): void
217    {
218        $shadowStore = new ShadowStore();
219        $registry = new TableDefinitionRegistry();
220        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
221        $parser = new PgSqlParser();
222        $guard = new PgSqlQueryGuard($parser);
223        $selectTransformer = new SelectTransformer();
224        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
225        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
226        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
227        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
228        $schemaParser = new PgSqlSchemaParser();
229        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
230        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
231        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
232        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
233        self::assertNotNull($plan->mutation());
234        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
235    }
236
237    public function testCreateTableReturnsDdlSimulated(): void
238    {
239        $shadowStore = new ShadowStore();
240        $registry = new TableDefinitionRegistry();
241        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
242        $parser = new PgSqlParser();
243        $guard = new PgSqlQueryGuard($parser);
244        $selectTransformer = new SelectTransformer();
245        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
246        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
247        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
248        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
249        $schemaParser = new PgSqlSchemaParser();
250        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
251        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
252        $plan = $rewriter->rewrite('CREATE TABLE orders (id INTEGER PRIMARY KEY, total NUMERIC(10,2))');
253        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
254        self::assertNotNull($plan->mutation());
255        self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
256    }
257
258    public function testDropTableReturnsDdlSimulated(): void
259    {
260        $shadowStore = new ShadowStore();
261        $registry = new TableDefinitionRegistry();
262        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
263        $parser = new PgSqlParser();
264        $guard = new PgSqlQueryGuard($parser);
265        $selectTransformer = new SelectTransformer();
266        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
267        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
268        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
269        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
270        $schemaParser = new PgSqlSchemaParser();
271        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
272        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
273        $plan = $rewriter->rewrite('DROP TABLE users');
274        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
275        self::assertNotNull($plan->mutation());
276        self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
277    }
278
279    public function testUnsupportedSqlThrowsException(): void
280    {
281        $shadowStore = new ShadowStore();
282        $registry = new TableDefinitionRegistry();
283        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
284        $parser = new PgSqlParser();
285        $guard = new PgSqlQueryGuard($parser);
286        $selectTransformer = new SelectTransformer();
287        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
288        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
289        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
290        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
291        $schemaParser = new PgSqlSchemaParser();
292        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
293        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
294        $this->expectException(UnsupportedSqlException::class);
295        $rewriter->rewrite('CREATE DATABASE test');
296    }
297
298    public function testEmptyInputThrowsException(): void
299    {
300        $shadowStore = new ShadowStore();
301        $registry = new TableDefinitionRegistry();
302        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
303        $parser = new PgSqlParser();
304        $guard = new PgSqlQueryGuard($parser);
305        $selectTransformer = new SelectTransformer();
306        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
307        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
308        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
309        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
310        $schemaParser = new PgSqlSchemaParser();
311        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
312        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
313        $this->expectException(UnsupportedSqlException::class);
314        $rewriter->rewrite('');
315    }
316
317    public function testRewriteIsDeterministic(): void
318    {
319        $shadowStore = new ShadowStore();
320        $registry = new TableDefinitionRegistry();
321        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
322        $parser = new PgSqlParser();
323        $guard = new PgSqlQueryGuard($parser);
324        $selectTransformer = new SelectTransformer();
325        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
326        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
327        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
328        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
329        $schemaParser = new PgSqlSchemaParser();
330        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
331        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
332        $sql = 'SELECT * FROM users WHERE id = 1';
333        $plan1 = $rewriter->rewrite($sql);
334        $plan2 = $rewriter->rewrite($sql);
335        self::assertSame($plan1->sql(), $plan2->sql());
336        self::assertSame($plan1->kind(), $plan2->kind());
337    }
338
339    public function testReadPlanHasNoMutation(): void
340    {
341        $shadowStore = new ShadowStore();
342        $registry = new TableDefinitionRegistry();
343        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
344        $parser = new PgSqlParser();
345        $guard = new PgSqlQueryGuard($parser);
346        $selectTransformer = new SelectTransformer();
347        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
348        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
349        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
350        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
351        $schemaParser = new PgSqlSchemaParser();
352        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
353        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
354        $plan = $rewriter->rewrite('SELECT * FROM users');
355        self::assertNull($plan->mutation());
356    }
357
358    public function testWritePlanHasNonNullMutation(): void
359    {
360        $shadowStore = new ShadowStore();
361        $registry = new TableDefinitionRegistry();
362        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
363        $parser = new PgSqlParser();
364        $guard = new PgSqlQueryGuard($parser);
365        $selectTransformer = new SelectTransformer();
366        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
367        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
368        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
369        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
370        $schemaParser = new PgSqlSchemaParser();
371        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
372        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
373        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
374        self::assertNotNull($plan->mutation());
375    }
376
377    public function testRewriteOutputIsNonEmpty(): void
378    {
379        $store = new ShadowStore();
380        $registry = new TableDefinitionRegistry();
381        $parser = new PgSqlSchemaParser();
382        $definition = $parser->parse(<<<'SQL'
383        CREATE TABLE users (
384            id INTEGER NOT NULL,
385            name VARCHAR(255) NOT NULL,
386            email VARCHAR(255) NOT NULL,
387            PRIMARY KEY (id)
388        )
389        SQL);
390        self::assertNotNull($definition);
391        $registry->register('users', $definition);
392        $parser = new PgSqlParser();
393        $schemaParser = new PgSqlSchemaParser();
394        $selectTransformer = new SelectTransformer();
395        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
396        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
397        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
398        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
399        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
400        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
401        $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
402        self::assertNotEmpty($plan->sql());
403        self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
404    }
405
406    public function testInsertRewriteOutputContainsSelect(): void
407    {
408        $store = new ShadowStore();
409        $registry = new TableDefinitionRegistry();
410        $parser = new PgSqlSchemaParser();
411        $definition = $parser->parse(<<<'SQL'
412        CREATE TABLE users (
413            id INTEGER NOT NULL,
414            name VARCHAR(255) NOT NULL,
415            email VARCHAR(255) NOT NULL,
416            PRIMARY KEY (id)
417        )
418        SQL);
419        self::assertNotNull($definition);
420        $registry->register('users', $definition);
421        $parser = new PgSqlParser();
422        $schemaParser = new PgSqlSchemaParser();
423        $selectTransformer = new SelectTransformer();
424        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
425        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
426        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
427        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
428        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
429        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
430        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
431        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
432        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'INSERT rewrite must produce a result-select query starting with SELECT or WITH...SELECT');
433    }
434    public function testMergeBuildsAtomicSynchronizationPlan(): void
435    {
436        $store = new ShadowStore();
437        $store->set('users', [['id' => 1, 'name' => 'old', 'email' => null]]);
438        $store->set('source', [['id' => 1, 'name' => 'updated'], ['id' => 2, 'name' => 'inserted']]);
439        $registry = new TableDefinitionRegistry();
440        $users = (new PgSqlSchemaParser())->parse(<<<'SQL'
441        CREATE TABLE users (
442            id INTEGER NOT NULL,
443            name VARCHAR(255) NOT NULL,
444            email VARCHAR(255) NOT NULL,
445            PRIMARY KEY (id)
446        )
447        SQL);
448        $source = (new PgSqlSchemaParser())->parse('CREATE TABLE source (id INTEGER PRIMARY KEY, name TEXT NOT NULL)');
449        self::assertNotNull($users);
450        self::assertNotNull($source);
451        $registry->register('users', $users);
452        $registry->register('source', $source);
453        $parser = new PgSqlParser();
454        $schemaParser = new PgSqlSchemaParser();
455        $selectTransformer = new SelectTransformer();
456        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
457        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
458        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
459        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
460        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
461        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('MERGE INTO users AS target USING source ON target.id = source.id ' . 'WHEN MATCHED THEN UPDATE SET name = source.name ' . 'WHEN NOT MATCHED THEN INSERT (id, name) VALUES (source.id, source.name)');
462        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
463        self::assertSame(AffectedRowsMode::Changed, $plan->affectedRowsMode());
464        self::assertInstanceOf(SynchronizeMutation::class, $plan->mutation());
465        self::assertStringContainsString('WHERE NOT (EXISTS', $plan->sql());
466        self::assertStringContainsString('UNION ALL', $plan->sql());
467    }
468    public function testDoBlockPassesThroughWithoutInspectingBodyTables(): void
469    {
470        $sql = 'DO $body$ BEGIN INSERT INTO physical_only VALUES (1); END $body$ LANGUAGE plpgsql';
471        $store = new ShadowStore();
472        $registry = new TableDefinitionRegistry();
473        $parser = new PgSqlParser();
474        $schemaParser = new PgSqlSchemaParser();
475        $selectTransformer = new SelectTransformer();
476        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
477        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
478        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
479        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
480        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
481        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite($sql);
482        self::assertSame(QueryKind::READ, $plan->kind());
483        self::assertSame($sql, $plan->sql());
484        self::assertNull($plan->mutation());
485    }
486    public function testChildPartitionSelectUsesFilteredParentShadowSource(): void
487    {
488        $store = new ShadowStore();
489        $store->set('logs', [['id' => 1, 'log_date' => '2024-05-01'], ['id' => 2, 'log_date' => '2025-05-01']]);
490        $registry = new TableDefinitionRegistry();
491        $parent = (new PgSqlSchemaParser())->parse('CREATE TABLE logs (id INTEGER, log_date DATE, PRIMARY KEY (id, log_date)) ' . 'PARTITION BY RANGE (log_date)');
492        self::assertNotNull($parent);
493        $registry->register('logs', $parent);
494        $registry->register('logs_2024', $parent->withPartitionRelation(new TablePartitionRelation('logs', "(log_date >= '2024-01-01'::date) AND (log_date < '2025-01-01'::date)")));
495        $parser = new PgSqlParser();
496        $schemaParser = new PgSqlSchemaParser();
497        $selectTransformer = new SelectTransformer();
498        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
499        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
500        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
501        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
502        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
503        $sql = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT id FROM logs_2024')->sql();
504        self::assertStringContainsString('"logs" AS MATERIALIZED', $sql);
505        self::assertStringContainsString('"logs_2024" AS MATERIALIZED (SELECT * FROM "logs" WHERE ' . "(log_date >= '2024-01-01'::date) AND (log_date < '2025-01-01'::date))", $sql);
506        self::assertStringEndsWith('SELECT id FROM logs_2024', $sql);
507    }
508    public function testChildPartitionWriteMutationTargetsRootParent(): void
509    {
510        $store = new ShadowStore();
511        $registry = new TableDefinitionRegistry();
512        $parent = (new PgSqlSchemaParser())->parse('CREATE TABLE logs (id INTEGER, log_date DATE, PRIMARY KEY (id, log_date)) ' . 'PARTITION BY RANGE (log_date)');
513        self::assertNotNull($parent);
514        $registry->register('logs', $parent);
515        $registry->register('logs_2024', $parent->withPartitionRelation(new TablePartitionRelation('logs', "log_date >= '2024-01-01'::date AND log_date < '2025-01-01'::date")));
516        $parser = new PgSqlParser();
517        $schemaParser = new PgSqlSchemaParser();
518        $selectTransformer = new SelectTransformer();
519        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
520        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
521        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
522        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
523        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
524        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite("INSERT INTO logs_2024 VALUES (1, '2024-05-01')");
525        self::assertNotNull($plan->mutation());
526        self::assertSame('logs', $plan->mutation()->tableName());
527        self::assertStringContainsString("CAST('2024-05-01' AS DATE) AS \"log_date\"", $plan->sql());
528    }
529    public function testDefaultPartitionExcludesOnlySpecificSiblingsOfSameParent(): void
530    {
531        $store = new ShadowStore();
532        $registry = new TableDefinitionRegistry();
533        $definition = new TableDefinition(['region'], ['region' => 'TEXT'], [], [], []);
534        $registry->register('accounts', $definition);
535        $registry->register('accounts_east', $definition->withPartitionRelation(new TablePartitionRelation('accounts', "region = 'east'")));
536        $registry->register('accounts_other', $definition->withPartitionRelation(new TablePartitionRelation('accounts', null)));
537        $registry->register('unrelated', $definition->withPartitionRelation(new TablePartitionRelation('another_parent', "region = 'west'")));
538        $parser = new PgSqlParser();
539        $schemaParser = new PgSqlSchemaParser();
540        $selectTransformer = new SelectTransformer();
541        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
542        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
543        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
544        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
545        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
546        $sql = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT * FROM accounts_other')->sql();
547        self::assertStringContainsString("SELECT * FROM \"accounts\" WHERE COALESCE(NOT ((region = 'east')), TRUE)", $sql);
548        self::assertStringNotContainsString("region = 'west'", $sql);
549    }
550    public function testNestedPartitionInsertAllocatesIdentityFromRootRows(): void
551    {
552        $store = new ShadowStore();
553        $store->set('root_table', [['id' => 5, 'region' => 'east']]);
554        $registry = new TableDefinitionRegistry();
555        $definition = new TableDefinition(['id', 'region'], ['id' => 'INTEGER', 'region' => 'TEXT'], ['id'], ['id'], [], identityStrategies: ['id' => \ZtdQuery\Schema\Key\IdentityGenerationStrategy::MaxValue]);
556        $registry->register('root_table', $definition);
557        $registry->register('child_table', $definition->withPartitionRelation(new TablePartitionRelation('root_table', "region = 'east'")));
558        $registry->register('grandchild_table', $definition->withPartitionRelation(new TablePartitionRelation('child_table', 'id < 10')));
559        $parser = new PgSqlParser();
560        $schemaParser = new PgSqlSchemaParser();
561        $selectTransformer = new SelectTransformer();
562        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
563        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
564        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
565        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
566        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
567        $sql = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite("INSERT INTO grandchild_table (region) VALUES ('east')")->sql();
568        self::assertStringContainsString('6 AS "id"', $sql);
569    }
570    public function testGeneratedExpressionIsPresentBeforeTheFirstShadowWrite(): void
571    {
572        $store = new ShadowStore();
573        $registry = new TableDefinitionRegistry();
574        $definition = (new PgSqlSchemaParser())->parse('CREATE TABLE orders (qty INTEGER, total INTEGER GENERATED ALWAYS AS (qty * 2) STORED)');
575        self::assertNotNull($definition);
576        $registry->register('orders', $definition);
577        $parser = new PgSqlParser();
578        $schemaParser = new PgSqlSchemaParser();
579        $selectTransformer = new SelectTransformer();
580        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
581        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
582        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
583        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
584        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
585        $sql = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT total FROM orders')->sql();
586        self::assertStringContainsString('(qty * 2) AS "total"', $sql);
587    }
588    public function testRegisteredViewIsKnownAndMaterialized(): void
589    {
590        $store = new ShadowStore();
591        $registry = new TableDefinitionRegistry();
592        $parser = new PgSqlParser();
593        $schemaParser = new PgSqlSchemaParser();
594        $definition = $schemaParser->parse(<<<'SQL'
595        CREATE TABLE users (
596            id INTEGER NOT NULL,
597            name VARCHAR(255) NOT NULL,
598            email VARCHAR(255) NOT NULL,
599            PRIMARY KEY (id)
600        )
601        SQL);
602        self::assertNotNull($definition);
603        $registry->register('users', $definition);
604        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
605        $selectTransformer = new SelectTransformer();
606        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
607        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
608        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
609        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
610        $resolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
611        $views = new ViewDefinitionSet();
612        $views->register('active_users', (new PgSqlViewDefinitionParser())->fromQuery('SELECT id FROM public.users'));
613        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $resolver, $parser, $views);
614        $sql = $rewriter->rewrite('SELECT * FROM active_users')->sql();
615        self::assertStringStartsWith('WITH "users" AS MATERIALIZED', $sql);
616        self::assertStringContainsString('"active_users" AS MATERIALIZED (SELECT id FROM users)', $sql);
617        $viewOnlyStore = new ShadowStore();
618        $viewOnlyRegistry = new TableDefinitionRegistry();
619        $viewOnlyViews = new ViewDefinitionSet();
620        $viewOnlyViews->register('constant_view', (new PgSqlViewDefinitionParser())->fromQuery('SELECT 1 AS id'));
621        $viewOnlyResolver = new PgSqlMutationResolver($viewOnlyStore, $viewOnlyRegistry, $schemaParser, $parser);
622        $viewOnlyRewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $viewOnlyStore, $viewOnlyRegistry, $transformer, $viewOnlyResolver, $parser, $viewOnlyViews);
623        $this->expectException(UnknownSchemaException::class);
624        $viewOnlyRewriter->rewrite('SELECT * FROM missing_table');
625    }
626    public function testRegisteredTableUpsertProjectsConflictExpression(): void
627    {
628        $store = new ShadowStore();
629        $registry = new TableDefinitionRegistry();
630        $definition = (new PgSqlSchemaParser())->parse(<<<'SQL'
631        CREATE TABLE users (
632            id INTEGER NOT NULL,
633            name VARCHAR(255) NOT NULL,
634            email VARCHAR(255) NOT NULL,
635            PRIMARY KEY (id)
636        )
637        SQL);
638        self::assertNotNull($definition);
639        $registry->register('users', $definition);
640        $parser = new PgSqlParser();
641        $schemaParser = new PgSqlSchemaParser();
642        $selectTransformer = new SelectTransformer();
643        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
644        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
645        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
646        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
647        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
648        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@example.com') " . 'ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name');
649        self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
650        self::assertStringNotContainsString('EXCLUDED.', $plan->sql());
651    }
652    public function testStoredTableUpsertProjectsConflictExpression(): void
653    {
654        $store = new ShadowStore();
655        $store->ensure('users');
656        $registry = new TableDefinitionRegistry();
657        $definition = (new PgSqlSchemaParser())->parse(<<<'SQL'
658        CREATE TABLE users (
659            id INTEGER NOT NULL,
660            name VARCHAR(255) NOT NULL,
661            email VARCHAR(255) NOT NULL,
662            PRIMARY KEY (id)
663        )
664        SQL);
665        self::assertNotNull($definition);
666        $registry->register('users', $definition);
667        $parser = new PgSqlParser();
668        $schemaParser = new PgSqlSchemaParser();
669        $selectTransformer = new SelectTransformer();
670        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
671        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
672        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
673        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
674        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
675        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@example.com') " . 'ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name');
676        self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
677        self::assertStringNotContainsString('EXCLUDED.', $plan->sql());
678    }
679    public function testSchemaQualifiedSelectUsesShadowCte(): void
680    {
681        $store = new ShadowStore();
682        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
683        $registry = new TableDefinitionRegistry();
684        $definition = (new PgSqlSchemaParser())->parse(<<<'SQL'
685        CREATE TABLE users (
686            id INTEGER NOT NULL,
687            name VARCHAR(255) NOT NULL,
688            email VARCHAR(255) NOT NULL,
689            PRIMARY KEY (id)
690        )
691        SQL);
692        self::assertNotNull($definition);
693        $registry->register('users', $definition);
694        $parser = new PgSqlParser();
695        $schemaParser = new PgSqlSchemaParser();
696        $selectTransformer = new SelectTransformer();
697        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
698        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
699        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
700        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
701        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
702        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT name FROM public.users');
703        self::assertStringStartsWith('WITH "users" AS MATERIALIZED', $plan->sql());
704        self::assertStringEndsWith('SELECT name FROM users', $plan->sql());
705    }
706    public function testExplainPassesThroughUnchanged(): void
707    {
708        $store = new ShadowStore();
709        $registry = new TableDefinitionRegistry();
710        $parser = new PgSqlParser();
711        $schemaParser = new PgSqlSchemaParser();
712        $selectTransformer = new SelectTransformer();
713        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
714        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
715        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
716        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
717        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
718        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
719        $sql = 'EXPLAIN (FORMAT JSON) SELECT * FROM users';
720        $plan = $rewriter->rewrite($sql);
721        self::assertSame(QueryKind::READ, $plan->kind());
722        self::assertSame($sql, $plan->sql());
723    }
724    public function testShowPassesThroughUnchanged(): void
725    {
726        $store = new ShadowStore();
727        $registry = new TableDefinitionRegistry();
728        $parser = new PgSqlParser();
729        $schemaParser = new PgSqlSchemaParser();
730        $selectTransformer = new SelectTransformer();
731        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
732        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
733        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
734        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
735        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
736        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
737        $sql = 'SHOW server_version';
738        $plan = $rewriter->rewrite($sql);
739        self::assertSame(QueryKind::READ, $plan->kind());
740        self::assertSame($sql, $plan->sql());
741    }
742    public function testTableFunctionDoesNotHideJoinedShadowRelation(): void
743    {
744        $store = new ShadowStore();
745        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
746        $registry = new TableDefinitionRegistry();
747        $definition = (new PgSqlSchemaParser())->parse(<<<'SQL'
748        CREATE TABLE users (
749            id INTEGER NOT NULL,
750            name VARCHAR(255) NOT NULL,
751            email VARCHAR(255) NOT NULL,
752            PRIMARY KEY (id)
753        )
754        SQL);
755        self::assertNotNull($definition);
756        $registry->register('users', $definition);
757        $parser = new PgSqlParser();
758        $schemaParser = new PgSqlSchemaParser();
759        $selectTransformer = new SelectTransformer();
760        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
761        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
762        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
763        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
764        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
765        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT day.value, users.name FROM generate_series(1, 3) AS day(value) LEFT JOIN users ON users.id = day.value');
766        self::assertStringStartsWith('WITH "users" AS MATERIALIZED', $plan->sql());
767        self::assertStringContainsString('generate_series(1, 3)', $plan->sql());
768    }
769    public function testLateralDerivedTableKeepsNestedPhysicalRelationInShadowScope(): void
770    {
771        $store = new ShadowStore();
772        $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
773        $registry = new TableDefinitionRegistry();
774        $definition = (new PgSqlSchemaParser())->parse(<<<'SQL'
775        CREATE TABLE users (
776            id INTEGER NOT NULL,
777            name VARCHAR(255) NOT NULL,
778            email VARCHAR(255) NOT NULL,
779            PRIMARY KEY (id)
780        )
781        SQL);
782        self::assertNotNull($definition);
783        $registry->register('users', $definition);
784        $parser = new PgSqlParser();
785        $schemaParser = new PgSqlSchemaParser();
786        $selectTransformer = new SelectTransformer();
787        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
788        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
789        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
790        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
791        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
792        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('SELECT selected.id FROM LATERAL (SELECT id FROM users) AS selected');
793        self::assertStringStartsWith('WITH "users" AS MATERIALIZED', $plan->sql());
794    }
795    public function testCteReferencesAreMatchedCaseInsensitivelyDuringSchemaValidation(): void
796    {
797        $registry = new TableDefinitionRegistry();
798        $definition = (new PgSqlSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
799        self::assertNotNull($definition);
800        $registry->register('known_table', $definition);
801        $store = new ShadowStore();
802        $parser = new PgSqlParser();
803        $schemaParser = new PgSqlSchemaParser();
804        $selectTransformer = new SelectTransformer();
805        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
806        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
807        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
808        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
809        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
810        $plan = (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users');
811        self::assertSame(QueryKind::READ, $plan->kind());
812    }
813    public function testUnknownTableAfterDeclaredCteIsRejected(): void
814    {
815        $registry = new TableDefinitionRegistry();
816        $definition = (new PgSqlSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
817        self::assertNotNull($definition);
818        $registry->register('known_table', $definition);
819        $this->expectException(UnknownSchemaException::class);
820        $this->expectExceptionMessage('missing_table');
821        $store = new ShadowStore();
822        $parser = new PgSqlParser();
823        $schemaParser = new PgSqlSchemaParser();
824        $selectTransformer = new SelectTransformer();
825        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
826        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
827        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
828        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
829        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
830        (new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users JOIN missing_table ON TRUE');
831    }
832    public function testSelectTransformsCteWithShadowData(): void
833    {
834        $shadowStore = new ShadowStore();
835        $registry = new TableDefinitionRegistry();
836        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
837        $parser = new PgSqlParser();
838        $guard = new PgSqlQueryGuard($parser);
839        $selectTransformer = new SelectTransformer();
840        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
841        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
842        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
843        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
844        $schemaParser = new PgSqlSchemaParser();
845        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
846        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
847        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
848        $plan = $rewriter->rewrite('SELECT * FROM users');
849        self::assertSame(QueryKind::READ, $plan->kind());
850        self::assertStringStartsWith('WITH', $plan->sql(), 'CTE-shadowed SELECT must start with WITH');
851        self::assertStringContainsString('AS MATERIALIZED', $plan->sql());
852        self::assertStringContainsString('"users"', $plan->sql());
853    }
854    public function testTruncateReturnsWriteSimulatedWithMutation(): void
855    {
856        $shadowStore = new ShadowStore();
857        $registry = new TableDefinitionRegistry();
858        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
859        $parser = new PgSqlParser();
860        $guard = new PgSqlQueryGuard($parser);
861        $selectTransformer = new SelectTransformer();
862        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
863        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
864        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
865        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
866        $schemaParser = new PgSqlSchemaParser();
867        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
868        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
869        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
870        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
871        self::assertNotNull($plan->mutation());
872        self::assertInstanceOf(TruncateMutation::class, $plan->mutation());
873    }
874    public function testMultiStatementThrowsException(): void
875    {
876        $shadowStore = new ShadowStore();
877        $registry = new TableDefinitionRegistry();
878        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
879        $parser = new PgSqlParser();
880        $guard = new PgSqlQueryGuard($parser);
881        $selectTransformer = new SelectTransformer();
882        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
883        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
884        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
885        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
886        $schemaParser = new PgSqlSchemaParser();
887        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
888        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
889        $this->expectException(UnsupportedSqlException::class);
890        $rewriter->rewrite('SELECT 1; SELECT 2');
891    }
892    public function testRewriteMultiple(): void
893    {
894        $shadowStore = new ShadowStore();
895        $registry = new TableDefinitionRegistry();
896        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
897        $parser = new PgSqlParser();
898        $guard = new PgSqlQueryGuard($parser);
899        $selectTransformer = new SelectTransformer();
900        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
901        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
902        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
903        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
904        $schemaParser = new PgSqlSchemaParser();
905        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
906        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
907        $multiPlan = $rewriter->rewriteMultiple("SELECT * FROM users; INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'b@c.com')");
908        self::assertSame(2, $multiPlan->count());
909        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
910        self::assertSame(QueryKind::WRITE_SIMULATED, $multiPlan->get(1)?->kind());
911    }
912    public function testRewriteMultipleEmpty(): void
913    {
914        $shadowStore = new ShadowStore();
915        $registry = new TableDefinitionRegistry();
916        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
917        $parser = new PgSqlParser();
918        $guard = new PgSqlQueryGuard($parser);
919        $selectTransformer = new SelectTransformer();
920        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
921        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
922        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
923        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
924        $schemaParser = new PgSqlSchemaParser();
925        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
926        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
927        $this->expectException(UnsupportedSqlException::class);
928        $rewriter->rewriteMultiple('');
929    }
930    public function testInsertResultSelectContainsColumnNames(): void
931    {
932        $shadowStore = new ShadowStore();
933        $registry = new TableDefinitionRegistry();
934        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
935        $parser = new PgSqlParser();
936        $guard = new PgSqlQueryGuard($parser);
937        $selectTransformer = new SelectTransformer();
938        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
939        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
940        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
941        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
942        $schemaParser = new PgSqlSchemaParser();
943        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
944        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
945        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
946        $sql = $plan->sql();
947        self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $sql, 'INSERT result-select must start with SELECT or WITH...SELECT');
948        self::assertStringContainsString('"id"', $sql);
949        self::assertStringContainsString('"name"', $sql);
950        self::assertStringContainsString('"email"', $sql);
951    }
952    public function testSelectWithEmptyShadowGeneratesEmptyCte(): void
953    {
954        $shadowStore = new ShadowStore();
955        $registry = new TableDefinitionRegistry();
956        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
957        $parser = new PgSqlParser();
958        $guard = new PgSqlQueryGuard($parser);
959        $selectTransformer = new SelectTransformer();
960        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
961        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
962        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
963        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
964        $schemaParser = new PgSqlSchemaParser();
965        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
966        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
967        $shadowStore->ensure('users');
968        $plan = $rewriter->rewrite('SELECT * FROM users');
969        $sql = $plan->sql();
970        self::assertStringStartsWith('WITH', $sql, 'Empty shadow CTE must start with WITH');
971        self::assertStringContainsString('WHERE FALSE', $sql);
972        self::assertStringContainsString('AS MATERIALIZED', $sql);
973    }
974    public function testSelectWithMultiRowShadowUsesValues(): void
975    {
976        $shadowStore = new ShadowStore();
977        $registry = new TableDefinitionRegistry();
978        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
979        $parser = new PgSqlParser();
980        $guard = new PgSqlQueryGuard($parser);
981        $selectTransformer = new SelectTransformer();
982        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
983        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
984        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
985        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
986        $schemaParser = new PgSqlSchemaParser();
987        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
988        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
989        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'], ['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com']]);
990        $plan = $rewriter->rewrite('SELECT * FROM users');
991        $sql = $plan->sql();
992        self::assertStringStartsWith('WITH', $sql, 'Multi-row shadow CTE must start with WITH');
993        self::assertStringContainsString('VALUES', $sql);
994        self::assertStringContainsString('AS MATERIALIZED', $sql);
995    }
996    public function testRewriteWithLeadingWhitespace(): void
997    {
998        $shadowStore = new ShadowStore();
999        $registry = new TableDefinitionRegistry();
1000        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1001        $parser = new PgSqlParser();
1002        $guard = new PgSqlQueryGuard($parser);
1003        $selectTransformer = new SelectTransformer();
1004        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1005        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1006        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1007        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1008        $schemaParser = new PgSqlSchemaParser();
1009        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1010        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1011        $plan = $rewriter->rewrite('  SELECT * FROM users  ');
1012        self::assertSame(QueryKind::READ, $plan->kind());
1013    }
1014    public function testRewriteWhitespaceOnlyThrows(): void
1015    {
1016        $shadowStore = new ShadowStore();
1017        $registry = new TableDefinitionRegistry();
1018        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1019        $parser = new PgSqlParser();
1020        $guard = new PgSqlQueryGuard($parser);
1021        $selectTransformer = new SelectTransformer();
1022        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1023        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1024        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1025        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1026        $schemaParser = new PgSqlSchemaParser();
1027        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1028        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1029        $this->expectException(UnsupportedSqlException::class);
1030        $rewriter->rewrite('   ');
1031    }
1032    public function testRewriteMultipleWhitespaceOnlyThrows(): void
1033    {
1034        $shadowStore = new ShadowStore();
1035        $registry = new TableDefinitionRegistry();
1036        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1037        $parser = new PgSqlParser();
1038        $guard = new PgSqlQueryGuard($parser);
1039        $selectTransformer = new SelectTransformer();
1040        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1041        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1042        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1043        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1044        $schemaParser = new PgSqlSchemaParser();
1045        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1046        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1047        $this->expectException(UnsupportedSqlException::class);
1048        $rewriter->rewriteMultiple('   ');
1049    }
1050    public function testRewriteEmptyInputForRewriteMultipleThrows(): void
1051    {
1052        $shadowStore = new ShadowStore();
1053        $registry = new TableDefinitionRegistry();
1054        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1055        $parser = new PgSqlParser();
1056        $guard = new PgSqlQueryGuard($parser);
1057        $selectTransformer = new SelectTransformer();
1058        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1059        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1060        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1061        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1062        $schemaParser = new PgSqlSchemaParser();
1063        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1064        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1065        $this->expectException(UnsupportedSqlException::class);
1066        $rewriter->rewriteMultiple('');
1067    }
1068    public function testSelectUnknownTableWithSchemaContextThrows(): void
1069    {
1070        $shadowStore = new ShadowStore();
1071        $registry = new TableDefinitionRegistry();
1072        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1073        $parser = new PgSqlParser();
1074        $guard = new PgSqlQueryGuard($parser);
1075        $selectTransformer = new SelectTransformer();
1076        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1077        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1078        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1079        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1080        $schemaParser = new PgSqlSchemaParser();
1081        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1082        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1083        $this->expectException(UnknownSchemaException::class);
1084        $rewriter->rewrite('SELECT * FROM nonexistent_table');
1085    }
1086    public function testSelectWithNoSchemaContextPassesThrough(): void
1087    {
1088        $shadowStore = new ShadowStore();
1089        $registry = new TableDefinitionRegistry();
1090        $parser = new PgSqlParser();
1091        $guard = new PgSqlQueryGuard($parser);
1092        $selectTransformer = new SelectTransformer();
1093        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1094        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1095        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1096        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1097        $schemaParser = new PgSqlSchemaParser();
1098        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1099        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1100        $plan = $rewriter->rewrite('SELECT * FROM anything');
1101        self::assertSame(QueryKind::READ, $plan->kind());
1102        self::assertSame('SELECT * FROM anything', $plan->sql());
1103    }
1104    public function testSelectTableExistsInShadowStoreNotRegistry(): void
1105    {
1106        $shadowStore = new ShadowStore();
1107        $registry = new TableDefinitionRegistry();
1108        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1109        $parser = new PgSqlParser();
1110        $guard = new PgSqlQueryGuard($parser);
1111        $selectTransformer = new SelectTransformer();
1112        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1113        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1114        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1115        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1116        $schemaParser = new PgSqlSchemaParser();
1117        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1118        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1119        $shadowStore->set('temp_table', [['id' => 1, 'val' => 'x']]);
1120        $plan = $rewriter->rewrite('SELECT * FROM temp_table');
1121        self::assertSame(QueryKind::READ, $plan->kind());
1122        self::assertStringContainsString('"temp_table" AS MATERIALIZED', $plan->sql());
1123    }
1124    public function testTruncateReturnsSelectOneWhereFalse(): void
1125    {
1126        $shadowStore = new ShadowStore();
1127        $registry = new TableDefinitionRegistry();
1128        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1129        $parser = new PgSqlParser();
1130        $guard = new PgSqlQueryGuard($parser);
1131        $selectTransformer = new SelectTransformer();
1132        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1133        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1134        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1135        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1136        $schemaParser = new PgSqlSchemaParser();
1137        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1138        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1139        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
1140        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
1141        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1142    }
1143    public function testCreateTableReturnsSelectOneWhereFalse(): void
1144    {
1145        $shadowStore = new ShadowStore();
1146        $registry = new TableDefinitionRegistry();
1147        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1148        $parser = new PgSqlParser();
1149        $guard = new PgSqlQueryGuard($parser);
1150        $selectTransformer = new SelectTransformer();
1151        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1152        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1153        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1154        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1155        $schemaParser = new PgSqlSchemaParser();
1156        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1157        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1158        $plan = $rewriter->rewrite('CREATE TABLE orders (id INTEGER PRIMARY KEY, total NUMERIC(10,2))');
1159        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
1160        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1161    }
1162    public function testDropTableReturnsSelectOneWhereFalse(): void
1163    {
1164        $shadowStore = new ShadowStore();
1165        $registry = new TableDefinitionRegistry();
1166        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1167        $parser = new PgSqlParser();
1168        $guard = new PgSqlQueryGuard($parser);
1169        $selectTransformer = new SelectTransformer();
1170        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1171        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1172        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1173        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1174        $schemaParser = new PgSqlSchemaParser();
1175        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1176        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1177        $plan = $rewriter->rewrite('DROP TABLE users');
1178        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
1179        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1180    }
1181    public function testCreateTableAsSelectReturnsDdlWithTransformedSql(): void
1182    {
1183        $shadowStore = new ShadowStore();
1184        $registry = new TableDefinitionRegistry();
1185        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1186        $parser = new PgSqlParser();
1187        $guard = new PgSqlQueryGuard($parser);
1188        $selectTransformer = new SelectTransformer();
1189        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1190        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1191        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1192        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1193        $schemaParser = new PgSqlSchemaParser();
1194        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1195        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1196        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
1197        $plan = $rewriter->rewrite('CREATE TABLE archive AS SELECT id, name FROM users');
1198        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1199        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1200        self::assertStringContainsString('SELECT id, name FROM users', $plan->sql());
1201    }
1202    public function testUpdateEnsuresDmlTargetViaShadowStore(): void
1203    {
1204        $shadowStore = new ShadowStore();
1205        $registry = new TableDefinitionRegistry();
1206        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1207        $parser = new PgSqlParser();
1208        $guard = new PgSqlQueryGuard($parser);
1209        $selectTransformer = new SelectTransformer();
1210        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1211        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1212        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1213        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1214        $schemaParser = new PgSqlSchemaParser();
1215        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1216        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1217        $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1218        self::assertSame([], $shadowStore->get('users'));
1219    }
1220    public function testDeleteEnsuresDmlTargetViaShadowStore(): void
1221    {
1222        $shadowStore = new ShadowStore();
1223        $registry = new TableDefinitionRegistry();
1224        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1225        $parser = new PgSqlParser();
1226        $guard = new PgSqlQueryGuard($parser);
1227        $selectTransformer = new SelectTransformer();
1228        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1229        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1230        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1231        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1232        $schemaParser = new PgSqlSchemaParser();
1233        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1234        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1235        $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1236        self::assertSame([], $shadowStore->get('users'));
1237    }
1238    public function testInsertSqlIsTransformed(): void
1239    {
1240        $shadowStore = new ShadowStore();
1241        $registry = new TableDefinitionRegistry();
1242        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1243        $parser = new PgSqlParser();
1244        $guard = new PgSqlQueryGuard($parser);
1245        $selectTransformer = new SelectTransformer();
1246        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1247        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1248        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1249        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1250        $schemaParser = new PgSqlSchemaParser();
1251        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1252        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1253        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (2, 'New', 'new@ex.com')");
1254        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1255        self::assertStringContainsString('SELECT', $plan->sql());
1256        self::assertStringContainsString('"id"', $plan->sql());
1257        self::assertStringContainsString('"name"', $plan->sql());
1258        self::assertStringContainsString('"email"', $plan->sql());
1259    }
1260    public function testUpdateSqlIsTransformed(): void
1261    {
1262        $shadowStore = new ShadowStore();
1263        $registry = new TableDefinitionRegistry();
1264        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1265        $parser = new PgSqlParser();
1266        $guard = new PgSqlQueryGuard($parser);
1267        $selectTransformer = new SelectTransformer();
1268        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1269        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1270        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1271        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1272        $schemaParser = new PgSqlSchemaParser();
1273        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1274        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1275        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@ex.com']]);
1276        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1277        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1278        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1279    }
1280    public function testDeleteSqlIsTransformed(): void
1281    {
1282        $shadowStore = new ShadowStore();
1283        $registry = new TableDefinitionRegistry();
1284        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1285        $parser = new PgSqlParser();
1286        $guard = new PgSqlQueryGuard($parser);
1287        $selectTransformer = new SelectTransformer();
1288        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1289        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1290        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1291        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1292        $schemaParser = new PgSqlSchemaParser();
1293        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1294        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1295        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@ex.com']]);
1296        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1297        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1298        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1299    }
1300    public function testRewriteMultipleAllReadStatements(): void
1301    {
1302        $shadowStore = new ShadowStore();
1303        $registry = new TableDefinitionRegistry();
1304        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1305        $parser = new PgSqlParser();
1306        $guard = new PgSqlQueryGuard($parser);
1307        $selectTransformer = new SelectTransformer();
1308        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1309        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1310        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1311        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1312        $schemaParser = new PgSqlSchemaParser();
1313        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1314        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1315        $multiPlan = $rewriter->rewriteMultiple('SELECT 1; SELECT 2');
1316        self::assertSame(2, $multiPlan->count());
1317        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
1318        self::assertSame(QueryKind::READ, $multiPlan->get(1)?->kind());
1319    }
1320    public function testBuildTableContextMergesRegistryAndShadow(): void
1321    {
1322        $shadowStore = new ShadowStore();
1323        $registry = new TableDefinitionRegistry();
1324        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1325        $parser = new PgSqlParser();
1326        $guard = new PgSqlQueryGuard($parser);
1327        $selectTransformer = new SelectTransformer();
1328        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1329        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1330        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1331        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1332        $schemaParser = new PgSqlSchemaParser();
1333        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1334        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1335        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@ex.com']]);
1336        $plan = $rewriter->rewrite('SELECT * FROM users');
1337        self::assertSame(QueryKind::READ, $plan->kind());
1338        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1339        self::assertStringContainsString("'Alice'", $plan->sql());
1340    }
1341    public function testBuildTableContextRegistryOnlyNoShadow(): void
1342    {
1343        $shadowStore = new ShadowStore();
1344        $registry = new TableDefinitionRegistry();
1345        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1346        $parser = new PgSqlParser();
1347        $guard = new PgSqlQueryGuard($parser);
1348        $selectTransformer = new SelectTransformer();
1349        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1350        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1351        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1352        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1353        $schemaParser = new PgSqlSchemaParser();
1354        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1355        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1356        $plan = $rewriter->rewrite('SELECT * FROM users');
1357        self::assertSame(QueryKind::READ, $plan->kind());
1358        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1359        self::assertStringContainsString('WHERE FALSE', $plan->sql());
1360    }
1361    public function testSelectWithShadowOnlyDataDerivesColumns(): void
1362    {
1363        $shadowStore = new ShadowStore();
1364        $registry = new TableDefinitionRegistry();
1365        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1366        $parser = new PgSqlParser();
1367        $guard = new PgSqlQueryGuard($parser);
1368        $selectTransformer = new SelectTransformer();
1369        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1370        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1371        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1372        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1373        $schemaParser = new PgSqlSchemaParser();
1374        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1375        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1376        $shadowStore->set('temp', [['a' => 1, 'b' => 'x']]);
1377        $plan = $rewriter->rewrite('SELECT * FROM temp');
1378        self::assertSame(QueryKind::READ, $plan->kind());
1379        self::assertStringContainsString('"temp" AS MATERIALIZED', $plan->sql());
1380    }
1381    public function testSkippedStatementPassesThrough(): void
1382    {
1383        $shadowStore = new ShadowStore();
1384        $registry = new TableDefinitionRegistry();
1385        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1386        $parser = new PgSqlParser();
1387        $guard = new PgSqlQueryGuard($parser);
1388        $selectTransformer = new SelectTransformer();
1389        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1390        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1391        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1392        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1393        $schemaParser = new PgSqlSchemaParser();
1394        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1395        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1396        $plan = $rewriter->rewrite('BEGIN');
1397        self::assertSame(QueryKind::SKIPPED, $plan->kind());
1398        self::assertSame('BEGIN', $plan->sql());
1399        self::assertNull($plan->mutation());
1400    }
1401    public function testSelectWithShadowOnlyDataMergesColumnsAcrossRows(): void
1402    {
1403        $shadowStore = new ShadowStore();
1404        $registry = new TableDefinitionRegistry();
1405        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1406        $parser = new PgSqlParser();
1407        $guard = new PgSqlQueryGuard($parser);
1408        $selectTransformer = new SelectTransformer();
1409        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1410        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1411        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1412        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1413        $schemaParser = new PgSqlSchemaParser();
1414        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1415        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1416        $shadowStore->set('temp', [['a' => 1, 'b' => 2], ['a' => 3, 'c' => 4]]);
1417        $plan = $rewriter->rewrite('SELECT * FROM temp');
1418        self::assertSame(QueryKind::READ, $plan->kind());
1419        self::assertStringContainsString('"temp" AS MATERIALIZED', $plan->sql());
1420    }
1421    public function testUpdateEnsuresDmlTargetInShadowStore(): void
1422    {
1423        $shadowStore = new ShadowStore();
1424        $registry = new TableDefinitionRegistry();
1425        $registry->register('products', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1426        $parser = new PgSqlParser();
1427        $guard = new PgSqlQueryGuard($parser);
1428        $selectTransformer = new SelectTransformer();
1429        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1430        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1431        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1432        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1433        $schemaParser = new PgSqlSchemaParser();
1434        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1435        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1436        $rewriter->rewrite("UPDATE products SET name = 'New' WHERE id = 1");
1437        self::assertSame([], $shadowStore->get('products'));
1438    }
1439    public function testDeleteEnsuresDmlTargetInShadowStore(): void
1440    {
1441        $shadowStore = new ShadowStore();
1442        $registry = new TableDefinitionRegistry();
1443        $registry->register('products', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1444        $parser = new PgSqlParser();
1445        $guard = new PgSqlQueryGuard($parser);
1446        $selectTransformer = new SelectTransformer();
1447        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1448        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1449        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1450        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1451        $schemaParser = new PgSqlSchemaParser();
1452        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1453        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1454        $rewriter->rewrite('DELETE FROM products WHERE id = 1');
1455        self::assertSame([], $shadowStore->get('products'));
1456    }
1457    public function testCreateTableAsSelectNoShadowReturnsSqlWhereFalse(): void
1458    {
1459        $shadowStore = new ShadowStore();
1460        $registry = new TableDefinitionRegistry();
1461        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1462        $parser = new PgSqlParser();
1463        $guard = new PgSqlQueryGuard($parser);
1464        $selectTransformer = new SelectTransformer();
1465        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1466        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1467        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1468        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1469        $schemaParser = new PgSqlSchemaParser();
1470        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1471        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1472        $plan = $rewriter->rewrite('CREATE TABLE archive AS SELECT id, name FROM users');
1473        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1474        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1475    }
1476    public function testAlterTableThrowsUnsupported(): void
1477    {
1478        $shadowStore = new ShadowStore();
1479        $registry = new TableDefinitionRegistry();
1480        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1481        $parser = new PgSqlParser();
1482        $guard = new PgSqlQueryGuard($parser);
1483        $selectTransformer = new SelectTransformer();
1484        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1485        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1486        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1487        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1488        $schemaParser = new PgSqlSchemaParser();
1489        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1490        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1491        $this->expectException(UnsupportedSqlException::class);
1492        $rewriter->rewrite('ALTER TABLE users ADD COLUMN age INTEGER');
1493    }
1494    public function testRewriteMultipleWithDdl(): void
1495    {
1496        $shadowStore = new ShadowStore();
1497        $registry = new TableDefinitionRegistry();
1498        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1499        $parser = new PgSqlParser();
1500        $guard = new PgSqlQueryGuard($parser);
1501        $selectTransformer = new SelectTransformer();
1502        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1503        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1504        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1505        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1506        $schemaParser = new PgSqlSchemaParser();
1507        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1508        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1509        $multiPlan = $rewriter->rewriteMultiple('SELECT 1; CREATE TABLE t (id INTEGER); DROP TABLE users');
1510        self::assertSame(3, $multiPlan->count());
1511        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
1512        self::assertSame(QueryKind::DDL_SIMULATED, $multiPlan->get(1)?->kind());
1513        self::assertSame(QueryKind::DDL_SIMULATED, $multiPlan->get(2)?->kind());
1514    }
1515    public function testSelectTableExistsInRegistryOnly(): void
1516    {
1517        $shadowStore = new ShadowStore();
1518        $registry = new TableDefinitionRegistry();
1519        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1520        $parser = new PgSqlParser();
1521        $guard = new PgSqlQueryGuard($parser);
1522        $selectTransformer = new SelectTransformer();
1523        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1524        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1525        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1526        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1527        $schemaParser = new PgSqlSchemaParser();
1528        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1529        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1530        $plan = $rewriter->rewrite('SELECT * FROM users WHERE id = 1');
1531        self::assertSame(QueryKind::READ, $plan->kind());
1532    }
1533    public function testSelectTableExistsInBothRegistryAndShadow(): void
1534    {
1535        $shadowStore = new ShadowStore();
1536        $registry = new TableDefinitionRegistry();
1537        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1538        $parser = new PgSqlParser();
1539        $guard = new PgSqlQueryGuard($parser);
1540        $selectTransformer = new SelectTransformer();
1541        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1542        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1543        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1544        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1545        $schemaParser = new PgSqlSchemaParser();
1546        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1547        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1548        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@ex.com']]);
1549        $plan = $rewriter->rewrite('SELECT * FROM users WHERE id = 1');
1550        self::assertSame(QueryKind::READ, $plan->kind());
1551        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
1552    }
1553    public function testCommitIsSkipped(): void
1554    {
1555        $shadowStore = new ShadowStore();
1556        $registry = new TableDefinitionRegistry();
1557        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1558        $parser = new PgSqlParser();
1559        $guard = new PgSqlQueryGuard($parser);
1560        $selectTransformer = new SelectTransformer();
1561        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1562        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1563        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1564        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1565        $schemaParser = new PgSqlSchemaParser();
1566        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1567        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1568        $plan = $rewriter->rewrite('COMMIT');
1569        self::assertSame(QueryKind::SKIPPED, $plan->kind());
1570        self::assertSame('COMMIT', $plan->sql());
1571    }
1572    public function testRollbackIsSkipped(): void
1573    {
1574        $shadowStore = new ShadowStore();
1575        $registry = new TableDefinitionRegistry();
1576        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1577        $parser = new PgSqlParser();
1578        $guard = new PgSqlQueryGuard($parser);
1579        $selectTransformer = new SelectTransformer();
1580        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1581        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1582        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1583        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1584        $schemaParser = new PgSqlSchemaParser();
1585        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1586        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1587        $plan = $rewriter->rewrite('ROLLBACK');
1588        self::assertSame(QueryKind::SKIPPED, $plan->kind());
1589        self::assertSame('ROLLBACK', $plan->sql());
1590    }
1591    public function testRewriteWithLeadingWhitespaceTrimsFirst(): void
1592    {
1593        $shadowStore = new ShadowStore();
1594        $registry = new TableDefinitionRegistry();
1595        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1596        $parser = new PgSqlParser();
1597        $guard = new PgSqlQueryGuard($parser);
1598        $selectTransformer = new SelectTransformer();
1599        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1600        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1601        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1602        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1603        $schemaParser = new PgSqlSchemaParser();
1604        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1605        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1606        $plan = $rewriter->rewrite('  BEGIN  ');
1607        self::assertSame(QueryKind::SKIPPED, $plan->kind());
1608    }
1609    public function testRewriteMultipleWithLeadingWhitespace(): void
1610    {
1611        $shadowStore = new ShadowStore();
1612        $registry = new TableDefinitionRegistry();
1613        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1614        $parser = new PgSqlParser();
1615        $guard = new PgSqlQueryGuard($parser);
1616        $selectTransformer = new SelectTransformer();
1617        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1618        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1619        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1620        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1621        $schemaParser = new PgSqlSchemaParser();
1622        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1623        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1624        $multiPlan = $rewriter->rewriteMultiple('  SELECT 1  ');
1625        self::assertSame(1, $multiPlan->count());
1626        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
1627    }
1628    public function testTruncateMutationTableName(): void
1629    {
1630        $shadowStore = new ShadowStore();
1631        $registry = new TableDefinitionRegistry();
1632        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1633        $parser = new PgSqlParser();
1634        $guard = new PgSqlQueryGuard($parser);
1635        $selectTransformer = new SelectTransformer();
1636        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1637        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1638        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1639        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1640        $schemaParser = new PgSqlSchemaParser();
1641        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1642        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1643        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
1644        self::assertNotNull($plan->mutation());
1645        self::assertInstanceOf(TruncateMutation::class, $plan->mutation());
1646        self::assertSame('users', $plan->mutation()->tableName());
1647    }
1648    public function testInsertMutationTableName(): void
1649    {
1650        $shadowStore = new ShadowStore();
1651        $registry = new TableDefinitionRegistry();
1652        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1653        $parser = new PgSqlParser();
1654        $guard = new PgSqlQueryGuard($parser);
1655        $selectTransformer = new SelectTransformer();
1656        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1657        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1658        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1659        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1660        $schemaParser = new PgSqlSchemaParser();
1661        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1662        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1663        $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
1664        self::assertNotNull($plan->mutation());
1665        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
1666        self::assertSame('users', $plan->mutation()->tableName());
1667    }
1668    public function testUpdateMutationTableName(): void
1669    {
1670        $shadowStore = new ShadowStore();
1671        $registry = new TableDefinitionRegistry();
1672        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1673        $parser = new PgSqlParser();
1674        $guard = new PgSqlQueryGuard($parser);
1675        $selectTransformer = new SelectTransformer();
1676        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1677        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1678        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1679        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1680        $schemaParser = new PgSqlSchemaParser();
1681        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1682        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1683        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1684        self::assertNotNull($plan->mutation());
1685        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
1686        self::assertSame('users', $plan->mutation()->tableName());
1687    }
1688    public function testDeleteMutationTableName(): void
1689    {
1690        $shadowStore = new ShadowStore();
1691        $registry = new TableDefinitionRegistry();
1692        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1693        $parser = new PgSqlParser();
1694        $guard = new PgSqlQueryGuard($parser);
1695        $selectTransformer = new SelectTransformer();
1696        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1697        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1698        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1699        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1700        $schemaParser = new PgSqlSchemaParser();
1701        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1702        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1703        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1704        self::assertNotNull($plan->mutation());
1705        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
1706        self::assertSame('users', $plan->mutation()->tableName());
1707    }
1708    public function testRewriteWithOnlyWhitespaceThrowsAfterTrim(): void
1709    {
1710        $shadowStore = new ShadowStore();
1711        $registry = new TableDefinitionRegistry();
1712        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1713        $parser = new PgSqlParser();
1714        $guard = new PgSqlQueryGuard($parser);
1715        $selectTransformer = new SelectTransformer();
1716        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1717        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1718        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1719        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1720        $schemaParser = new PgSqlSchemaParser();
1721        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1722        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1723        $this->expectException(UnsupportedSqlException::class);
1724        $rewriter->rewrite("\t \n");
1725    }
1726    public function testRewriteMultipleWithOnlyWhitespaceThrowsAfterTrim(): void
1727    {
1728        $shadowStore = new ShadowStore();
1729        $registry = new TableDefinitionRegistry();
1730        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1731        $parser = new PgSqlParser();
1732        $guard = new PgSqlQueryGuard($parser);
1733        $selectTransformer = new SelectTransformer();
1734        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1735        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1736        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1737        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1738        $schemaParser = new PgSqlSchemaParser();
1739        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1740        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1741        $this->expectException(UnsupportedSqlException::class);
1742        $rewriter->rewriteMultiple("\t \n");
1743    }
1744    public function testRewriteLeadingWhitespaceTrimsBeforeClassify(): void
1745    {
1746        $shadowStore = new ShadowStore();
1747        $registry = new TableDefinitionRegistry();
1748        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1749        $parser = new PgSqlParser();
1750        $guard = new PgSqlQueryGuard($parser);
1751        $selectTransformer = new SelectTransformer();
1752        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1753        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1754        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1755        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1756        $schemaParser = new PgSqlSchemaParser();
1757        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1758        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1759        $plan = $rewriter->rewrite("\n  SELECT * FROM users\n  ");
1760        self::assertSame(QueryKind::READ, $plan->kind());
1761    }
1762    public function testRewriteMultipleLeadingWhitespaceTrimsBeforeClassify(): void
1763    {
1764        $shadowStore = new ShadowStore();
1765        $registry = new TableDefinitionRegistry();
1766        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1767        $parser = new PgSqlParser();
1768        $guard = new PgSqlQueryGuard($parser);
1769        $selectTransformer = new SelectTransformer();
1770        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1771        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1772        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1773        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1774        $schemaParser = new PgSqlSchemaParser();
1775        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1776        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1777        $multiPlan = $rewriter->rewriteMultiple("\n  SELECT 1\n  ");
1778        self::assertSame(1, $multiPlan->count());
1779        self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
1780    }
1781    public function testDropTableThenCreateDoesNotThrow(): void
1782    {
1783        $shadowStore = new ShadowStore();
1784        $registry = new TableDefinitionRegistry();
1785        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1786        $parser = new PgSqlParser();
1787        $guard = new PgSqlQueryGuard($parser);
1788        $selectTransformer = new SelectTransformer();
1789        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1790        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1791        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1792        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1793        $schemaParser = new PgSqlSchemaParser();
1794        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1795        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1796        $plan = $rewriter->rewrite('DROP TABLE IF EXISTS users');
1797        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1798        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
1799        self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
1800    }
1801    public function testInsertDoesNotCallEnsureDmlTarget(): void
1802    {
1803        $shadowStore = new ShadowStore();
1804        $registry = new TableDefinitionRegistry();
1805        $registry->register('products', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1806        $parser = new PgSqlParser();
1807        $guard = new PgSqlQueryGuard($parser);
1808        $selectTransformer = new SelectTransformer();
1809        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1810        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1811        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1812        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1813        $schemaParser = new PgSqlSchemaParser();
1814        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1815        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1816        $rewriter->rewrite("INSERT INTO products (id, name) VALUES (1, 'Test')");
1817        self::assertSame([], $shadowStore->get('products'));
1818    }
1819    public function testUpdateEnsuresDmlTargetIsCalledForUpdate(): void
1820    {
1821        $shadowStore = new ShadowStore();
1822        $registry = new TableDefinitionRegistry();
1823        $registry->register('items', new TableDefinition(['id', 'qty'], ['id' => 'INTEGER', 'qty' => 'INTEGER'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'qty' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')]));
1824        $parser = new PgSqlParser();
1825        $guard = new PgSqlQueryGuard($parser);
1826        $selectTransformer = new SelectTransformer();
1827        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1828        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1829        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1830        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1831        $schemaParser = new PgSqlSchemaParser();
1832        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1833        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1834        $rewriter->rewrite('UPDATE items SET qty = 5 WHERE id = 1');
1835        self::assertSame([], $shadowStore->get('items'));
1836    }
1837    public function testDeleteEnsuresDmlTargetIsCalledForDelete(): void
1838    {
1839        $shadowStore = new ShadowStore();
1840        $registry = new TableDefinitionRegistry();
1841        $registry->register('items', new TableDefinition(['id', 'qty'], ['id' => 'INTEGER', 'qty' => 'INTEGER'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'qty' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')]));
1842        $parser = new PgSqlParser();
1843        $guard = new PgSqlQueryGuard($parser);
1844        $selectTransformer = new SelectTransformer();
1845        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1846        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1847        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1848        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1849        $schemaParser = new PgSqlSchemaParser();
1850        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1851        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1852        $rewriter->rewrite('DELETE FROM items WHERE id = 1');
1853        self::assertSame([], $shadowStore->get('items'));
1854    }
1855    public function testBuildTableContextShadowOnlyDerivesDifferentColumnsAcrossRows(): void
1856    {
1857        $shadowStore = new ShadowStore();
1858        $registry = new TableDefinitionRegistry();
1859        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1860        $parser = new PgSqlParser();
1861        $guard = new PgSqlQueryGuard($parser);
1862        $selectTransformer = new SelectTransformer();
1863        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1864        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1865        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1866        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1867        $schemaParser = new PgSqlSchemaParser();
1868        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1869        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1870        $shadowStore->set('misc', [['a' => 1], ['a' => 2, 'b' => 3], ['a' => 4, 'c' => 5]]);
1871        $plan = $rewriter->rewrite('SELECT * FROM misc');
1872        $sql = $plan->sql();
1873        self::assertStringContainsString('"misc" AS MATERIALIZED', $sql);
1874        self::assertStringContainsString('"a"', $sql);
1875        self::assertStringContainsString('"b"', $sql);
1876        self::assertStringContainsString('"c"', $sql);
1877    }
1878    public function testBuildTableContextRegistryAndShadowBothIncluded(): void
1879    {
1880        $shadowStore = new ShadowStore();
1881        $registry = new TableDefinitionRegistry();
1882        $registry->register('table_a', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')]));
1883        $registry->register('table_b', new TableDefinition(['val'], ['val' => 'TEXT'], [], [], [], ['val' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1884        $shadowStore->set('table_a', [['id' => 1]]);
1885        $parser = new PgSqlParser();
1886        $guard = new PgSqlQueryGuard($parser);
1887        $selectTransformer = new SelectTransformer();
1888        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1889        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1890        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1891        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1892        $schemaParser = new PgSqlSchemaParser();
1893        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1894        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1895        $plan = $rewriter->rewrite('SELECT * FROM table_a JOIN table_b ON TRUE');
1896        $sql = $plan->sql();
1897        self::assertStringContainsString('"table_a" AS MATERIALIZED', $sql);
1898        self::assertStringContainsString('"table_b" AS MATERIALIZED', $sql);
1899    }
1900    public function testSelectWithShadowOnlyHasSchemaContext(): void
1901    {
1902        $shadowStore = new ShadowStore();
1903        $registry = new TableDefinitionRegistry();
1904        $parser = new PgSqlParser();
1905        $guard = new PgSqlQueryGuard($parser);
1906        $selectTransformer = new SelectTransformer();
1907        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1908        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1909        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1910        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1911        $schemaParser = new PgSqlSchemaParser();
1912        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1913        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1914        $shadowStore->set('my_table', [['x' => 1]]);
1915        $plan = $rewriter->rewrite('SELECT * FROM my_table');
1916        self::assertSame(QueryKind::READ, $plan->kind());
1917        self::assertStringContainsString('"my_table" AS MATERIALIZED', $plan->sql());
1918    }
1919    public function testSelectUnknownTableWithShadowContextThrows(): void
1920    {
1921        $shadowStore = new ShadowStore();
1922        $registry = new TableDefinitionRegistry();
1923        $parser = new PgSqlParser();
1924        $guard = new PgSqlQueryGuard($parser);
1925        $selectTransformer = new SelectTransformer();
1926        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1927        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1928        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1929        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1930        $schemaParser = new PgSqlSchemaParser();
1931        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1932        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1933        $shadowStore->set('known_table', [['x' => 1]]);
1934        $this->expectException(UnknownSchemaException::class);
1935        $rewriter->rewrite('SELECT * FROM unknown_table');
1936    }
1937    public function testCreateTableWithoutAsSelectReturnsSqlWhereFalse(): void
1938    {
1939        $shadowStore = new ShadowStore();
1940        $registry = new TableDefinitionRegistry();
1941        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1942        $parser = new PgSqlParser();
1943        $guard = new PgSqlQueryGuard($parser);
1944        $selectTransformer = new SelectTransformer();
1945        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1946        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1947        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1948        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1949        $schemaParser = new PgSqlSchemaParser();
1950        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1951        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1952        $plan = $rewriter->rewrite('CREATE TABLE orders (id INTEGER, amount NUMERIC)');
1953        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
1954    }
1955    public function testCreateTableAsSelectTransformsSelectPart(): void
1956    {
1957        $shadowStore = new ShadowStore();
1958        $registry = new TableDefinitionRegistry();
1959        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1960        $parser = new PgSqlParser();
1961        $guard = new PgSqlQueryGuard($parser);
1962        $selectTransformer = new SelectTransformer();
1963        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1964        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1965        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1966        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1967        $schemaParser = new PgSqlSchemaParser();
1968        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1969        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1970        $plan = $rewriter->rewrite('CREATE TABLE archive AS SELECT * FROM users');
1971        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
1972        self::assertStringContainsString('users', $plan->sql());
1973    }
1974    public function testRewriteMultipleEmptyStringThrows(): void
1975    {
1976        $shadowStore = new ShadowStore();
1977        $registry = new TableDefinitionRegistry();
1978        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1979        $parser = new PgSqlParser();
1980        $guard = new PgSqlQueryGuard($parser);
1981        $selectTransformer = new SelectTransformer();
1982        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1983        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1984        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1985        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1986        $schemaParser = new PgSqlSchemaParser();
1987        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
1988        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
1989        $this->expectException(UnsupportedSqlException::class);
1990        $rewriter->rewriteMultiple('');
1991    }
1992    public function testRewriteMultipleSingleStatement(): void
1993    {
1994        $shadowStore = new ShadowStore();
1995        $registry = new TableDefinitionRegistry();
1996        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
1997        $parser = new PgSqlParser();
1998        $guard = new PgSqlQueryGuard($parser);
1999        $selectTransformer = new SelectTransformer();
2000        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2001        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2002        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2003        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2004        $schemaParser = new PgSqlSchemaParser();
2005        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2006        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2007        $plans = $rewriter->rewriteMultiple('SELECT 1');
2008        self::assertCount(1, $plans->plans());
2009    }
2010    public function testRewriteMultipleMultiStatement(): void
2011    {
2012        $shadowStore = new ShadowStore();
2013        $registry = new TableDefinitionRegistry();
2014        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2015        $parser = new PgSqlParser();
2016        $guard = new PgSqlQueryGuard($parser);
2017        $selectTransformer = new SelectTransformer();
2018        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2019        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2020        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2021        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2022        $schemaParser = new PgSqlSchemaParser();
2023        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2024        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2025        $plans = $rewriter->rewriteMultiple('SELECT 1; SELECT 2');
2026        self::assertCount(2, $plans->plans());
2027    }
2028    public function testBuildTableContextShadowOnlyDifferentColumnKeysDetailed(): void
2029    {
2030        $shadowStore = new ShadowStore();
2031        $registry = new TableDefinitionRegistry();
2032        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2033        $parser = new PgSqlParser();
2034        $guard = new PgSqlQueryGuard($parser);
2035        $selectTransformer = new SelectTransformer();
2036        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2037        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2038        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2039        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2040        $schemaParser = new PgSqlSchemaParser();
2041        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2042        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2043        $rewriter->rewrite('DELETE FROM users WHERE id = 1');
2044        self::assertSame([], $shadowStore->get('users'));
2045    }
2046    public function testBuildTableContextShadowOnlyDifferentColumnKeys(): void
2047    {
2048        $shadowStore = new ShadowStore();
2049        $registry = new TableDefinitionRegistry();
2050        $parser = new PgSqlParser();
2051        $guard = new PgSqlQueryGuard($parser);
2052        $selectTransformer = new SelectTransformer();
2053        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2054        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2055        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2056        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2057        $schemaParser = new PgSqlSchemaParser();
2058        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2059        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2060        $shadowStore->set('my_table', [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob', 'extra' => 'val']]);
2061        $plan = $rewriter->rewrite('SELECT * FROM my_table');
2062        self::assertStringContainsString('"my_table" AS MATERIALIZED', $plan->sql());
2063        self::assertStringContainsString('"extra"', $plan->sql());
2064    }
2065    public function testTruncateReturnsSelectWhereFalse(): void
2066    {
2067        $shadowStore = new ShadowStore();
2068        $registry = new TableDefinitionRegistry();
2069        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2070        $parser = new PgSqlParser();
2071        $guard = new PgSqlQueryGuard($parser);
2072        $selectTransformer = new SelectTransformer();
2073        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2074        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2075        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2076        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2077        $schemaParser = new PgSqlSchemaParser();
2078        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2079        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2080        $plan = $rewriter->rewrite('TRUNCATE TABLE users');
2081        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
2082        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2083        self::assertInstanceOf(TruncateMutation::class, $plan->mutation());
2084    }
2085    public function testCreateTableAsSelectWithRegisteredSourceTable(): void
2086    {
2087        $shadowStore = new ShadowStore();
2088        $registry = new TableDefinitionRegistry();
2089        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2090        $parser = new PgSqlParser();
2091        $guard = new PgSqlQueryGuard($parser);
2092        $selectTransformer = new SelectTransformer();
2093        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2094        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2095        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2096        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2097        $schemaParser = new PgSqlSchemaParser();
2098        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2099        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2100        $plan = $rewriter->rewrite('CREATE TABLE archive AS SELECT id, name FROM users');
2101        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
2102        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
2103    }
2104    public function testCreateTableNotAsSelectReturnsSqlWhereFalse(): void
2105    {
2106        $shadowStore = new ShadowStore();
2107        $registry = new TableDefinitionRegistry();
2108        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2109        $parser = new PgSqlParser();
2110        $guard = new PgSqlQueryGuard($parser);
2111        $selectTransformer = new SelectTransformer();
2112        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2113        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2114        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2115        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2116        $schemaParser = new PgSqlSchemaParser();
2117        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2118        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2119        $plan = $rewriter->rewrite('CREATE TABLE new_table (id INTEGER, name TEXT)');
2120        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
2121        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
2122        self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
2123    }
2124    public function testRewriteInsertReturnsWriteSimulated(): void
2125    {
2126        $shadowStore = new ShadowStore();
2127        $registry = new TableDefinitionRegistry();
2128        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2129        $parser = new PgSqlParser();
2130        $guard = new PgSqlQueryGuard($parser);
2131        $selectTransformer = new SelectTransformer();
2132        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2133        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2134        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2135        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2136        $schemaParser = new PgSqlSchemaParser();
2137        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2138        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2139        $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice')");
2140        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2141        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
2142    }
2143    public function testRewriteInsertUsesDefaultsFromRegistryWithoutShadowRows(): void
2144    {
2145        $definition = (new PgSqlSchemaParser())->parse("CREATE TABLE settings (id INTEGER, label TEXT DEFAULT 'new')");
2146        self::assertNotNull($definition);
2147        $registry = new TableDefinitionRegistry();
2148        $registry->register('settings', $definition);
2149        $store = new ShadowStore();
2150        $parser = new PgSqlParser();
2151        $schemaParser = new PgSqlSchemaParser();
2152        $selectTransformer = new SelectTransformer();
2153        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2154        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2155        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2156        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2157        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2158        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2159        $plan = $rewriter->rewrite('INSERT INTO settings (id) VALUES (1)');
2160        self::assertStringContainsString("'new'", $plan->sql());
2161        self::assertStringContainsString('AS "label"', $plan->sql());
2162    }
2163    public function testRewriteInsertUsesIdentityStrategyFromRegistryWithoutShadowRows(): void
2164    {
2165        $definition = (new PgSqlSchemaParser())->parse('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)');
2166        self::assertNotNull($definition);
2167        $registry = new TableDefinitionRegistry();
2168        $registry->register('users', $definition);
2169        $store = new ShadowStore();
2170        $parser = new PgSqlParser();
2171        $schemaParser = new PgSqlSchemaParser();
2172        $selectTransformer = new SelectTransformer();
2173        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2174        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2175        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2176        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2177        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2178        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2179        $plan = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Alice')");
2180        self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $plan->sql());
2181    }
2182    public function testRewriteUpdateReturnsWriteSimulated(): void
2183    {
2184        $shadowStore = new ShadowStore();
2185        $registry = new TableDefinitionRegistry();
2186        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2187        $parser = new PgSqlParser();
2188        $guard = new PgSqlQueryGuard($parser);
2189        $selectTransformer = new SelectTransformer();
2190        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2191        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2192        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2193        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2194        $schemaParser = new PgSqlSchemaParser();
2195        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2196        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2197        $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
2198        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2199        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
2200    }
2201    public function testRewriteDeleteReturnsWriteSimulated(): void
2202    {
2203        $shadowStore = new ShadowStore();
2204        $registry = new TableDefinitionRegistry();
2205        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2206        $parser = new PgSqlParser();
2207        $guard = new PgSqlQueryGuard($parser);
2208        $selectTransformer = new SelectTransformer();
2209        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2210        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2211        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2212        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2213        $schemaParser = new PgSqlSchemaParser();
2214        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2215        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2216        $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
2217        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2218        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
2219    }
2220    public function testRewriteDropTableReturnsSelectWhereFalse(): void
2221    {
2222        $shadowStore = new ShadowStore();
2223        $registry = new TableDefinitionRegistry();
2224        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2225        $parser = new PgSqlParser();
2226        $guard = new PgSqlQueryGuard($parser);
2227        $selectTransformer = new SelectTransformer();
2228        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2229        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2230        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2231        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2232        $schemaParser = new PgSqlSchemaParser();
2233        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2234        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2235        $plan = $rewriter->rewrite('DROP TABLE IF EXISTS users');
2236        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
2237        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
2238        self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
2239    }
2240    public function testRewriteUpdateEnsuresShadowStoreEntry(): void
2241    {
2242        $shadowStore = new ShadowStore();
2243        $registry = new TableDefinitionRegistry();
2244        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2245        $parser = new PgSqlParser();
2246        $guard = new PgSqlQueryGuard($parser);
2247        $selectTransformer = new SelectTransformer();
2248        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2249        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2250        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2251        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2252        $schemaParser = new PgSqlSchemaParser();
2253        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2254        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2255        self::assertFalse(array_key_exists('users', $shadowStore->getAll()));
2256        $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
2257        self::assertTrue(array_key_exists('users', $shadowStore->getAll()));
2258    }
2259    public function testRewriteDeleteEnsuresShadowStoreEntry(): void
2260    {
2261        $shadowStore = new ShadowStore();
2262        $registry = new TableDefinitionRegistry();
2263        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2264        $parser = new PgSqlParser();
2265        $guard = new PgSqlQueryGuard($parser);
2266        $selectTransformer = new SelectTransformer();
2267        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2268        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2269        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2270        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2271        $schemaParser = new PgSqlSchemaParser();
2272        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2273        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2274        self::assertFalse(array_key_exists('users', $shadowStore->getAll()));
2275        $rewriter->rewrite('DELETE FROM users WHERE id = 1');
2276        self::assertTrue(array_key_exists('users', $shadowStore->getAll()));
2277    }
2278    public function testRewriteInsertDoesNotCallEnsureDmlTarget(): void
2279    {
2280        $shadowStore = new ShadowStore();
2281        $registry = new TableDefinitionRegistry();
2282        $registry->register('items', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2283        $parser = new PgSqlParser();
2284        $guard = new PgSqlQueryGuard($parser);
2285        $selectTransformer = new SelectTransformer();
2286        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2287        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2288        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2289        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2290        $schemaParser = new PgSqlSchemaParser();
2291        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2292        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2293        $rewriter->rewrite("INSERT INTO items (id, name) VALUES (1, 'Test')");
2294        self::assertFalse(array_key_exists('items', $shadowStore->getAll()));
2295    }
2296    public function testRewriteWithLeadingTrailingWhitespaceProducesValidSql(): void
2297    {
2298        $shadowStore = new ShadowStore();
2299        $registry = new TableDefinitionRegistry();
2300        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2301        $parser = new PgSqlParser();
2302        $guard = new PgSqlQueryGuard($parser);
2303        $selectTransformer = new SelectTransformer();
2304        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2305        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2306        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2307        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2308        $schemaParser = new PgSqlSchemaParser();
2309        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2310        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2311        $plan = $rewriter->rewrite("  \t SELECT * FROM users \n ");
2312        self::assertSame(QueryKind::READ, $plan->kind());
2313    }
2314    public function testRewriteMultipleWithLeadingTrailingWhitespaceProducesValidSql(): void
2315    {
2316        $shadowStore = new ShadowStore();
2317        $registry = new TableDefinitionRegistry();
2318        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2319        $parser = new PgSqlParser();
2320        $guard = new PgSqlQueryGuard($parser);
2321        $selectTransformer = new SelectTransformer();
2322        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2323        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2324        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2325        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2326        $schemaParser = new PgSqlSchemaParser();
2327        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2328        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2329        $multiPlan = $rewriter->rewriteMultiple("  \t SELECT 1 \n ");
2330        self::assertSame(1, $multiPlan->count());
2331    }
2332    public function testRewriteCreateTableAsSelectNonCreateStatementDoesNotEnterCtasBranch(): void
2333    {
2334        $shadowStore = new ShadowStore();
2335        $registry = new TableDefinitionRegistry();
2336        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'VARCHAR(255)', 'email' => 'TEXT'], ['id'], ['id', 'name'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER'), 'name' => new ColumnDeclaration(ColumnTypeFamily::STRING, 'VARCHAR(255)'), 'email' => new ColumnDeclaration(ColumnTypeFamily::TEXT, 'TEXT')]));
2337        $parser = new PgSqlParser();
2338        $guard = new PgSqlQueryGuard($parser);
2339        $selectTransformer = new SelectTransformer();
2340        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2341        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2342        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2343        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2344        $schemaParser = new PgSqlSchemaParser();
2345        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2346        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2347        $plan = $rewriter->rewrite('DROP TABLE IF EXISTS users');
2348        self::assertSame('SELECT 1 WHERE FALSE', $plan->sql());
2349        self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
2350    }
2351    public function testBuildTableContextShadowOnlyWithEmptyRowsNoCte(): void
2352    {
2353        $shadowStore = new ShadowStore();
2354        $registry = new TableDefinitionRegistry();
2355        $registry->register('empty_table', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], [], ['id' => new ColumnDeclaration(ColumnTypeFamily::INTEGER, 'INTEGER')]));
2356        $parser = new PgSqlParser();
2357        $guard = new PgSqlQueryGuard($parser);
2358        $selectTransformer = new SelectTransformer();
2359        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2360        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2361        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2362        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2363        $schemaParser = new PgSqlSchemaParser();
2364        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2365        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2366        $plan = $rewriter->rewrite('SELECT * FROM empty_table');
2367        self::assertStringContainsString('"empty_table" AS MATERIALIZED', $plan->sql());
2368    }
2369    public function testBuildTableContextShadowOnlyWithRowsDeriveColumns(): void
2370    {
2371        $shadowStore = new ShadowStore();
2372        $registry = new TableDefinitionRegistry();
2373        $parser = new PgSqlParser();
2374        $guard = new PgSqlQueryGuard($parser);
2375        $selectTransformer = new SelectTransformer();
2376        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2377        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2378        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2379        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2380        $schemaParser = new PgSqlSchemaParser();
2381        $mutationResolver = new PgSqlMutationResolver($shadowStore, $registry, $schemaParser, $parser);
2382        $rewriter = new PgSqlRewriter($guard, $shadowStore, $registry, $transformer, $mutationResolver, $parser);
2383        $shadowStore->set('derived', [['x' => 1, 'y' => 2]]);
2384        $plan = $rewriter->rewrite('SELECT * FROM derived');
2385        self::assertStringContainsString('"derived" AS MATERIALIZED', $plan->sql());
2386        self::assertStringContainsString('"x"', $plan->sql());
2387        self::assertStringContainsString('"y"', $plan->sql());
2388    }
2389    public function testSelectRewritesTableAfterBlockComment(): void
2390    {
2391        $registry = new TableDefinitionRegistry();
2392        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2393        $store = new ShadowStore();
2394        $store->set('users', [['id' => 1]]);
2395        $parser = new PgSqlParser();
2396        $schemaParser = new PgSqlSchemaParser();
2397        $selectTransformer = new SelectTransformer();
2398        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2399        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2400        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2401        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2402        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2403        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2404        $plan = $rewriter->rewrite('SELECT * FROM/* table */users');
2405        self::assertSame(QueryKind::READ, $plan->kind());
2406        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
2407    }
2408    public function testSelectIgnoresSqlKeywordsInsideLeadingLineComment(): void
2409    {
2410        $registry = new TableDefinitionRegistry();
2411        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2412        $store = new ShadowStore();
2413        $store->set('users', [['id' => 1]]);
2414        $parser = new PgSqlParser();
2415        $schemaParser = new PgSqlSchemaParser();
2416        $selectTransformer = new SelectTransformer();
2417        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2418        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2419        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2420        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2421        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2422        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2423        $sql = "-- SELECT * FROM other_table WHERE DELETE UPDATE INSERT\nSELECT * FROM users";
2424        $plan = $rewriter->rewrite($sql);
2425        self::assertSame(QueryKind::READ, $plan->kind());
2426        self::assertStringContainsString('"users" AS MATERIALIZED', $plan->sql());
2427        self::assertStringNotContainsString('"other_table" AS MATERIALIZED', $plan->sql());
2428    }
2429    public function testInsertResolvesTargetAfterBlockComment(): void
2430    {
2431        $registry = new TableDefinitionRegistry();
2432        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2433        $store = new ShadowStore();
2434        $parser = new PgSqlParser();
2435        $schemaParser = new PgSqlSchemaParser();
2436        $selectTransformer = new SelectTransformer();
2437        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2438        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2439        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2440        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2441        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2442        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2443        $plan = $rewriter->rewrite('INSERT INTO/* table */users (id) VALUES (1)');
2444        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2445        self::assertInstanceOf(InsertMutation::class, $plan->mutation());
2446        self::assertSame('users', $plan->mutation()->tableName());
2447    }
2448    public function testUpdateResolvesTargetAfterBlockComment(): void
2449    {
2450        $registry = new TableDefinitionRegistry();
2451        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2452        $store = new ShadowStore();
2453        $parser = new PgSqlParser();
2454        $schemaParser = new PgSqlSchemaParser();
2455        $selectTransformer = new SelectTransformer();
2456        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2457        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2458        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2459        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2460        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2461        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2462        $plan = $rewriter->rewrite('UPDATE/* table */users SET id = 2 WHERE id = 1');
2463        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2464        self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
2465        self::assertSame('users', $plan->mutation()->tableName());
2466    }
2467    public function testDeleteResolvesTargetAfterBlockComment(): void
2468    {
2469        $registry = new TableDefinitionRegistry();
2470        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
2471        $store = new ShadowStore();
2472        $parser = new PgSqlParser();
2473        $schemaParser = new PgSqlSchemaParser();
2474        $selectTransformer = new SelectTransformer();
2475        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2476        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2477        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2478        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2479        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2480        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2481        $plan = $rewriter->rewrite('DELETE FROM/* table */users WHERE id = 1');
2482        self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
2483        self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
2484        self::assertSame('users', $plan->mutation()->tableName());
2485    }
2486    public function testUpdatePreservesDoubledQuoteStringBeforeWhere(): void
2487    {
2488        $registry = new TableDefinitionRegistry();
2489        $registry->register('notes', new TableDefinition(['id', 'body'], ['id' => 'INTEGER', 'body' => 'TEXT'], ['id'], [], []));
2490        $store = new ShadowStore();
2491        $parser = new PgSqlParser();
2492        $schemaParser = new PgSqlSchemaParser();
2493        $selectTransformer = new SelectTransformer();
2494        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2495        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2496        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2497        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2498        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2499        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2500        $plan = $rewriter->rewrite("UPDATE notes SET body = 'it''s updated' WHERE id = 1");
2501        self::assertStringContainsString("'it''s updated' AS \"body\"", $plan->sql());
2502        self::assertStringContainsString('WHERE id = 1', $plan->sql());
2503    }
2504    public function testUpdatePreservesDollarQuotedStringBeforeWhere(): void
2505    {
2506        $registry = new TableDefinitionRegistry();
2507        $registry->register('notes', new TableDefinition(['id', 'body'], ['id' => 'INTEGER', 'body' => 'TEXT'], ['id'], [], []));
2508        $store = new ShadowStore();
2509        $parser = new PgSqlParser();
2510        $schemaParser = new PgSqlSchemaParser();
2511        $selectTransformer = new SelectTransformer();
2512        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2513        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2514        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2515        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2516        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2517        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2518        $plan = $rewriter->rewrite('UPDATE notes SET body = $$reference to notes table$$ WHERE id = 1');
2519        self::assertStringContainsString('$$reference to notes table$$ AS "body"', $plan->sql());
2520        self::assertStringContainsString('WHERE id = 1', $plan->sql());
2521    }
2522    public function testUpdatePreservesEscapeStringBeforeWhere(): void
2523    {
2524        $registry = new TableDefinitionRegistry();
2525        $registry->register('notes', new TableDefinition(['id', 'body'], ['id' => 'INTEGER', 'body' => 'TEXT'], ['id'], [], []));
2526        $store = new ShadowStore();
2527        $parser = new PgSqlParser();
2528        $schemaParser = new PgSqlSchemaParser();
2529        $selectTransformer = new SelectTransformer();
2530        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2531        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2532        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2533        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2534        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2535        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2536        $plan = $rewriter->rewrite("UPDATE notes SET body = E'line1\\nline2' WHERE id = 1");
2537        self::assertStringContainsString("E'line1\\nline2' AS \"body\"", $plan->sql());
2538        self::assertStringContainsString('WHERE id = 1', $plan->sql());
2539    }
2540    public function testUpdateDoesNotPromoteMaterializedUnknownTable(): void
2541    {
2542        $store = new ShadowStore();
2543        $store->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
2544        $registry = new TableDefinitionRegistry();
2545        $parser = new PgSqlParser();
2546        $schemaParser = new PgSqlSchemaParser();
2547        $selectTransformer = new SelectTransformer();
2548        $insertTransformer = new InsertTransformer($parser, $selectTransformer);
2549        $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
2550        $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
2551        $transformer = new PgSqlTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
2552        $mutationResolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2553        $rewriter = new PgSqlRewriter(new PgSqlQueryGuard($parser), $store, $registry, $transformer, $mutationResolver, $parser);
2554        $this->expectException(UnknownSchemaException::class);
2555        try {
2556            $rewriter->rewrite("UPDATE late_table SET name = 'Bob' WHERE id = 1");
2557        } finally {
2558            self::assertSame(ShadowTableState::Materialized, $store->state('late_table'));
2559        }
2560    }
2561    public function testTransactionStatementParsesShadowTransactionOperations(): void
2562    {
2563        $parser = new PgSqlParser();
2564        $schemaParser = new PgSqlSchemaParser();
2565        $registry = new TableDefinitionRegistry();
2566        $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
2567        self::assertNotNull($definition);
2568        $registry->register('users', $definition);
2569        $store = new ShadowStore();
2570        $views = new ViewDefinitionSet();
2571        $guard = new PgSqlQueryGuard($parser);
2572        $select = new SelectTransformer();
2573        $transformer = new PgSqlTransformer($parser, $select, new InsertTransformer($parser, $select), new UpdateTransformer($parser, $select), new DeleteTransformer($parser, $select));
2574        $resolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2575        $rewriter = new PgSqlRewriter($guard, $store, $registry, $transformer, $resolver, $parser, $views);
2576        self::assertEquals(\ZtdQuery\Sql\TransactionStatement::begin(), $rewriter->transactionStatement('BEGIN'));
2577        self::assertNull($rewriter->transactionStatement('SELECT 1'));
2578    }
2579    public function testSplitStatementsPreservesQuotedSemicolons(): void
2580    {
2581        $parser = new PgSqlParser();
2582        $schemaParser = new PgSqlSchemaParser();
2583        $registry = new TableDefinitionRegistry();
2584        $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
2585        self::assertNotNull($definition);
2586        $registry->register('users', $definition);
2587        $store = new ShadowStore();
2588        $views = new ViewDefinitionSet();
2589        $guard = new PgSqlQueryGuard($parser);
2590        $select = new SelectTransformer();
2591        $transformer = new PgSqlTransformer($parser, $select, new InsertTransformer($parser, $select), new UpdateTransformer($parser, $select), new DeleteTransformer($parser, $select));
2592        $resolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2593        $rewriter = new PgSqlRewriter($guard, $store, $registry, $transformer, $resolver, $parser, $views);
2594        self::assertSame(["SELECT ';'", 'SELECT 2'], $rewriter->splitStatements("SELECT ';'; SELECT 2"));
2595    }
2596    public function testEmptyResultSelectProducesNoRows(): void
2597    {
2598        $parser = new PgSqlParser();
2599        $schemaParser = new PgSqlSchemaParser();
2600        $registry = new TableDefinitionRegistry();
2601        $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
2602        self::assertNotNull($definition);
2603        $registry->register('users', $definition);
2604        $store = new ShadowStore();
2605        $views = new ViewDefinitionSet();
2606        $guard = new PgSqlQueryGuard($parser);
2607        $select = new SelectTransformer();
2608        $transformer = new PgSqlTransformer($parser, $select, new InsertTransformer($parser, $select), new UpdateTransformer($parser, $select), new DeleteTransformer($parser, $select));
2609        $resolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2610        $rewriter = new PgSqlRewriter($guard, $store, $registry, $transformer, $resolver, $parser, $views);
2611        self::assertSame('SELECT 1 WHERE FALSE', $rewriter->emptyResultSelect());
2612    }
2613    public function testCommitRewriteStateRetainsAllocatedSequenceValues(): void
2614    {
2615        $parser = new PgSqlParser();
2616        $schemaParser = new PgSqlSchemaParser();
2617        $registry = new TableDefinitionRegistry();
2618        $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
2619        self::assertNotNull($definition);
2620        $registry->register('users', $definition);
2621        $store = new ShadowStore();
2622        $views = new ViewDefinitionSet();
2623        $guard = new PgSqlQueryGuard($parser);
2624        $select = new SelectTransformer();
2625        $transformer = new PgSqlTransformer($parser, $select, new InsertTransformer($parser, $select), new UpdateTransformer($parser, $select), new DeleteTransformer($parser, $select));
2626        $resolver = new PgSqlMutationResolver($store, $registry, $schemaParser, $parser);
2627        $rewriter = new PgSqlRewriter($guard, $store, $registry, $transformer, $resolver, $parser, $views);
2628        $serial = $schemaParser->parse('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)');
2629        self::assertNotNull($serial);
2630        $registry->register('users', $serial);
2631        $first = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Ada')");
2632        self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $first->sql());
2633        $rewriter->commitRewriteState();
2634        $second = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Bob')");
2635        self::assertStringContainsString('CAST(2 AS INTEGER) AS "id"', $second->sql());
2636    }
2637}
2638