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