Skip to content

Commit 943a303

Browse files
Fixed incorrect generation of union queries (#46)
* Fixed incorrect generation of union queries * Changed to add SELECT * FROM before each union part. * Changed to wrap the query before adding a final order by/limit/offset for union queries. * This is needed because otherwise these clauses are applied only to the last union subquery. * Changed to don't run tests in the non-integration mode * Added more tests * Fixed case when offset is set without a limit clause
1 parent 9e31536 commit 943a303

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

src/Query/Grammar.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,104 @@ protected function wrapJsonFieldAndPath($column)
122122

123123
return [$field, $path];
124124
}
125+
126+
/**
127+
* Wrap a union subquery in parentheses.
128+
*
129+
* @param string $sql
130+
* @return string
131+
*/
132+
protected function wrapUnion($sql): string
133+
{
134+
return 'SELECT * FROM ('.$sql.')';
135+
}
136+
137+
/**
138+
* Compile the "union" queries attached to the main query.
139+
*
140+
* @param Builder $query
141+
* @return string
142+
*/
143+
protected function compileUnions(Builder $query): string
144+
{
145+
$sql = '';
146+
147+
foreach ($query->unions as $union) {
148+
$sql .= $this->compileUnion($union);
149+
}
150+
151+
return ltrim($sql);
152+
}
153+
154+
/**
155+
* Compile a select query into SQL.
156+
*
157+
* @param Builder $query
158+
* @return string
159+
*/
160+
public function compileSelect(Builder $query): string
161+
{
162+
$sql = parent::compileSelect($query);
163+
164+
if (! empty($query->unionOrders) || isset($query->unionLimit) || isset($query->unionOffset)) {
165+
$sql = "SELECT * FROM (".$sql.") ";
166+
167+
if (! empty($query->unionOrders)) {
168+
$sql .= ' '.$this->compileOrders($query, $query->unionOrders);
169+
}
170+
171+
if (isset($query->unionLimit)) {
172+
$sql .= ' '.$this->compileLimit($query, $query->unionLimit);
173+
}
174+
175+
if (isset($query->unionOffset)) {
176+
$sql .= ' '.$this->compileUnionOffset($query, $query->unionOffset);
177+
}
178+
}
179+
180+
return ltrim($sql);
181+
}
182+
183+
/**
184+
* Compile the "offset" portions of the query.
185+
*
186+
* @param Builder $query
187+
* @param $offset
188+
* @return string
189+
*/
190+
protected function compileOffset(Builder $query, $offset): string
191+
{
192+
return $this->compileOffsetWithLimit($offset, $query->limit);
193+
}
194+
195+
/**
196+
* Compile the "offset" portions of the final union query.
197+
*
198+
* @param Builder $query
199+
* @param $offset
200+
* @return string
201+
*/
202+
protected function compileUnionOffset(Builder $query, $offset): string
203+
{
204+
return $this->compileOffsetWithLimit($offset, $query->unionLimit);
205+
}
206+
207+
/**
208+
* Compile the "offset" portions of the query taking into account "limit" portion.
209+
*
210+
* @param $offset
211+
* @param $limit
212+
* @return string
213+
*/
214+
private function compileOffsetWithLimit($offset, $limit): string
215+
{
216+
// OFFSET is not valid without LIMIT
217+
// Add a huge LIMIT clause
218+
if (! isset($limit)) {
219+
// 9223372036854775807 - max 64-bit integer
220+
return ' LIMIT 9223372036854775807 OFFSET '.(int) $offset;
221+
}
222+
223+
return ' OFFSET '.(int) $offset;
224+
}
125225
}

