packages/ztd-query-core/tests/Unit/Rewrite/ShadowIdentityAllocatorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\TestCase;
9use ZtdQuery\Rewrite\ShadowIdentityAllocator;
10use ZtdQuery\Schema\Key\IdentityGenerationStrategy;
11
12#[CoversClass(ShadowIdentityAllocator::class)]
13final class ShadowIdentityAllocatorTest extends TestCase
14{
15    public function testMaxValueIdentityAdvancesPastRowsAndSurvivesDeletion(): void
16    {
17        $allocator = new ShadowIdentityAllocator();
18        $strategies = ['id' => IdentityGenerationStrategy::MaxValue];
19
20        self::assertSame(['id' => 8], $allocator->allocateMissing('users', $strategies, ['name'], [['id' => 7]]));
21        self::assertSame(['id' => 9], $allocator->allocateMissing('users', $strategies, ['name'], []));
22    }
23
24    public function testSequenceIdentityDoesNotUseExplicitRowMaximum(): void
25    {
26        $allocator = new ShadowIdentityAllocator();
27        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
28
29        self::assertSame(['id' => 1], $allocator->allocateMissing('users', $strategies, [], [['id' => 99]]));
30        self::assertSame(['id' => 2], $allocator->allocateMissing('users', $strategies, [], []));
31        self::assertSame([], $allocator->allocateMissing('users', $strategies, ['id'], []));
32    }
33
34    public function testGeneratedStateIsSeparatedByTableAndColumn(): void
35    {
36        $allocator = new ShadowIdentityAllocator();
37
38        self::assertSame(
39            ['id' => 1, 'tenant_id' => 1],
40            $allocator->allocateMissing('users', [
41                'id' => IdentityGenerationStrategy::Sequence,
42                'tenant_id' => IdentityGenerationStrategy::Sequence,
43            ], [], []),
44        );
45        self::assertSame(
46            ['id' => 1],
47            $allocator->allocateMissing('orders', ['id' => IdentityGenerationStrategy::Sequence], [], []),
48        );
49        self::assertSame(
50            ['id' => 2, 'tenant_id' => 2],
51            $allocator->allocateMissing('users', [
52                'id' => IdentityGenerationStrategy::Sequence,
53                'tenant_id' => IdentityGenerationStrategy::Sequence,
54            ], [], []),
55        );
56    }
57
58    public function testExplicitIdentityDoesNotPreventOtherIdentitiesFromBeingAllocated(): void
59    {
60        $allocator = new ShadowIdentityAllocator();
61        $strategies = [
62            'id' => IdentityGenerationStrategy::Sequence,
63            'tenant_id' => IdentityGenerationStrategy::Sequence,
64        ];
65
66        self::assertSame(
67            ['tenant_id' => 1],
68            $allocator->allocateMissing('users', $strategies, ['id'], []),
69        );
70        self::assertSame(
71            ['id' => 1, 'tenant_id' => 2],
72            $allocator->allocateMissing('users', $strategies, [], []),
73        );
74    }
75
76    public function testMaxValueUsesOnlyIntegerRows(): void
77    {
78        $allocator = new ShadowIdentityAllocator();
79        $rows = [
80            ['id' => null],
81            ['id' => 'prefix99'],
82            ['id' => '20suffix'],
83            ['id' => 18.9],
84            ['id' => false],
85            ['id' => '-4'],
86            ['id' => '12'],
87            ['id' => 15],
88        ];
89
90        self::assertSame(
91            ['id' => 16],
92            $allocator->allocateMissing('users', ['id' => IdentityGenerationStrategy::MaxValue], [], $rows),
93        );
94        self::assertSame(
95            ['id' => 1],
96            (new ShadowIdentityAllocator())->allocateMissing(
97                'empty',
98                ['id' => IdentityGenerationStrategy::MaxValue],
99                [],
100                [['id' => 'invalid'], ['id' => 0.9]],
101            ),
102        );
103        self::assertSame(
104            ['id' => 13],
105            (new ShadowIdentityAllocator())->allocateMissing(
106                'strings',
107                ['id' => IdentityGenerationStrategy::MaxValue],
108                [],
109                [['id' => '12']],
110            ),
111        );
112        self::assertSame(
113            ['id' => 1],
114            (new ShadowIdentityAllocator())->allocateMissing(
115                'non-integers',
116                ['id' => IdentityGenerationStrategy::MaxValue],
117                [],
118                [['id' => true], ['id' => 18.0]],
119            ),
120        );
121    }
122
123    public function testSelectAllocationContinuesFromMaterializedRows(): void
124    {
125        $allocator = new ShadowIdentityAllocator();
126        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
127
128        self::assertSame(
129            ['id' => 1],
130            $allocator->allocateSelectStarts('users', $strategies, [], []),
131        );
132        self::assertSame(
133            ['id' => 3],
134            $allocator->allocateMissing('users', $strategies, ['name'], [['id' => 1], ['id' => 2]]),
135        );
136    }
137
138    public function testSelectAllocationUsesStrategyStateAndAllColumns(): void
139    {
140        $allocator = new ShadowIdentityAllocator();
141
142        self::assertSame(
143            [
144                'max_id' => 8,
145                'sequence_id' => 1,
146            ],
147            $allocator->allocateSelectStarts(
148                'users',
149                [
150                    'max_id' => IdentityGenerationStrategy::MaxValue,
151                    'sequence_id' => IdentityGenerationStrategy::Sequence,
152                ],
153                [],
154                [['max_id' => 7, 'sequence_id' => 99]],
155            ),
156        );
157        self::assertSame(
158            ['sequence_id' => 100],
159            $allocator->allocateMissing(
160                'users',
161                ['sequence_id' => IdentityGenerationStrategy::Sequence],
162                [],
163                [['sequence_id' => 99]],
164            ),
165        );
166    }
167
168    public function testSelectAllocationSkipsProvidedIdentityColumns(): void
169    {
170        self::assertSame(
171            ['generated_id' => 1],
172            (new ShadowIdentityAllocator())->allocateSelectStarts(
173                'users',
174                [
175                    'provided_id' => IdentityGenerationStrategy::Sequence,
176                    'generated_id' => IdentityGenerationStrategy::Sequence,
177                ],
178                ['provided_id'],
179                [],
180            ),
181        );
182    }
183
184    public function testCommitProjectionBeginProjectionUncommittedPreparationDoesNotConsumeIdentity(): void
185    {
186        $allocator = new ShadowIdentityAllocator();
187        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
188
189        $allocator->beginProjection();
190        self::assertSame(['id' => 1], $allocator->allocateMissing('users', $strategies, ['name'], []));
191        $allocator->beginProjection();
192        self::assertSame(['id' => 1], $allocator->allocateMissing('users', $strategies, ['name'], []));
193        $allocator->commitProjection();
194        $allocator->beginProjection();
195        self::assertSame(['id' => 2], $allocator->allocateMissing('users', $strategies, ['name'], [['id' => 1]]));
196    }
197
198    public function testBeginProjectionStartsFromWhatWasCommitted(): void
199    {
200        $allocator = new ShadowIdentityAllocator();
201        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
202        $allocator->beginProjection();
203        $allocator->allocateMissing('order', $strategies, [], []);
204        $allocator->commitProjection();
205
206        $allocator->beginProjection();
207
208        self::assertSame(['id' => 2], $allocator->allocateMissing('order', $strategies, [], []));
209    }
210
211    public function testBeginProjectionDiscardsWhatAProjectionNeverCommitted(): void
212    {
213        $allocator = new ShadowIdentityAllocator();
214        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
215        $allocator->beginProjection();
216        $allocator->allocateMissing('order', $strategies, [], []);
217
218        $allocator->beginProjection();
219
220        self::assertSame(['id' => 1], $allocator->allocateMissing('order', $strategies, [], []));
221    }
222
223    public function testAllocateMissingLeavesAColumnTheStatementWroteAlone(): void
224    {
225        $allocator = new ShadowIdentityAllocator();
226        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
227
228        self::assertSame([], $allocator->allocateMissing('order', $strategies, ['id'], []));
229    }
230
231    public function testAllocateSelectStartsAnswersTheFirstNumberTheQueryWouldTake(): void
232    {
233        $allocator = new ShadowIdentityAllocator();
234        $strategies = ['id' => IdentityGenerationStrategy::MaxValue];
235
236        self::assertSame(
237            ['id' => 4],
238            $allocator->allocateSelectStarts('order', $strategies, [], [['id' => 3]]),
239        );
240    }
241
242    public function testAllocateSelectStartsLeavesAColumnTheStatementWroteAlone(): void
243    {
244        $allocator = new ShadowIdentityAllocator();
245        $strategies = ['id' => IdentityGenerationStrategy::Sequence];
246
247        self::assertSame([], $allocator->allocateSelectStarts('order', $strategies, ['id'], []));
248    }
249
250    public function testNextAfterExistingRowsGoesPastTheLargestNumberAlreadyThere(): void
251    {
252        self::assertSame(8, (new ShadowIdentityAllocator())->nextAfterExistingRows(
253            'id',
254            [['id' => 3], ['id' => 7], ['id' => 5]],
255            1,
256        ));
257    }
258
259    public function testNextAfterExistingRowsKeepsTheNumberItWasGivenWhereNothingIsLarger(): void
260    {
261        self::assertSame(9, (new ShadowIdentityAllocator())->nextAfterExistingRows('id', [['id' => 3]], 9));
262    }
263
264    public function testIntegerValueReadsANumberHoweverTheDriverSpelledIt(): void
265    {
266        $allocator = new ShadowIdentityAllocator();
267
268        self::assertSame(7, $allocator->integerValue(7));
269        self::assertSame(7, $allocator->integerValue('7'));
270    }
271
272    public function testIntegerValueIsNothingForAValueThatIsNotAWholeNumber(): void
273    {
274        $allocator = new ShadowIdentityAllocator();
275
276        self::assertNull($allocator->integerValue('seven'));
277        self::assertNull($allocator->integerValue(null));
278        self::assertNull($allocator->integerValue(1.5));
279    }
280    public function testNextValueReadsAMaxValueColumnOffTheRowsEveryTime(): void
281    {
282        $allocator = new ShadowIdentityAllocator();
283
284        self::assertSame(
285            8,
286            $allocator->nextValue('users', 'id', IdentityGenerationStrategy::MaxValue, [['id' => 7]]),
287        );
288    }
289
290    public function testNextValueStartsASequenceAtOneHoweverManyRowsAreThere(): void
291    {
292        $allocator = new ShadowIdentityAllocator();
293
294        self::assertSame(
295            1,
296            $allocator->nextValue('users', 'id', IdentityGenerationStrategy::Sequence, [['id' => 7]]),
297        );
298    }
299
300}
301