packages/sql-fixture/tests/Unit/Plan/PlanPrinterTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Plan;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Test;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use SqlFixture\Plan\ColumnRef;
13use SqlFixture\Plan\FixturePlan;
14use SqlFixture\Plan\PlanParser;
15use SqlFixture\Plan\PlanPrinter;
16use SqlFixture\Plan\PlanSyntaxException;
17use SqlFixture\Plan\Relation;
18use SqlFixture\Plan\RelationKind;
19use SqlFixture\Plan\RelationSide;
20
21#[CoversClass(PlanPrinter::class)]
22#[UsesClass(PlanParser::class)]
23#[UsesClass(FixturePlan::class)]
24#[UsesClass(Relation::class)]
25#[UsesClass(ColumnRef::class)]
26#[UsesClass(RelationKind::class)]
27#[UsesClass(RelationSide::class)]
28#[UsesClass(PlanSyntaxException::class)]
29#[CoversClass(\SqlFixture\Plan\Printing\PlanTables::class)]
30#[CoversClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
31#[CoversClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
32#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
33#[UsesClass(\SqlFixture\Plan\Parsing\RelationCursor::class)]
34#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
35#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
36#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
37#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
38#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
39#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
40#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
41#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
42#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
43#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
44#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
45#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
46#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
47#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
48#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
49#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
50#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
51final class PlanPrinterTest extends TestCase
52{
53 #[Test]
54 public function testPrintsATableOnlyPlanAsItsTableName(): void
55 {
56 self::assertSame('order', (new PlanPrinter())->print(FixturePlan::table('order')));
57 }
58
59 #[Test]
60 public function testPrintsARelationWithSpacesAroundTheOperator(): void
61 {
62 $plan = (new PlanParser())->parse('order.id<order_detail.order_id');
63
64 self::assertSame('order.id < order_detail.order_id', (new PlanPrinter())->print($plan));
65 }
66
67 #[Test]
68 public function testRegroupsRelationsThatShareALeftEnd(): void
69 {
70 $plan = (new PlanParser())->parse(
71 'order.id < order_detail.order_id, order.id < shipment.order_id'
72 );
73
74 self::assertSame(
75 'order.id < [order_detail.order_id, shipment.order_id]',
76 (new PlanPrinter())->print($plan)
77 );
78 }
79
80 #[Test]
81 public function testDoesNotGroupRelationsWithDifferentOperators(): void
82 {
83 $plan = (new PlanParser())->parse('order.id < order_detail.order_id, order.id - shipment.order_id');
84
85 self::assertSame(
86 'order.id < order_detail.order_id, order.id - shipment.order_id',
87 (new PlanPrinter())->print($plan)
88 );
89 }
90
91 #[Test]
92 public function testDoesNotGroupRelationsWithDifferentOptionalMarkers(): void
93 {
94 $plan = (new PlanParser())->parse('order.id < order_detail.order_id, order.id <? shipment.order_id');
95
96 self::assertSame(
97 'order.id < order_detail.order_id, order.id <? shipment.order_id',
98 (new PlanPrinter())->print($plan)
99 );
100 }
101
102 #[Test]
103 public function testPrintsOptionalMarkersOnTheSideTheyBelongTo(): void
104 {
105 $plan = (new PlanParser())->parse('order.id ?< order_detail.order_id');
106
107 self::assertSame('order.id ?< order_detail.order_id', (new PlanPrinter())->print($plan));
108 }
109
110 #[Test]
111 #[DataProvider('providerRoundTrips')]
112 public function readingBackWhatWasPrintedGivesTheSameText(string $plan, string $expected): void
113 {
114 $printed = (new PlanPrinter())->print((new PlanParser())->parse($plan));
115 $reprinted = (new PlanPrinter())->print((new PlanParser())->parse($printed));
116
117 self::assertSame($expected, $printed);
118 self::assertSame($printed, $reprinted);
119 }
120
121 /**
122 * @return array<string, array{string, string}>
123 */
124 public static function providerRoundTrips(): array
125 {
126 return [
127 'table only' => ['order', 'order'],
128 'one to many' => ['order.id < order_detail.order_id', 'order.id < order_detail.order_id'],
129 'many to one' => ['order_detail.order_id > order.id', 'order_detail.order_id > order.id'],
130 'one to one' => ['order.id - order_shipping.order_id', 'order.id - order_shipping.order_id'],
131 'grouped' => [
132 'order.id < [order_detail.order_id, shipment.order_id]',
133 'order.id < [order_detail.order_id, shipment.order_id]',
134 ],
135 'several relations' => [
136 'order.id < order_detail.order_id, order.customer_id > customer.id',
137 'order.id < order_detail.order_id, order.customer_id > customer.id',
138 ],
139 'composite' => [
140 'order.(shop_id, no) < order_detail.(shop_id, order_no)',
141 'order.(shop_id, no) < order_detail.(shop_id, order_no)',
142 ],
143 'optional parent' => ['order_detail.order_id >? order.id', 'order_detail.order_id >? order.id'],
144 'newline separated' => [
145 "order.id < order_detail.order_id\norder_detail.product_id > product.id",
146 'order.id < order_detail.order_id, order_detail.product_id > product.id',
147 ],
148 'quoting is dropped' => ['`order`.`id` < "order_detail"."order_id"', 'order.id < order_detail.order_id'],
149 ];
150 }
151
152 #[Test]
153 public function testPrintsASingleRelationWithoutAPlan(): void
154 {
155 $relation = Relation::oneToMany('order.id', 'order_detail.order_id');
156
157 self::assertSame('order.id < order_detail.order_id', (new PlanPrinter())->printRelation($relation));
158 }
159
160 #[Test]
161 public function testAStandaloneTableIsWrittenOutAlongsideTheRelations(): void
162 {
163 $plan = (new PlanParser())->parse('a.id < b.a_id, audit_log');
164
165 self::assertSame('a.id < b.a_id, audit_log', (new PlanPrinter())->print($plan));
166 }
167
168 #[Test]
169 public function testSeveralStandaloneTablesKeepTheOrderTheyWereNamedIn(): void
170 {
171 $plan = (new PlanParser())->parse('audit_log, a.id < b.a_id, feature_flag');
172
173 self::assertSame('a.id < b.a_id, audit_log, feature_flag', (new PlanPrinter())->print($plan));
174 }
175 public function testPrintRelationIncludesOptionalEndpointMarkers(): void
176 {
177 self::assertSame('a.id <? b.a_id', (new PlanPrinter())->printRelation(Relation::oneToMany('a.id', 'b.a_id', true)));
178 }
179}
180