Skip to content

[8.x] Add JSON grammar support to MySQL and SQLite schema usage #36363

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

Closed
wants to merge 2 commits into from
Closed
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
33 changes: 33 additions & 0 deletions src/Illuminate/Database/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Illuminate\Database;

use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use RuntimeException;

abstract class Grammar
{
Expand Down Expand Up @@ -62,6 +64,13 @@ public function wrap($value, $prefixAlias = false)
return $this->wrapAliasedValue($value, $prefixAlias);
}

// If the given value is a JSON selector we will wrap it differently than a
// traditional value. We will need to split this path and wrap each part
// wrapped, etc. Otherwise, we will simply wrap the value as a string.
if ($this->isJsonSelector($value)) {
return $this->wrapJsonSelector($value);
}

return $this->wrapSegments(explode('.', $value));
}

Expand Down Expand Up @@ -116,6 +125,30 @@ protected function wrapValue($value)
return $value;
}

/**
* Wrap the given JSON selector.
*
* @param string $value
* @return string
*
* @throws \RuntimeException
*/
protected function wrapJsonSelector($value)
{
throw new RuntimeException('This database engine does not support JSON operations.');
}

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

/**
* Convert an array of column names into a delimited string.
*
Expand Down
55 changes: 0 additions & 55 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use RuntimeException;

class Grammar extends BaseGrammar
Expand Down Expand Up @@ -1144,49 +1143,6 @@ public function compileSavepointRollBack($name)
return 'ROLLBACK TO SAVEPOINT '.$name;
}

/**
* Wrap a value in keyword identifiers.
*
* @param \Illuminate\Database\Query\Expression|string $value
* @param bool $prefixAlias
* @return string
*/
public function wrap($value, $prefixAlias = false)
{
if ($this->isExpression($value)) {
return $this->getValue($value);
}

// If the value being wrapped has a column alias we will need to separate out
// the pieces so we can wrap each of the segments of the expression on its
// own, and then join these both back together using the "as" connector.
if (stripos($value, ' as ') !== false) {
return $this->wrapAliasedValue($value, $prefixAlias);
}

// If the given value is a JSON selector we will wrap it differently than a
// traditional value. We will need to split this path and wrap each part
// wrapped, etc. Otherwise, we will simply wrap the value as a string.
if ($this->isJsonSelector($value)) {
return $this->wrapJsonSelector($value);
}

return $this->wrapSegments(explode('.', $value));
}

/**
* Wrap the given JSON selector.
*
* @param string $value
* @return string
*
* @throws \RuntimeException
*/
protected function wrapJsonSelector($value)
{
throw new RuntimeException('This database engine does not support JSON operations.');
}

/**
* Wrap the given JSON selector for boolean values.
*
Expand Down Expand Up @@ -1240,17 +1196,6 @@ protected function wrapJsonPath($value, $delimiter = '->')
return '\'$."'.str_replace($delimiter, '"."', $value).'"\'';
}

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

/**
* Concatenate an array of segments, removing empties.
*
Expand Down
31 changes: 31 additions & 0 deletions src/Illuminate/Database/Schema/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,37 @@ public function wrapTable($table)
);
}

/**
* Split the given JSON selector into the field and the optional path and wrap them separately.
*
* @param string $column
* @return array
*/
protected function wrapJsonFieldAndPath($column)
{
$parts = explode('->', $column, 2);

$field = $this->wrap($parts[0]);

$path = count($parts) > 1 ? ', '.$this->wrapJsonPath($parts[1], '->') : '';

return [$field, $path];
}

/**
* Wrap the given JSON path.
*
* @param string $value
* @param string $delimiter
* @return string
*/
protected function wrapJsonPath($value, $delimiter = '->')
{
$value = preg_replace("/([\\\\]+)?\\'/", "''", $value);

return '\'$."'.str_replace($delimiter, '"."', $value).'"\'';
}

/**
* Wrap a value in keyword identifiers.
*
Expand Down
29 changes: 25 additions & 4 deletions src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -920,8 +920,12 @@ protected function typeComputed(Fluent $column)
*/
protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->virtualAs)) {
return " as ({$column->virtualAs})";
if (! is_null($virtualAs = $column->virtualAs)) {
if ($this->isJsonSelector($virtualAs)) {
$virtualAs = $this->wrapJsonSelector($virtualAs);
}

return " as ({$virtualAs})";
}
}

Expand All @@ -934,8 +938,12 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column)
*/
protected function modifyStoredAs(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->storedAs)) {
return " as ({$column->storedAs}) stored";
if (! is_null($storedAs = $column->storedAs)) {
if ($this->isJsonSelector($storedAs)) {
$storedAs = $this->wrapJsonSelector($storedAs);
}

return " as ({$storedAs}) stored";
}
}

