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