packages/sql-fixture/tests/Unit/Plan/RelationTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Plan;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Test;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFixture\Plan\ColumnRef;
12use SqlFixture\Plan\PlanSyntaxException;
13use SqlFixture\Plan\Relation;
14use SqlFixture\Plan\RelationKind;
15use SqlFixture\Plan\RelationSide;
16
17#[CoversClass(Relation::class)]
18#[UsesClass(ColumnRef::class)]
19#[UsesClass(RelationKind::class)]
20#[UsesClass(RelationSide::class)]
21#[UsesClass(PlanSyntaxException::class)]
22#[UsesClass(\SqlFixture\Plan\FixturePlan::class)]
23#[UsesClass(\SqlFixture\Plan\PlanParser::class)]
24#[UsesClass(\SqlFixture\Plan\PlanPrinter::class)]
25#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
26#[UsesClass(\SqlFixture\Plan\Parsing\RelationCursor::class)]
27#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
28#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
29#[UsesClass(\SqlFixture\Plan\Printing\PlanTables::class)]
30#[UsesClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
31#[UsesClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
32#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
33#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
34#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
35#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
36#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
37#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
38#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
39#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
40#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
41#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
42#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
43#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
44#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
45#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
46#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
47final class RelationTest extends TestCase
48{
49 #[Test]
50 public function testKeepsTheSidesInTheOrderTheyWereWritten(): void
51 {
52 $relation = new Relation(
53 ColumnRef::of('order', 'id'),
54 RelationKind::OneToMany,
55 ColumnRef::of('order_detail', 'order_id')
56 );
57
58 self::assertSame('order', $relation->left->table);
59 self::assertSame('order_detail', $relation->right->table);
60 }
61
62 #[Test]
63 public function testOneToManyMakesTheLeftSideTheParent(): void
64 {
65 $relation = new Relation(
66 ColumnRef::of('order', 'id'),
67 RelationKind::OneToMany,
68 ColumnRef::of('order_detail', 'order_id')
69 );
70
71 self::assertSame('order', $relation->parent()->table);
72 self::assertSame('order_detail', $relation->child()->table);
73 }
74
75 #[Test]
76 public function testManyToOneDescribesTheSameShapeWrittenBackwards(): void
77 {
78 $relation = new Relation(
79 ColumnRef::of('order_detail', 'order_id'),
80 RelationKind::ManyToOne,
81 ColumnRef::of('order', 'id')
82 );
83
84 self::assertSame('order', $relation->parent()->table);
85 self::assertSame('order_detail', $relation->child()->table);
86 }
87
88 #[Test]
89 public function testColumnMapPointsChildColumnsAtParentColumns(): void
90 {
91 $relation = new Relation(
92 ColumnRef::of('order', 'id'),
93 RelationKind::OneToMany,
94 ColumnRef::of('order_detail', 'order_id')
95 );
96
97 self::assertSame(['order_id' => 'id'], $relation->columnMap());
98 }
99
100 #[Test]
101 public function testColumnMapPairsCompositeKeysPositionally(): void
102 {
103 $relation = new Relation(
104 ColumnRef::of('order', 'shop_id', 'no'),
105 RelationKind::OneToMany,
106 ColumnRef::of('order_detail', 'shop_id', 'order_no')
107 );
108
109 self::assertSame(['shop_id' => 'shop_id', 'order_no' => 'no'], $relation->columnMap());
110 }
111
112 #[Test]
113 public function testColumnMapReadsTheSameWrittenEitherWay(): void
114 {
115 $forwards = new Relation(
116 ColumnRef::of('order', 'id'),
117 RelationKind::OneToMany,
118 ColumnRef::of('order_detail', 'order_id')
119 );
120 $backwards = new Relation(
121 ColumnRef::of('order_detail', 'order_id'),
122 RelationKind::ManyToOne,
123 ColumnRef::of('order', 'id')
124 );
125
126 self::assertSame($forwards->columnMap(), $backwards->columnMap());
127 }
128
129 #[Test]
130 public function testParentIsOptionalAMarkerNextToTheParentMakesTheParentOptional(): void
131 {
132 $relation = new Relation(
133 ColumnRef::of('order_detail', 'order_id'),
134 RelationKind::ManyToOne,
135 ColumnRef::of('order', 'id'),
136 false,
137 true
138 );
139
140 self::assertTrue($relation->parentIsOptional());
141 self::assertFalse($relation->childIsOptional());
142 }
143
144 #[Test]
145 public function testChildIsOptionalAMarkerNextToTheChildMakesTheChildOptional(): void
146 {
147 $relation = new Relation(
148 ColumnRef::of('order', 'id'),
149 RelationKind::OneToMany,
150 ColumnRef::of('order_detail', 'order_id'),
151 false,
152 true
153 );
154
155 self::assertTrue($relation->childIsOptional());
156 self::assertFalse($relation->parentIsOptional());
157 }
158
159 #[Test]
160 public function testBothSidesAreRequiredByDefault(): void
161 {
162 $relation = new Relation(
163 ColumnRef::of('order', 'id'),
164 RelationKind::OneToMany,
165 ColumnRef::of('order_detail', 'order_id')
166 );
167
168 self::assertFalse($relation->parentIsOptional());
169 self::assertFalse($relation->childIsOptional());
170 }
171
172 #[Test]
173 public function testChildIsCollectionOnlyOneToOneHoldsASingleChildRow(): void
174 {
175 $many = new Relation(
176 ColumnRef::of('order', 'id'),
177 RelationKind::OneToMany,
178 ColumnRef::of('order_detail', 'order_id')
179 );
180 $one = new Relation(
181 ColumnRef::of('order', 'id'),
182 RelationKind::OneToOne,
183 ColumnRef::of('order_shipping', 'order_id')
184 );
185
186 self::assertTrue($many->childIsCollection());
187 self::assertFalse($one->childIsCollection());
188 }
189
190 #[Test]
191 public function testTablesListsBothEnds(): void
192 {
193 $relation = new Relation(
194 ColumnRef::of('order', 'id'),
195 RelationKind::OneToMany,
196 ColumnRef::of('order_detail', 'order_id')
197 );
198
199 self::assertSame(['order', 'order_detail'], $relation->tables());
200 }
201
202 #[Test]
203 public function testASelfReferenceNamesItsTableOnce(): void
204 {
205 $relation = new Relation(
206 ColumnRef::of('category', 'id'),
207 RelationKind::OneToMany,
208 ColumnRef::of('category', 'parent_id')
209 );
210
211 self::assertSame(['category'], $relation->tables());
212 }
213
214 #[Test]
215 public function testNamedConstructorsBuildEachOperator(): void
216 {
217 self::assertSame(RelationKind::OneToMany, Relation::oneToMany('a.id', 'b.a_id')->kind);
218 self::assertSame(RelationKind::ManyToOne, Relation::manyToOne('b.a_id', 'a.id')->kind);
219 self::assertSame(RelationKind::OneToOne, Relation::oneToOne('a.id', 'b.a_id')->kind);
220 }
221
222 #[Test]
223 public function testNamedConstructorsReadEndpointsWrittenAsStrings(): void
224 {
225 $relation = Relation::oneToMany('order.(shop_id, no)', 'order_detail.(shop_id, order_no)');
226
227 self::assertSame(['shop_id', 'no'], $relation->left->columns);
228 self::assertSame(['shop_id', 'order_no'], $relation->right->columns);
229 }
230
231 #[Test]
232 public function testNamedConstructorsAlsoTakeColumnRefs(): void
233 {
234 $relation = Relation::oneToMany(ColumnRef::of('order', 'id'), ColumnRef::of('order_detail', 'order_id'));
235
236 self::assertSame('order.id', $relation->left->toString());
237 }
238
239 #[Test]
240 public function testOneToManyCanMarkTheChildOptional(): void
241 {
242 self::assertTrue(Relation::oneToMany('a.id', 'b.a_id', true)->childIsOptional());
243 self::assertFalse(Relation::oneToMany('a.id', 'b.a_id')->childIsOptional());
244 }
245
246 #[Test]
247 public function testManyToOneCanMarkTheParentOptional(): void
248 {
249 self::assertTrue(Relation::manyToOne('b.a_id', 'a.id', true)->parentIsOptional());
250 self::assertFalse(Relation::manyToOne('b.a_id', 'a.id')->parentIsOptional());
251 }
252
253 #[Test]
254 public function testMinimumChildRowsARequiredChildIsGeneratedEvenWhenNoneAreGiven(): void
255 {
256 self::assertSame(1, Relation::oneToMany('order.id', 'order_detail.order_id')->minimumChildRows());
257 }
258
259 #[Test]
260 public function testAnOptionalChildMayEndUpWithNoRows(): void
261 {
262 self::assertSame(0, Relation::oneToMany('order.id', 'order_detail.order_id', true)->minimumChildRows());
263 }
264
265 #[Test]
266 public function testMaximumChildRowsACollectionChildHasNoUpperBound(): void
267 {
268 self::assertNull(Relation::oneToMany('order.id', 'order_detail.order_id')->maximumChildRows());
269 }
270
271 #[Test]
272 public function testAOneToOneChildIsCappedAtOneRow(): void
273 {
274 self::assertSame(1, Relation::oneToOne('order.id', 'order_shipping.order_id')->maximumChildRows());
275 }
276
277 #[Test]
278 public function testAnOptionalOneToOneChildRangesFromNoneToOne(): void
279 {
280 $relation = Relation::oneToOne('order.id', 'order_shipping.order_id', true);
281
282 self::assertSame(0, $relation->minimumChildRows());
283 self::assertSame(1, $relation->maximumChildRows());
284 }
285
286 #[Test]
287 public function testNamedConstructorsLeaveTheLeftSideRequired(): void
288 {
289 self::assertFalse(Relation::oneToMany('a.id', 'b.a_id', true)->leftOptional);
290 self::assertFalse(Relation::manyToOne('b.a_id', 'a.id', true)->leftOptional);
291 self::assertFalse(Relation::oneToOne('a.id', 'b.a_id', true)->leftOptional);
292 }
293
294 #[Test]
295 public function testOneToManyMarksOnlyTheRightSideOptional(): void
296 {
297 $relation = Relation::oneToMany('a.id', 'b.a_id', true);
298
299 self::assertFalse($relation->leftOptional);
300 self::assertTrue($relation->rightOptional);
301 }
302
303 #[Test]
304 public function testOneToOneMarksOnlyTheRightSideOptional(): void
305 {
306 $relation = Relation::oneToOne('a.id', 'b.a_id', true);
307
308 self::assertFalse($relation->leftOptional);
309 self::assertTrue($relation->rightOptional);
310 }
311
312 #[Test]
313 public function testManyToOneMarksOnlyTheRightSideOptional(): void
314 {
315 $relation = Relation::manyToOne('b.a_id', 'a.id', true);
316
317 self::assertFalse($relation->leftOptional);
318 self::assertTrue($relation->rightOptional);
319 }
320
321 #[Test]
322 public function testOneToManyLeavesBothSidesRequiredByDefault(): void
323 {
324 $relation = Relation::oneToMany('a.id', 'b.a_id');
325
326 self::assertFalse($relation->leftOptional);
327 self::assertFalse($relation->rightOptional);
328 }
329
330 #[Test]
331 public function testManyToOneLeavesBothSidesRequiredByDefault(): void
332 {
333 $relation = Relation::manyToOne('b.a_id', 'a.id');
334
335 self::assertFalse($relation->leftOptional);
336 self::assertFalse($relation->rightOptional);
337 }
338
339 #[Test]
340 public function testOneToOneLeavesBothSidesRequiredByDefault(): void
341 {
342 $relation = Relation::oneToOne('a.id', 'b.a_id');
343
344 self::assertFalse($relation->leftOptional);
345 self::assertFalse($relation->rightOptional);
346 }
347}
348