packages/ztd-query-core/tests/Unit/Shadow/Mutation/Table/SynchronizeMutationTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow\Mutation\Table;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Exception\DuplicateKeyException;
11use ZtdQuery\Exception\NotNullViolationException;
12use ZtdQuery\Schema\Key\CandidateKeyConflict;
13use ZtdQuery\Schema\Key\CandidateKeySet;
14use ZtdQuery\Schema\TableDefinition;
15use ZtdQuery\Shadow\Mutation\Table\SynchronizeMutation;
16use ZtdQuery\Shadow\ShadowStore;
17
18#[CoversClass(SynchronizeMutation::class)]
19#[UsesClass(CandidateKeyConflict::class)]
20#[UsesClass(CandidateKeySet::class)]
21#[UsesClass(DuplicateKeyException::class)]
22#[UsesClass(NotNullViolationException::class)]
23#[UsesClass(ShadowStore::class)]
24#[UsesClass(TableDefinition::class)]
25#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyMatch::class)]
26#[UsesClass(\ZtdQuery\Shadow\Row\RowMatch::class)]
27#[UsesClass(\ZtdQuery\Shadow\Row\RowMultiset::class)]
28#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
29final class SynchronizeMutationTest extends TestCase
30{
31 public function testTableNameApplyReplacesTheCompleteTableState(): void
32 {
33 $store = new ShadowStore();
34 $store->set('users', [['id' => 1, 'name' => 'old']]);
35 $rows = [
36 ['id' => 1, 'name' => 'updated'],
37 ['id' => 2, 'name' => 'inserted'],
38 ];
39
40 $mutation = new SynchronizeMutation('users');
41 $mutation->apply($store, $rows);
42
43 self::assertSame($rows, $store->get('users'));
44 self::assertSame('users', $mutation->tableName());
45 }
46
47 public function testApplyRejectsDuplicateCandidateKeys(): void
48 {
49 $definition = new TableDefinition(
50 ['id', 'email'],
51 ['id' => 'INTEGER', 'email' => 'TEXT'],
52 ['id'],
53 ['id'],
54 ['users_email_key' => ['email']],
55 );
56 $mutation = new SynchronizeMutation('users', $definition, 'MERGE INTO users');
57
58 $this->expectException(DuplicateKeyException::class);
59
60 $mutation->apply(new ShadowStore(), [
61 ['id' => 1, 'email' => 'same@example.com'],
62 ['id' => 2, 'email' => 'same@example.com'],
63 ]);
64 }
65
66 public function testApplyRejectsNullInNotNullColumn(): void
67 {
68 $definition = new TableDefinition(
69 ['id'],
70 ['id' => 'INTEGER'],
71 ['id'],
72 ['id'],
73 [],
74 );
75 $mutation = new SynchronizeMutation('users', $definition, 'MERGE INTO users');
76
77 $this->expectException(NotNullViolationException::class);
78
79 $mutation->apply(new ShadowStore(), [['id' => null]]);
80 }
81
82 public function testAffectedRowCountCountsMixedInsertUpdateAndDelete(): void
83 {
84 $definition = new TableDefinition(
85 ['id', 'name'],
86 ['id' => 'INTEGER', 'name' => 'TEXT'],
87 ['id'],
88 ['id'],
89 [],
90 );
91 $mutation = new SynchronizeMutation('users', $definition);
92
93 self::assertSame(3, $mutation->affectedRowCount(
94 [
95 ['id' => 3, 'name' => 'deleted'],
96 ['id' => 1, 'name' => 'old'],
97 ['id' => 4, 'name' => 'unchanged'],
98 ],
99 [
100 ['id' => 1, 'name' => 'updated'],
101 ['id' => 2, 'name' => 'inserted'],
102 ['id' => 4, 'name' => 'unchanged'],
103 ],
104 ));
105 }
106
107 public function testAffectedRowCountUsesMultisetDifferenceWithoutPrimaryKey(): void
108 {
109 $mutation = new SynchronizeMutation('logs');
110
111 self::assertSame(2, $mutation->affectedRowCount(
112 [['message' => 'same'], ['message' => 'old-one'], ['message' => 'old-two']],
113 [['message' => 'same'], ['message' => 'new-one'], ['message' => 'new-two']],
114 ));
115 }
116
117 public function testAffectedRowCountWithoutPrimaryKeyIgnoresRowOrdering(): void
118 {
119 $mutation = new SynchronizeMutation('logs');
120
121 self::assertSame(0, $mutation->affectedRowCount(
122 [['message' => 'first'], ['message' => 'second']],
123 [['message' => 'second'], ['message' => 'first']],
124 ));
125 }
126
127 public function testAffectedRowCountMatchesEveryPrimaryKeyAfterAnEarlierMatch(): void
128 {
129 $definition = new TableDefinition(
130 ['id'],
131 ['id' => 'INTEGER'],
132 ['id'],
133 ['id'],
134 [],
135 );
136 $mutation = new SynchronizeMutation('users', $definition);
137 $rows = [['id' => 1], ['id' => 2]];
138
139 self::assertSame(0, $mutation->affectedRowCount($rows, $rows));
140 self::assertSame(2, $mutation->affectedRowCount([['name' => 'missing']], [['id' => 1]]));
141 }
142}
143