packages/sql-faker/tests/Unit/MySql/Generation/Rewrite/IntoClauseRuleTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\MySql\Generation\Rewrite;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Token\ProductionOccurrence;
12use SqlFaker\Generation\Token\TerminalOccurrence;
13use SqlFaker\Generation\Token\TerminalSequence;
14use SqlFaker\MySql\Generation\Rewrite\IntoClauseRule;
15
16#[CoversClass(IntoClauseRule::class)]
17#[UsesClass(ProductionOccurrence::class)]
18#[UsesClass(TerminalOccurrence::class)]
19#[UsesClass(TerminalSequence::class)]
20final class IntoClauseRuleTest extends TestCase
21{
22    public function testRewriteRemovesViewDestinationsAndKeepsIndependentSelectOutput(): void
23    {
24        $view = new TerminalOccurrence('INTO', 10, [0, 1, 2], ['view_query_block', 'query_specification', 'into_clause']);
25        $outfile = new TerminalOccurrence('OUTFILE', 11, [0, 1, 2], ['view_query_block', 'query_specification', 'into_clause']);
26        $path = new TerminalOccurrence('TEXT_STRING', 12, [0, 1, 2], ['view_query_block', 'query_specification', 'into_clause']);
27        $outer = new TerminalOccurrence('INTO', 13, [3, 4], ['select_stmt', 'into_clause']);
28        $terminals = [$view, $outfile, $path, $outer];
29        $input = new TerminalSequence($terminals, $terminals, [], [
30            new ProductionOccurrence(0, null, 'view_query_block', 0),
31            new ProductionOccurrence(1, 0, 'query_specification', 0),
32            new ProductionOccurrence(2, 1, 'into_clause', 0),
33            new ProductionOccurrence(3, null, 'select_stmt', 0),
34            new ProductionOccurrence(4, 3, 'into_clause', 0),
35        ]);
36        $rule = new IntoClauseRule();
37        $result = $rule->rewrite($input);
38        self::assertSame([$outer], $result->terminals);
39        self::assertSame($input->original, $result->original);
40        self::assertSame($input->productions, $result->productions);
41        self::assertSame(['sql/sql_yacc.yy:view_query_block:into'], $result->rewrites);
42        self::assertSame($result, $rule->rewrite($result));
43    }
44
45    public function testRewriteRemovesOnlySubqueryDestinations(): void
46    {
47        $inner = new TerminalOccurrence('INTO', 4, [0, 1, 2], ['stmt', 'subquery', 'into_clause']);
48        $outer = new TerminalOccurrence('INTO', 5, [0, 3], ['stmt', 'into_clause']);
49        $input = new TerminalSequence([$inner, $outer], [$inner, $outer], [], [new ProductionOccurrence(2, 1, 'into_clause', 0), new ProductionOccurrence(3, 0, 'into_clause', 0), new ProductionOccurrence(6, 0, 'into_clause', 0)]);
50        $rule = new IntoClauseRule();
51        $result = $rule->rewrite($input);
52        self::assertSame([$outer], $result->terminals);
53        self::assertSame($input->original, $result->original);
54        self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
55        self::assertSame($input->productions, $result->productions);
56        self::assertSame(['sql/parse_tree_nodes.cc:PT_subquery:into'], $result->rewrites);
57        self::assertSame($result, $rule->rewrite($result));
58    }
59
60    #[DataProvider('providerSetOperations')]
61    public function testRewritePreservesTheFinalDestinationAcrossSetOperations(string $operator): void
62    {
63        $first = new TerminalOccurrence('INTO', 10, [0, 1], ['query_expression_body', 'into_clause']);
64        $union = new TerminalOccurrence($operator, 11, [0], ['query_expression_body']);
65        $last = new TerminalOccurrence('INTO', 12, [0, 2], ['query_expression_body', 'into_clause']);
66        $nestedUnion = new TerminalOccurrence('UNION_SYM', 13, [0, 3], ['query_expression_body', 'query_expression_body']);
67        $input = new TerminalSequence([$first, $union, $last, $nestedUnion], [$first, $union, $last, $nestedUnion], [], [new ProductionOccurrence(1, 0, 'into_clause', 0), new ProductionOccurrence(2, 0, 'into_clause', 0)]);
68        $rule = new IntoClauseRule();
69        $result = $rule->rewrite($input);
70        self::assertSame([$union, $last, $nestedUnion], $result->terminals);
71        self::assertSame($input->original, $result->original);
72        self::assertCount(count($result->terminals), array_unique(array_map(static fn ($terminal): int => $terminal->id, $result->terminals)));
73        self::assertSame($input->productions, $result->productions);
74        self::assertSame(['sql/sql_lex.cc:new_set_operation_query:into'], $result->rewrites);
75        self::assertSame($result, $rule->rewrite($result));
76    }
77
78    public function testRewriteKeepsOneDestinationPerSelectStatement(): void
79    {
80        $first = new TerminalOccurrence('INTO', 10, [0, 1, 2], ['select_stmt', 'query_specification', 'into_clause']);
81        $last = new TerminalOccurrence('INTO', 11, [0, 3], ['select_stmt', 'into_clause']);
82        $other = new TerminalOccurrence('INTO', 12, [4, 5], ['select_stmt', 'into_clause']);
83        $nested = new TerminalOccurrence('INTO', 13, [0, 6, 7], ['select_stmt', 'subquery', 'into_clause']);
84        $input = new TerminalSequence([$first, $nested, $last, $other], [$first, $nested, $last, $other], [], [new ProductionOccurrence(2, 1, 'into_clause', 0), new ProductionOccurrence(7, 6, 'into_clause', 0), new ProductionOccurrence(3, 0, 'into_clause', 0), new ProductionOccurrence(5, 4, 'into_clause', 0)]);
85        $rule = new IntoClauseRule();
86        $result = $rule->rewrite($input);
87        self::assertSame([$last, $other], $result->terminals);
88        self::assertSame($input->original, $result->original);
89        self::assertSame($input->productions, $result->productions);
90        self::assertContains('sql/parse_tree_nodes.cc:PT_select_stmt:multiple-into', $result->rewrites);
91        self::assertSame($result, $rule->rewrite($result));
92    }
93
94    public function testRewritePreservesDestinationsBeforeUnrelatedOperatorUses(): void
95    {
96        $into = new TerminalOccurrence('INTO', 10, [0, 1], ['query_expression_body', 'into_clause']);
97        $ordinary = new TerminalOccurrence('SELECT_SYM', 11, [0], ['query_expression_body']);
98        $unrelated = new TerminalOccurrence('UNION_SYM', 12, [0], ['other']);
99        $unowned = new TerminalOccurrence('UNION_SYM', 13);
100        $input = new TerminalSequence([$into, $ordinary, $unrelated, $unowned], [], [], [new ProductionOccurrence(1, 0, 'into_clause', 0)]);
101        self::assertSame($input, (new IntoClauseRule())->rewrite($input));
102    }
103
104    /**
105     * @return iterable<string, array{string}>
106     */
107    public static function providerSetOperations(): iterable
108    {
109        yield 'union' => ['UNION_SYM'];
110        yield 'except' => ['EXCEPT_SYM'];
111        yield 'intersect' => ['INTERSECT_SYM'];
112    }
113}
114