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