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