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)]
49final class PlanPrinterTest extends TestCase
50{
51 #[Test]
52 public function testPrintsATableOnlyPlanAsItsTableName(): void
53 {
54 self::assertSame('order', (new PlanPrinter())->print(FixturePlan::table('order')));
55 }
56
57 #[Test]
58 public function testPrintsARelationWithSpacesAroundTheOperator(): void
59 {
60 $plan = (new PlanParser())->parse('order.id<order_detail.order_id');
61
62 self::assertSame('order.id < order_detail.order_id', (new PlanPrinter())->print($plan));
63 }
64
65 #[Test]
66 public function testRegroupsRelationsThatShareALeftEnd(): void
67 {
68 $plan = (new PlanParser())->parse(
69 'order.id < order_detail.order_id, order.id < shipment.order_id'
70 );
71
72 self::assertSame(
73 'order.id < [order_detail.order_id, shipment.order_id]',
74 (new PlanPrinter())->print($plan)
75 );
76 }
77
78 #[Test]
79 public function testDoesNotGroupRelationsWithDifferentOperators(): void
80 {
81 $plan = (new PlanParser())->parse('order.id < order_detail.order_id, order.id - shipment.order_id');
82
83 self::assertSame(
84 'order.id < order_detail.order_id, order.id - shipment.order_id',
85 (new PlanPrinter())->print($plan)
86 );
87 }
88
89 #[Test]
90 public function testDoesNotGroupRelationsWithDifferentOptionalMarkers(): void
91 {
92 $plan = (new PlanParser())->parse('order.id < order_detail.order_id, order.id <? shipment.order_id');
93
94 self::assertSame(
95 'order.id < order_detail.order_id, order.id <? shipment.order_id',
96 (new PlanPrinter())->print($plan)
97 );
98 }
99
100 #[Test]
101 public function testPrintsOptionalMarkersOnTheSideTheyBelongTo(): void
102 {
103 $plan = (new PlanParser())->parse('order.id ?< order_detail.order_id');
104
105 self::assertSame('order.id ?< order_detail.order_id', (new PlanPrinter())->print($plan));
106 }
107
108 #[Test]
109 #[DataProvider('providerRoundTrips')]
110 public function readingBackWhatWasPrintedGivesTheSameText(string $plan, string $expected): void
111 {
112 $printed = (new PlanPrinter())->print((new PlanParser())->parse($plan));
113 $reprinted = (new PlanPrinter())->print((new PlanParser())->parse($printed));
114
115 self::assertSame($expected, $printed);
116 self::assertSame($printed, $reprinted);
117 }
118
119 /**
120 * @return array<string, array{string, string}>
121 */
122 public static function providerRoundTrips(): array
123 {
124 return [
125 'table only' => ['order', 'order'],
126 'one to many' => ['order.id < order_detail.order_id', 'order.id < order_detail.order_id'],
127 'many to one' => ['order_detail.order_id > order.id', 'order_detail.order_id > order.id'],
128 'one to one' => ['order.id - order_shipping.order_id', 'order.id - order_shipping.order_id'],
129 'grouped' => [
130 'order.id < [order_detail.order_id, shipment.order_id]',
131 'order.id < [order_detail.order_id, shipment.order_id]',
132 ],
133 'several relations' => [
134 'order.id < order_detail.order_id, order.customer_id > customer.id',
135 'order.id < order_detail.order_id, order.customer_id > customer.id',
136 ],
137 'composite' => [
138 'order.(shop_id, no) < order_detail.(shop_id, order_no)',
139 'order.(shop_id, no) < order_detail.(shop_id, order_no)',
140 ],
141 'optional parent' => ['order_detail.order_id >? order.id', 'order_detail.order_id >? order.id'],
142 'newline separated' => [
143 "order.id < order_detail.order_id\norder_detail.product_id > product.id",
144 'order.id < order_detail.order_id, order_detail.product_id > product.id',
145 ],
146 'quoting is dropped' => ['`order`.`id` < "order_detail"."order_id"', 'order.id < order_detail.order_id'],
147 ];
148 }
149
150 #[Test]
151 public function testPrintsASingleRelationWithoutAPlan(): void
152 {
153 $relation = Relation::oneToMany('order.id', 'order_detail.order_id');
154
155 self::assertSame('order.id < order_detail.order_id', (new PlanPrinter())->printRelation($relation));
156 }
157
158 #[Test]
159 public function testAStandaloneTableIsWrittenOutAlongsideTheRelations(): void
160 {
161 $plan = (new PlanParser())->parse('a.id < b.a_id, audit_log');
162
163 self::assertSame('a.id < b.a_id, audit_log', (new PlanPrinter())->print($plan));
164 }
165
166 #[Test]
167 public function testSeveralStandaloneTablesKeepTheOrderTheyWereNamedIn(): void
168 {
169 $plan = (new PlanParser())->parse('audit_log, a.id < b.a_id, feature_flag');
170
171 self::assertSame('a.id < b.a_id, audit_log, feature_flag', (new PlanPrinter())->print($plan));
172 }
173 public function testPrintRelationIncludesOptionalEndpointMarkers(): void
174 {
175 self::assertSame('a.id <? b.a_id', (new PlanPrinter())->printRelation(Relation::oneToMany('a.id', 'b.a_id', true)));
176 }
177}
178