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