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