From e4bd21f4c2bd82c191faadffcf418fda2f37921f Mon Sep 17 00:00:00 2001 From: Jonas Staudenmeir Date: Thu, 13 Sep 2018 16:27:02 +0200 Subject: [PATCH] [5.8] Support JSON queries on MariaDB (#25517) * Simplify SQLiteGrammar and SqlServerGrammar * Support JSON queries on MariaDB --- .../Database/Query/Grammars/Grammar.php | 12 ++++--- .../Database/Query/Grammars/MySqlGrammar.php | 20 ++++++------ .../Database/Query/Grammars/SQLiteGrammar.php | 6 +--- .../Query/Grammars/SqlServerGrammar.php | 6 ++-- tests/Database/DatabaseQueryBuilderTest.php | 32 +++++++++++-------- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 14d2af25e72c..043f9b79c6a7 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -966,15 +966,16 @@ protected function wrapJsonSelector($value) * Split the given JSON selector into the field and the optional path and wrap them separately. * * @param string $column + * @param string $delimiter * @return array */ - protected function wrapJsonFieldAndPath($column) + protected function wrapJsonFieldAndPath($column, $delimiter = '->') { - $parts = explode('->', $column, 2); + $parts = explode($delimiter, $column, 2); $field = $this->wrap($parts[0]); - $path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1]) : ''; + $path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1], $delimiter) : ''; return [$field, $path]; } @@ -983,11 +984,12 @@ protected function wrapJsonFieldAndPath($column) * Wrap the given JSON path. * * @param string $value + * @param string $delimiter * @return string */ - protected function wrapJsonPath($value) + protected function wrapJsonPath($value, $delimiter = '->') { - return '\'$."'.str_replace('->', '"."', $value).'"\''; + return '\'$."'.str_replace($delimiter, '"."', $value).'"\''; } /** diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 20c26d8624a3..20b1daac4a0a 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -61,7 +61,9 @@ public function compileSelect(Builder $query) */ protected function compileJsonContains($column, $value) { - return 'json_contains('.$this->wrap($column).', '.$value.')'; + list($field, $path) = $this->wrapJsonFieldAndPath($column); + + return 'json_contains('.$field.', '.$value.$path.')'; } /** @@ -317,16 +319,16 @@ protected function wrapValue($value) */ protected function wrapJsonSelector($value) { - $delimiter = Str::contains($value, '->>') - ? '->>' - : '->'; + $delimiter = Str::contains($value, '->>') ? '->>' : '->'; - $path = explode($delimiter, $value); + list($field, $path) = $this->wrapJsonFieldAndPath($value, $delimiter); - $field = $this->wrapSegments(explode('.', array_shift($path))); + $selector = 'json_extract('.$field.$path.')'; + + if ($delimiter === '->>') { + $selector = 'json_unquote('.$selector.')'; + } - return sprintf('%s'.$delimiter.'\'$.%s\'', $field, collect($path)->map(function ($part) { - return '"'.$part.'"'; - })->implode('.')); + return $selector; } } diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 1220009e1fc9..722f4ad78573 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -297,11 +297,7 @@ public function compileTruncate(Builder $query) */ protected function wrapJsonSelector($value) { - $parts = explode('->', $value, 2); - - $field = $this->wrap($parts[0]); - - $path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1]) : ''; + list($field, $path) = $this->wrapJsonFieldAndPath($value); $selector = 'json_extract('.$field.$path.')'; diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index aed124ece841..26392de1594f 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -476,11 +476,9 @@ protected function wrapValue($value) */ protected function wrapJsonSelector($value) { - $parts = explode('->', $value, 2); + list($field, $path) = $this->wrapJsonFieldAndPath($value); - $field = $this->wrapSegments(explode('.', array_shift($parts))); - - return 'json_value('.$field.', '.$this->wrapJsonPath($parts[0]).')'; + return 'json_value('.$field.$path.')'; } /** diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index d1ee2db3184e..92f89be80d23 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -2003,7 +2003,7 @@ public function testMySqlWrappingJsonWithString() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->sku', '=', 'foo-bar'); - $this->assertEquals('select * from `users` where `items`->\'$."sku"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."sku"\') = ?', $builder->toSql()); $this->assertCount(1, $builder->getRawBindings()['where']); $this->assertEquals('foo-bar', $builder->getRawBindings()['where'][0]); } @@ -2012,35 +2012,41 @@ public function testMySqlWrappingJsonWithInteger() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->price', '=', 1); - $this->assertEquals('select * from `users` where `items`->\'$."price"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."price"\') = ?', $builder->toSql()); } public function testMySqlWrappingJsonWithDouble() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->price', '=', 1.5); - $this->assertEquals('select * from `users` where `items`->\'$."price"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."price"\') = ?', $builder->toSql()); } public function testMySqlWrappingJsonWithBoolean() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->available', '=', true); - $this->assertEquals('select * from `users` where `items`->\'$."available"\' = true', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."available"\') = true', $builder->toSql()); } public function testMySqlWrappingJsonWithBooleanAndIntegerThatLooksLikeOne() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->available', '=', true)->where('items->active', '=', false)->where('items->number_available', '=', 0); - $this->assertEquals('select * from `users` where `items`->\'$."available"\' = true and `items`->\'$."active"\' = false and `items`->\'$."number_available"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."available"\') = true and json_extract(`items`, \'$."active"\') = false and json_extract(`items`, \'$."number_available"\') = ?', $builder->toSql()); } public function testMySqlWrappingJsonWithoutQuote() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->>sku', '=', 'foo-bar'); - $this->assertEquals('select * from `users` where `items`->>\'$."sku"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_unquote(json_extract(`items`, \'$."sku"\')) = ?', $builder->toSql()); + $this->assertCount(1, $builder->getRawBindings()['where']); + $this->assertEquals('foo-bar', $builder->getRawBindings()['where'][0]); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->where('items->>price->>in_usd', '=', 'foo-bar'); + $this->assertEquals('select * from `users` where json_unquote(json_extract(`items`, \'$."price"."in_usd"\')) = ?', $builder->toSql()); $this->assertCount(1, $builder->getRawBindings()['where']); $this->assertEquals('foo-bar', $builder->getRawBindings()['where'][0]); } @@ -2053,15 +2059,15 @@ public function testMySqlWrappingJson() $builder = $this->getMySqlBuilder(); $builder->select('items->price')->from('users')->where('users.items->price', '=', 1)->orderBy('items->price'); - $this->assertEquals('select `items`->\'$."price"\' from `users` where `users`.`items`->\'$."price"\' = ? order by `items`->\'$."price"\' asc', $builder->toSql()); + $this->assertEquals('select json_extract(`items`, \'$."price"\') from `users` where json_extract(`users`.`items`, \'$."price"\') = ? order by json_extract(`items`, \'$."price"\') asc', $builder->toSql()); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->price->in_usd', '=', 1); - $this->assertEquals('select * from `users` where `items`->\'$."price"."in_usd"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."price"."in_usd"\') = ?', $builder->toSql()); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('items->price->in_usd', '=', 1)->where('items->age', '=', 2); - $this->assertEquals('select * from `users` where `items`->\'$."price"."in_usd"\' = ? and `items`->\'$."age"\' = ?', $builder->toSql()); + $this->assertEquals('select * from `users` where json_extract(`items`, \'$."price"."in_usd"\') = ? and json_extract(`items`, \'$."age"\') = ?', $builder->toSql()); } public function testPostgresWrappingJson() @@ -2689,12 +2695,12 @@ public function testWhereJsonContainsMySql() $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->whereJsonContains('users.options->languages', ['en']); - $this->assertEquals('select * from `users` where json_contains(`users`.`options`->\'$."languages"\', ?)', $builder->toSql()); + $this->assertEquals('select * from `users` where json_contains(`users`.`options`, ?, \'$."languages"\')', $builder->toSql()); $this->assertEquals(['["en"]'], $builder->getBindings()); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonContains('options->languages', new Raw("'[\"en\"]'")); - $this->assertEquals('select * from `users` where `id` = ? or json_contains(`options`->\'$."languages"\', \'["en"]\')', $builder->toSql()); + $this->assertEquals('select * from `users` where `id` = ? or json_contains(`options`, \'["en"]\', \'$."languages"\')', $builder->toSql()); $this->assertEquals([1], $builder->getBindings()); } @@ -2747,12 +2753,12 @@ public function testWhereJsonDoesntContainMySql() { $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->whereJsonDoesntContain('options->languages', ['en']); - $this->assertEquals('select * from `users` where not json_contains(`options`->\'$."languages"\', ?)', $builder->toSql()); + $this->assertEquals('select * from `users` where not json_contains(`options`, ?, \'$."languages"\')', $builder->toSql()); $this->assertEquals(['["en"]'], $builder->getBindings()); $builder = $this->getMySqlBuilder(); $builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonDoesntContain('options->languages', new Raw("'[\"en\"]'")); - $this->assertEquals('select * from `users` where `id` = ? or not json_contains(`options`->\'$."languages"\', \'["en"]\')', $builder->toSql()); + $this->assertEquals('select * from `users` where `id` = ? or not json_contains(`options`, \'["en"]\', \'$."languages"\')', $builder->toSql()); $this->assertEquals([1], $builder->getBindings()); }