packages/sql-faker/tests/Unit/Generation/Plan/ProductionPatternTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Generation\Plan;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\TestCase;
9use SqlFaker\Generation\Plan\ProductionPattern;
10
11#[CoversClass(ProductionPattern::class)]
12#[\PHPUnit\Framework\Attributes\UsesClass(\SqlFaker\Generation\Derivation\CompletionCosts::class)]
13#[\PHPUnit\Framework\Attributes\UsesClass(\SqlFaker\Generation\Derivation\DerivationNode::class)]
14final class ProductionPatternTest extends TestCase
15{
16    public function testContainingRequiresEveryNamedSymbol(): void
17    {
18        $pattern = ProductionPattern::containing('FOREIGN', 'KEY');
19
20        self::assertTrue($pattern->matches(['CONSTRAINT', 'FOREIGN', 'KEY', 'REFERENCES']));
21        self::assertFalse($pattern->matches(['CONSTRAINT', 'FOREIGN', 'REFERENCES']));
22    }
23
24    public function testExactlyMatchesOrderAndCardinality(): void
25    {
26        $pattern = ProductionPattern::exactly(first: 'CONSTRAINT', second: 'name');
27
28        self::assertTrue($pattern->matches(['CONSTRAINT', 'name']));
29        self::assertFalse($pattern->matches(['name', 'CONSTRAINT']));
30        self::assertFalse($pattern->matches(['CONSTRAINT', 'name', 'FOREIGN']));
31    }
32
33    public function testExactlyCanSelectAnEmptyProduction(): void
34    {
35        $pattern = ProductionPattern::exactly();
36
37        self::assertTrue($pattern->matches([]));
38        self::assertFalse($pattern->matches(['COMMA']));
39    }
40
41    public function testNonEmptyRejectsOnlyAnEmptyProduction(): void
42    {
43        $pattern = ProductionPattern::nonEmpty();
44
45        self::assertTrue($pattern->matches(['name']));
46        self::assertFalse($pattern->matches([]));
47    }
48
49    public function testMatchesAcceptsAnAlternativeContainingEveryNamedSymbol(): void
50    {
51        self::assertTrue(ProductionPattern::containing('FOREIGN', 'KEY')->matches(['CONSTRAINT', 'FOREIGN', 'KEY']));
52    }
53
54    public function testMatchesRejectsAnAlternativeMissingANamedSymbol(): void
55    {
56        self::assertFalse(ProductionPattern::containing('FOREIGN', 'KEY')->matches(['FOREIGN']));
57    }
58
59    public function testMatchesAcceptsOnlyTheAlternativeWrittenExactly(): void
60    {
61        self::assertTrue(ProductionPattern::exactly('cmdlist', 'ecmd')->matches(['cmdlist', 'ecmd']));
62        self::assertFalse(ProductionPattern::exactly('cmdlist', 'ecmd')->matches(['ecmd', 'cmdlist']));
63    }
64
65    public function testMatchesRefusesOnlyTheEmptyAlternative(): void
66    {
67        self::assertTrue(ProductionPattern::nonEmpty()->matches(['ecmd']));
68        self::assertFalse(ProductionPattern::nonEmpty()->matches([]));
69    }
70    public function testAtSelectsAnOrdinalIndependentlyOfItsSymbols(): void
71    {
72        $pattern = ProductionPattern::at(2);
73        self::assertTrue($pattern->matches(['T'], 2));
74        self::assertFalse($pattern->matches(['T'], 1));
75        self::assertFalse($pattern->matches(['T']));
76    }
77    public function testAllOfBooleanPatternsRetainEveryAllowedAlternative(): void
78    {
79        $pattern = ProductionPattern::allOf(
80            ProductionPattern::anyOf(ProductionPattern::containing('VALUES'), ProductionPattern::containing('SELECT')),
81            ProductionPattern::excluding(ProductionPattern::containing('WITH')),
82        );
83        self::assertTrue($pattern->matches(['VALUES', 'row']));
84        self::assertTrue($pattern->matches(['SELECT', 'expr']));
85        self::assertFalse($pattern->matches(['WITH', 'SELECT', 'expr']));
86        self::assertFalse($pattern->matches(['DELETE', 'table']));
87    }
88
89
90    public function testAnyOfAcceptsEitherFormWithoutAcceptingUnrelatedForms(): void
91    {
92        $pattern = ProductionPattern::anyOf(ProductionPattern::containing('VALUES'), ProductionPattern::containing('SELECT'));
93        self::assertTrue($pattern->matches(['VALUES', 'row']));
94        self::assertTrue($pattern->matches(['SELECT', 'expr']));
95        self::assertFalse($pattern->matches(['UPDATE', 'table']));
96    }
97
98    public function testExcludingCanExcludeAnOrdinalOrAnEmptyAlternative(): void
99    {
100        $pattern = ProductionPattern::excluding(ProductionPattern::at(1));
101        self::assertTrue($pattern->matches(['ID'], 0));
102        self::assertFalse($pattern->matches(['ID'], 1));
103        self::assertFalse(ProductionPattern::excluding(ProductionPattern::exactly())->matches([]));
104    }
105
106
107    public function testAnyOfKeepsTheThirdAlternativeAvailable(): void
108    {
109        $pattern = ProductionPattern::anyOf(
110            ProductionPattern::containing('INSERT'),
111            ProductionPattern::containing('UPDATE'),
112            ProductionPattern::containing('DELETE'),
113        );
114        self::assertTrue($pattern->matches(['DELETE', 'table']));
115        self::assertFalse($pattern->matches(['SELECT', 'table']));
116    }
117
118    public function testAllOfEnforcesEveryCondition(): void
119    {
120        $pattern = ProductionPattern::allOf(
121            ProductionPattern::nonEmpty(),
122            ProductionPattern::containing('INSERT'),
123            ProductionPattern::excluding(ProductionPattern::containing('WITH')),
124        );
125        self::assertFalse($pattern->matches(['WITH', 'INSERT']));
126        self::assertTrue($pattern->matches(['INSERT']));
127    }
128
129}
130