Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add JsonPathExpression to support json path clauses #19463
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
base: 5.next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,8 @@ | |||||
| use Cake\Database\Driver; | ||||||
| use Cake\Database\DriverFeatureEnum; | ||||||
| use Cake\Database\Expression\DistinctComparisonExpression; | ||||||
| use Cake\Database\Expression\FunctionExpression; | ||||||
| use Cake\Database\Expression\QueryExpression; | ||||||
| use Cake\Database\Expression\StringAggExpression; | ||||||
| use Cake\Database\Query; | ||||||
| use Cake\Database\Query\SelectQuery; | ||||||
|
|
@@ -39,6 +41,7 @@ class Mysql extends Driver | |||||
| protected function _expressionTranslators(): array | ||||||
| { | ||||||
| return [ | ||||||
| FunctionExpression::class => '_transformFunctionExpression', | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| StringAggExpression::class => 'transformStringAggExpression', | ||||||
| DistinctComparisonExpression::class => 'transformDistinctComparisonExpression', | ||||||
| ]; | ||||||
|
|
@@ -355,6 +358,33 @@ public function version(): string | |||||
| return $this->_version; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Receives a FunctionExpression and changes it so that it conforms to this SQL dialect. | ||||||
| * | ||||||
| * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to transform. | ||||||
| * @return void | ||||||
| */ | ||||||
| protected function _transformFunctionExpression(FunctionExpression $expression): void | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless this is an overridden function we don't need the underscore prefix.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| { | ||||||
| if ($this->isMariadb()) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| switch ($expression->getName()) { | ||||||
| case 'JSON_EXISTS': | ||||||
| $expression | ||||||
| ->setName('JSON_CONTAINS_PATH') | ||||||
| ->iterateParts(function ($p, $key) { | ||||||
| if ($key === 1) { | ||||||
| return new QueryExpression(["'all'", $p], ['literal'], ',', parentheses: false); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have a separate class for a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be helpful. Hoping one of you makes the call.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @LordSimal @ADmad Do you have any opinions on this so I can work on final update?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no strong opinion about this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @othercorey Adding a new class as markstory suggested would be cleaner instead of adding more switches to QueryExpression. |
||||||
| } | ||||||
|
|
||||||
| return $p; | ||||||
| }); | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Get PDO ATTR_SSL_KEY id. | ||||||
| * | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| use Cake\Database\Driver; | ||
| use Cake\Database\DriverFeatureEnum; | ||
| use Cake\Database\Expression\FunctionExpression; | ||
| use Cake\Database\Expression\QueryExpression; | ||
| use Cake\Database\Expression\StringAggExpression; | ||
| use Cake\Database\Expression\TupleComparison; | ||
| use Cake\Database\Schema\SchemaDialect; | ||
|
|
@@ -325,6 +326,22 @@ protected function _transformFunctionExpression(FunctionExpression $expression): | |
| case 'JSON_VALUE': | ||
| $expression->setName('JSON_EXTRACT'); | ||
| break; | ||
| case 'JSON_EXISTS': | ||
| $expression | ||
| ->setName('JSON_TYPE') | ||
| ->iterateParts(function ($p, $key) { | ||
| if ($key === 1) { | ||
| return new QueryExpression( | ||
| [$p, ') IS NOT NULL AND (1'], | ||
| [null, 'literal'], | ||
| '', | ||
| parentheses: false, | ||
| ); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cake makes modifying function expressions into something else a nightmare. I'm not sure if this is worth it or not, but it's one way to support it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is worth the effort. SQLite is a fantastic database when it is a suitable choice. |
||
| } | ||
|
|
||
| return $p; | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| <?php | ||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) | ||
| * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) | ||
| * | ||
| * Licensed under The MIT License | ||
| * For full copyright and license information, please see the LICENSE.txt | ||
| * Redistributions of files must retain the above copyright notice. | ||
| * | ||
| * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) | ||
| * @link https://cakephp.org CakePHP(tm) Project | ||
| * @since 5.4.0 | ||
| * @license https://opensource.org/licenses/mit-license.php MIT License | ||
| */ | ||
| namespace Cake\Database\Expression; | ||
|
|
||
| use Cake\Database\ExpressionInterface; | ||
| use Cake\Database\TypedResultInterface; | ||
| use Cake\Database\TypedResultTrait; | ||
| use Cake\Database\ValueBinder; | ||
| use Closure; | ||
|
|
||
| /** | ||
| * Represents a JSON path expression with optional clauses for each json function. | ||
| */ | ||
| class JsonPathExpression implements ExpressionInterface, TypedResultInterface | ||
| { | ||
| use TypedResultTrait; | ||
|
|
||
| public const BEHAVIOR_NULL = 'NULL'; | ||
| public const BEHAVIOR_ERROR = 'ERROR'; | ||
| public const BEHAVIOR_DEFAULT = 'DEFAULT'; | ||
|
|
||
| /** | ||
| * @var string | ||
| */ | ||
| protected string $path; | ||
|
|
||
| /** | ||
| * @var array<string, array{value: mixed, type: string|null}> | ||
| */ | ||
| protected array $passing = []; | ||
|
|
||
| /** | ||
| * @var string|null | ||
| */ | ||
| protected ?string $returning = null; | ||
|
|
||
| /** | ||
| * @var array{behavior: string, value: mixed}|null | ||
| */ | ||
| protected ?array $onEmpty = null; | ||
|
|
||
| /** | ||
| * @var array{behavior: string, value: mixed}|null | ||
| */ | ||
| protected ?array $onError = null; | ||
|
|
||
| /** | ||
| * Constructor. | ||
| * | ||
| * @param string $path The json path | ||
| */ | ||
| public function __construct(string $path) | ||
| { | ||
| $this->path = $path; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the RETURNING clause. | ||
| * | ||
| * Not all database engines support all clauses. Check for support | ||
| * before using. | ||
| * | ||
| * @param string $type The sql data type to return | ||
| * @return $this | ||
| */ | ||
| public function returning(string $type) | ||
| { | ||
| $this->returning = $type; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the PASSING clause. | ||
| * | ||
| * Not all database engines support all clauses. Check for support | ||
| * before using. | ||
| * | ||
| * @param array<string, string|int|float|bool> $passing Mapping of variable name to value. | ||
| * @param array<string, string|int> $types Optional mapping of variable name to binding type. | ||
| * @return $this | ||
| */ | ||
| public function passing(array $passing, array $types = []) | ||
| { | ||
| foreach ($passing as $name => $value) { | ||
| $type = $types[$name] ?? null; | ||
| if ($type === null) { | ||
| $type = match (true) { | ||
| is_string($value) => 'string', | ||
| is_int($value) => 'integer', | ||
| is_float($value) => 'float', | ||
| is_bool($value) => 'boolean', | ||
| default => null, | ||
| }; | ||
| } | ||
|
|
||
| $this->passing[$name] = ['value' => $value, 'type' => $type]; | ||
|
Check failure on line 111 in src/Database/Expression/JsonPathExpression.php
|
||
| } | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the ON EMPTY clause. | ||
| * | ||
| * Not all database engines support all clauses. Check for support | ||
| * before using. | ||
| * | ||
| * @param self::BEHAVIOR_* $behavior The behavior on empty (NULL, ERROR, or DEFAULT). | ||
| * @param mixed $value The default value if behavior is DEFAULT. | ||
| * @return $this | ||
| */ | ||
| public function onEmpty(string $behavior, mixed $value = null) | ||
| { | ||
| $this->onEmpty = ['behavior' => $behavior, 'value' => $value]; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the ON ERROR clause. | ||
| * | ||
| * Not all database engines support all clauses. Check for support | ||
| * before using. | ||
| * | ||
| * @param self::BEHAVIOR_* $behavior The behavior on error (NULL, ERROR, or DEFAULT). | ||
| * @param mixed $value The default value if behavior is DEFAULT. | ||
| * @return $this | ||
| */ | ||
| public function onError(string $behavior, mixed $value = null) | ||
| { | ||
| $this->onError = ['behavior' => $behavior, 'value' => $value]; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| public function sql(ValueBinder $binder): string | ||
| { | ||
| $sql = "'{$this->path}'"; | ||
|
|
||
| if ($this->passing) { | ||
| $passing = []; | ||
| foreach ($this->passing as $name => ['value' => $value, 'type' => $type]) { | ||
| if ($value instanceof ExpressionInterface) { | ||
| $exprSql = $value->sql($binder); | ||
| } else { | ||
| $placeholder = $binder->placeholder('param'); | ||
| $binder->bind($placeholder, $value, $type); | ||
| $exprSql = $placeholder; | ||
| } | ||
| $passing[] = sprintf('%s AS %s', $exprSql, $name); | ||
| } | ||
| $sql .= ' PASSING ' . implode(', ', $passing); | ||
| } | ||
|
|
||
| if ($this->returning) { | ||
| $sql .= ' RETURNING ' . $this->returning; | ||
| } | ||
|
|
||
| if ($this->onEmpty !== null) { | ||
| $sql .= ' ' . $this->behaviorSql($this->onEmpty, 'EMPTY', $binder); | ||
| } | ||
|
|
||
| if ($this->onError !== null) { | ||
| $sql .= ' ' . $this->behaviorSql($this->onError, 'ERROR', $binder); | ||
| } | ||
|
|
||
| return $sql; | ||
| } | ||
|
|
||
| /** | ||
| * Generates the SQL for ON EMPTY or ON ERROR clauses. | ||
| * | ||
| * @param array $clause The clause configuration. | ||
| * @param string $type The type of clause (EMPTY or ERROR). | ||
| * @param \Cake\Database\ValueBinder $binder The value binder. | ||
| * @return string | ||
| */ | ||
| protected function behaviorSql(array $clause, string $type, ValueBinder $binder): string | ||
| { | ||
| $behavior = strtoupper($clause['behavior']); | ||
| if ($behavior === self::BEHAVIOR_DEFAULT) { | ||
| $value = $clause['value']; | ||
| if ($value instanceof ExpressionInterface) { | ||
| $value = $value->sql($binder); | ||
| } elseif (is_string($value)) { | ||
| $value = sprintf("'%s'", str_replace("'", "''", $value)); | ||
| } elseif (is_bool($value)) { | ||
| $value = $value ? 'TRUE' : 'FALSE'; | ||
| } elseif ($value === null) { | ||
| $value = 'NULL'; | ||
| } | ||
|
|
||
| // DEFAULT clause require a literal value | ||
| return sprintf('DEFAULT %s ON %s', $value, $type); | ||
| } | ||
|
|
||
| return sprintf('%s ON %s', $behavior, $type); | ||
| } | ||
|
|
||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| public function traverse(Closure $callback) | ||
| { | ||
| foreach ($this->passing as ['value' => $value]) { | ||
| if ($value instanceof ExpressionInterface) { | ||
| $callback($value); | ||
| $value->traverse($callback); | ||
| } | ||
| } | ||
|
|
||
| if ($this->onEmpty !== null && $this->onEmpty['value'] instanceof ExpressionInterface) { | ||
| $callback($this->onEmpty['value']); | ||
| $this->onEmpty['value']->traverse($callback); | ||
| } | ||
|
|
||
| if ($this->onError !== null && $this->onError['value'] instanceof ExpressionInterface) { | ||
| $callback($this->onError['value']); | ||
| $this->onError['value']->traverse($callback); | ||
| } | ||
|
|
||
| return $this; | ||
| } | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why specifically postgres 12 here? According to https://endoflife.date/postgresql 12 is EOL'd since over a year.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
12.0 adds the JSONB_PATH_EXISTS() function. Since we were already transforming JSON_VALUE to JSONB_PATH_QUERY, thought it would be expected.
17.0 adds the sql standard functions we should be using.