Skip to content

Commit c1d8a25

Browse files
committed
DB queries containing JSON paths support array index references
e.g., DB::table('owner') ->where('packages[1]->name', 'laravel/framework') ->update(['packages[1]->versions[0]' => '9.0.0']); Stop compiling: UPDATE `owner` SET `packages` = JSON_SET(`packages`, $"[1]"."versions[0]", '9.0.0') WHERE json_unquote(json_extract(`packages[1]`, '$."name"')) = 'laravel/framework'; ... Instead avoid escaping array dereference characters: UPDATE `owner` SET `framework` = JSON_SET(`framework`, $[1]."versions"[0], '9.0.0') WHERE json_unquote(json_extract(`packages[1]`, '$."name"')) = 'laravel/framework';
1 parent 128614b commit c1d8a25

File tree

3 files changed

+72
-1
lines changed

3 files changed

+72
-1
lines changed

src/Illuminate/Database/Query/Grammars/Grammar.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Database\Query\Builder;
77
use Illuminate\Database\Query\JoinClause;
88
use Illuminate\Support\Arr;
9+
use Illuminate\Support\Str;
910
use RuntimeException;
1011

1112
class Grammar extends BaseGrammar
@@ -1223,7 +1224,34 @@ protected function wrapJsonPath($value, $delimiter = '->')
12231224
{
12241225
$value = preg_replace("/([\\\\]+)?\\'/", "''", $value);
12251226

1226-
return '\'$."'.str_replace($delimiter, '"."', $value).'"\'';
1227+
$jsonPath = collect(explode($delimiter, $value))
1228+
->map(function ($segment) {
1229+
return $this->wrapJsonPathSegment($segment);
1230+
})
1231+
->join('.');
1232+
1233+
return "'$".(str_starts_with($jsonPath, '[') ? '' : '.').$jsonPath."'";
1234+
}
1235+
1236+
/**
1237+
* Wrap the given JSON path segment.
1238+
*
1239+
* @param string $segment
1240+
* @return string
1241+
*/
1242+
protected function wrapJsonPathSegment($segment)
1243+
{
1244+
if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
1245+
$key = Str::beforeLast($segment, $parts[0]);
1246+
1247+
if (! empty($key)) {
1248+
return '"'.$key.'"'.$parts[0];
1249+
}
1250+
1251+
return $parts[0];
1252+
}
1253+
1254+
return '"'.$segment.'"';
12271255
}
12281256

12291257
/**

tests/Database/DatabaseQueryBuilderTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,6 +2883,29 @@ public function testMySqlUpdateWrappingJsonArray()
28832883
]);
28842884
}
28852885

2886+
public function testMySqlUpdateWrappingJsonPathArrayIndex()
2887+
{
2888+
$grammar = new MySqlGrammar;
2889+
$processor = m::mock(Processor::class);
2890+
2891+
$connection = $this->createMock(ConnectionInterface::class);
2892+
$connection->expects($this->once())
2893+
->method('update')
2894+
->with(
2895+
'update `users` set `options` = json_set(`options`, \'$[1]."2fa"\', false), `meta` = json_set(`meta`, \'$."tags"[0][2]\', ?) where `active` = ?',
2896+
[
2897+
'large',
2898+
1,
2899+
]
2900+
);
2901+
2902+
$builder = new Builder($connection, $grammar, $processor);
2903+
$builder->from('users')->where('active', 1)->update([
2904+
'options->[1]->2fa' => false,
2905+
'meta->tags[0][2]' => 'large',
2906+
]);
2907+
}
2908+
28862909
public function testMySqlUpdateWithJsonPreparesBindingsCorrectly()
28872910
{
28882911
$grammar = new MySqlGrammar;

tests/Integration/Database/DatabaseMySqlConnectionTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ public function jsonWhereNullDataProvider()
106106
'nested key exists and null' => [true, 'nested->value', ['nested' => ['value' => null]]],
107107
'nested key exists and "null"' => [false, 'nested->value', ['nested' => ['value' => 'null']]],
108108
'nested key exists and not null' => [false, 'nested->value', ['nested' => ['value' => false]]],
109+
'array index not exists' => [false, '[0]', [1 => 'invalid']],
110+
'array index exists and null' => [true, '[0]', [null]],
111+
'array index exists and "null"' => [false, '[0]', ['null']],
112+
'array index exists and not null' => [false, '[0]', [false]],
113+
'nested array index not exists' => [false, 'nested[0]', ['nested' => [1 => 'nested->invalid']]],
114+
'nested array index exists and null' => [true, 'nested->value[1]', ['nested' => ['value' => [0, null]]]],
115+
'nested array index exists and "null"' => [false, 'nested->value[1]', ['nested' => ['value' => [0, 'null']]]],
116+
'nested array index exists and not null' => [false, 'nested->value[1]', ['nested' => ['value' => [0, false]]]],
109117
];
110118
}
119+
120+
public function testJsonPathUpdate()
121+
{
122+
DB::table(self::TABLE)->insert([
123+
[self::JSON_COL => '{"foo":["bar"]}'],
124+
[self::JSON_COL => '{"foo":["baz"]}'],
125+
]);
126+
$updatedCount = DB::table(self::TABLE)->where(self::JSON_COL.'->foo[0]', 'baz')->update([
127+
self::JSON_COL.'->foo[0]' => 'updated',
128+
]);
129+
$this->assertSame(1, $updatedCount);
130+
}
111131
}

0 commit comments

Comments
 (0)