packages/ztd-query-core/tests/Unit/Shadow/ShadowApplicationTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Shadow;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use Tests\Fake\FakeSqlRewriter;
11use ZtdQuery\Connection\Exception\DatabaseException;
12use ZtdQuery\Connection\ResultSet;
13use ZtdQuery\Schema\Key\IdentityGenerationStrategy;
14use ZtdQuery\Schema\TableDefinition;
15use ZtdQuery\Schema\TableDefinitionRegistry;
16use ZtdQuery\Shadow\Mutation\MutationImpact;
17use ZtdQuery\Shadow\Mutation\Row\DeleteMutation;
18use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
19use ZtdQuery\Shadow\ReferentialIntegrityEnforcer;
20use ZtdQuery\Shadow\Row\RowPairing;
21use ZtdQuery\Shadow\ShadowApplication;
22use ZtdQuery\Shadow\ShadowStore;
23
24#[CoversClass(ShadowApplication::class)]
25#[UsesClass(ShadowStore::class)]
26#[UsesClass(ReferentialIntegrityEnforcer::class)]
27#[UsesClass(TableDefinition::class)]
28#[UsesClass(TableDefinitionRegistry::class)]
29#[UsesClass(InsertMutation::class)]
30#[UsesClass(DeleteMutation::class)]
31#[UsesClass(MutationImpact::class)]
32#[UsesClass(ResultSet::class)]
33#[UsesClass(DatabaseException::class)]
34#[UsesClass(\ZtdQuery\Exception\DuplicateKeyException::class)]
35#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyConflict::class)]
36#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyMatch::class)]
37#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeySet::class)]
38#[UsesClass(\ZtdQuery\Shadow\ForeignKeyCascade::class)]
39#[UsesClass(\ZtdQuery\Shadow\ForeignKeyEnds::class)]
40#[UsesClass(\ZtdQuery\Shadow\ForeignKeyIntegrity::class)]
41#[UsesClass(\ZtdQuery\Shadow\Mutation\ConflictSearch::class)]
42#[UsesClass(\ZtdQuery\Shadow\Mutation\RowConstraints::class)]
43#[UsesClass(\ZtdQuery\Shadow\ParentKeyLookup::class)]
44#[UsesClass(\ZtdQuery\Shadow\Row\RowMatch::class)]
45#[UsesClass(\ZtdQuery\Shadow\Row\RowMultiset::class)]
46#[UsesClass(\ZtdQuery\Shadow\Row\TableTransition::class)]
47#[UsesClass(\ZtdQuery\Shadow\TableTransitions::class)]
48#[UsesClass(RowPairing::class)]
49#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
50final class ShadowApplicationTest extends TestCase
51{
52    public function testApplyWritesTheRowsAMutationDescribesIntoTheShadow(): void
53    {
54        $store = new ShadowStore();
55        $store->set('users', []);
56        $registry = new TableDefinitionRegistry();
57        $application = new ShadowApplication(
58            $store,
59            new ReferentialIntegrityEnforcer($registry),
60            $registry,
61            new FakeSqlRewriter($store, $registry),
62        );
63
64        $application->apply(new InsertMutation('users'), new ResultSet([['id' => 1]], []), 'INSERT');
65
66        self::assertSame([['id' => 1]], $store->get('users'));
67    }
68
69    public function testApplyAnswersWhatTheStatementCameTo(): void
70    {
71        $store = new ShadowStore();
72        $store->set('users', []);
73        $registry = new TableDefinitionRegistry();
74        $application = new ShadowApplication(
75            $store,
76            new ReferentialIntegrityEnforcer($registry),
77            $registry,
78            new FakeSqlRewriter($store, $registry),
79        );
80
81        $impact = $application->apply(new InsertMutation('users'), new ResultSet([['id' => 1]], []), 'INSERT');
82
83        self::assertTrue($impact->isInsertLike());
84    }
85
86    public function testApplyLeavesTheShadowAsItWasWhenTheStatementIsRefused(): void
87    {
88        $store = new ShadowStore();
89        $store->set('users', [['id' => 1]]);
90        $registry = new TableDefinitionRegistry();
91        $registry->register('users', new TableDefinition(['id'], [], ['id'], ['id'], ['PRIMARY' => ['id']]));
92        $application = new ShadowApplication(
93            $store,
94            new ReferentialIntegrityEnforcer($registry),
95            $registry,
96            new FakeSqlRewriter($store, $registry),
97        );
98
99        try {
100            $application->apply(
101                new InsertMutation('users', ['id'], false, $registry->get('users'), 'INSERT', true),
102                new ResultSet([['id' => 1]], []),
103                'INSERT',
104            );
105        } catch (DatabaseException) {
106            self::assertSame([['id' => 1]], $store->get('users'));
107
108            return;
109        }
110
111        self::fail('Inserting a duplicate key was expected to be refused.');
112    }
113
114    public function testLastInsertIdOfAnswersTheIdentityTheTableSaysIsNumbered(): void
115    {
116        $store = new ShadowStore();
117        $store->set('users', []);
118        $registry = new TableDefinitionRegistry();
119        $registry->register('users', new TableDefinition(
120            ['id'],
121            [],
122            ['id'],
123            [],
124            [],
125            identityStrategies: ['id' => IdentityGenerationStrategy::MaxValue],
126        ));
127        $application = new ShadowApplication(
128            $store,
129            new ReferentialIntegrityEnforcer($registry),
130            $registry,
131            new FakeSqlRewriter($store, $registry),
132        );
133        $mutation = new InsertMutation('users');
134        $impact = $application->apply($mutation, new ResultSet([['id' => 7]], []), 'INSERT');
135
136        self::assertSame('7', $application->lastInsertIdOf($mutation, $impact));
137    }
138
139    public function testLastInsertIdOfFallsBackToASingleColumnKey(): void
140    {
141        $store = new ShadowStore();
142        $store->set('users', []);
143        $registry = new TableDefinitionRegistry();
144        $registry->register('users', new TableDefinition(['id'], [], ['id'], [], []));
145        $application = new ShadowApplication(
146            $store,
147            new ReferentialIntegrityEnforcer($registry),
148            $registry,
149            new FakeSqlRewriter($store, $registry),
150        );
151        $mutation = new InsertMutation('users');
152        $impact = $application->apply($mutation, new ResultSet([['id' => 7]], []), 'INSERT');
153
154        self::assertSame('7', $application->lastInsertIdOf($mutation, $impact));
155    }
156
157    public function testLastInsertIdOfIsNothingForAStatementThatInsertedNothing(): void
158    {
159        $store = new ShadowStore();
160        $store->set('users', [['id' => 1]]);
161        $registry = new TableDefinitionRegistry();
162        $application = new ShadowApplication(
163            $store,
164            new ReferentialIntegrityEnforcer($registry),
165            $registry,
166            new FakeSqlRewriter($store, $registry),
167        );
168        $mutation = new DeleteMutation('users', ['id']);
169        $impact = $application->apply($mutation, new ResultSet([['id' => 1]], []), 'DELETE');
170
171        self::assertNull($application->lastInsertIdOf($mutation, $impact));
172    }
173
174    public function testLastInsertIdOfIsNothingWhereNoColumnIsNumbered(): void
175    {
176        $store = new ShadowStore();
177        $store->set('users', []);
178        $registry = new TableDefinitionRegistry();
179        $registry->register('users', new TableDefinition(['a', 'b'], [], ['a', 'b'], [], []));
180        $application = new ShadowApplication(
181            $store,
182            new ReferentialIntegrityEnforcer($registry),
183            $registry,
184            new FakeSqlRewriter($store, $registry),
185        );
186        $mutation = new InsertMutation('users');
187        $impact = $application->apply($mutation, new ResultSet([['a' => 1, 'b' => 2]], []), 'INSERT');
188
189        self::assertNull($application->lastInsertIdOf($mutation, $impact));
190    }
191}
192