packages/sql-faker/tests/Unit/Sqlite/Generation/Rewrite/UpsertSourceRuleTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Sqlite\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\Derivation\DerivationTrace;
12use SqlFaker\Generation\Token\ProductionOccurrence;
13use SqlFaker\Generation\Token\TerminalOccurrence;
14use SqlFaker\Generation\Token\TerminalSequence;
15use SqlFaker\Grammar\Model\NonTerminal;
16use SqlFaker\Grammar\Model\Production;
17use SqlFaker\Grammar\Model\Terminal;
18use SqlFaker\Sqlite\Generation\Rewrite\UpsertSourceRule;
19
20#[CoversClass(UpsertSourceRule::class)]
21#[UsesClass(DerivationTrace::class)]
22#[UsesClass(ProductionOccurrence::class)]
23#[UsesClass(TerminalOccurrence::class)]
24#[UsesClass(TerminalSequence::class)]
25#[UsesClass(NonTerminal::class)]
26#[UsesClass(Production::class)]
27#[UsesClass(Terminal::class)]
28final class UpsertSourceRuleTest extends TestCase
29{
30    /**
31     * @param list<string> $expected
32     */
33    #[DataProvider('providerSources')]
34    public function testRewriteSeparatesOnlyAmbiguousInsertSources(TerminalSequence $input, array $expected): void
35    {
36        $rule = new UpsertSourceRule();
37        $result = $rule->rewrite($input);
38        self::assertSame($expected, $result->names());
39        self::assertSame($input->original, $result->original);
40        self::assertSame($input->productions, $result->productions);
41        self::assertSame($input->terminals[0], $result->terminals[0]);
42        self::assertSame($result, $rule->rewrite($result));
43        $ids = array_column($result->terminals, 'id');
44        self::assertSame($ids, array_values(array_unique($ids)));
45    }
46
47    /**
48     * @return iterable<array{TerminalSequence, list<string>}>
49     */
50    public static function providerSources(): iterable
51    {
52        foreach (['cmd', 'ordinary'] as $command) {
53            foreach (['ON', 'RETURNING', null] as $upsert) {
54                foreach ([true, false] as $from) {
55                    foreach (['none', 'where_opt', 'groupby_opt', 'having_opt', 'window_clause', 'orderby_opt', 'limit_opt'] as $tail) {
56                        $trace = new DerivationTrace($command);
57                        $trace->expand(0, new Production([new Terminal('INSERT'), new NonTerminal('select'), new NonTerminal('upsert')]), 0);
58                        $trace->expand(1, new Production([new NonTerminal('oneselect')]), 0);
59                        $trace->expand(1, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt'), new NonTerminal('tail')]), 0);
60                        $trace->expand(3, new Production($from ? [new Terminal('FROM'), new Terminal('TABLE')] : []), 0);
61                        $offset = $from ? 5 : 3;
62                        $trace->expand($offset, new Production($tail === 'where_opt' ? [new Terminal('WHERE'), new Terminal('EXPR')] : []), 0);
63                        $offset += $tail === 'where_opt' ? 2 : 0;
64                        $trace->expand($offset, new Production(in_array($tail, ['none', 'where_opt'], true) ? [] : [new Terminal($tail), new Terminal('EXPR')]), 0);
65                        $offset += in_array($tail, ['none', 'where_opt'], true) ? 0 : 2;
66                        $trace->expand($offset, new Production($upsert === null ? [] : [new Terminal($upsert), new Terminal('REST')]), 0);
67                        $input = $trace->terminals();
68                        $expected = $input->names();
69                        if ($command === 'cmd' && $upsert === 'ON' && $from && $tail === 'none') {
70                            array_splice($expected, 5, 0, ['WHERE', 'INTEGER']);
71                        }
72                        yield [$input, $expected];
73                    }
74                }
75            }
76        }
77    }
78
79    public function testRewritePreservesEmptyAndNonSelectCommands(): void
80    {
81        $empty = new TerminalSequence([], [], [], [new ProductionOccurrence(0, null, 'cmd', 0), new ProductionOccurrence(1, 0, 'select', 0), new ProductionOccurrence(2, 0, 'upsert', 0)]);
82        self::assertSame($empty, (new UpsertSourceRule())->rewrite($empty));
83        $trace = new DerivationTrace('cmd');
84        $trace->expand(0, new Production([new Terminal('DEFAULT'), new Terminal('VALUES'), new NonTerminal('upsert')]), 0);
85        $trace->expand(2, new Production([new Terminal('ON')]), 0);
86        $input = $trace->terminals();
87        self::assertSame($input, (new UpsertSourceRule())->rewrite($input));
88    }
89
90    public function testRewriteAddsTheClauseToTheOuterSourceAndPreservesItsNestedSelect(): void
91    {
92        $trace = new DerivationTrace('cmd');
93        $trace->expand(0, new Production([new NonTerminal('select'), new NonTerminal('upsert')]), 0);
94        $trace->expand(0, new Production([new NonTerminal('oneselect')]), 0);
95        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt')]), 0);
96        $trace->expand(2, new Production([new Terminal('FROM'), new Terminal('LP'), new NonTerminal('select'), new Terminal('RP')]), 0);
97        $trace->expand(4, new Production([new NonTerminal('oneselect')]), 0);
98        $trace->expand(4, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt')]), 0);
99        $trace->expand(6, new Production([new Terminal('FROM'), new Terminal('TABLE')]), 0);
100        $trace->expand(8, new Production([]), 0);
101        $trace->expand(9, new Production([]), 0);
102        $trace->expand(9, new Production([new Terminal('ON'), new Terminal('CONFLICT'), new Terminal('DO'), new Terminal('NOTHING')]), 0);
103        $input = $trace->terminals();
104        $rule = new UpsertSourceRule();
105        $result = $rule->rewrite($input);
106        self::assertSame(['SELECT', 'STAR', 'FROM', 'LP', 'SELECT', 'STAR', 'FROM', 'TABLE', 'RP', 'WHERE', 'INTEGER', 'ON', 'CONFLICT', 'DO', 'NOTHING'], $result->names());
107        self::assertSame(['src/parse.y:on_using:upsert-ambiguity'], $result->rewrites);
108        self::assertSame('src/parse.y:on_using:upsert-ambiguity', $result->terminals[9]->rewrite);
109        self::assertSame(['cmd', 'select', 'oneselect', 'where_opt'], $result->terminals[9]->rules);
110        self::assertSame(-1, $result->terminals[9]->id);
111        self::assertSame(-2, $result->terminals[10]->id);
112        self::assertSame($input->productions, $result->productions);
113        self::assertSame($input->original, $result->original);
114        self::assertSame($result, $rule->rewrite($result));
115    }
116    public function testRewriteOnlyCompletesTheLastCompoundOperand(): void
117    {
118        $trace = new DerivationTrace('cmd');
119        $trace->expand(0, new Production([new NonTerminal('select'), new NonTerminal('upsert')]), 0);
120        $trace->expand(0, new Production([new NonTerminal('selectnowith')]), 0);
121        $trace->expand(0, new Production([new NonTerminal('selectnowith'), new Terminal('UNION'), new NonTerminal('oneselect')]), 1);
122        $trace->expand(0, new Production([new NonTerminal('oneselect')]), 0);
123        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt')]), 0);
124        $trace->expand(2, new Production([new Terminal('FROM'), new Terminal('LEFT')]), 0);
125        $trace->expand(4, new Production([]), 0);
126        $trace->expand(5, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt')]), 0);
127        $trace->expand(7, new Production([new Terminal('FROM'), new Terminal('RIGHT')]), 0);
128        $trace->expand(9, new Production([]), 0);
129        $trace->expand(9, new Production([new Terminal('ON'), new Terminal('CONFLICT')]), 0);
130        $input = $trace->terminals();
131        $result = (new UpsertSourceRule())->rewrite($input);
132        self::assertSame(['SELECT', 'STAR', 'FROM', 'LEFT', 'UNION', 'SELECT', 'STAR', 'FROM', 'RIGHT', 'WHERE', 'INTEGER', 'ON', 'CONFLICT'], $result->names());
133        self::assertSame($input->productions, $result->productions);
134    }
135
136    public function testRewritePreservesAnEarlierFromWhenTheFinalOperandIsValues(): void
137    {
138        $trace = new DerivationTrace('cmd');
139        $trace->expand(0, new Production([new NonTerminal('select'), new NonTerminal('upsert')]), 0);
140        $trace->expand(0, new Production([new NonTerminal('selectnowith')]), 0);
141        $trace->expand(0, new Production([new NonTerminal('selectnowith'), new Terminal('UNION'), new NonTerminal('oneselect')]), 1);
142        $trace->expand(0, new Production([new NonTerminal('oneselect')]), 0);
143        $trace->expand(0, new Production([new Terminal('SELECT'), new Terminal('STAR'), new NonTerminal('from'), new NonTerminal('where_opt')]), 0);
144        $trace->expand(2, new Production([new Terminal('FROM'), new Terminal('LEFT')]), 0);
145        $trace->expand(4, new Production([]), 0);
146        $trace->expand(5, new Production([new Terminal('VALUES'), new Terminal('LP'), new Terminal('INTEGER'), new Terminal('RP')]), 0);
147        $trace->expand(9, new Production([new Terminal('ON'), new Terminal('CONFLICT')]), 0);
148        $input = $trace->terminals();
149        self::assertSame($input, (new UpsertSourceRule())->rewrite($input));
150    }
151}
152