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