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