packages/ztd-query-core/tests/Unit/Shadow/ShadowStoreTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use stdClass;
11use ZtdQuery\Exception\MissingPrimaryKeyException;
12use ZtdQuery\Shadow\ShadowStore;
13use ZtdQuery\Shadow\ShadowTableState;
14
15#[CoversClass(ShadowStore::class)]
16#[UsesClass(MissingPrimaryKeyException::class)]
17#[UsesClass(ShadowTableState::class)]
18#[UsesClass(\ZtdQuery\Shadow\Row\RowMatch::class)]
19#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
20class ShadowStoreTest extends TestCase
21{
22 public function testRestoreSnapshotRestoreSnapshotRestoreIncludesRowsAndInitializedPresence(): void
23 {
24 $store = new ShadowStore();
25 $store->set('users', [['id' => 1]]);
26 $snapshot = $store->snapshot();
27
28 $store->insert('users', [['id' => 2]]);
29 $store->set('temporary_table', []);
30 $store->restore($snapshot);
31
32 self::assertSame([['id' => 1]], $store->get('users'));
33 self::assertFalse($store->has('temporary_table'));
34 }
35
36 public function testInsertAppendsRows(): void
37 {
38 $store = new ShadowStore();
39 $store->insert('users', [['id' => 1, 'name' => 'Alice']]);
40 $store->insert('users', [['id' => 2, 'name' => 'Bob']]);
41
42 self::assertSame(
43 [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']],
44 $store->get('users')
45 );
46 }
47
48 public function testGetReturnsEmptyArrayForMissingTable(): void
49 {
50 $store = new ShadowStore();
51
52 self::assertSame([], $store->get('missing'));
53 }
54
55 public function testHasDistinguishesMissingFromIntentionallyEmptyTable(): void
56 {
57 $store = new ShadowStore();
58
59 self::assertFalse($store->has('events'));
60
61 $store->ensure('events');
62
63 self::assertTrue($store->has('events'));
64 self::assertSame([], $store->get('events'));
65 }
66
67 public function testStateDistinguishesMutationMaterializationFromInitialization(): void
68 {
69 $store = new ShadowStore();
70
71 self::assertSame(ShadowTableState::Missing, $store->state('events'));
72
73 $store->insert('events', [['id' => 1]]);
74 self::assertSame(ShadowTableState::Materialized, $store->state('events'));
75
76 $store->ensure('events');
77 self::assertSame(ShadowTableState::Initialized, $store->state('events'));
78 }
79
80 public function testRemoveClearsRowsAndInitialization(): void
81 {
82 $store = new ShadowStore();
83 $store->set('events', [['id' => 1]]);
84
85 $store->remove('events');
86
87 self::assertSame(ShadowTableState::Missing, $store->state('events'));
88 self::assertSame([], $store->get('events'));
89 }
90
91 public function testGetAllReturnsCurrentTables(): void
92 {
93 $store = new ShadowStore();
94 $store->set('users', [['id' => 1]]);
95 $store->set('orders', [['id' => 10]]);
96
97 self::assertSame(
98 [
99 'users' => [['id' => 1]],
100 'orders' => [['id' => 10]],
101 ],
102 $store->getAll()
103 );
104 }
105
106 public function testDeleteUsesPrimaryKeysWhenProvided(): void
107 {
108 $store = new ShadowStore();
109 $store->set('users', [
110 ['id' => 1, 'name' => 'Alice'],
111 ['id' => 2, 'name' => 'Bob'],
112 ]);
113
114 $store->delete('users', [['id' => 1, 'name' => 'Alice']], ['id']);
115
116 self::assertSame([['id' => 2, 'name' => 'Bob']], $store->get('users'));
117 }
118
119 public function testDeleteFallsBackToRowMatchWithoutPrimaryKeys(): void
120 {
121 $store = new ShadowStore();
122 $store->set('logs', [
123 ['id' => 1, 'payload' => 'A'],
124 ['id' => 2, 'payload' => 'B'],
125 ]);
126
127 $store->delete('logs', [['id' => 2, 'payload' => 'B']]);
128
129 self::assertSame([['id' => 1, 'payload' => 'A']], $store->get('logs'));
130 }
131
132 public function testUpdateReplacesMatchingRow(): void
133 {
134 $store = new ShadowStore();
135 $store->set('users', [
136 ['id' => 1, 'name' => 'Alice'],
137 ['id' => 2, 'name' => 'Bob'],
138 ]);
139
140 $store->update('users', [['id' => 2, 'name' => 'Bobby']], ['id']);
141
142 self::assertSame([
143 ['id' => 1, 'name' => 'Alice'],
144 ['id' => 2, 'name' => 'Bobby'],
145 ], $store->get('users'));
146 }
147
148 public function testUpdateWithoutPrimaryKeysThrows(): void
149 {
150 $store = new ShadowStore();
151 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
152
153 $this->expectException(MissingPrimaryKeyException::class);
154 $store->update('users', [['id' => 1, 'name' => 'Updated']], []);
155 }
156
157 public function testEnsureCreatesEmptyTable(): void
158 {
159 $store = new ShadowStore();
160 $store->ensure('events');
161
162 self::assertSame([], $store->get('events'));
163 }
164
165 public function testClearRemovesAllData(): void
166 {
167 $store = new ShadowStore();
168 $store->set('users', [['id' => 1]]);
169 $store->set('orders', [['id' => 10]]);
170
171 $store->clear();
172
173 self::assertSame([], $store->getAll());
174 }
175
176 public function testDeleteNoOpWhenTableMissing(): void
177 {
178 $store = new ShadowStore();
179 $store->delete('missing', [['id' => 1]], ['id']);
180
181 self::assertSame([], $store->get('missing'));
182 }
183
184 public function testUpdateNoOpWhenNoMatch(): void
185 {
186 $store = new ShadowStore();
187 $store->set('users', [['id' => 1, 'name' => 'Alice']]);
188
189 $store->update('users', [['id' => 2, 'name' => 'Bob']], ['id']);
190
191 self::assertSame([['id' => 1, 'name' => 'Alice']], $store->get('users'));
192 }
193
194 public function testSetPreservesTheCallersSparseRowKeys(): void
195 {
196 $store = new ShadowStore();
197
198 $store->set('order', [4 => ['id' => 1], 9 => ['id' => 2]]);
199
200 self::assertSame([4 => ['id' => 1], 9 => ['id' => 2]], $store->get('order'));
201 }
202
203 public function testSnapshotIsARecordOfTheRowsRatherThanAViewOfThem(): void
204 {
205 $store = new ShadowStore();
206 $store->set('order', [['id' => 1]]);
207
208 $snapshot = $store->snapshot();
209 $store->set('order', []);
210
211 self::assertSame([['id' => 1]], $snapshot->get('order'));
212 }
213
214 public function testUpdateIdentifiedWritesTheRowTheOldKeyPointsAt(): void
215 {
216 $store = new ShadowStore();
217 $store->set('order', [['id' => 1, 'total' => 100]]);
218
219 $store->updateIdentified(
220 'order',
221 [['identity' => ['id' => 1], 'row' => ['id' => 2, 'total' => 200]]],
222 ['id'],
223 );
224
225 self::assertSame([['id' => 2, 'total' => 200]], $store->get('order'));
226 }
227 public function testSnapshotPreservesOpaqueValuesAndSparseKeysThroughUpdates(): void
228 {
229 $payload = new stdClass();
230 $rows = [7 => ['id' => 1, 'payload' => ['object' => $payload]]];
231 $store = new ShadowStore();
232 $store->set('users', $rows);
233 $snapshot = $store->snapshot();
234
235 $store->update('users', [['id' => 1, 'payload' => null]], ['id']);
236
237 self::assertSame([7 => ['id' => 1, 'payload' => null]], $store->get('users'));
238 $store->restore($snapshot);
239 self::assertSame($rows, $store->get('users'));
240 }
241
242}
243