Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.2] Added correct MySQL JSON bool handling and updating grammar #13242

Merged
merged 2 commits into from
May 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 109 additions & 2 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Str;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JsonExpression;

class MySqlGrammar extends Grammar
{
Expand Down Expand Up @@ -81,7 +82,38 @@ protected function compileLock(Builder $query, $value)
*/
public function compileUpdate(Builder $query, $values)
{
$sql = parent::compileUpdate($query, $values);
$table = $this->wrapTable($query->from);

// Each one of the columns in the update statements needs to be wrapped in the
// keyword identifiers, also a place-holder needs to be created for each of
// the values in the list of bindings so we can make the sets statements.
$columns = [];

foreach ($values as $key => $value) {
if ($this->isJsonSelector($key)) {
$columns[] = $this->prepareJsonUpdateColumn($key, new JsonExpression($value));
} else {
$columns[] = $this->wrap($key).' = '.$this->parameter($value);
}
}

$columns = implode(', ', $columns);

// If the query has any "join" clauses, we will setup the joins on the builder
// and compile them so we can attach them to this update, as update queries
// can get join statements to attach to other tables when they're needed.
if (isset($query->joins)) {
$joins = ' '.$this->compileJoins($query, $query->joins);
} else {
$joins = '';
}

// Of course, update queries may also be constrained by where clauses so we'll
// need to compile the where clauses and attach it to the query so only the
// intended records are updated by the SQL statements we generate to run.
$where = $this->compileWheres($query);

$sql = rtrim("update {$table}{$joins} set $columns $where");

if (isset($query->orders)) {
$sql .= ' '.$this->compileOrders($query, $query->orders);
Expand All @@ -94,6 +126,26 @@ public function compileUpdate(Builder $query, $values)
return rtrim($sql);
}

/**
* Prepares the update column for JSON selectors using the JSON_SET MySQL function.
*
* @param string $key
* @param JsonExpression $value
* @return string
*/
protected function prepareJsonUpdateColumn($key, JsonExpression $value)
{
$path = explode('->', $key);

$field = $this->wrapValue(array_shift($path));

$accessor = '"$.'.implode('.', $path).'"';

$sanitizedValue = $value->getValue();

return "{$field} = json_set({$field}, {$accessor}, {$sanitizedValue})";
}

/**
* Compile a delete statement into SQL.
*
Expand Down Expand Up @@ -125,6 +177,61 @@ public function compileDelete(Builder $query)
return $sql;
}

/**
* Check for a JSON selector.
*
* @param string $value
* @return bool
*/
protected function isJsonSelector($value)
{
return Str::contains($value, '->');
}

/**
* Compile a basic where clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $where
* @return string
*/
protected function whereBasic(Builder $query, $where)
{
// If we have a JSON selector here we'll simply
// convert it to a JsonExpression which then
// sets the value correctly on the query
if ($this->isJsonSelector($where['column']) && is_bool($where['value'])) {
$this->removeWhereBindingFromQuery($query, $where);

$where['value'] = new JsonExpression($where['value']);
}

$value = $this->parameter($where['value']);

return $this->wrap($where['column']).' '.$where['operator'].' '.$value;
}

/**
* Removes one where binding from the query.
*
* @param Builder $query
* @param array $where
* @return void
*/
protected function removeWhereBindingFromQuery(Builder $query, $where)
{
$wheres = $query->wheres;
$offset = array_search($where, $wheres);

if ($offset !== false) {
$whereBindings = $query->getRawBindings()['where'];

unset($whereBindings[$offset]);

$query->setBindings($whereBindings, 'where');
}
}

/**
* Wrap a single string in keyword identifiers.
*
Expand All @@ -137,7 +244,7 @@ protected function wrapValue($value)
return $value;
}

if (Str::contains($value, '->')) {
if ($this->isJsonSelector($value)) {
return $this->wrapJsonSelector($value);
}

Expand Down
68 changes: 68 additions & 0 deletions src/Illuminate/Database/Query/JsonExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Illuminate\Database\Query;

class JsonExpression extends Expression
{
/**
* The value of the expression.
*
* @var mixed
*/
protected $value;

/**
* Create a new raw query expression.
*
* @param mixed $value
* @return void
*/
public function __construct($value)
{
$this->value = $this->getJsonValue($value);
}

/**
* Get the value of a JSON using the correct type.
*
* @param mixed $value
* @return string
*/
protected function getJsonValue($value)
{
switch ($type = gettype($value)) {
case 'boolean':
return $value ? 'true' : 'false';
case 'integer':
case 'double':
return $value;
case 'string':
return '?';
case 'object':
case 'array':
return '?';
}

throw new \InvalidArgumentException('JSON value is of illegal type: '.$type);
}

/**
* Get the value of the expression.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}

/**
* Get the value of the expression.
*
* @return string
*/
public function __toString()
{
return (string) $this->getValue();
}
}
56 changes: 56 additions & 0 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,62 @@ public function testMySqlWrapping()
$this->assertEquals('select * from `users`', $builder->toSql());
}

public function testMySqlUpdateWrappingJson()
{
$grammar = new Illuminate\Database\Query\Grammars\MySqlGrammar;
$processor = m::mock('Illuminate\Database\Query\Processors\Processor');

// Couldn't get mockery to work
$connection = $this->getMock('Illuminate\Database\ConnectionInterface');
$connection->expects($this->once())
->method('update')
->with(
$this->equalTo('update `users` set `name` = json_set(`name`, "$.first_name", ?), `name` = json_set(`name`, "$.last_name", ?) where `active` = ?'),
$this->equalTo(['John', 'Doe', 1])
);

$builder = new Builder($connection, $grammar, $processor);

$result = $builder->from('users')->where('active', '=', 1)->update(['name->first_name' => 'John', 'name->last_name' => 'Doe']);
}

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->assertCount(1, $builder->getRawBindings()['where']);
$this->assertEquals('foo-bar', $builder->getRawBindings()['where'][0]);
}

public function testMySqlWrappingJsonWithInteger()
{
$builder = $this->getMySqlBuilder();
$builder->select('*')->from('users')->where('items->price', '=', 1);
$this->assertEquals('select * from `users` where `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());
}

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());
}

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());
}

public function testMySqlWrappingJson()
{
$builder = $this->getMySqlBuilder();
Expand Down