Skip to content

Commit

Permalink
Added correct MySQL JSON bool handling and updating grammar
Browse files Browse the repository at this point in the history
  • Loading branch information
sebwas committed Apr 21, 2016
1 parent 934c915 commit 7ccd4a1
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 2 deletions.
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

0 comments on commit 7ccd4a1

Please sign in to comment.