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