Skip to content

Commit d8b1329

Browse files
authored
feat: Port whereJsonContainsKey methods + CompilesJsonPaths from Laravel (#7699)
1 parent 3a55984 commit d8b1329

File tree

5 files changed

+119
-94
lines changed

5 files changed

+119
-94
lines changed

src/Query/Grammars/SQLiteGrammar.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,16 @@ protected function compileJsonLength($column, $operator, $value): string
211211
return 'json_array_length(' . $field . $path . ') ' . $operator . ' ' . $value;
212212
}
213213

214+
/**
215+
* Compile a "JSON contains key" statement into SQL.
216+
*/
217+
protected function compileJsonContainsKey(string $column): string
218+
{
219+
[$field, $path] = $this->wrapJsonFieldAndPath($column);
220+
221+
return 'json_type(' . $field . $path . ') is not null';
222+
}
223+
214224
/**
215225
* Compile the columns for an update statement.
216226
*/

src/Schema/Grammars/SQLiteGrammar.php

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use Hyperf\Database\Connection;
1717
use Hyperf\Database\Schema\Blueprint;
1818
use Hyperf\Database\Schema\Grammars\Grammar;
19-
use Hyperf\Stringable\Str;
2019
use Hyperf\Support\Fluent;
2120
use RuntimeException;
2221

@@ -925,52 +924,6 @@ protected function wrapJsonSelector(string $value): string
925924
return 'json_extract(' . $field . $path . ')';
926925
}
927926

928-
/**
929-
* Split the given JSON selector into the field and the optional path and wrap them separately.
930-
*/
931-
protected function wrapJsonFieldAndPath(string $column): array
932-
{
933-
$parts = explode('->', $column, 2);
934-
935-
$field = $this->wrap($parts[0]);
936-
937-
$path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : '';
938-
939-
return [$field, $path];
940-
}
941-
942-
/**
943-
* Wrap the given JSON path.
944-
*/
945-
protected function wrapJsonPath(string $value, string $delimiter = '->'): string
946-
{
947-
$value = preg_replace("/([\\\\]+)?\\'/", "''", $value);
948-
949-
$jsonPath = collect(explode($delimiter, $value))
950-
->map(fn ($segment) => $this->wrapJsonPathSegment($segment))
951-
->implode('.');
952-
953-
return "'$" . (str_starts_with($jsonPath, '[') ? '' : '.') . $jsonPath . "'";
954-
}
955-
956-
/**
957-
* Wrap the given JSON path segment.
958-
*/
959-
protected function wrapJsonPathSegment(string $segment): string
960-
{
961-
if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
962-
$key = Str::beforeLast($segment, $parts[0]);
963-
964-
if (! empty($key)) {
965-
return '"' . $key . '"' . $parts[0];
966-
}
967-
968-
return $parts[0];
969-
}
970-
971-
return '"' . $segment . '"';
972-
}
973-
974927
/**
975928
* Get the SQL for a nullable column modifier.
976929
*/
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
13+
namespace HyperfTest\Database\SQLite;
14+
15+
use Hyperf\Database\Connection;
16+
use Hyperf\Database\ConnectionInterface;
17+
use Hyperf\Database\Query\Builder;
18+
use Hyperf\Database\Query\Processors\Processor;
19+
use Hyperf\Database\SQLite\Query\Grammars\SQLiteGrammar;
20+
use Mockery as m;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* @internal
25+
* @coversNothing
26+
*/
27+
class DatabaseSQLiteQueryBuilderTest extends TestCase
28+
{
29+
protected function tearDown(): void
30+
{
31+
m::close();
32+
}
33+
34+
public function testToRawSql()
35+
{
36+
$connection = m::mock(Connection::class);
37+
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
38+
$grammar = new SQLiteGrammar();
39+
40+
$bindings = array_map(fn ($value) => $connection->escape($value, false), ['foo']);
41+
42+
$query = $grammar->substituteBindingsIntoRawSql(
43+
'select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = ?',
44+
$bindings,
45+
);
46+
47+
$this->assertSame('select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
48+
}
49+
50+
public function testWhereJsonContainsKeySqlite()
51+
{
52+
$builder = $this->getSQLiteBuilder();
53+
$builder->select('*')->from('users')->whereJsonContainsKey('users.options->languages');
54+
$this->assertSame('select * from "users" where json_type("users"."options", \'$."languages"\') is not null', $builder->toSql());
55+
56+
$builder = $this->getSQLiteBuilder();
57+
$builder->select('*')->from('users')->whereJsonContainsKey('options->language->primary');
58+
$this->assertSame('select * from "users" where json_type("options", \'$."language"."primary"\') is not null', $builder->toSql());
59+
60+
$builder = $this->getSQLiteBuilder();
61+
$builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonContainsKey('options->languages');
62+
$this->assertSame('select * from "users" where "id" = ? or json_type("options", \'$."languages"\') is not null', $builder->toSql());
63+
64+
$builder = $this->getSQLiteBuilder();
65+
$builder->select('*')->from('users')->whereJsonContainsKey('options->languages[0][1]');
66+
$this->assertSame('select * from "users" where json_type("options", \'$."languages"[0][1]\') is not null', $builder->toSql());
67+
}
68+
69+
public function testWhereJsonDoesntContainKeySqlite()
70+
{
71+
$builder = $this->getSQLiteBuilder();
72+
$builder->select('*')->from('users')->whereJsonDoesntContainKey('options->languages');
73+
$this->assertSame('select * from "users" where not json_type("options", \'$."languages"\') is not null', $builder->toSql());
74+
75+
$builder = $this->getSQLiteBuilder();
76+
$builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonDoesntContainKey('options->languages');
77+
$this->assertSame('select * from "users" where "id" = ? or not json_type("options", \'$."languages"\') is not null', $builder->toSql());
78+
79+
$builder = $this->getSQLiteBuilder();
80+
$builder->select('*')->from('users')->whereJsonDoesntContainKey('options->languages[0][1]');
81+
$this->assertSame('select * from "users" where not json_type("options", \'$."languages"[0][1]\') is not null', $builder->toSql());
82+
}
83+
84+
public function testSQLiteUpdateWrappingJsonPathArrayIndex()
85+
{
86+
$connection = m::mock(ConnectionInterface::class);
87+
$connection->shouldReceive('update')
88+
->once()
89+
->with('update "users" set "options" = json_patch(ifnull("options", json(\'{}\')), json(?)), "meta" = json_patch(ifnull("meta", json(\'{}\')), json(?)) where json_extract("options", \'$[1]."2fa"\') = true', [
90+
'{"[1]":{"2fa":false}}',
91+
'{"tags[0][2]":"large"}',
92+
])
93+
->andReturn(1);
94+
95+
$builder = new Builder($connection, new SQLiteGrammar(), new Processor());
96+
$result = $builder->from('users')->where('options->[1]->2fa', true)->update([
97+
'options->[1]->2fa' => false,
98+
'meta->tags[0][2]' => 'large',
99+
]);
100+
101+
$this->assertEquals(1, $result);
102+
}
103+
104+
protected function getSQLiteBuilder(): Builder
105+
{
106+
return new Builder(m::mock(ConnectionInterface::class), new SQLiteGrammar(), new Processor());
107+
}
108+
}

tests/DatabaseSQLiteQueryGrammarTest.php

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* @internal
2525
* @coversNothing
2626
*/
27-
class DatabaseSQLiteBuilderTest extends TestCase
27+
class DatabaseSQLiteSchemaBuilderTest extends TestCase
2828
{
2929
public function testCreateDatabase()
3030
{

0 commit comments

Comments
 (0)