packages/ztd-query-sqlite/tests/Unit/Shadow/Resolution/InsertMutationResolverTest.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\UnsupportedSqlException;
11use ZtdQuery\Platform\Sqlite\Shadow\Resolution\InsertMutationResolver;
12use ZtdQuery\Platform\Sqlite\Sql\SqliteLexicalMasker;
13use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
14use ZtdQuery\Schema\TableDefinition;
15use ZtdQuery\Schema\TableDefinitionRegistry;
16use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
17use ZtdQuery\Shadow\Mutation\Row\ReplaceMutation;
18use ZtdQuery\Shadow\Mutation\UpsertMutation;
19use ZtdQuery\Shadow\ShadowStore;
20
21#[CoversClass(InsertMutationResolver::class)]
22#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Resolution\MutationTableLookup::class)]
23#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentColumnParser::class)]
24#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\AssignmentParser::class)]
25#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Expression\ValueListParser::class)]
26#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Insert\InsertClauseParser::class)]
27#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\ExpressionSpan::class)]
28#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\IdentifierDecoder::class)]
29#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\LiteralMasker::class)]
30#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\OpaqueSqlSpan::class)]
31#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\QuotedSpan::class)]
32#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Lexing\TopLevelKeywordScanner::class)]
33#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser::class)]
34#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementClassifier::class)]
35#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\StatementStructure::class)]
36#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Statement\TargetTableParser::class)]
37#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Dml\Update\UpdateClauseParser::class)]
38#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ArithmeticExpressionParser::class)]
39#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ComparisonExpressionParser::class)]
40#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionCursor::class)]
41#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\ExpressionTokenDecoder::class)]
42#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\LogicalExpressionParser::class)]
43#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\PrimaryExpressionParser::class)]
44#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile::class)]
45#[UsesClass(SqliteLexicalMasker::class)]
46#[UsesClass(SqliteParser::class)]
47#[UsesClass(\ZtdQuery\Platform\Sqlite\Sql\Relation\SqliteSelectRelationParser::class)]
48#[UsesClass(\ZtdQuery\Platform\Sqlite\Shadow\Mutation\Upsert\SqliteUpsertExpressionParser::class)]
49final class InsertMutationResolverTest extends TestCase
50{
51 public function testResolveInsertInsert(): void
52 {
53 $store = new ShadowStore();
54 $registry = new TableDefinitionRegistry();
55 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
56 $mutation = $resolver->resolveInsert("INSERT INTO users (id, name) VALUES (1, 'Alice')");
57 self::assertInstanceOf(InsertMutation::class, $mutation);
58 }
59
60 public function testResolveInsertInsertIgnore(): void
61 {
62 $registry = new TableDefinitionRegistry();
63 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
64 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
65 $mutation = $resolver->resolveInsert("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'Alice')");
66 self::assertInstanceOf(InsertMutation::class, $mutation);
67 }
68
69 public function testResolveInsertReplace(): void
70 {
71 $store = new ShadowStore();
72 $registry = new TableDefinitionRegistry();
73 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
74 $mutation = $resolver->resolveInsert("REPLACE INTO users (id, name) VALUES (1, 'Alice')");
75 self::assertInstanceOf(ReplaceMutation::class, $mutation);
76 }
77
78 public function testResolveInsertInsertOrReplace(): void
79 {
80 $store = new ShadowStore();
81 $registry = new TableDefinitionRegistry();
82 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
83 $mutation = $resolver->resolveInsert("INSERT OR REPLACE INTO users (id, name) VALUES (1, 'Alice')");
84 self::assertInstanceOf(ReplaceMutation::class, $mutation);
85 }
86
87 public function testResolveInsertUpsert(): void
88 {
89 $registry = new TableDefinitionRegistry();
90 $registry->register('users', new TableDefinition(['id', 'name', 'email'], ['id' => 'INTEGER', 'name' => 'TEXT', 'email' => 'TEXT'], ['id'], ['id', 'name'], []));
91 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
92 $mutation = $resolver->resolveInsert("INSERT INTO users (id, name) VALUES (1, 'Alice') ON CONFLICT (id) DO UPDATE SET name = upper(users.name)");
93 self::assertInstanceOf(UpsertMutation::class, $mutation);
94 }
95
96 public function testResolveInsertInsertWithoutTargetThrows(): void
97 {
98 $resolver = new InsertMutationResolver(new SqliteParser(), new TableDefinitionRegistry());
99 $this->expectException(UnsupportedSqlException::class);
100 $resolver->resolveInsert('INSERT');
101 }
102
103 public function testResolveInsertInsertTableName(): void
104 {
105 $resolver = new InsertMutationResolver(new SqliteParser(), new TableDefinitionRegistry());
106 $mutation = $resolver->resolveInsert('INSERT INTO orders (id) VALUES (1)');
107 self::assertInstanceOf(InsertMutation::class, $mutation);
108 self::assertSame('orders', $mutation->tableName());
109 }
110
111 public function testResolveInsertUpsertTableNameAndUpdateColumns(): void
112 {
113 $registry = new TableDefinitionRegistry();
114 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
115 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
116 $mutation = $resolver->resolveInsert("INSERT INTO users (id, name) VALUES (1, 'Alice') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
117 self::assertInstanceOf(UpsertMutation::class, $mutation);
118 self::assertSame('users', $mutation->tableName());
119 }
120
121 public function testResolveInsertReplaceTableName(): void
122 {
123 $registry = new TableDefinitionRegistry();
124 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
125 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
126 $mutation = $resolver->resolveInsert("REPLACE INTO users (id, name) VALUES (1, 'Alice')");
127 self::assertInstanceOf(ReplaceMutation::class, $mutation);
128 self::assertSame('users', $mutation->tableName());
129 }
130
131 public function testResolveInsertInsertIgnoreWithPrimaryKeys(): void
132 {
133 $registry = new TableDefinitionRegistry();
134 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
135 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
136 $mutation = $resolver->resolveInsert("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'x')");
137 self::assertInstanceOf(InsertMutation::class, $mutation);
138 self::assertSame('users', $mutation->tableName());
139 }
140
141 public function testResolveInsertUpsertWithDefinitionPrimaryKeys(): void
142 {
143 $store = new ShadowStore();
144 $registry = new TableDefinitionRegistry();
145 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
146 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
147 $mutation = $resolver->resolveInsert("INSERT INTO users (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
148 self::assertInstanceOf(UpsertMutation::class, $mutation);
149 }
150
151 public function testResolveInsertInsertIgnoreWithPrimaryKeysReturnsInsertMutation(): void
152 {
153 $store = new ShadowStore();
154 $registry = new TableDefinitionRegistry();
155 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
156 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
157 $mutation = $resolver->resolveInsert("INSERT OR IGNORE INTO users (id, name) VALUES (1, 'a')");
158 self::assertInstanceOf(InsertMutation::class, $mutation);
159 }
160
161 public function testResolveInsertInsertOnConflictDoNothingIsIgnore(): void
162 {
163 $store = new ShadowStore();
164 $store->set('users', [['id' => 1, 'name' => 'existing']]);
165 $registry = new TableDefinitionRegistry();
166 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
167 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
168 $mutation = $resolver->resolveInsert("INSERT INTO users (id, name) VALUES (1, 'a') ON CONFLICT DO NOTHING");
169 self::assertInstanceOf(InsertMutation::class, $mutation);
170 $mutation->apply($store, [['id' => 1, 'name' => 'ignored']]);
171 self::assertSame([['id' => 1, 'name' => 'existing']], $store->get('users'));
172 }
173
174 public function testResolveInsertInsertIsReplaceReturnsPrimaryKeys(): void
175 {
176 $store = new ShadowStore();
177 $registry = new TableDefinitionRegistry();
178 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
179 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
180 $mutation = $resolver->resolveInsert("REPLACE INTO users (id, name) VALUES (1, 'a')");
181 self::assertInstanceOf(ReplaceMutation::class, $mutation);
182 self::assertSame('users', $mutation->tableName());
183 }
184
185 public function testResolveInsertUpsertWithDefinitionReturnsPrimaryKeys(): void
186 {
187 $store = new ShadowStore();
188 $registry = new TableDefinitionRegistry();
189 $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
190 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
191 $mutation = $resolver->resolveInsert("INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
192 self::assertInstanceOf(UpsertMutation::class, $mutation);
193 }
194
195 public function testResolveInsertUpsertWithoutDefinitionReturnsEmptyPrimaryKeys(): void
196 {
197 $store = new ShadowStore();
198 $registry = new TableDefinitionRegistry();
199 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
200 $mutation = $resolver->resolveInsert("INSERT INTO t (id, name) VALUES (1, 'a') ON CONFLICT (id) DO UPDATE SET name = excluded.name");
201 self::assertInstanceOf(UpsertMutation::class, $mutation);
202 }
203
204 public function testResolveInsertInsertIgnoreWithDefinitionReturnsPrimaryKeys(): void
205 {
206 $store = new ShadowStore();
207 $registry = new TableDefinitionRegistry();
208 $registry->register('t', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
209 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
210 $mutation = $resolver->resolveInsert("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'a')");
211 self::assertInstanceOf(InsertMutation::class, $mutation);
212 }
213
214 public function testResolveInsertInsertIgnoreWithoutDefinitionReturnsEmptyPrimaryKeys(): void
215 {
216 $store = new ShadowStore();
217 $registry = new TableDefinitionRegistry();
218 $resolver = new InsertMutationResolver(new SqliteParser(), $registry);
219 $mutation = $resolver->resolveInsert("INSERT OR IGNORE INTO t (id, name) VALUES (1, 'a')");
220 self::assertInstanceOf(InsertMutation::class, $mutation);
221 }
222
223 public function testResolveOnConflictConsumesNativeEvaluationMetadata(): void
224 {
225 $registry = new TableDefinitionRegistry();
226 $registry->register('users', new TableDefinition(['id', 'name'], ['id' => 'INTEGER', 'name' => 'TEXT'], ['id'], [], []));
227 $store = new ShadowStore();
228 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
229 $mutation = (new InsertMutationResolver(new SqliteParser(), $registry))->resolveOnConflict("INSERT INTO users(id, name) VALUES (1, 'bob') ON CONFLICT(id) DO UPDATE SET name = upper(EXCLUDED.name)", 'users');
230 $mutation->apply($store, [['id' => 1, 'name' => 'bob', '__ztd_upsert_value_0' => 'BOB']]);
231 self::assertSame([['id' => 1, 'name' => 'BOB']], $store->get('users'));
232 }
233
234 public function testResolveOnConflictDoNothingPreservesExistingRow(): void
235 {
236 $registry = new TableDefinitionRegistry();
237 $registry->register('users', new TableDefinition(['id', 'name'], [], ['id'], [], []));
238 $store = new ShadowStore();
239 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
240 $mutation = (new InsertMutationResolver(new SqliteParser(), $registry))->resolveOnConflict("INSERT INTO users(id, name) VALUES (1, 'Bob') ON CONFLICT(id) DO NOTHING", 'users');
241 $mutation->apply($store, [['id' => 1, 'name' => 'Bob']]);
242 self::assertSame([['id' => 1, 'name' => 'Alice']], $store->get('users'));
243 }
244
245}
246