packages/sql-faker/tests/Unit/Generation/Plan/Compilation/ScopedGenerationTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Generation\Plan\Compilation;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlFaker\Generation\Exception\LexicalException;
11use SqlFaker\Generation\Plan\Compilation\ScopedGeneration;
12use SqlFaker\Generation\Plan\GenerationPlan;
13use SqlFaker\Generation\Plan\LexemeConstraint;
14use SqlFaker\Generation\Token\TerminalOccurrence;
15use SqlFaker\Generation\Token\TerminalSequence;
16
17#[CoversClass(ScopedGeneration::class)]
18#[UsesClass(GenerationPlan::class)]
19#[UsesClass(LexemeConstraint::class)]
20#[UsesClass(TerminalOccurrence::class)]
21#[UsesClass(TerminalSequence::class)]
22final class ScopedGenerationTest extends TestCase
23{
24    public function testLexicalPlanRewrittenAndSiblingTokensKeepTheirOwnDomains(): void
25    {
26        $sequence = new TerminalSequence([
27            new TerminalOccurrence('ID', 1, [0, 10]),
28            new TerminalOccurrence('ID', -1, [0, 20], rewrite: 'inserted'),
29            new TerminalOccurrence('ID', 3, [0, 30]),
30        ]);
31        $scope = new ScopedGeneration($sequence, [10 => ['ID' => LexemeConstraint::oneOf('target')], 20 => ['ID' => LexemeConstraint::oneOf('source')]]);
32        $plan = $scope->lexicalPlan($sequence, GenerationPlan::all(), static fn (int $count): int => 0);
33        self::assertSame('target', $plan->lexemeAt('ID', 0));
34        self::assertSame('source', $plan->lexemeAt('ID', 1));
35        self::assertNull($plan->lexemeAt('ID', 2));
36    }
37
38    public function testLexicalPlanExplicitLexemesCannotSilentlyOverrideScopedConditions(): void
39    {
40        $sequence = new TerminalSequence([new TerminalOccurrence('ID', 1, [0])]);
41        $scope = new ScopedGeneration($sequence, [0 => ['ID' => LexemeConstraint::oneOf('target')]]);
42        $this->expectException(LexicalException::class);
43        $this->expectExceptionMessage('Explicit lexeme contradicts the plan for ID.');
44        $scope->lexicalPlan($sequence, GenerationPlan::all()->withLexemes(['ID' => ['different']]), static fn (int $count): int => 0);
45    }
46
47    public function testConstraintResolvesNearestAncestorWithoutAffectingOtherTokens(): void
48    {
49        $outer = LexemeConstraint::oneOf('outer');
50        $inner = LexemeConstraint::oneOf('inner');
51        $scope = new ScopedGeneration(new TerminalSequence([]), [0 => ['ID' => $outer], 10 => ['ID' => $inner]]);
52        self::assertSame($inner, $scope->constraint(new TerminalOccurrence('ID', 1, [0, 10, 20])));
53        self::assertSame($outer, $scope->constraint(new TerminalOccurrence('ID', 2, [0, 30])));
54        self::assertNull($scope->constraint(new TerminalOccurrence('STRING', 3, [0, 10])));
55    }
56
57    public function testLexicalPlanPreservesLegacySpellingsWhenARewriterRenamesTokens(): void
58    {
59        $sequence = new TerminalSequence([new TerminalOccurrence('RENAMED', 1, [0])], original: [new TerminalOccurrence('ID', 1, [0])]);
60        $scope = new ScopedGeneration($sequence, [0 => ['RENAMED' => LexemeConstraint::oneOf('users', 'orders')]]);
61        $plan = $scope->lexicalPlan($sequence, GenerationPlan::all()->withLexemes(['ID' => ['orders']]), static fn (int $count): int => 0);
62        self::assertSame('orders', $plan->lexemeAt('RENAMED', 0));
63    }
64
65
66    public function testLexicalPlanMatchesRenamedOccurrencesByIdentityAndHonorsFinalTokenOverrides(): void
67    {
68        $sequence = new TerminalSequence([
69            new TerminalOccurrence('RENAMED', 2, [0]),
70            new TerminalOccurrence('OVERRIDDEN', 1, [0]),
71        ], original: [new TerminalOccurrence('ID', 1, [0]), new TerminalOccurrence('ID', 2, [0])]);
72        $scope = new ScopedGeneration($sequence);
73        $plan = $scope->lexicalPlan($sequence, GenerationPlan::all()->withLexemes([
74            'ID' => ['first', 'second'], 'OVERRIDDEN' => ['final'],
75        ]), static fn (int $count): int => 0);
76        self::assertSame('second', $plan->lexemeAt('RENAMED', 0));
77        self::assertSame('final', $plan->lexemeAt('OVERRIDDEN', 0));
78    }
79
80}
81