packages/ztd-query-sqlite/tests/Unit/Shadow/Resolution/RowMutationResolverTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow\Resolution;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Exception\UnknownSchemaException;
11use ZtdQuery\Exception\UnsupportedSqlException;
12use ZtdQuery\Platform\Sqlite\Shadow\Resolution\RowMutationResolver;
13use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
14use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
15use ZtdQuery\Schema\TableDefinition;
16use ZtdQuery\Schema\TableDefinitionRegistry;
17use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
18use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
19use ZtdQuery\Shadow\ShadowStore;
20use ZtdQuery\Shadow\ShadowTableState;
21
22#[CoversClass(RowMutationResolver::class)]
23#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\MutationTableLookup::class)]
24#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
25#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
26#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
27#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
28#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
29#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
30#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
31#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
32#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
33#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
34#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
35#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
36#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
37#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
38#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
39#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
40#[UsesClass(SqliteLexicalMasker::class)]
41#[UsesClass(SqliteParser::class)]
42#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
43final class RowMutationResolverTest extends TestCase
44{
45    public function testResolveUpdate(): void
46    {
47        $store = new ShadowStore();
48        $store->ensure('users');
49        $resolver = new RowMutationResolver(new SqliteParser(), new TableDefinitionRegistry(), $store);
50        $mutation = $resolver->resolveUpdate("UPDATE users SET name = 'Bob' WHERE id = 1");
51        self::assertInstanceOf(UpdateMutation::class, $mutation);
52    }
53
54    public function testResolveDelete(): void
55    {
56        $registry = new TableDefinitionRegistry();
57        $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
58        $store = new ShadowStore();
59        $store->ensure('users');
60        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
61        $mutation = $resolver->resolveDelete('DELETE FROM users WHERE id = 1');
62        self::assertInstanceOf(DeleteMutation::class, $mutation);
63    }
64
65    public function testResolveDeleteUnknownTableThrows(): void
66    {
67        $store = new ShadowStore();
68        $resolver = new RowMutationResolver(new SqliteParser(), new TableDefinitionRegistry(), $store);
69        $this->expectException(UnknownSchemaException::class);
70        $resolver->resolveDelete('DELETE FROM unknown_table WHERE id = 1');
71    }
72
73    public function testResolveUpdateWithoutTargetThrows(): void
74    {
75        $resolver = new RowMutationResolver(new SqliteParser(), new TableDefinitionRegistry(), new ShadowStore());
76        $this->expectException(UnsupportedSqlException::class);
77        $resolver->resolveUpdate('UPDATE');
78    }
79
80    public function testResolveDeleteWithoutTargetThrows(): void
81    {
82        $resolver = new RowMutationResolver(new SqliteParser(), new TableDefinitionRegistry(), new ShadowStore());
83        $this->expectException(UnsupportedSqlException::class);
84        $resolver->resolveDelete('DELETE');
85    }
86
87    public function testResolveDeleteWithExistingRows(): void
88    {
89        $store = new ShadowStore();
90        $store->ensure('users');
91        $store->set('users', [['id' => 1, 'name' => 'Alice']]);
92        $registry = new TableDefinitionRegistry();
93        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
94        $mutation = $resolver->resolveDelete('DELETE FROM users WHERE id = 1');
95        self::assertInstanceOf(DeleteMutation::class, $mutation);
96    }
97
98    public function testResolveUpdateWithPrimaryKeys(): void
99    {
100        $registry = new TableDefinitionRegistry();
101        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
102        $store = new ShadowStore();
103        $store->ensure('users');
104        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
105        $mutation = $resolver->resolveUpdate("UPDATE users SET name = 'Bob' WHERE id = 1");
106        self::assertInstanceOf(UpdateMutation::class, $mutation);
107    }
108
109    public function testResolveDeleteFullTableWithoutWhere(): void
110    {
111        $registry = new TableDefinitionRegistry();
112        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
113        $store = new ShadowStore();
114        $store->ensure('users');
115        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
116        $mutation = $resolver->resolveDelete('DELETE FROM users');
117        self::assertInstanceOf(DeleteMutation::class, $mutation);
118    }
119
120    public function testResolveUpdateWithoutContextThrowsUnknownSchema(): void
121    {
122        $store = new ShadowStore();
123        $registry = new TableDefinitionRegistry();
124        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
125        $this->expectException(UnknownSchemaException::class);
126        $resolver->resolveUpdate('UPDATE t SET x = 1');
127    }
128
129    public function testResolveDeleteTableName(): void
130    {
131        $store = new ShadowStore();
132        $store->ensure('orders');
133        $store->set('orders', [['id' => 1]]);
134        $registry = new TableDefinitionRegistry();
135        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
136        $mutation = $resolver->resolveDelete('DELETE FROM orders WHERE id = 1');
137        self::assertInstanceOf(DeleteMutation::class, $mutation);
138        self::assertSame('orders', $mutation->tableName());
139    }
140
141    public function testResolveDeleteLowercaseSql(): void
142    {
143        $registry = new TableDefinitionRegistry();
144        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
145        $store = new ShadowStore();
146        $store->ensure('users');
147        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
148        $mutation = $resolver->resolveDelete('delete from users where id = 1');
149        self::assertInstanceOf(DeleteMutation::class, $mutation);
150    }
151
152    public function testResolveUpdateEnsuresShadowStoreWithDefinition(): void
153    {
154        $store = new ShadowStore();
155        $registry = new TableDefinitionRegistry();
156        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
157        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
158        $mutation = $resolver->resolveUpdate("UPDATE users SET name = 'Bob' WHERE id = 1");
159        self::assertInstanceOf(UpdateMutation::class, $mutation);
160        self::assertSame(ShadowTableState::Initialized, $store->state('users'));
161    }
162
163    public function testResolveUpdatePrimaryKeysFromDefinition(): void
164    {
165        $store = new ShadowStore();
166        $registry = new TableDefinitionRegistry();
167        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
168        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
169        $mutation = $resolver->resolveUpdate("UPDATE users SET name = 'Bob' WHERE id = 1");
170        self::assertInstanceOf(UpdateMutation::class, $mutation);
171    }
172
173    public function testResolveUpdateWithoutDefinitionHasEmptyPrimaryKeys(): void
174    {
175        $store = new ShadowStore();
176        $store->ensure('users');
177        $store->set('users', [['id' => 1, 'name' => 'x']]);
178        $registry = new TableDefinitionRegistry();
179        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
180        $mutation = $resolver->resolveUpdate("UPDATE users SET name = 'Bob' WHERE id = 1");
181        self::assertInstanceOf(UpdateMutation::class, $mutation);
182    }
183
184    public function testResolveDeleteEnsuresShadowStore(): void
185    {
186        $store = new ShadowStore();
187        $registry = new TableDefinitionRegistry();
188        $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
189        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
190        $mutation = $resolver->resolveDelete('DELETE FROM users WHERE id = 1');
191        self::assertInstanceOf(DeleteMutation::class, $mutation);
192        self::assertSame([], $store->get('users'));
193    }
194
195    public function testResolveDeleteWithoutDefinitionAndNoRowsThrows(): void
196    {
197        $store = new ShadowStore();
198        $registry = new TableDefinitionRegistry();
199        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
200        $this->expectException(UnknownSchemaException::class);
201        $resolver->resolveDelete('DELETE FROM unknown_table WHERE id = 1');
202    }
203
204    public function testResolveDeleteWithOnlyTableNameNoWhereNoTrailing(): void
205    {
206        $store = new ShadowStore();
207        $registry = new TableDefinitionRegistry();
208        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
209        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
210        $mutation = $resolver->resolveDelete('DELETE FROM users');
211        self::assertInstanceOf(DeleteMutation::class, $mutation);
212    }
213
214    public function testResolveDeleteWithStrippedCommentsMatchesRegex(): void
215    {
216        $store = new ShadowStore();
217        $registry = new TableDefinitionRegistry();
218        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
219        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
220        $mutation = $resolver->resolveDelete('DELETE FROM users');
221        self::assertInstanceOf(DeleteMutation::class, $mutation);
222    }
223
224    public function testResolveUpdateWithoutTargetContextThrowsUnknownSchema(): void
225    {
226        $store = new ShadowStore();
227        $registry = new TableDefinitionRegistry();
228        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
229        $this->expectException(UnknownSchemaException::class);
230        $resolver->resolveUpdate('UPDATE users SET x = 1');
231    }
232
233    public function testResolveDeleteWithCommentsMatchesRegex(): void
234    {
235        $store = new ShadowStore();
236        $registry = new TableDefinitionRegistry();
237        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
238        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
239        $mutation = $resolver->resolveDelete('/* comment */ DELETE FROM users WHERE id = 1');
240        self::assertInstanceOf(DeleteMutation::class, $mutation);
241    }
242
243    public function testResolveDeleteCaseInsensitiveRegex(): void
244    {
245        $store = new ShadowStore();
246        $registry = new TableDefinitionRegistry();
247        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
248        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
249        $mutation = $resolver->resolveDelete('delete from users where id = 1');
250        self::assertInstanceOf(DeleteMutation::class, $mutation);
251    }
252
253    public function testResolveUpdateWithDefinitionReturnsPrimaryKeys(): void
254    {
255        $store = new ShadowStore();
256        $registry = new TableDefinitionRegistry();
257        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
258        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
259        $mutation = $resolver->resolveUpdate("UPDATE t SET name = 'Bob' WHERE id = 1");
260        self::assertInstanceOf(UpdateMutation::class, $mutation);
261    }
262
263    public function testResolveUpdateWithoutDefinitionReturnsEmptyPrimaryKeys(): void
264    {
265        $store = new ShadowStore();
266        $registry = new TableDefinitionRegistry();
267        $store->ensure('t');
268        $store->set('t', [['id' => 1, 'name' => 'Alice']]);
269        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
270        $mutation = $resolver->resolveUpdate("UPDATE t SET name = 'Bob' WHERE id = 1");
271        self::assertInstanceOf(UpdateMutation::class, $mutation);
272    }
273
274    public function testResolveDeleteWithDefinitionReturnsPrimaryKeys(): void
275    {
276        $store = new ShadowStore();
277        $registry = new TableDefinitionRegistry();
278        $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
279        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
280        $mutation = $resolver->resolveDelete('DELETE FROM t WHERE id = 1');
281        self::assertInstanceOf(DeleteMutation::class, $mutation);
282    }
283
284    public function testResolveDeleteWithoutDefinitionButWithShadowRows(): void
285    {
286        $store = new ShadowStore();
287        $registry = new TableDefinitionRegistry();
288        $store->ensure('t');
289        $store->set('t', [['id' => 1]]);
290        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
291        $mutation = $resolver->resolveDelete('DELETE FROM t WHERE id = 1');
292        self::assertInstanceOf(DeleteMutation::class, $mutation);
293    }
294
295    public function testResolveDeleteFullTableLowercaseMatchesRegex(): void
296    {
297        $registry = new TableDefinitionRegistry();
298        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
299        $store = new ShadowStore();
300        $store->ensure('t');
301        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
302        $mutation = $resolver->resolveDelete('delete from t');
303        self::assertInstanceOf(DeleteMutation::class, $mutation);
304    }
305
306    public function testResolveDeleteEnsuresShadowStoreEntryCreated(): void
307    {
308        $store = new ShadowStore();
309        $registry = new TableDefinitionRegistry();
310        $registry->register('t', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
311        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
312        self::assertSame([], $store->getAll());
313        $resolver->resolveDelete('DELETE FROM t WHERE id = 1');
314        self::assertArrayHasKey('t', $store->getAll());
315    }
316
317    public function testResolveDeleteWithLeadingWhitespaceLowercase(): void
318    {
319        $store = new ShadowStore();
320        $registry = new TableDefinitionRegistry();
321        $registry->register('users', new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], [], []));
322        $store->ensure('users');
323        $resolver = new RowMutationResolver(new SqliteParser(), $registry, $store);
324        $mutation = $resolver->resolveDelete('  delete from users where id = 1  ');
325        self::assertInstanceOf(DeleteMutation::class, $mutation);
326    }
327
328}
329