Expand Down Expand Up @@ -1097,4 +1105,17 @@ protected function wrapValue($value)

return $value;
}

/**
* Wrap the given JSON selector.
*
* @param string $value
* @return string
*/
protected function wrapJsonSelector($value)
{
[$field, $path] = $this->wrapJsonFieldAndPath($value);

return 'json_unquote(json_extract('.$field.$path.'))';
}
}
29 changes: 25 additions & 4 deletions src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -846,8 +846,12 @@ protected function typeComputed(Fluent $column)
*/
protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->virtualAs)) {
return " as ({$column->virtualAs})";
if (! is_null($virtualAs = $column->virtualAs)) {
if ($this->isJsonSelector($virtualAs)) {
$virtualAs = $this->wrapJsonSelector($virtualAs);
}

return " as ({$virtualAs})";
}
}

Expand All @@ -860,8 +864,12 @@ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column)
*/
protected function modifyStoredAs(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->storedAs)) {
return " as ({$column->storedAs}) stored";
if (! is_null($storedAs = $column->storedAs)) {
if ($this->isJsonSelector($storedAs)) {
$storedAs = $this->wrapJsonSelector($storedAs);
}

return " as ({$storedAs}) stored";
}
}

Expand Down Expand Up @@ -910,4 +918,17 @@ protected function modifyIncrement(Blueprint $blueprint, Fluent $column)
return ' primary key autoincrement';
}
}

/**
* Wrap the given JSON selector.
*
* @param string $value
* @return string
*/
protected function wrapJsonSelector($value)
{
[$field, $path] = $this->wrapJsonFieldAndPath($value);

return 'json_extract('.$field.$path.')';
}
}
88 changes: 88 additions & 0 deletions tests/Database/DatabaseMySqlSchemaGrammarTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,94 @@ public function testCreateDatabase()
);
}

public function testCreateTableWithVirtualAsColumn()
{
$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8');
$conn->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8_unicode_ci');
$conn->shouldReceive('getConfig')->once()->with('engine')->andReturn(null);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_column');
$blueprint->string('my_other_column')->virtualAs('my_column');

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_column` varchar(255) not null, `my_other_column` varchar(255) as (my_column)) default character set utf8 collate 'utf8_unicode_ci'", $statements[0]);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_json_column');
$blueprint->string('my_other_column')->virtualAs('my_json_column->some_attribute');

$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->andReturn(null);

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_json_column` varchar(255) not null, `my_other_column` varchar(255) as (json_unquote(json_extract(`my_json_column`, '$.\"some_attribute\"'))))", $statements[0]);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_json_column');
$blueprint->string('my_other_column')->virtualAs('my_json_column->some_attribute->nested');

$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->andReturn(null);

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_json_column` varchar(255) not null, `my_other_column` varchar(255) as (json_unquote(json_extract(`my_json_column`, '$.\"some_attribute\".\"nested\"'))))", $statements[0]);
}

public function testCreateTableWithStoredAsColumn()
{
$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8');
$conn->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8_unicode_ci');
$conn->shouldReceive('getConfig')->once()->with('engine')->andReturn(null);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_column');
$blueprint->string('my_other_column')->storedAs('my_column');

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_column` varchar(255) not null, `my_other_column` varchar(255) as (my_column) stored) default character set utf8 collate 'utf8_unicode_ci'", $statements[0]);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_json_column');
$blueprint->string('my_other_column')->storedAs('my_json_column->some_attribute');

$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->andReturn(null);

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_json_column` varchar(255) not null, `my_other_column` varchar(255) as (json_unquote(json_extract(`my_json_column`, '$.\"some_attribute\"'))) stored)", $statements[0]);

$blueprint = new Blueprint('users');
$blueprint->create();
$blueprint->string('my_json_column');
$blueprint->string('my_other_column')->storedAs('my_json_column->some_attribute->nested');

$conn = $this->getConnection();
$conn->shouldReceive('getConfig')->andReturn(null);

$statements = $blueprint->toSql($conn, $this->getGrammar());

$this->assertCount(1, $statements);
$this->assertSame("create table `users` (`my_json_column` varchar(255) not null, `my_other_column` varchar(255) as (json_unquote(json_extract(`my_json_column`, '$.\"some_attribute\".\"nested\"'))) stored)", $statements[0]);
}

public function testDropDatabaseIfExists()
{
$statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a');
Expand Down
Loading