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