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