packages/sql-fixture/tests/Unit/Plan/Choice/ChoiceSyntaxTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Plan\Choice;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFixture\Fixture\Choice\ChoiceValueException;
11use SqlFixture\Fixture\GenerationRun;
12use SqlFixture\Fixture\RowSpec;
13use SqlFixture\Plan\Choice\ChoiceCase;
14use SqlFixture\Plan\Choice\ChoiceDefinitionException;
15use SqlFixture\Plan\Choice\ChoiceSyntax as Subject;
16use SqlFixture\Plan\ColumnRef;
17use SqlFixture\Plan\FixturePlan;
18use SqlFixture\Plan\Parsing\RelationCursor;
19use SqlFixture\Plan\Relation;
20use SqlFixture\Plan\RelationChoice;
21
22#[CoversClass(Subject::class)]
23#[UsesClass(\SqlFixture\Fixture\Choice\CaseSelection::class)]
24#[UsesClass(\SqlFixture\Fixture\Choice\ChoiceBindings::class)]
25#[UsesClass(ChoiceValueException::class)]
26#[UsesClass(\SqlFixture\Fixture\Choice\InverseRelations::class)]
27#[UsesClass(\SqlFixture\Fixture\Choice\ResolvedRow::class)]
28#[UsesClass(\SqlFixture\Fixture\Choice\RowChoices::class)]
29#[UsesClass(GenerationRun::class)]
30#[UsesClass(\SqlFixture\Fixture\OverrideRows::class)]
31#[UsesClass(RowSpec::class)]
32#[UsesClass(\SqlFixture\Fixture\TableOverrides::class)]
33#[UsesClass(ChoiceCase::class)]
34#[UsesClass(ChoiceDefinitionException::class)]
35#[UsesClass(\SqlFixture\Plan\Choice\ChoiceLiteral::class)]
36#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
37#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
38#[UsesClass(ColumnRef::class)]
39#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
40#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
41#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
42#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
43#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
44#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
45#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
46#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
47#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
48#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
49#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
50#[UsesClass(FixturePlan::class)]
51#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
52#[UsesClass(RelationCursor::class)]
53#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
54#[UsesClass(\SqlFixture\Plan\PlanParser::class)]
55#[UsesClass(\SqlFixture\Plan\PlanPrinter::class)]
56#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
57#[UsesClass(\SqlFixture\Plan\PlanSyntaxException::class)]
58#[UsesClass(\SqlFixture\Plan\Printing\PlanTables::class)]
59#[UsesClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
60#[UsesClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
61#[UsesClass(Relation::class)]
62#[UsesClass(RelationChoice::class)]
63#[UsesClass(\SqlFixture\Plan\RelationKind::class)]
64#[UsesClass(\SqlFixture\Plan\RelationSide::class)]
65#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
66#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
67final class ChoiceSyntaxTest extends TestCase
68{
69    public function testParseReadsCasesAndFallback(): void
70    {
71        $choice = (new Subject())->parse("choice comments.kind { 'post' { comments.target_id > posts.id } null {} otherwise { comments.target_id > videos.id } }");
72        self::assertSame('comments.kind', $choice->discriminator->toString());
73        self::assertSame(['post', null], array_column($choice->cases, 'value'));
74        self::assertCount(1, $choice->fallback->relations ?? []);
75    }
76
77    public function testPrintPreservesCompositeRelationsAndEscapedLiterals(): void
78    {
79        $choice = RelationChoice::on('comments.kind')->when("post's", Relation::manyToOne('comments.(tenant, target_id)', 'posts.(tenant, id)'))->otherwise();
80        $text = (new Subject())->print($choice);
81        self::assertSame("choice comments.kind { 'post''s' { comments.(tenant, target_id) > posts.(tenant, id) } otherwise {  } }", $text);
82        self::assertEquals($choice, (new Subject())->parse($text));
83    }
84
85    public function testReadRelationsAllowsEmptyBranchesAndQuotedNames(): void
86    {
87        self::assertSame([], (new Subject())->readRelations(new RelationCursor('{}')));
88        $relations = (new Subject())->readRelations(new RelationCursor('{ `a}`.id < b.a_id }'));
89        self::assertSame('a}', $relations[0]->parent()->table);
90    }
91
92
93    public function testParseKeepsOperatorTextInsideValues(): void
94    {
95        $plan = FixturePlan::from("choice c.kind { '<>' { c.id > a.id } }");
96        self::assertSame('<>', $plan->choices[0]->cases[0]->value);
97        self::assertSame($plan->toString(), FixturePlan::from($plan->toString())->toString());
98    }
99
100    public function testParseAcceptsCompactFallbackAndTrailingWhitespace(): void
101    {
102        $choice = (new Subject())->parse("choice c.kind { 'a' {} otherwise{c.id > a.id} }  \n");
103        self::assertSame('a', $choice->fallback?->relations[0]->parent()->table);
104    }
105}
106