packages/ztd-query-core/tests/Unit/Schema/Key/CandidateKeySetTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Schema\Key;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Schema\Key\CandidateKeyConflict;
11use ZtdQuery\Schema\Key\CandidateKeySet;
12
13#[CoversClass(CandidateKeySet::class)]
14#[UsesClass(CandidateKeyConflict::class)]
15#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyMatch::class)]
16final class CandidateKeySetTest extends TestCase
17{
18 public function testFindsConflictAcrossPrimaryAndUniqueCandidateKeys(): void
19 {
20 $keys = CandidateKeySet::fromSchema(['id'], ['users_email' => ['email']]);
21 $rows = [
22 ['id' => 1, 'email' => 'alice@example.com'],
23 ['id' => 2, 'email' => 'bob@example.com'],
24 ];
25
26 $primaryConflict = $keys->findConflict(['id' => 1, 'email' => 'new@example.com'], $rows);
27 $uniqueConflict = $keys->findConflict(['id' => 3, 'email' => 'bob@example.com'], $rows);
28
29 self::assertInstanceOf(CandidateKeyConflict::class, $primaryConflict);
30 self::assertSame('PRIMARY', $primaryConflict->keyName);
31 self::assertSame(0, $primaryConflict->rowIndex);
32 self::assertInstanceOf(CandidateKeyConflict::class, $uniqueConflict);
33 self::assertSame('users_email', $uniqueConflict->keyName);
34 self::assertSame(1, $uniqueConflict->rowIndex);
35 }
36
37 public function testNullDoesNotConflictOnCompositeUniqueKey(): void
38 {
39 $keys = CandidateKeySet::fromSchema([], ['tenant_slug' => ['tenant_id', 'slug']]);
40 $rows = [['tenant_id' => 1, 'slug' => null]];
41
42 self::assertNull($keys->findConflict(['tenant_id' => 1, 'slug' => null], $rows));
43 }
44
45 public function testIncompletePrimaryKeyDoesNotPreventUniqueKeyConflictDetection(): void
46 {
47 $keys = CandidateKeySet::fromSchema(['id'], ['users_email' => ['email']]);
48 $rows = [['id' => 1, 'email' => 'alice@example.com']];
49
50 $conflict = $keys->findConflict(['id' => null, 'email' => 'alice@example.com'], $rows);
51
52 self::assertInstanceOf(CandidateKeyConflict::class, $conflict);
53 self::assertSame('users_email', $conflict->keyName);
54 }
55
56 public function testMissingCandidateKeyValueDoesNotConflictWithStoredNull(): void
57 {
58 $keys = CandidateKeySet::fromSchema([], ['tenant_slug' => ['tenant_id', 'slug']]);
59 $rows = [['tenant_id' => 1, 'slug' => null]];
60
61 self::assertNull($keys->findConflict(['tenant_id' => 1], $rows));
62 }
63
64 public function testMissingStoredKeyValueDoesNotConflict(): void
65 {
66 $keys = CandidateKeySet::fromSchema([], ['users_email' => ['email']]);
67
68 self::assertNull($keys->findConflict(['email' => 'alice@example.com'], [['id' => 1]]));
69 }
70
71 public function testEmptyKeySetNeverTreatsEqualRowsAsConflicting(): void
72 {
73 $keys = CandidateKeySet::fromSchema([]);
74 $row = ['name' => 'same'];
75
76 self::assertNull($keys->findConflict($row, [$row]));
77 }
78
79 public function testFromSchemaPutsThePrimaryKeyBeforeEveryUniqueOne(): void
80 {
81 $keys = CandidateKeySet::fromSchema(['id'], ['email' => ['email']]);
82
83 self::assertSame(['PRIMARY', 'email'], array_keys($keys->keys()));
84 }
85
86 public function testFromSchemaNamesOnlyTheUniqueKeysWhereThereIsNoPrimaryOne(): void
87 {
88 $keys = CandidateKeySet::fromSchema([], ['email' => ['email']]);
89
90 self::assertSame(['email' => ['email']], $keys->keys());
91 }
92
93 public function testKeysAnswersTheColumnsOfEveryKeyItWasBuiltFrom(): void
94 {
95 $keys = new CandidateKeySet(['PRIMARY' => ['id']]);
96
97 self::assertSame(['PRIMARY' => ['id']], $keys->keys());
98 }
99
100 public function testFindConflictIsNothingWhereNoRowCarriesTheSameKey(): void
101 {
102 $keys = CandidateKeySet::fromSchema(['id']);
103
104 self::assertNull($keys->findConflict(['id' => 2], [['id' => 1]]));
105 }
106}
107