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