packages/ztd-query-sqlite/tests/Unit/Rewrite/SqliteRewriterTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PDO;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use ZtdQuery\Exception\UnknownSchemaException;
11use ZtdQuery\Exception\UnsupportedSqlException;
12use ZtdQuery\Platform\Sqlite\Rewrite\Returning\SqliteReturningProjectionParser;
13use ZtdQuery\Platform\Sqlite\Rewrite\SqliteQueryGuard;
14use ZtdQuery\Platform\Sqlite\Rewrite\SqliteRewriter;
15use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\DeleteTransformer;
16use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertTransformer;
17use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SelectTransformer;
18use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\SqliteTransformer;
19use ZtdQuery\Platform\Sqlite\Rewrite\Transformer\UpdateTransformer;
20use ZtdQuery\Platform\Sqlite\Schema\SqliteSchemaParser;
21use ZtdQuery\Platform\Sqlite\Schema\View\SqliteViewDefinitionParser;
22use ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\AlterTableMutation;
23use ZtdQuery\Platform\Sqlite\Shadow\SqliteMutationResolver;
24use ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter;
25use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
26use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
27use ZtdQuery\Platform\Sqlite\Sql\Value\SqliteCastRenderer;
28use ZtdQuery\Rewrite\QueryKind;
29use ZtdQuery\Schema\TableDefinition;
30use ZtdQuery\Schema\TableDefinitionRegistry;
31use ZtdQuery\Schema\ViewDefinitionSet;
32use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
33use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
34use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
35use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
36use ZtdQuery\Shadow\Mutation\Table\CreateTableMutation;
37use ZtdQuery\Shadow\Mutation\Table\DropTableMutation;
38use ZtdQuery\Shadow\Mutation\UpsertMutation;
39use ZtdQuery\Shadow\ShadowStore;
40use ZtdQuery\Shadow\ShadowTableState;
41
42#[CoversClass(SqliteRewriter::class)]
43#[UsesClass(AlterTableMutation::class)]
44#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AddColumnResolver::class)]
45#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\AlteredTableProjection::class)]
46#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\ColumnDefinitionEditor::class)]
47#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\DropColumnResolver::class)]
48#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Table\Alter\RenameResolver::class)]
49#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\InsertMutationResolver::class)]
50#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\MutationTableLookup::class)]
51#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\RowMutationResolver::class)]
52#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\TableMutationResolver::class)]
53#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Alter\AlterOperationParser::class)]
54#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Attach\AttachTokens::class)]
55#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
56#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
57#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
58#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
59#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
60#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
61#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
62#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
63#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
64#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
65#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
66#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Returning\ReturningItemParser::class)]
67#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
68#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
69#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
70#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Transaction\TransactionTokens::class)]
71#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
72#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ArithmeticExpressionParser::class)]
73#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ComparisonExpressionParser::class)]
74#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionCursor::class)]
75#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionTokenDecoder::class)]
76#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\LogicalExpressionParser::class)]
77#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\PrimaryExpressionParser::class)]
78#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Context\ReferencedTableLookup::class)]
79#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Context\TableContextBuilder::class)]
80#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteDependencies::class)]
81#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteHeaderParser::class)]
82#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CtePrefixMerger::class)]
83#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\CteReferences::class)]
84#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\FullTextColumns::class)]
85#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\MatchExpressionRewriter::class)]
86#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Index\IndexHintTokens::class)]
87#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\CastTypeMapper::class)]
88#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueExpressionRenderer::class)]
89#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\ValueLiteralRenderer::class)]
90#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\SqlEdits::class)]
91#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\StatementRewriter::class)]
92#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\UpsertConflictPredicate::class)]
93#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\UpsertExpressionBinder::class)]
94#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\ColumnDefinitionParser::class)]
95#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableBodyParser::class)]
96#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\TableDefinitionBuilder::class)]
97#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Create\VirtualTableParser::class)]
98#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyEntryParser::class)]
99#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\ForeignKeyTokens::class)]
100#[UsesClass(SqliteCastRenderer::class)]
101#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\SqliteColumnTypeMapper::class)]
102#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Cte\SqliteCteShadowComposer::class)]
103#[UsesClass(\ZtdQuery\Platform\Sqlite\Schema\Key\SqliteForeignKeyDefinitionParser::class)]
104#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\FullText\SqliteFullTextSearchRewriter::class)]
105#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\GeneratedColumn\SqliteGeneratedColumnProjector::class)]
106#[UsesClass(SqliteIdentifierQuoter::class)]
107#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Attach\SqliteInMemoryAttachStatement::class)]
108#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Index\SqliteIndexHintStripper::class)]
109#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
110#[UsesClass(SqliteLexicalMasker::class)]
111#[UsesClass(SqliteMutationResolver::class)]
112#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Upsert\SqliteNativeUpsertProjector::class)]
113#[UsesClass(SqliteParser::class)]
114#[UsesClass(SqliteQueryGuard::class)]
115#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Diagnostic\SqliteReadOnlyDiagnosticStatement::class)]
116#[UsesClass(SqliteReturningProjectionParser::class)]
117#[UsesClass(SqliteSchemaParser::class)]
118#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
119#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Transaction\SqliteTransactionStatementParser::class)]
120#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\SqliteUpsertExpressionParser::class)]
121#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Value\SqliteValueRenderer::class)]
122#[UsesClass(SqliteViewDefinitionParser::class)]
123#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\View\SqliteViewShadowRenderer::class)]
124#[UsesClass(DeleteTransformer::class)]
125#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertRowRenderer::class)]
126#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\InsertSelectRenderer::class)]
127#[UsesClass(InsertTransformer::class)]
128#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\Insert\InsertProjectionBuilder::class)]
129#[UsesClass(SelectTransformer::class)]
130#[UsesClass(\ZtdQuery\Platform\Sqlite\Rewrite\Transformer\Select\ShadowCteRenderer::class)]
131#[UsesClass(SqliteTransformer::class)]
132#[UsesClass(UpdateTransformer::class)]
133final class SqliteRewriterTest extends \PHPUnit\Framework\TestCase
134{
135 public function testSelectReturnsReadKind(): void
136 {
137 $registry = new TableDefinitionRegistry();
138 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
139 $store = new ShadowStore();
140 $parser = new SqliteParser();
141 $guard = new SqliteQueryGuard($parser);
142 $schemaParser = new SqliteSchemaParser();
143 $selectTransformer = new SelectTransformer();
144 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
145 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
146 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
147 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
148 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
149 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
150 $plan = $rewriter->rewrite('SELECT * FROM users');
151 self::assertSame(QueryKind::READ, $plan->kind());
152 self::assertNull($plan->mutation());
153 self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
154 }
155
156 public function testInsertReturnsWriteSimulatedWithMutation(): void
157 {
158 $registry = new TableDefinitionRegistry();
159 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
160 $store = new ShadowStore();
161 $parser = new SqliteParser();
162 $guard = new SqliteQueryGuard($parser);
163 $schemaParser = new SqliteSchemaParser();
164 $selectTransformer = new SelectTransformer();
165 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
166 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
167 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
168 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
169 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
170 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
171 $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
172 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
173 self::assertInstanceOf(InsertMutation::class, $plan->mutation());
174 self::assertSame('users', $plan->mutation()->tableName());
175 self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
176 }
177
178 public function testUpdateReturnsWriteSimulatedWithMutation(): void
179 {
180 $registry = new TableDefinitionRegistry();
181 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
182 $store = new ShadowStore();
183 $store->ensure('users');
184 $parser = new SqliteParser();
185 $guard = new SqliteQueryGuard($parser);
186 $schemaParser = new SqliteSchemaParser();
187 $selectTransformer = new SelectTransformer();
188 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
189 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
190 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
191 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
192 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
193 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
194 $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
195 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
196 self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
197 self::assertSame('users', $plan->mutation()->tableName());
198 self::assertStringContainsString('"users"."id" AS "__ztd_original_id"', $plan->sql());
199 self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
200 }
201
202 public function testDeleteReturnsWriteSimulatedWithMutation(): void
203 {
204 $registry = new TableDefinitionRegistry();
205 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
206 $store = new ShadowStore();
207 $store->ensure('users');
208 $parser = new SqliteParser();
209 $guard = new SqliteQueryGuard($parser);
210 $schemaParser = new SqliteSchemaParser();
211 $selectTransformer = new SelectTransformer();
212 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
213 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
214 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
215 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
216 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
217 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
218 $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
219 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
220 self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
221 self::assertSame('users', $plan->mutation()->tableName());
222 self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql());
223 }
224
225 public function testCreateTableReturnsDdlSimulated(): void
226 {
227 $store = new ShadowStore();
228 $registry = new TableDefinitionRegistry();
229 $parser = new SqliteParser();
230 $guard = new SqliteQueryGuard($parser);
231 $schemaParser = new SqliteSchemaParser();
232 $selectTransformer = new SelectTransformer();
233 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
234 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
235 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
236 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
237 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
238 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
239 $plan = $rewriter->rewrite('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)');
240 self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
241 self::assertInstanceOf(CreateTableMutation::class, $plan->mutation());
242 }
243
244 public function testDropTableReturnsDdlSimulated(): void
245 {
246 $registry = new TableDefinitionRegistry();
247 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
248 $store = new ShadowStore();
249 $parser = new SqliteParser();
250 $guard = new SqliteQueryGuard($parser);
251 $schemaParser = new SqliteSchemaParser();
252 $selectTransformer = new SelectTransformer();
253 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
254 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
255 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
256 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
257 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
258 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
259 $plan = $rewriter->rewrite('DROP TABLE users');
260 self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
261 self::assertInstanceOf(DropTableMutation::class, $plan->mutation());
262 }
263
264 public function testUnsupportedSqlThrowsException(): void
265 {
266 $store = new ShadowStore();
267 $registry = new TableDefinitionRegistry();
268 $parser = new SqliteParser();
269 $guard = new SqliteQueryGuard($parser);
270 $schemaParser = new SqliteSchemaParser();
271 $selectTransformer = new SelectTransformer();
272 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
273 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
274 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
275 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
276 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
277 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
278 $this->expectException(UnsupportedSqlException::class);
279 $rewriter->rewrite('CREATE INDEX idx ON users (name)');
280 }
281
282 public function testEmptyInputThrowsException(): void
283 {
284 $store = new ShadowStore();
285 $registry = new TableDefinitionRegistry();
286 $parser = new SqliteParser();
287 $guard = new SqliteQueryGuard($parser);
288 $schemaParser = new SqliteSchemaParser();
289 $selectTransformer = new SelectTransformer();
290 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
291 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
292 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
293 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
294 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
295 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
296 $this->expectException(UnsupportedSqlException::class);
297 $rewriter->rewrite('');
298 }
299
300 public function testRewriteIsDeterministic(): void
301 {
302 $registry = new TableDefinitionRegistry();
303 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
304 $store = new ShadowStore();
305 $parser = new SqliteParser();
306 $guard = new SqliteQueryGuard($parser);
307 $schemaParser = new SqliteSchemaParser();
308 $selectTransformer = new SelectTransformer();
309 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
310 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
311 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
312 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
313 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
314 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
315 $plan1 = $rewriter->rewrite('SELECT * FROM users');
316 $plan2 = $rewriter->rewrite('SELECT * FROM users');
317 self::assertSame($plan1->sql(), $plan2->sql());
318 self::assertSame($plan1->kind(), $plan2->kind());
319 }
320
321 public function testReadPlanHasNoMutation(): void
322 {
323 $registry = new TableDefinitionRegistry();
324 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
325 $store = new ShadowStore();
326 $parser = new SqliteParser();
327 $guard = new SqliteQueryGuard($parser);
328 $schemaParser = new SqliteSchemaParser();
329 $selectTransformer = new SelectTransformer();
330 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
331 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
332 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
333 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
334 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
335 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
336 $plan = $rewriter->rewrite('SELECT * FROM users');
337 self::assertSame(QueryKind::READ, $plan->kind());
338 self::assertNull($plan->mutation());
339 }
340
341 public function testWritePlanHasNonNullMutation(): void
342 {
343 $registry = new TableDefinitionRegistry();
344 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
345 $store = new ShadowStore();
346 $parser = new SqliteParser();
347 $guard = new SqliteQueryGuard($parser);
348 $schemaParser = new SqliteSchemaParser();
349 $selectTransformer = new SelectTransformer();
350 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
351 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
352 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
353 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
354 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
355 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
356 $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
357 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
358 self::assertNotNull($plan->mutation());
359 self::assertInstanceOf(InsertMutation::class, $plan->mutation());
360 }
361
362 public function testRewriteOutputIsNonEmpty(): void
363 {
364 $rewriterStore = new ShadowStore();
365 $rewriterRegistry = new TableDefinitionRegistry();
366 $rewriterParser = new SqliteSchemaParser();
367 $rewriterDefinition = $rewriterParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
368 self::assertNotNull($rewriterDefinition);
369 $rewriterRegistry->register('users', $rewriterDefinition);
370 $rewriter2Store = $rewriterStore;
371 $rewriter2Registry = $rewriterRegistry;
372 $rewriter2Parser = new SqliteParser();
373 $rewriter2SchemaParser = new SqliteSchemaParser();
374 $rewriter2SelectTransformer = new SelectTransformer();
375 $rewriter2InsertTransformer = new InsertTransformer($rewriter2Parser, $rewriter2SelectTransformer);
376 $rewriter2UpdateTransformer = new UpdateTransformer($rewriter2Parser, $rewriter2SelectTransformer);
377 $rewriter2DeleteTransformer = new DeleteTransformer($rewriter2Parser, $rewriter2SelectTransformer);
378 $rewriter2Transformer = new SqliteTransformer($rewriter2Parser, $rewriter2SelectTransformer, $rewriter2InsertTransformer, $rewriter2UpdateTransformer, $rewriter2DeleteTransformer);
379 $rewriter2MutationResolver = new SqliteMutationResolver($rewriter2Store, $rewriter2Registry, $rewriter2SchemaParser, $rewriter2Parser);
380 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriter2Parser), $rewriter2Store, $rewriter2Registry, $rewriter2Transformer, $rewriter2MutationResolver, $rewriter2Parser);
381 $plan = $rewriter->rewrite('SELECT id, name, email FROM users WHERE id = 1');
382 self::assertNotEmpty($plan->sql());
383 self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
384 }
385
386 public function testInsertRewriteOutputContainsSelect(): void
387 {
388 $rewriterStore = new ShadowStore();
389 $rewriterRegistry = new TableDefinitionRegistry();
390 $rewriterParser = new SqliteSchemaParser();
391 $rewriterDefinition = $rewriterParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
392 self::assertNotNull($rewriterDefinition);
393 $rewriterRegistry->register('users', $rewriterDefinition);
394 $rewriter2Store = $rewriterStore;
395 $rewriter2Registry = $rewriterRegistry;
396 $rewriter2Parser = new SqliteParser();
397 $rewriter2SchemaParser = new SqliteSchemaParser();
398 $rewriter2SelectTransformer = new SelectTransformer();
399 $rewriter2InsertTransformer = new InsertTransformer($rewriter2Parser, $rewriter2SelectTransformer);
400 $rewriter2UpdateTransformer = new UpdateTransformer($rewriter2Parser, $rewriter2SelectTransformer);
401 $rewriter2DeleteTransformer = new DeleteTransformer($rewriter2Parser, $rewriter2SelectTransformer);
402 $rewriter2Transformer = new SqliteTransformer($rewriter2Parser, $rewriter2SelectTransformer, $rewriter2InsertTransformer, $rewriter2UpdateTransformer, $rewriter2DeleteTransformer);
403 $rewriter2MutationResolver = new SqliteMutationResolver($rewriter2Store, $rewriter2Registry, $rewriter2SchemaParser, $rewriter2Parser);
404 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriter2Parser), $rewriter2Store, $rewriter2Registry, $rewriter2Transformer, $rewriter2MutationResolver, $rewriter2Parser);
405 $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com')");
406 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
407 self::assertMatchesRegularExpression('/^(?:WITH\b|SELECT\b)/i', $plan->sql(), 'INSERT rewrite must produce a result-select query starting with SELECT or WITH...SELECT');
408 }
409
410 public function testGeneratedExpressionIsPresentBeforeTheFirstShadowWrite(): void
411 {
412 $store = new ShadowStore();
413 $registry = new TableDefinitionRegistry();
414 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE orders (qty INTEGER, total INTEGER GENERATED ALWAYS AS (qty * 2) STORED)');
415 self::assertNotNull($definition);
416 $registry->register('orders', $definition);
417 $rewriterStore = $store;
418 $rewriterRegistry = $registry;
419 $rewriterParser = new SqliteParser();
420 $rewriterSchemaParser = new SqliteSchemaParser();
421 $rewriterSelectTransformer = new SelectTransformer();
422 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
423 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
424 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
425 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
426 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
427 $sql = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('SELECT total FROM orders')->sql();
428 self::assertStringContainsString('(qty * 2) AS "total"', $sql);
429 }
430
431 public function testRegisteredViewIsKnownAndMaterialized(): void
432 {
433 $store = new ShadowStore();
434 $registry = new TableDefinitionRegistry();
435 $parser = new SqliteParser();
436 $schemaParser = new SqliteSchemaParser();
437 $definition = $schemaParser->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
438 self::assertNotNull($definition);
439 $registry->register('users', $definition);
440 $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
441 $selectTransformer = new SelectTransformer();
442 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
443 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
444 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
445 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
446 $resolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
447 $views = new ViewDefinitionSet();
448 $views->register('active_users', (new SqliteViewDefinitionParser())->fromQuery('SELECT id FROM main.users'));
449 $rewriter = new SqliteRewriter(new SqliteQueryGuard($parser), $store, $registry, $transformer, $resolver, $parser, $views);
450 $sql = $rewriter->rewrite('SELECT * FROM active_users')->sql();
451 self::assertStringStartsWith('WITH "users" AS', $sql);
452 self::assertStringContainsString('"active_users" AS (SELECT id FROM users)', $sql);
453 $viewOnlyStore = new ShadowStore();
454 $viewOnlyRegistry = new TableDefinitionRegistry();
455 $viewOnlyViews = new ViewDefinitionSet();
456 $viewOnlyViews->register('constant_view', (new SqliteViewDefinitionParser())->fromQuery('SELECT 1 AS id'));
457 $viewOnlyResolver = new SqliteMutationResolver($viewOnlyStore, $viewOnlyRegistry, $schemaParser, $parser);
458 $viewOnlyRewriter = new SqliteRewriter(new SqliteQueryGuard($parser), $viewOnlyStore, $viewOnlyRegistry, $transformer, $viewOnlyResolver, $parser, $viewOnlyViews);
459 $this->expectException(UnknownSchemaException::class);
460 $viewOnlyRewriter->rewrite('SELECT * FROM missing_table');
461 }
462
463 public function testCteReferencesAreMatchedCaseInsensitivelyDuringSchemaValidation(): void
464 {
465 $registry = new TableDefinitionRegistry();
466 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
467 self::assertNotNull($definition);
468 $registry->register('known_table', $definition);
469 $rewriterStore = new ShadowStore();
470 $rewriterRegistry = $registry;
471 $rewriterParser = new SqliteParser();
472 $rewriterSchemaParser = new SqliteSchemaParser();
473 $rewriterSelectTransformer = new SelectTransformer();
474 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
475 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
476 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
477 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
478 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
479 $plan = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users');
480 self::assertSame(QueryKind::READ, $plan->kind());
481 }
482
483 public function testUnknownTableAfterDeclaredCteIsRejected(): void
484 {
485 $registry = new TableDefinitionRegistry();
486 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE known_table (id INTEGER PRIMARY KEY)');
487 self::assertNotNull($definition);
488 $registry->register('known_table', $definition);
489 $this->expectException(UnknownSchemaException::class);
490 $this->expectExceptionMessage('missing_table');
491 $rewriterStore = new ShadowStore();
492 $rewriterRegistry = $registry;
493 $rewriterParser = new SqliteParser();
494 $rewriterSchemaParser = new SqliteSchemaParser();
495 $rewriterSelectTransformer = new SelectTransformer();
496 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
497 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
498 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
499 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
500 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
501 (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('WITH users AS (SELECT 1 AS id) SELECT * FROM Users JOIN missing_table ON TRUE');
502 }
503
504 public function testInMemoryAttachPassesThroughUnchanged(): void
505 {
506 $rewriterStore = new ShadowStore();
507 $rewriterRegistry = new TableDefinitionRegistry();
508 $rewriterParser = new SqliteParser();
509 $rewriterSchemaParser = new SqliteSchemaParser();
510 $rewriterSelectTransformer = new SelectTransformer();
511 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
512 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
513 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
514 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
515 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
516 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
517 $sql = "ATTACH DATABASE ':memory:' AS db2";
518 $plan = $rewriter->rewrite($sql);
519 self::assertSame(QueryKind::READ, $plan->kind());
520 self::assertSame($sql, $plan->sql());
521 }
522
523 public function testPersistentAttachRemainsUnsupported(): void
524 {
525 $rewriterStore = new ShadowStore();
526 $rewriterRegistry = new TableDefinitionRegistry();
527 $rewriterParser = new SqliteParser();
528 $rewriterSchemaParser = new SqliteSchemaParser();
529 $rewriterSelectTransformer = new SelectTransformer();
530 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
531 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
532 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
533 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
534 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
535 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
536 $this->expectException(UnsupportedSqlException::class);
537 $rewriter->rewrite("ATTACH 'test.sqlite' AS db2");
538 }
539
540 public function testSchemaQualifiedSelectUsesShadowCte(): void
541 {
542 $store = new ShadowStore();
543 $store->set('users', [['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com']]);
544 $registry = new TableDefinitionRegistry();
545 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL)');
546 self::assertNotNull($definition);
547 $registry->register('users', $definition);
548 $rewriterStore = $store;
549 $rewriterRegistry = $registry;
550 $rewriterParser = new SqliteParser();
551 $rewriterSchemaParser = new SqliteSchemaParser();
552 $rewriterSelectTransformer = new SelectTransformer();
553 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
554 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
555 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
556 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
557 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
558 $plan = (new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser))->rewrite('SELECT name FROM main.users');
559 self::assertStringStartsWith('WITH "users" AS', $plan->sql());
560 self::assertStringEndsWith('SELECT name FROM users', $plan->sql());
561 }
562
563 public function testReadOnlyDiagnosticsPassThroughUnchanged(): void
564 {
565 $rewriterStore = new ShadowStore();
566 $rewriterRegistry = new TableDefinitionRegistry();
567 $rewriterParser = new SqliteParser();
568 $rewriterSchemaParser = new SqliteSchemaParser();
569 $rewriterSelectTransformer = new SelectTransformer();
570 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
571 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
572 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
573 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
574 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
575 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
576 $sql = 'EXPLAIN QUERY PLAN SELECT * FROM users';
577 $plan = $rewriter->rewrite($sql);
578 self::assertSame(QueryKind::READ, $plan->kind());
579 self::assertSame($sql, $plan->sql());
580 }
581
582 public function testDeleteFromWithoutWhereReturnsWriteSimulated(): void
583 {
584 $registry = new TableDefinitionRegistry();
585 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
586 $store = new ShadowStore();
587 $store->ensure('users');
588 $parser = new SqliteParser();
589 $guard = new SqliteQueryGuard($parser);
590 $schemaParser = new SqliteSchemaParser();
591 $selectTransformer = new SelectTransformer();
592 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
593 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
594 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
595 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
596 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
597 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
598 $plan = $rewriter->rewrite('DELETE FROM users');
599 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
600 self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
601 }
602
603 public function testReplaceReturnsWriteSimulatedWithMutation(): void
604 {
605 $registry = new TableDefinitionRegistry();
606 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
607 $store = new ShadowStore();
608 $parser = new SqliteParser();
609 $guard = new SqliteQueryGuard($parser);
610 $schemaParser = new SqliteSchemaParser();
611 $selectTransformer = new SelectTransformer();
612 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
613 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
614 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
615 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
616 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
617 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
618 $plan = $rewriter->rewrite("REPLACE INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
619 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
620 self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
621 }
622
623 public function testInsertOrReplaceReturnsWriteSimulatedWithReplaceMutation(): void
624 {
625 $registry = new TableDefinitionRegistry();
626 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
627 $store = new ShadowStore();
628 $parser = new SqliteParser();
629 $guard = new SqliteQueryGuard($parser);
630 $schemaParser = new SqliteSchemaParser();
631 $selectTransformer = new SelectTransformer();
632 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
633 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
634 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
635 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
636 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
637 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
638 $plan = $rewriter->rewrite("INSERT OR REPLACE INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
639 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
640 self::assertInstanceOf(ReplaceMutation::class, $plan->mutation());
641 }
642
643 public function testInsertOnConflictReturnsUpsertMutation(): void
644 {
645 $registry = new TableDefinitionRegistry();
646 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
647 $store = new ShadowStore();
648 $parser = new SqliteParser();
649 $guard = new SqliteQueryGuard($parser);
650 $schemaParser = new SqliteSchemaParser();
651 $selectTransformer = new SelectTransformer();
652 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
653 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
654 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
655 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
656 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
657 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
658 $plan = $rewriter->rewrite("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
659 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
660 self::assertInstanceOf(UpsertMutation::class, $plan->mutation());
661 self::assertStringContainsString('__ztd_upsert_value_0', $plan->sql());
662 self::assertStringNotContainsString('excluded.', $plan->sql());
663 }
664
665 public function testMultiStatementThrowsException(): void
666 {
667 $store = new ShadowStore();
668 $registry = new TableDefinitionRegistry();
669 $parser = new SqliteParser();
670 $guard = new SqliteQueryGuard($parser);
671 $schemaParser = new SqliteSchemaParser();
672 $selectTransformer = new SelectTransformer();
673 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
674 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
675 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
676 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
677 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
678 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
679 $this->expectException(UnsupportedSqlException::class);
680 $rewriter->rewrite('SELECT 1; SELECT 2');
681 }
682
683 public function testInsertUsesDefaultsFromRegistryWithoutShadowRows(): void
684 {
685 $definition = (new SqliteSchemaParser())->parse("CREATE TABLE settings (id INTEGER, label TEXT DEFAULT 'new')");
686 self::assertNotNull($definition);
687 $registry = new TableDefinitionRegistry();
688 $registry->register('settings', $definition);
689 $rewriterStore = new ShadowStore();
690 $rewriterRegistry = $registry;
691 $rewriterParser = new SqliteParser();
692 $rewriterSchemaParser = new SqliteSchemaParser();
693 $rewriterSelectTransformer = new SelectTransformer();
694 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
695 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
696 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
697 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
698 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
699 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
700 $plan = $rewriter->rewrite('INSERT INTO settings (id) VALUES (1)');
701 self::assertStringContainsString("'new'", $plan->sql());
702 self::assertStringContainsString('AS "label"', $plan->sql());
703 }
704
705 public function testInsertUsesIdentityStrategyFromRegistryWithoutShadowRows(): void
706 {
707 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
708 self::assertNotNull($definition);
709 $registry = new TableDefinitionRegistry();
710 $registry->register('users', $definition);
711 $rewriterStore = new ShadowStore();
712 $rewriterRegistry = $registry;
713 $rewriterParser = new SqliteParser();
714 $rewriterSchemaParser = new SqliteSchemaParser();
715 $rewriterSelectTransformer = new SelectTransformer();
716 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
717 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
718 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
719 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
720 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
721 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
722 $plan = $rewriter->rewrite("INSERT INTO users (name) VALUES ('Alice')");
723 self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $plan->sql());
724 }
725
726 public function testRewriteMultiple(): void
727 {
728 $registry = new TableDefinitionRegistry();
729 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
730 $store = new ShadowStore();
731 $parser = new SqliteParser();
732 $guard = new SqliteQueryGuard($parser);
733 $schemaParser = new SqliteSchemaParser();
734 $selectTransformer = new SelectTransformer();
735 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
736 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
737 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
738 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
739 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
740 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
741 $multiPlan = $rewriter->rewriteMultiple("SELECT * FROM users; INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'a@b.com')");
742 self::assertSame(2, $multiPlan->count());
743 self::assertSame(QueryKind::READ, $multiPlan->get(0)?->kind());
744 self::assertSame(QueryKind::WRITE_SIMULATED, $multiPlan->get(1)?->kind());
745 }
746
747 public function testSelectWithShadowDataIncludesCte(): void
748 {
749 $registry = new TableDefinitionRegistry();
750 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
751 $store = new ShadowStore();
752 $store->ensure('users');
753 $store->insert('users', [['id' => 1, 'name' => 'Alice', 'email' => 'a@b.com']]);
754 $parser = new SqliteParser();
755 $guard = new SqliteQueryGuard($parser);
756 $schemaParser = new SqliteSchemaParser();
757 $selectTransformer = new SelectTransformer();
758 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
759 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
760 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
761 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
762 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
763 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
764 $plan = $rewriter->rewrite('SELECT * FROM users');
765 self::assertStringStartsWith('WITH', $plan->sql());
766 self::assertStringContainsString('"users"', $plan->sql());
767 self::assertStringContainsString('SELECT', strtoupper($plan->sql()));
768 }
769
770 public function testSelectUnknownTableWithSchemaContextThrows(): void
771 {
772 $registry = new TableDefinitionRegistry();
773 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
774 $store = new ShadowStore();
775 $parser = new SqliteParser();
776 $guard = new SqliteQueryGuard($parser);
777 $schemaParser = new SqliteSchemaParser();
778 $selectTransformer = new SelectTransformer();
779 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
780 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
781 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
782 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
783 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
784 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
785 $this->expectException(UnknownSchemaException::class);
786 $rewriter->rewrite('SELECT * FROM nonexistent');
787 }
788
789 public function testSelectKnownTableNoSchemaContextDoesNotThrow(): void
790 {
791 $store = new ShadowStore();
792 $registry = new TableDefinitionRegistry();
793 $parser = new SqliteParser();
794 $guard = new SqliteQueryGuard($parser);
795 $schemaParser = new SqliteSchemaParser();
796 $selectTransformer = new SelectTransformer();
797 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
798 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
799 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
800 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
801 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
802 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
803 $plan = $rewriter->rewrite('SELECT * FROM whatever');
804 self::assertSame(QueryKind::READ, $plan->kind());
805 }
806
807 public function testSelectWithShadowStoreOnlyHasSchemaContext(): void
808 {
809 $store = new ShadowStore();
810 $store->ensure('users');
811 $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
812 $registry = new TableDefinitionRegistry();
813 $parser = new SqliteParser();
814 $guard = new SqliteQueryGuard($parser);
815 $schemaParser = new SqliteSchemaParser();
816 $selectTransformer = new SelectTransformer();
817 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
818 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
819 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
820 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
821 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
822 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
823 $plan = $rewriter->rewrite('SELECT * FROM users');
824 self::assertSame(QueryKind::READ, $plan->kind());
825 self::assertStringContainsString('WITH', $plan->sql());
826 }
827
828 public function testUpdateEnsuresShadowStore(): void
829 {
830 $registry = new TableDefinitionRegistry();
831 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
832 $store = new ShadowStore();
833 $parser = new SqliteParser();
834 $guard = new SqliteQueryGuard($parser);
835 $schemaParser = new SqliteSchemaParser();
836 $selectTransformer = new SelectTransformer();
837 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
838 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
839 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
840 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
841 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
842 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
843 $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
844 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
845 self::assertNotNull($plan->mutation());
846 self::assertSame('users', $plan->mutation()->tableName());
847 }
848
849 public function testDeleteFromWithoutWhereReturnsSqlWithSelectWhere0(): void
850 {
851 $registry = new TableDefinitionRegistry();
852 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
853 $store = new ShadowStore();
854 $store->ensure('users');
855 $parser = new SqliteParser();
856 $guard = new SqliteQueryGuard($parser);
857 $schemaParser = new SqliteSchemaParser();
858 $selectTransformer = new SelectTransformer();
859 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
860 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
861 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
862 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
863 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
864 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
865 $plan = $rewriter->rewrite('DELETE FROM users');
866 self::assertStringContainsString('FROM "users"', $plan->sql());
867 }
868
869 public function testDdlSimulatedReturnsSqlSelectWhere0(): void
870 {
871 $store = new ShadowStore();
872 $registry = new TableDefinitionRegistry();
873 $parser = new SqliteParser();
874 $guard = new SqliteQueryGuard($parser);
875 $schemaParser = new SqliteSchemaParser();
876 $selectTransformer = new SelectTransformer();
877 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
878 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
879 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
880 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
881 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
882 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
883 $plan = $rewriter->rewrite('CREATE TABLE t (id INTEGER PRIMARY KEY)');
884 self::assertSame('SELECT 1 WHERE 0', $plan->sql());
885 self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
886 }
887
888 public function testAlterTableUsesShadowedMigrationSelect(): void
889 {
890 $registry = new TableDefinitionRegistry();
891 $registry->register('people', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
892 $store = new ShadowStore();
893 $store->set('people', [['id' => 1, 'name' => 'Alice']]);
894 $rewriterStore = $store;
895 $rewriterRegistry = $registry;
896 $rewriterParser = new SqliteParser();
897 $rewriterSchemaParser = new SqliteSchemaParser();
898 $rewriterSelectTransformer = new SelectTransformer();
899 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
900 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
901 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
902 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
903 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
904 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
905 $plan = $rewriter->rewrite('ALTER TABLE people ADD COLUMN age INTEGER DEFAULT 7');
906 self::assertSame(QueryKind::DDL_SIMULATED, $plan->kind());
907 self::assertInstanceOf(AlterTableMutation::class, $plan->mutation());
908 self::assertStringContainsString('WITH "people" AS', $plan->sql());
909 self::assertStringContainsString('SELECT "id", "name", 7 AS "age" FROM "people"', $plan->sql());
910 }
911
912 public function testRemovedTableIsShadowedInsteadOfFallingThrough(): void
913 {
914 $registry = new TableDefinitionRegistry();
915 $registry->register('people', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
916 $registry->markRemoved('people');
917 $rewriterStore = new ShadowStore();
918 $rewriterRegistry = $registry;
919 $rewriterParser = new SqliteParser();
920 $rewriterSchemaParser = new SqliteSchemaParser();
921 $rewriterSelectTransformer = new SelectTransformer();
922 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
923 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
924 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
925 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
926 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
927 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
928 $plan = $rewriter->rewrite('SELECT id, name FROM people');
929 self::assertStringContainsString('WITH "people" AS', $plan->sql());
930 self::assertStringContainsString('WHERE 0', $plan->sql());
931 }
932
933 public function testRewriteMultipleEmptyThrows(): void
934 {
935 $store = new ShadowStore();
936 $registry = new TableDefinitionRegistry();
937 $parser = new SqliteParser();
938 $guard = new SqliteQueryGuard($parser);
939 $schemaParser = new SqliteSchemaParser();
940 $selectTransformer = new SelectTransformer();
941 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
942 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
943 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
944 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
945 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
946 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
947 $this->expectException(UnsupportedSqlException::class);
948 $rewriter->rewriteMultiple('');
949 }
950
951 public function testSelectWithRegistryOnlyBuildTableContext(): void
952 {
953 $registry = new TableDefinitionRegistry();
954 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
955 $store = new ShadowStore();
956 $parser = new SqliteParser();
957 $guard = new SqliteQueryGuard($parser);
958 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
964 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
965 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
966 $plan = $rewriter->rewrite('SELECT * FROM users');
967 self::assertSame(QueryKind::READ, $plan->kind());
968 self::assertStringContainsString('WITH', $plan->sql());
969 self::assertStringContainsString('WHERE 0', $plan->sql());
970 }
971
972 public function testSelectWithShadowDataColumnsInferred(): void
973 {
974 $store = new ShadowStore();
975 $store->ensure('users');
976 $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
977 $registry = new TableDefinitionRegistry();
978 $parser = new SqliteParser();
979 $guard = new SqliteQueryGuard($parser);
980 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
986 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
987 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
988 $plan = $rewriter->rewrite('SELECT * FROM users');
989 self::assertStringContainsString('"users"', $plan->sql());
990 self::assertStringContainsString('"id"', $plan->sql());
991 self::assertStringContainsString('"name"', $plan->sql());
992 }
993
994 public function testDeleteWithWhereProducesTransformedSql(): void
995 {
996 $registry = new TableDefinitionRegistry();
997 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
998 $store = new ShadowStore();
999 $store->ensure('users');
1000 $parser = new SqliteParser();
1001 $guard = new SqliteQueryGuard($parser);
1002 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1008 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1009 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1010 $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1011 self::assertStringContainsString('SELECT', $plan->sql());
1012 self::assertStringContainsString('WHERE id = 1', $plan->sql());
1013 }
1014
1015 public function testDeleteEnsuresShadowStore(): void
1016 {
1017 $registry = new TableDefinitionRegistry();
1018 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1019 $store = new ShadowStore();
1020 $parser = new SqliteParser();
1021 $guard = new SqliteQueryGuard($parser);
1022 $schemaParser = new SqliteSchemaParser();
1023 $selectTransformer = new SelectTransformer();
1024 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1025 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1026 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1027 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1028 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1029 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1030 $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1031 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1032 self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
1033 }
1034
1035 public function testSelectExistingInShadowStoreIsNotUnknown(): void
1036 {
1037 $store = new ShadowStore();
1038 $store->ensure('users');
1039 $store->set('users', [['id' => 1]]);
1040 $registry = new TableDefinitionRegistry();
1041 $parser = new SqliteParser();
1042 $guard = new SqliteQueryGuard($parser);
1043 $schemaParser = new SqliteSchemaParser();
1044 $selectTransformer = new SelectTransformer();
1045 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1046 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1047 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1048 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1049 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1050 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1051 $plan = $rewriter->rewrite('SELECT * FROM users');
1052 self::assertSame(QueryKind::READ, $plan->kind());
1053 }
1054
1055 public function testInsertWithShadowDataProducesTransformedSql(): void
1056 {
1057 $registry = new TableDefinitionRegistry();
1058 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1059 $store = new ShadowStore();
1060 $store->ensure('users');
1061 $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
1062 $parser = new SqliteParser();
1063 $guard = new SqliteQueryGuard($parser);
1064 $schemaParser = new SqliteSchemaParser();
1065 $selectTransformer = new SelectTransformer();
1066 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1067 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1068 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1069 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1070 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1071 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1072 $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (2, 'Bob')");
1073 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1074 self::assertStringContainsString('SELECT', $plan->sql());
1075 self::assertNotNull($plan->mutation());
1076 }
1077
1078 public function testBuildTableContextMergesColumnsFromMultipleRows(): void
1079 {
1080 $store = new ShadowStore();
1081 $store->ensure('users');
1082 $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
1083 $store->insert('users', [['id' => 2, 'name' => 'Bob', 'email' => 'b@b.com']]);
1084 $registry = new TableDefinitionRegistry();
1085 $parser = new SqliteParser();
1086 $guard = new SqliteQueryGuard($parser);
1087 $schemaParser = new SqliteSchemaParser();
1088 $selectTransformer = new SelectTransformer();
1089 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1090 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1091 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1092 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1093 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1094 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1095 $plan = $rewriter->rewrite('SELECT * FROM users');
1096 self::assertStringContainsString('"id"', $plan->sql());
1097 self::assertStringContainsString('"name"', $plan->sql());
1098 self::assertStringContainsString('"email"', $plan->sql());
1099 }
1100
1101 public function testBuildTableContextUsesDefinitionColumns(): void
1102 {
1103 $registry = new TableDefinitionRegistry();
1104 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1105 $store = new ShadowStore();
1106 $store->ensure('users');
1107 $store->insert('users', [['id' => 1]]);
1108 $parser = new SqliteParser();
1109 $guard = new SqliteQueryGuard($parser);
1110 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1116 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1117 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1118 $plan = $rewriter->rewrite('SELECT * FROM users');
1119 self::assertStringContainsString('"name"', $plan->sql());
1120 }
1121
1122 public function testBuildTableContextRegistryOnlyTableIncluded(): void
1123 {
1124 $registry = new TableDefinitionRegistry();
1125 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1126 $registry->register('orders', new TableDefinition(['id', 'amount'], ['id' => 'INTEGER', 'amount' => 'REAL'], ['id'], [], []));
1127 $store = new ShadowStore();
1128 $parser = new SqliteParser();
1129 $guard = new SqliteQueryGuard($parser);
1130 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1136 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1137 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1138 $plan = $rewriter->rewrite('SELECT * FROM users');
1139 self::assertStringContainsString('"users"', $plan->sql());
1140 }
1141
1142 public function testSelectTableInShadowStoreNotUnknown(): void
1143 {
1144 $store = new ShadowStore();
1145 $store->ensure('users');
1146 $registry = new TableDefinitionRegistry();
1147 $registry->register('orders', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1148 $parser = new SqliteParser();
1149 $guard = new SqliteQueryGuard($parser);
1150 $schemaParser = new SqliteSchemaParser();
1151 $selectTransformer = new SelectTransformer();
1152 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1153 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1154 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1155 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1156 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1157 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1158 $this->expectException(UnknownSchemaException::class);
1159 $rewriter->rewrite('SELECT * FROM nonexistent');
1160 }
1161
1162 public function testDeleteFromQuotedTableWithoutWhereReturnsSqlWhere0(): void
1163 {
1164 $registry = new TableDefinitionRegistry();
1165 $registry->register('my_table', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1166 $store = new ShadowStore();
1167 $store->ensure('my_table');
1168 $parser = new SqliteParser();
1169 $guard = new SqliteQueryGuard($parser);
1170 $schemaParser = new SqliteSchemaParser();
1171 $selectTransformer = new SelectTransformer();
1172 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1173 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1174 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1175 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1176 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1177 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1178 $plan = $rewriter->rewrite('DELETE FROM "my_table"');
1179 self::assertStringContainsString('FROM "my_table"', $plan->sql());
1180 }
1181
1182 public function testDeleteFromWithSemicolonAndWhitespace(): void
1183 {
1184 $registry = new TableDefinitionRegistry();
1185 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1186 $store = new ShadowStore();
1187 $store->ensure('users');
1188 $parser = new SqliteParser();
1189 $guard = new SqliteQueryGuard($parser);
1190 $schemaParser = new SqliteSchemaParser();
1191 $selectTransformer = new SelectTransformer();
1192 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1193 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1194 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1195 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1196 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1197 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1198 $plan = $rewriter->rewrite('DELETE FROM users ;');
1199 self::assertStringContainsString('FROM "users"', $plan->sql());
1200 }
1201
1202 public function testUpdateEnsuresShadowStoreCalledOnTarget(): void
1203 {
1204 $registry = new TableDefinitionRegistry();
1205 $registry->register('t', new TableDefinition(['id', 'val'], ['id' => 'INTEGER', 'val' => 'TEXT'], ['id'], [], []));
1206 $store = new ShadowStore();
1207 $parser = new SqliteParser();
1208 $guard = new SqliteQueryGuard($parser);
1209 $schemaParser = new SqliteSchemaParser();
1210 $selectTransformer = new SelectTransformer();
1211 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1212 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1213 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1214 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1215 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1216 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1217 $plan = $rewriter->rewrite("UPDATE t SET val = 'x' WHERE id = 1");
1218 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1219 self::assertSame([], $store->get('t'));
1220 }
1221
1222 public function testDeleteEnsuresShadowStoreCalledOnTarget(): void
1223 {
1224 $registry = new TableDefinitionRegistry();
1225 $registry->register('t', new TableDefinition(['id', 'val'], ['id' => 'INTEGER', 'val' => 'TEXT'], ['id'], [], []));
1226 $store = new ShadowStore();
1227 $parser = new SqliteParser();
1228 $guard = new SqliteQueryGuard($parser);
1229 $schemaParser = new SqliteSchemaParser();
1230 $selectTransformer = new SelectTransformer();
1231 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1232 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1233 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1234 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1235 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1236 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1237 $plan = $rewriter->rewrite('DELETE FROM t WHERE id = 1');
1238 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1239 self::assertSame([], $store->get('t'));
1240 }
1241
1242 public function testBuildTableContextShadowStoreEmptyRowsNoDefinitionPassesThrough(): void
1243 {
1244 $store = new ShadowStore();
1245 $store->ensure('users');
1246 $registry = new TableDefinitionRegistry();
1247 $parser = new SqliteParser();
1248 $guard = new SqliteQueryGuard($parser);
1249 $schemaParser = new SqliteSchemaParser();
1250 $selectTransformer = new SelectTransformer();
1251 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1252 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1253 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1254 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1255 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1256 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1257 $plan = $rewriter->rewrite('SELECT 1');
1258 self::assertSame(QueryKind::READ, $plan->kind());
1259 }
1260
1261 public function testHasSchemaContextWithRegistryOnly(): void
1262 {
1263 $store = new ShadowStore();
1264 $registry = new TableDefinitionRegistry();
1265 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1266 $parser = new SqliteParser();
1267 $guard = new SqliteQueryGuard($parser);
1268 $schemaParser = new SqliteSchemaParser();
1269 $selectTransformer = new SelectTransformer();
1270 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1271 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1272 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1273 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1274 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1275 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1276 $this->expectException(UnknownSchemaException::class);
1277 $rewriter->rewrite('SELECT * FROM nonexistent');
1278 }
1279
1280 public function testHasSchemaContextWithShadowStoreOnly(): void
1281 {
1282 $store = new ShadowStore();
1283 $store->ensure('users');
1284 $store->insert('users', [['id' => 1]]);
1285 $registry = new TableDefinitionRegistry();
1286 $parser = new SqliteParser();
1287 $guard = new SqliteQueryGuard($parser);
1288 $schemaParser = new SqliteSchemaParser();
1289 $selectTransformer = new SelectTransformer();
1290 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1291 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1292 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1293 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1294 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1295 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1296 $this->expectException(UnknownSchemaException::class);
1297 $rewriter->rewrite('SELECT * FROM nonexistent');
1298 }
1299
1300 public function testDeleteFromBacktickQuotedTableReturnsSqlWhere0(): void
1301 {
1302 $registry = new TableDefinitionRegistry();
1303 $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1304 $store = new ShadowStore();
1305 $store->ensure('t');
1306 $parser = new SqliteParser();
1307 $guard = new SqliteQueryGuard($parser);
1308 $schemaParser = new SqliteSchemaParser();
1309 $selectTransformer = new SelectTransformer();
1310 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1311 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1312 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1313 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1314 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1315 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1316 $plan = $rewriter->rewrite('DELETE FROM `t`');
1317 self::assertStringContainsString('FROM "t"', $plan->sql());
1318 }
1319
1320 public function testDeleteFromBracketQuotedTableReturnsSqlWhere0(): void
1321 {
1322 $registry = new TableDefinitionRegistry();
1323 $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1324 $store = new ShadowStore();
1325 $store->ensure('t');
1326 $parser = new SqliteParser();
1327 $guard = new SqliteQueryGuard($parser);
1328 $schemaParser = new SqliteSchemaParser();
1329 $selectTransformer = new SelectTransformer();
1330 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1331 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1332 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1333 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1334 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1335 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1336 $plan = $rewriter->rewrite('DELETE FROM [t]');
1337 self::assertStringContainsString('FROM "t"', $plan->sql());
1338 }
1339
1340 public function testUpdateEnsuresShadowStoreForTargetTable(): void
1341 {
1342 $registry = new TableDefinitionRegistry();
1343 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1344 $store = new ShadowStore();
1345 $parser = new SqliteParser();
1346 $guard = new SqliteQueryGuard($parser);
1347 $schemaParser = new SqliteSchemaParser();
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 SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1353 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1354 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1355 $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1356 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1357 self::assertNotNull($plan->mutation());
1358 self::assertSame([], $store->get('users'));
1359 }
1360
1361 public function testDeleteEnsuresShadowStoreForTargetTable(): void
1362 {
1363 $registry = new TableDefinitionRegistry();
1364 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1365 $store = new ShadowStore();
1366 $parser = new SqliteParser();
1367 $guard = new SqliteQueryGuard($parser);
1368 $schemaParser = new SqliteSchemaParser();
1369 $selectTransformer = new SelectTransformer();
1370 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1371 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1372 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1373 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1374 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1375 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1376 $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1377 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1378 self::assertNotNull($plan->mutation());
1379 }
1380
1381 public function testDeleteFromLowercaseReturnsSqlWhere0(): void
1382 {
1383 $registry = new TableDefinitionRegistry();
1384 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1385 $store = new ShadowStore();
1386 $store->ensure('users');
1387 $parser = new SqliteParser();
1388 $guard = new SqliteQueryGuard($parser);
1389 $schemaParser = new SqliteSchemaParser();
1390 $selectTransformer = new SelectTransformer();
1391 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1392 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1393 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1394 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1395 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1396 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1397 $plan = $rewriter->rewrite('delete from users');
1398 self::assertStringContainsString('FROM "users"', $plan->sql());
1399 }
1400
1401 public function testBuildTableContextIncludesMultipleTablesFromRegistry(): void
1402 {
1403 $registry = new TableDefinitionRegistry();
1404 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1405 $registry->register('orders', new TableDefinition(['oid', 'uid'], ['oid' => 'INTEGER', 'uid' => 'INTEGER'], ['oid'], [], []));
1406 $store = new ShadowStore();
1407 $parser = new SqliteParser();
1408 $guard = new SqliteQueryGuard($parser);
1409 $schemaParser = new SqliteSchemaParser();
1410 $selectTransformer = new SelectTransformer();
1411 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1412 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1413 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1414 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1415 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1416 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1417 $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.uid');
1418 self::assertSame(QueryKind::READ, $plan->kind());
1419 }
1420
1421 public function testBuildTableContextWithShadowStoreColumnsInferred(): void
1422 {
1423 $registry = new TableDefinitionRegistry();
1424 $store = new ShadowStore();
1425 $store->ensure('users');
1426 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
1427 $parser = new SqliteParser();
1428 $guard = new SqliteQueryGuard($parser);
1429 $schemaParser = new SqliteSchemaParser();
1430 $selectTransformer = new SelectTransformer();
1431 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1432 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1433 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1434 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1435 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1436 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1437 $plan = $rewriter->rewrite('SELECT * FROM users');
1438 self::assertSame(QueryKind::READ, $plan->kind());
1439 self::assertStringContainsString('WITH', $plan->sql());
1440 self::assertStringContainsString('"id"', $plan->sql());
1441 self::assertStringContainsString('"name"', $plan->sql());
1442 }
1443
1444 public function testBuildTableContextSkipsAlreadyAddedFromShadowStore(): void
1445 {
1446 $registry = new TableDefinitionRegistry();
1447 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1448 $store = new ShadowStore();
1449 $store->ensure('users');
1450 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
1451 $parser = new SqliteParser();
1452 $guard = new SqliteQueryGuard($parser);
1453 $schemaParser = new SqliteSchemaParser();
1454 $selectTransformer = new SelectTransformer();
1455 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1456 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1457 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1458 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1459 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1460 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1461 $plan = $rewriter->rewrite('SELECT * FROM users');
1462 self::assertSame(QueryKind::READ, $plan->kind());
1463 self::assertStringContainsString('WITH', $plan->sql());
1464 self::assertSame(1, substr_count($plan->sql(), '"users" AS'));
1465 }
1466
1467 public function testInsertDoesNotEnsureShadowStoreForTargetTable(): void
1468 {
1469 $registry = new TableDefinitionRegistry();
1470 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1471 $store = new ShadowStore();
1472 $parser = new SqliteParser();
1473 $guard = new SqliteQueryGuard($parser);
1474 $schemaParser = new SqliteSchemaParser();
1475 $selectTransformer = new SelectTransformer();
1476 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1477 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1478 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1479 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1480 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1481 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1482 $plan = $rewriter->rewrite("INSERT INTO users (id, name) VALUES (1, 'Alice')");
1483 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1484 }
1485
1486 public function testDeleteFromWithCommentsReturnsSqlWhere0(): void
1487 {
1488 $registry = new TableDefinitionRegistry();
1489 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1490 $store = new ShadowStore();
1491 $store->ensure('users');
1492 $parser = new SqliteParser();
1493 $guard = new SqliteQueryGuard($parser);
1494 $schemaParser = new SqliteSchemaParser();
1495 $selectTransformer = new SelectTransformer();
1496 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1497 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1498 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1499 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1500 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1501 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1502 $plan = $rewriter->rewrite('/* comment */ DELETE FROM users');
1503 self::assertStringContainsString('FROM "users"', $plan->sql());
1504 }
1505
1506 public function testSelectRewritesTableAfterBlockComment(): void
1507 {
1508 $registry = new TableDefinitionRegistry();
1509 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1510 $store = new ShadowStore();
1511 $store->set('users', [['id' => 1]]);
1512 $rewriterStore = $store;
1513 $rewriterRegistry = $registry;
1514 $rewriterParser = new SqliteParser();
1515 $rewriterSchemaParser = new SqliteSchemaParser();
1516 $rewriterSelectTransformer = new SelectTransformer();
1517 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1518 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1519 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1520 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1521 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1522 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1523 $plan = $rewriter->rewrite('SELECT * FROM/* table */users');
1524 self::assertSame(QueryKind::READ, $plan->kind());
1525 self::assertStringContainsString('"users" AS', $plan->sql());
1526 }
1527
1528 public function testSelectIgnoresSqlKeywordsInsideLeadingLineComment(): void
1529 {
1530 $registry = new TableDefinitionRegistry();
1531 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1532 $store = new ShadowStore();
1533 $store->set('users', [['id' => 1]]);
1534 $rewriterStore = $store;
1535 $rewriterRegistry = $registry;
1536 $rewriterParser = new SqliteParser();
1537 $rewriterSchemaParser = new SqliteSchemaParser();
1538 $rewriterSelectTransformer = new SelectTransformer();
1539 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1540 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1541 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1542 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1543 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1544 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1545 $sql = "-- SELECT * FROM other_table WHERE DELETE UPDATE INSERT\nSELECT * FROM users";
1546 $plan = $rewriter->rewrite($sql);
1547 self::assertSame(QueryKind::READ, $plan->kind());
1548 self::assertStringContainsString('"users" AS', $plan->sql());
1549 self::assertStringNotContainsString('"other_table" AS', $plan->sql());
1550 }
1551
1552 public function testInsertResolvesTargetAfterBlockComment(): void
1553 {
1554 $registry = new TableDefinitionRegistry();
1555 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1556 $rewriterStore = new ShadowStore();
1557 $rewriterRegistry = $registry;
1558 $rewriterParser = new SqliteParser();
1559 $rewriterSchemaParser = new SqliteSchemaParser();
1560 $rewriterSelectTransformer = new SelectTransformer();
1561 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1562 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1563 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1564 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1565 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1566 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1567 $plan = $rewriter->rewrite('INSERT INTO/* table */users (id) VALUES (1)');
1568 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1569 self::assertInstanceOf(InsertMutation::class, $plan->mutation());
1570 self::assertSame('users', $plan->mutation()->tableName());
1571 }
1572
1573 public function testUpdateResolvesTargetAfterBlockComment(): void
1574 {
1575 $registry = new TableDefinitionRegistry();
1576 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1577 $rewriterStore = new ShadowStore();
1578 $rewriterRegistry = $registry;
1579 $rewriterParser = new SqliteParser();
1580 $rewriterSchemaParser = new SqliteSchemaParser();
1581 $rewriterSelectTransformer = new SelectTransformer();
1582 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1583 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1584 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1585 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1586 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1587 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1588 $plan = $rewriter->rewrite('UPDATE/* table */users SET id = 2 WHERE id = 1');
1589 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1590 self::assertInstanceOf(UpdateMutation::class, $plan->mutation());
1591 self::assertSame('users', $plan->mutation()->tableName());
1592 }
1593
1594 public function testDeleteResolvesTargetAfterBlockComment(): void
1595 {
1596 $registry = new TableDefinitionRegistry();
1597 $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
1598 $rewriterStore = new ShadowStore();
1599 $rewriterRegistry = $registry;
1600 $rewriterParser = new SqliteParser();
1601 $rewriterSchemaParser = new SqliteSchemaParser();
1602 $rewriterSelectTransformer = new SelectTransformer();
1603 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1604 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1605 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1606 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1607 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1608 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1609 $plan = $rewriter->rewrite('DELETE FROM/* table */users WHERE id = 1');
1610 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1611 self::assertInstanceOf(DeleteMutation::class, $plan->mutation());
1612 self::assertSame('users', $plan->mutation()->tableName());
1613 }
1614
1615 public function testUpdateEnsuresShadowStoreEntry(): void
1616 {
1617 $registry = new TableDefinitionRegistry();
1618 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1619 $store = new ShadowStore();
1620 $parser = new SqliteParser();
1621 $guard = new SqliteQueryGuard($parser);
1622 $schemaParser = new SqliteSchemaParser();
1623 $selectTransformer = new SelectTransformer();
1624 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1625 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1626 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1627 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1628 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1629 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1630 $plan = $rewriter->rewrite("UPDATE users SET name = 'Bob' WHERE id = 1");
1631 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1632 self::assertArrayHasKey('users', $store->getAll());
1633 }
1634
1635 public function testDeleteEnsuresShadowStoreEntry(): void
1636 {
1637 $registry = new TableDefinitionRegistry();
1638 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1639 $store = new ShadowStore();
1640 $parser = new SqliteParser();
1641 $guard = new SqliteQueryGuard($parser);
1642 $schemaParser = new SqliteSchemaParser();
1643 $selectTransformer = new SelectTransformer();
1644 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1645 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1646 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1647 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1648 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1649 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1650 $plan = $rewriter->rewrite('DELETE FROM users WHERE id = 1');
1651 self::assertSame(QueryKind::WRITE_SIMULATED, $plan->kind());
1652 self::assertArrayHasKey('users', $store->getAll());
1653 }
1654
1655 public function testSelectWithMultipleRegisteredTablesIncludesAll(): void
1656 {
1657 $registry = new TableDefinitionRegistry();
1658 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1659 $registry->register('orders', new TableDefinition(['id', 'user_id'], ['id' => 'INTEGER', 'user_id' => 'INTEGER'], ['id'], [], []));
1660 $store = new ShadowStore();
1661 $parser = new SqliteParser();
1662 $guard = new SqliteQueryGuard($parser);
1663 $schemaParser = new SqliteSchemaParser();
1664 $selectTransformer = new SelectTransformer();
1665 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1666 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1667 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1668 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1669 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1670 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1671 $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
1672 self::assertSame(QueryKind::READ, $plan->kind());
1673 $sql = $plan->sql();
1674 self::assertStringContainsString('"users"', $sql);
1675 self::assertStringContainsString('"orders"', $sql);
1676 }
1677
1678 public function testSelectWithShadowStoreAndRegistryTablesMerged(): void
1679 {
1680 $registry = new TableDefinitionRegistry();
1681 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
1682 $store = new ShadowStore();
1683 $store->ensure('orders');
1684 $store->set('orders', [['id' => 1, 'user_id' => 1]]);
1685 $parser = new SqliteParser();
1686 $guard = new SqliteQueryGuard($parser);
1687 $schemaParser = new SqliteSchemaParser();
1688 $selectTransformer = new SelectTransformer();
1689 $insertTransformer = new InsertTransformer($parser, $selectTransformer);
1690 $updateTransformer = new UpdateTransformer($parser, $selectTransformer);
1691 $deleteTransformer = new DeleteTransformer($parser, $selectTransformer);
1692 $transformer = new SqliteTransformer($parser, $selectTransformer, $insertTransformer, $updateTransformer, $deleteTransformer);
1693 $mutationResolver = new SqliteMutationResolver($store, $registry, $schemaParser, $parser);
1694 $rewriter = new SqliteRewriter($guard, $store, $registry, $transformer, $mutationResolver, $parser);
1695 $plan = $rewriter->rewrite('SELECT * FROM users JOIN orders ON users.id = orders.user_id');
1696 self::assertSame(QueryKind::READ, $plan->kind());
1697 $sql = $plan->sql();
1698 self::assertStringContainsString('"users"', $sql);
1699 self::assertStringContainsString('"orders"', $sql);
1700 }
1701
1702 public function testUpdateDoesNotPromoteMaterializedUnknownTable(): void
1703 {
1704 $store = new ShadowStore();
1705 $store->insert('late_table', [['id' => 1, 'name' => 'Alice']]);
1706 $rewriterStore = $store;
1707 $rewriterRegistry = new TableDefinitionRegistry();
1708 $rewriterParser = new SqliteParser();
1709 $rewriterSchemaParser = new SqliteSchemaParser();
1710 $rewriterSelectTransformer = new SelectTransformer();
1711 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1712 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1713 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1714 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1715 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1716 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1717 try {
1718 $rewriter->rewrite("UPDATE late_table SET name = 'Bob' WHERE id = 1");
1719 self::fail('Expected an unknown schema exception.');
1720 } catch (UnknownSchemaException) {
1721 self::assertSame(ShadowTableState::Materialized, $store->state('late_table'));
1722 }
1723 }
1724
1725 public function testTransactionStatementRestoresSnapshotOnRollback(): void
1726 {
1727 $store = new ShadowStore();
1728 $store->set('users', [['id' => 1]]);
1729 $rewriterStore = $store;
1730 $rewriterRegistry = new TableDefinitionRegistry();
1731 $rewriterParser = new SqliteParser();
1732 $rewriterSchemaParser = new SqliteSchemaParser();
1733 $rewriterSelectTransformer = new SelectTransformer();
1734 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1735 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1736 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1737 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1738 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1739 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1740 $transactions = new \ZtdQuery\Shadow\ShadowTransactions($store);
1741 $begin = $rewriter->transactionStatement('BEGIN');
1742 $rollback = $rewriter->transactionStatement('ROLLBACK');
1743 self::assertNotNull($begin);
1744 self::assertNotNull($rollback);
1745 $begin->apply($transactions);
1746 $store->set('users', [['id' => 2]]);
1747 $rollback->apply($transactions);
1748 self::assertSame([['id' => 1]], $store->get('users'));
1749 self::assertNull($rewriter->transactionStatement('SELECT 1'));
1750 }
1751
1752 public function testSplitStatementsPreservesQuotedSemicolons(): void
1753 {
1754 $rewriterStore = new ShadowStore();
1755 $rewriterRegistry = new TableDefinitionRegistry();
1756 $rewriterParser = new SqliteParser();
1757 $rewriterSchemaParser = new SqliteSchemaParser();
1758 $rewriterSelectTransformer = new SelectTransformer();
1759 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1760 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1761 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1762 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1763 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1764 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1765 self::assertSame(["SELECT 'a;b'", 'SELECT 2'], $rewriter->splitStatements("SELECT 'a;b'; SELECT 2;"));
1766 }
1767
1768 public function testEmptyResultSelectProducesNoRows(): void
1769 {
1770 $rewriterStore = new ShadowStore();
1771 $rewriterRegistry = new TableDefinitionRegistry();
1772 $rewriterParser = new SqliteParser();
1773 $rewriterSchemaParser = new SqliteSchemaParser();
1774 $rewriterSelectTransformer = new SelectTransformer();
1775 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1776 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1777 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1778 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1779 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1780 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1781 $statement = (new PDO('sqlite::memory:'))->query($rewriter->emptyResultSelect());
1782 self::assertNotFalse($statement);
1783 self::assertSame([], $statement->fetchAll());
1784 }
1785
1786 public function testCommitRewriteStateKeepsGeneratedIdentitiesAcrossRewrites(): void
1787 {
1788 $registry = new TableDefinitionRegistry();
1789 $definition = (new SqliteSchemaParser())->parse('CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)');
1790 self::assertNotNull($definition);
1791 $registry->register('users', $definition);
1792 $rewriterStore = new ShadowStore();
1793 $rewriterRegistry = $registry;
1794 $rewriterParser = new SqliteParser();
1795 $rewriterSchemaParser = new SqliteSchemaParser();
1796 $rewriterSelectTransformer = new SelectTransformer();
1797 $rewriterInsertTransformer = new InsertTransformer($rewriterParser, $rewriterSelectTransformer);
1798 $rewriterUpdateTransformer = new UpdateTransformer($rewriterParser, $rewriterSelectTransformer);
1799 $rewriterDeleteTransformer = new DeleteTransformer($rewriterParser, $rewriterSelectTransformer);
1800 $rewriterTransformer = new SqliteTransformer($rewriterParser, $rewriterSelectTransformer, $rewriterInsertTransformer, $rewriterUpdateTransformer, $rewriterDeleteTransformer);
1801 $rewriterMutationResolver = new SqliteMutationResolver($rewriterStore, $rewriterRegistry, $rewriterSchemaParser, $rewriterParser);
1802 $rewriter = new SqliteRewriter(new SqliteQueryGuard($rewriterParser), $rewriterStore, $rewriterRegistry, $rewriterTransformer, $rewriterMutationResolver, $rewriterParser);
1803 $first = $rewriter->rewrite("INSERT INTO users(name) VALUES ('Alice')");
1804 $rewriter->commitRewriteState();
1805 $second = $rewriter->rewrite("INSERT INTO users(name) VALUES ('Bob')");
1806 self::assertStringContainsString('CAST(1 AS INTEGER) AS "id"', $first->sql());
1807 self::assertStringContainsString('CAST(2 AS INTEGER) AS "id"', $second->sql());
1808 }
1809}
1810