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