tests/Hybrid/UnionTest.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace SingleStore\Laravel\Tests\Hybrid;
4+
5+
use Illuminate\Support\Facades\DB;
6+
use SingleStore\Laravel\Schema\Blueprint;
7+
use SingleStore\Laravel\Tests\BaseTest;
8+
9+
class UnionTest extends BaseTest
10+
{
11+
use HybridTestHelpers;
12+
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
if ($this->runHybridIntegrations()) {
18+
$this->createTable(function (Blueprint $table) {
19+
$table->id();
20+
});
21+
22+
DB::table('test')->insert([
23+
['id' => 1],
24+
['id' => 2],
25+
['id' => 3],
26+
['id' => 4],
27+
['id' => 100],
28+
]);
29+
}
30+
}
31+
32+
/** @test */
33+
function union() {
34+
if (! $this->runHybridIntegrations()) {
35+
return;
36+
}
37+
38+
$first = DB::table('test')->where('id', '<', 3);
39+
$second = DB::table('test')->where('id', '>', 5);
40+
$res = $first->union($second)->get();
41+
42+
$indexes = array_map(function ($value): int {
43+
return $value->id;
44+
}, $res->toArray());
45+
sort($indexes);
46+
47+
$this->assertEquals([1, 2, 100], $indexes);
48+
}
49+
50+
/** @test */
51+
function unionAll() {
52+
if (! $this->runHybridIntegrations()) {
53+
return;
54+
}
55+
56+
$first = DB::table('test')->where('id', '<', 4);
57+
$second = DB::table('test')->where('id', '>', 2);
58+
$res = $first->unionAll($second)->get();
59+
60+
$indexes = array_map(function ($value): int {
61+
return $value->id;
62+
}, $res->toArray());
63+
sort($indexes);
64+
65+
$this->assertEquals([1, 2, 3, 3, 4, 100], $indexes);
66+
}
67+
68+
/** @test */
69+
function unionWithOrderByLimitAndOffset() {
70+
if (! $this->runHybridIntegrations()) {
71+
return;
72+
}
73+
74+
$first = DB::table('test')->where('id', '<', 3);
75+
$second = DB::table('test')->where('id', '>', 5);
76+
$res = $first->union($second)->orderBy('id')->limit(1)->offset(1)->get();
77+
78+
$indexes = array_map(function ($value): int {
79+
return $value->id;
80+
}, $res->toArray());
81+
82+
$this->assertEquals([2], $indexes);
83+
}
84+
85+
/** @test */
86+
function unionWithOrderBy() {
87+
if (! $this->runHybridIntegrations()) {
88+
return;
89+
}
90+
91+
$first = DB::table('test')->where('id', '<', 3);
92+
$second = DB::table('test')->where('id', '>', 5);
93+
$res = $first->union($second)->orderBy('id')->get();
94+
95+
$indexes = array_map(function ($value): int {
96+
return $value->id;
97+
}, $res->toArray());
98+
99+
$this->assertEquals([1, 2, 100], $indexes);
100+
}
101+
102+
/** @test */
103+
function unionWithLimit() {
104+
if (! $this->runHybridIntegrations()) {
105+
return;
106+
}
107+
108+
$first = DB::table('test')->where('id', '<', 3);
109+
$second = DB::table('test')->where('id', '>', 5);
110+
$res = $first->union($second)->limit(2)->get();
111+
112+
$this->assertCount(2, $res);
113+
}
114+
115+
/** @test */
116+
function unionWithOffset() {
117+
if (! $this->runHybridIntegrations()) {
118+
return;
119+
}
120+
121+
$first = DB::table('test')->where('id', '<', 3);
122+
$second = DB::table('test')->where('id', '>', 5);
123+
$res = $first->union($second)->offset(1)->get();
124+
125+
$this->assertCount(2, $res);
126+
}
127+
128+
/** @test */
129+
function unionWithInnerOffset() {
130+
if (! $this->runHybridIntegrations()) {
131+
return;
132+
}
133+
134+
$first = DB::table('test')->where('id', '<', 3)->offset(1)->orderBy('id');
135+
$second = DB::table('test')->where('id', '>', 5);
136+
$res = $first->union($second)->get();
137+
138+
$indexes = array_map(function ($value): int {
139+
return $value->id;
140+
}, $res->toArray());
141+
sort($indexes);
142+
143+
$this->assertEquals([2, 100], $indexes);
144+
}
145+
}

0 commit comments

Comments
 (0)