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