init
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
<?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 4.1.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* This represents an SQL aggregate function expression in an SQL statement.
|
||||
* Calls can be constructed by passing the name of the function and a list of params.
|
||||
* For security reasons, all params passed are quoted by default unless
|
||||
* explicitly told otherwise.
|
||||
*/
|
||||
class AggregateExpression extends FunctionExpression implements WindowInterface
|
||||
{
|
||||
/**
|
||||
* @var \Cake\Database\Expression\QueryExpression|null
|
||||
*/
|
||||
protected ?QueryExpression $filter = null;
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Expression\WindowExpression|null
|
||||
*/
|
||||
protected ?WindowExpression $window = null;
|
||||
|
||||
/**
|
||||
* Adds conditions to the FILTER clause. The conditions are the same format as
|
||||
* `Query::where()`.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The conditions to filter on.
|
||||
* @param array<string, string> $types Associative array of type names used to bind values to query
|
||||
* @return $this
|
||||
* @see \Cake\Database\Query::where()
|
||||
*/
|
||||
public function filter(ExpressionInterface|Closure|array|string $conditions, array $types = [])
|
||||
{
|
||||
$this->filter ??= new QueryExpression();
|
||||
|
||||
if ($conditions instanceof Closure) {
|
||||
$conditions = $conditions(new QueryExpression());
|
||||
}
|
||||
|
||||
$this->filter->add($conditions, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an empty `OVER()` window expression or a named window expression.
|
||||
*
|
||||
* @param string|null $name Window name
|
||||
* @return $this
|
||||
*/
|
||||
public function over(?string $name = null)
|
||||
{
|
||||
$window = $this->getWindow();
|
||||
if ($name) {
|
||||
// Set name manually in case this was chained from FunctionsBuilder wrapper
|
||||
$window->name($name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function partition(ExpressionInterface|Closure|array|string $partitions)
|
||||
{
|
||||
$this->getWindow()->partition($partitions);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function order(ExpressionInterface|Closure|array|string $fields)
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.0.0',
|
||||
'AggregateExpression::order() is deprecated. Use AggregateExpression::orderBy() instead.',
|
||||
);
|
||||
|
||||
return $this->orderBy($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderBy(ExpressionInterface|Closure|array|string $fields)
|
||||
{
|
||||
$this->getWindow()->orderBy($fields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0)
|
||||
{
|
||||
$this->getWindow()->range($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rows(?int $start, ?int $end = 0)
|
||||
{
|
||||
$this->getWindow()->rows($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function groups(?int $start, ?int $end = 0)
|
||||
{
|
||||
$this->getWindow()->groups($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function frame(
|
||||
string $type,
|
||||
ExpressionInterface|string|int|null $startOffset,
|
||||
string $startDirection,
|
||||
ExpressionInterface|string|int|null $endOffset,
|
||||
string $endDirection,
|
||||
) {
|
||||
$this->getWindow()->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeCurrent()
|
||||
{
|
||||
$this->getWindow()->excludeCurrent();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeGroup()
|
||||
{
|
||||
$this->getWindow()->excludeGroup();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeTies()
|
||||
{
|
||||
$this->getWindow()->excludeTies();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns or creates WindowExpression for function.
|
||||
*
|
||||
* @return \Cake\Database\Expression\WindowExpression
|
||||
*/
|
||||
protected function getWindow(): WindowExpression
|
||||
{
|
||||
return $this->window ??= new WindowExpression();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$sql = parent::sql($binder);
|
||||
if ($this->filter !== null) {
|
||||
$sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')';
|
||||
}
|
||||
if ($this->window !== null) {
|
||||
if ($this->window->isNamedOnly()) {
|
||||
$sql .= ' OVER ' . $this->window->sql($binder);
|
||||
} else {
|
||||
$sql .= ' OVER (' . $this->window->sql($binder) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
parent::traverse($callback);
|
||||
if ($this->filter !== null) {
|
||||
$callback($this->filter);
|
||||
$this->filter->traverse($callback);
|
||||
}
|
||||
if ($this->window !== null) {
|
||||
$callback($this->window);
|
||||
$this->window->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
$count = parent::count();
|
||||
if ($this->window !== null) {
|
||||
$count += 1;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this object and its subtree of expressions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
if ($this->filter !== null) {
|
||||
$this->filter = clone $this->filter;
|
||||
}
|
||||
if ($this->window !== null) {
|
||||
$this->window = clone $this->window;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An expression object that represents a SQL BETWEEN snippet
|
||||
*/
|
||||
class BetweenExpression implements ExpressionInterface, FieldInterface
|
||||
{
|
||||
use ExpressionTypeCasterTrait;
|
||||
use FieldTrait;
|
||||
|
||||
/**
|
||||
* The first value in the expression
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $_from;
|
||||
|
||||
/**
|
||||
* The second value in the expression
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $_to;
|
||||
|
||||
/**
|
||||
* The data type for the from and to arguments
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $_type;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values in between the range.
|
||||
* @param mixed $from The initial value of the range.
|
||||
* @param mixed $to The ending value in the comparison range.
|
||||
* @param string|null $type The data type name to bind the values with.
|
||||
*/
|
||||
public function __construct(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $type = null)
|
||||
{
|
||||
if ($type !== null) {
|
||||
$from = $this->_castToExpression($from, $type);
|
||||
$to = $this->_castToExpression($to, $type);
|
||||
}
|
||||
|
||||
$this->_field = $field;
|
||||
$this->_from = $from;
|
||||
$this->_to = $to;
|
||||
$this->_type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$parts = [
|
||||
'from' => $this->_from,
|
||||
'to' => $this->_to,
|
||||
];
|
||||
|
||||
$field = $this->_field;
|
||||
if ($field instanceof ExpressionInterface) {
|
||||
$field = $field->sql($binder);
|
||||
}
|
||||
|
||||
foreach ($parts as $name => $part) {
|
||||
if ($part instanceof ExpressionInterface) {
|
||||
$parts[$name] = $part->sql($binder);
|
||||
continue;
|
||||
}
|
||||
$parts[$name] = $this->_bindValue($part, $binder, $this->_type);
|
||||
}
|
||||
assert(is_string($field));
|
||||
|
||||
return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
foreach ([$this->_field, $this->_from, $this->_to] as $part) {
|
||||
if ($part instanceof ExpressionInterface) {
|
||||
$callback($part);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a value in the placeholder generator and returns the generated placeholder
|
||||
*
|
||||
* @param mixed $value The value to bind
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use
|
||||
* @param string|null $type The type of $value
|
||||
* @return string generated placeholder
|
||||
*/
|
||||
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type): string
|
||||
{
|
||||
$placeholder = $binder->placeholder('c');
|
||||
$binder->bind($placeholder, $value, $type);
|
||||
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a deep clone of this expression.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach (['_field', '_from', '_to'] as $part) {
|
||||
if ($this->{$part} instanceof ExpressionInterface) {
|
||||
$this->{$part} = clone $this->{$part};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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 4.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Chronos\ChronosDate;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\TypedResultInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use DateTimeInterface;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Trait that holds shared functionality for case related expressions.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait CaseExpressionTrait
|
||||
{
|
||||
/**
|
||||
* Infers the abstract type for the given value.
|
||||
*
|
||||
* @param mixed $value The value for which to infer the type.
|
||||
* @return string|null The abstract type, or `null` if it could not be inferred.
|
||||
*/
|
||||
protected function inferType(mixed $value): ?string
|
||||
{
|
||||
$type = null;
|
||||
|
||||
if (is_string($value)) {
|
||||
$type = 'string';
|
||||
} elseif (is_int($value)) {
|
||||
$type = 'integer';
|
||||
} elseif (is_float($value)) {
|
||||
$type = 'float';
|
||||
} elseif (is_bool($value)) {
|
||||
$type = 'boolean';
|
||||
} elseif ($value instanceof ChronosDate) {
|
||||
$type = 'date';
|
||||
} elseif ($value instanceof DateTimeInterface) {
|
||||
$type = 'datetime';
|
||||
} elseif (
|
||||
$value instanceof Stringable
|
||||
) {
|
||||
$type = 'string';
|
||||
} elseif (
|
||||
$this->_typeMap !== null &&
|
||||
$value instanceof IdentifierExpression
|
||||
) {
|
||||
$type = $this->_typeMap->type($value->getIdentifier());
|
||||
} elseif ($value instanceof TypedResultInterface) {
|
||||
$type = $value->getReturnType();
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a nullable value to SQL.
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use.
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The value to compile.
|
||||
* @param string|null $type The value type.
|
||||
* @return string
|
||||
*/
|
||||
protected function compileNullableValue(ValueBinder $binder, mixed $value, ?string $type = null): string
|
||||
{
|
||||
if (
|
||||
$type !== null &&
|
||||
!($value instanceof ExpressionInterface)
|
||||
) {
|
||||
$value = $this->_castToExpression($value, $type);
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$value = 'NULL';
|
||||
} elseif ($value instanceof Query) {
|
||||
$value = sprintf('(%s)', $value->sql($binder));
|
||||
} elseif ($value instanceof ExpressionInterface) {
|
||||
$value = $value->sql($binder);
|
||||
} else {
|
||||
$placeholder = $binder->placeholder('c');
|
||||
$binder->bind($placeholder, $value, $type);
|
||||
$value = $placeholder;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
<?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 4.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\TypedResultInterface;
|
||||
use Cake\Database\TypeMapTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Represents a SQL case statement with a fluid API
|
||||
*/
|
||||
class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
|
||||
{
|
||||
use CaseExpressionTrait;
|
||||
use ExpressionTypeCasterTrait;
|
||||
use TypeMapTrait;
|
||||
|
||||
/**
|
||||
* The names of the clauses that are valid for use with the
|
||||
* `clause()` method.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $validClauseNames = [
|
||||
'value',
|
||||
'when',
|
||||
'else',
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether this is a simple case expression.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $isSimpleVariant = false;
|
||||
|
||||
/**
|
||||
* The case value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected mixed $value = null;
|
||||
|
||||
/**
|
||||
* The case value type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $valueType = null;
|
||||
|
||||
/**
|
||||
* The `WHEN ... THEN ...` expressions.
|
||||
*
|
||||
* @var array<\Cake\Database\Expression\WhenThenExpression>
|
||||
*/
|
||||
protected array $when = [];
|
||||
|
||||
/**
|
||||
* Buffer that holds values and types for use with `then()`.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected ?array $whenBuffer = null;
|
||||
|
||||
/**
|
||||
* The else part result value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected mixed $else = null;
|
||||
|
||||
/**
|
||||
* The else part result type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $elseType = null;
|
||||
|
||||
/**
|
||||
* The return type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $returnType = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* When a value is set, the syntax generated is
|
||||
* `CASE case_value WHEN when_value ... END` (simple case),
|
||||
* where the `when_value`'s are compared against the
|
||||
* `case_value`.
|
||||
*
|
||||
* When no value is set, the syntax generated is
|
||||
* `CASE WHEN when_conditions ... END` (searched case),
|
||||
* where the conditions hold the comparisons.
|
||||
*
|
||||
* Note that `null` is a valid case value, and thus should
|
||||
* only be passed if you actually want to create the simple
|
||||
* case expression variant!
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
|
||||
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
|
||||
* from the value.
|
||||
*/
|
||||
public function __construct(mixed $value = null, ?string $type = null)
|
||||
{
|
||||
if (func_num_args() > 0) {
|
||||
if (
|
||||
$value !== null &&
|
||||
!is_scalar($value) &&
|
||||
!(is_object($value) && !($value instanceof Closure))
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'The `$value` argument must be either `null`, a scalar value, an object, ' .
|
||||
'or an instance of `\%s`, `%s` given.',
|
||||
ExpressionInterface::class,
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
|
||||
if (
|
||||
$value !== null &&
|
||||
$type === null &&
|
||||
!($value instanceof ExpressionInterface)
|
||||
) {
|
||||
$type = $this->inferType($value);
|
||||
}
|
||||
$this->valueType = $type;
|
||||
|
||||
$this->isSimpleVariant = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a
|
||||
* self-contained expression that holds both the value for `WHEN`
|
||||
* and the value for `THEN`.
|
||||
*
|
||||
* ### Order based syntax
|
||||
*
|
||||
* When passing a value other than a self-contained
|
||||
* `\Cake\Database\Expression\WhenThenExpression`,
|
||||
* instance, the `WHEN ... THEN ...` statement must be closed off with
|
||||
* a call to `then()` before invoking `when()` again or `else()`:
|
||||
*
|
||||
* ```
|
||||
* $queryExpression
|
||||
* ->case($query->identifier('Table.column'))
|
||||
* ->when(true)
|
||||
* ->then('Yes')
|
||||
* ->when(false)
|
||||
* ->then('No')
|
||||
* ->else('Maybe');
|
||||
* ```
|
||||
*
|
||||
* ### Self-contained expressions
|
||||
*
|
||||
* When passing an instance of `\Cake\Database\Expression\WhenThenExpression`,
|
||||
* being it directly, or via a callable, then there is no need to close
|
||||
* using `then()` on this object, instead the statement will be closed
|
||||
* on the `\Cake\Database\Expression\WhenThenExpression`
|
||||
* object using
|
||||
* `\Cake\Database\Expression\WhenThenExpression::then()`.
|
||||
*
|
||||
* Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`,
|
||||
* and must return one, being it the same object, or a custom one:
|
||||
*
|
||||
* ```
|
||||
* $queryExpression
|
||||
* ->case()
|
||||
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
|
||||
* return $whenThen
|
||||
* ->when(['Table.column' => true])
|
||||
* ->then('Yes');
|
||||
* })
|
||||
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
|
||||
* return $whenThen
|
||||
* ->when(['Table.column' => false])
|
||||
* ->then('No');
|
||||
* })
|
||||
* ->else('Maybe');
|
||||
* ```
|
||||
*
|
||||
* ### Type handling
|
||||
*
|
||||
* The types provided via the `$type` argument will be merged with the
|
||||
* type map set for this expression. When using callables for `$when`,
|
||||
* the `\Cake\Database\Expression\WhenThenExpression`
|
||||
* instance received by the callables will inherit that type map, however
|
||||
* the types passed here will _not_ be merged in case of using callables,
|
||||
* instead the types must be passed in
|
||||
* `\Cake\Database\Expression\WhenThenExpression::when()`:
|
||||
*
|
||||
* ```
|
||||
* $queryExpression
|
||||
* ->case()
|
||||
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
|
||||
* return $whenThen
|
||||
* ->when(['unmapped_column' => true], ['unmapped_column' => 'bool'])
|
||||
* ->then('Yes');
|
||||
* })
|
||||
* ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
|
||||
* return $whenThen
|
||||
* ->when(['unmapped_column' => false], ['unmapped_column' => 'bool'])
|
||||
* ->then('No');
|
||||
* })
|
||||
* ->else('Maybe');
|
||||
* ```
|
||||
*
|
||||
* ### User data safety
|
||||
*
|
||||
* When passing user data, be aware that allowing a user defined array
|
||||
* to be passed, is a potential SQL injection vulnerability, as it
|
||||
* allows for raw SQL to slip in!
|
||||
*
|
||||
* The following is _unsafe_ usage that must be avoided:
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when($userData)
|
||||
* ```
|
||||
*
|
||||
* A safe variant for the above would be to define a single type for
|
||||
* the value:
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when($userData, 'integer')
|
||||
* ```
|
||||
*
|
||||
* This way an exception would be triggered when an array is passed for
|
||||
* the value, thus preventing raw SQL from slipping in, and all other
|
||||
* types of values would be forced to be bound as an integer.
|
||||
*
|
||||
* Another way to safely pass user data is when using a conditions
|
||||
* array, and passing user data only on the value side of the array
|
||||
* entries, which will cause them to be bound:
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when([
|
||||
* 'Table.column' => $userData,
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* Lastly, data can also be bound manually:
|
||||
*
|
||||
* ```
|
||||
* $query
|
||||
* ->select([
|
||||
* 'val' => $query->expr()
|
||||
* ->case()
|
||||
* ->when($query->expr(':userData'))
|
||||
* ->then(123)
|
||||
* ])
|
||||
* ->bind(':userData', $userData, 'integer')
|
||||
* ```
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an
|
||||
* array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is
|
||||
* _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If
|
||||
* you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to
|
||||
* be a non-array, and then always binds the data), use a conditions array where the user data is only passed on
|
||||
* the value side of the array entries, or custom bindings!
|
||||
* @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
|
||||
* conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
|
||||
* @return $this
|
||||
* @throws \LogicException In case this a closing `then()` call is required before calling this method.
|
||||
* @throws \LogicException In case the callable doesn't return an instance of
|
||||
* `\Cake\Database\Expression\WhenThenExpression`.
|
||||
*/
|
||||
public function when(mixed $when, array|string|null $type = null)
|
||||
{
|
||||
if ($this->whenBuffer !== null) {
|
||||
throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
|
||||
}
|
||||
|
||||
if ($when instanceof Closure) {
|
||||
$when = $when(new WhenThenExpression($this->getTypeMap()));
|
||||
if (!($when instanceof WhenThenExpression)) {
|
||||
throw new LogicException(sprintf(
|
||||
'`when()` callables must return an instance of `\%s`, `%s` given.',
|
||||
WhenThenExpression::class,
|
||||
get_debug_type($when),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($when instanceof WhenThenExpression) {
|
||||
$this->when[] = $when;
|
||||
} else {
|
||||
$this->whenBuffer = ['when' => $when, 'type' => $type];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `THEN` result value for the last `WHEN ... THEN ...`
|
||||
* statement that was opened using `when()`.
|
||||
*
|
||||
* ### Order based syntax
|
||||
*
|
||||
* This method can only be invoked in case `when()` was previously
|
||||
* used with a value other than a closure or an instance of
|
||||
* `\Cake\Database\Expression\WhenThenExpression`:
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when(['Table.column' => true])
|
||||
* ->then('Yes')
|
||||
* ->when(['Table.column' => false])
|
||||
* ->then('No')
|
||||
* ->else('Maybe');
|
||||
* ```
|
||||
*
|
||||
* The following would all fail with an exception:
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when(['Table.column' => true])
|
||||
* ->when(['Table.column' => false])
|
||||
* // ...
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when(['Table.column' => true])
|
||||
* ->else('Maybe')
|
||||
* // ...
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->then('Yes')
|
||||
* // ...
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* $case
|
||||
* ->when(['Table.column' => true])
|
||||
* ->then('Yes')
|
||||
* ->then('No')
|
||||
* // ...
|
||||
* ```
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
|
||||
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
|
||||
* value.
|
||||
* @return $this
|
||||
* @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an
|
||||
* instance of `\Cake\Database\Expression\WhenThenExpression`.
|
||||
*/
|
||||
public function then(mixed $result, ?string $type = null)
|
||||
{
|
||||
if ($this->whenBuffer === null) {
|
||||
throw new LogicException('Cannot call `then()` before `when()`.');
|
||||
}
|
||||
|
||||
$whenThen = (new WhenThenExpression($this->getTypeMap()))
|
||||
->when($this->whenBuffer['when'], $this->whenBuffer['type'])
|
||||
->then($result, $type);
|
||||
|
||||
$this->whenBuffer = null;
|
||||
|
||||
$this->when[] = $whenThen;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `ELSE` result value.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
|
||||
* @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
|
||||
* value.
|
||||
* @return $this
|
||||
* @throws \LogicException In case a closing `then()` call is required before calling this method.
|
||||
* @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an
|
||||
* instance of `\Cake\Database\ExpressionInterface`, or `null`.
|
||||
*/
|
||||
public function else(mixed $result, ?string $type = null)
|
||||
{
|
||||
if ($this->whenBuffer !== null) {
|
||||
throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
|
||||
}
|
||||
|
||||
if (
|
||||
$result !== null &&
|
||||
!is_scalar($result) &&
|
||||
!(is_object($result) && !($result instanceof Closure))
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'The `$result` argument must be either `null`, a scalar value, an object, ' .
|
||||
'or an instance of `\%s`, `%s` given.',
|
||||
ExpressionInterface::class,
|
||||
get_debug_type($result),
|
||||
));
|
||||
}
|
||||
|
||||
$type ??= $this->inferType($result);
|
||||
|
||||
$this->else = $result;
|
||||
$this->elseType = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the abstract type that this expression will return.
|
||||
*
|
||||
* If no type has been explicitly set via `setReturnType()`, this
|
||||
* method will try to obtain the type from the result types of the
|
||||
* `then()` and `else() `calls. All types must be identical in order
|
||||
* for this to work, otherwise the type will default to `string`.
|
||||
*
|
||||
* @return string
|
||||
* @see CaseStatementExpression::then()
|
||||
*/
|
||||
public function getReturnType(): string
|
||||
{
|
||||
if ($this->returnType !== null) {
|
||||
return $this->returnType;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
foreach ($this->when as $when) {
|
||||
$type = $when->getResultType();
|
||||
if ($type !== null) {
|
||||
$types[] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->elseType !== null) {
|
||||
$types[] = $this->elseType;
|
||||
}
|
||||
|
||||
$types = array_unique($types);
|
||||
if (count($types) === 1) {
|
||||
return $types[0];
|
||||
}
|
||||
|
||||
return 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the abstract type that this expression will return.
|
||||
*
|
||||
* If no type is being explicitly set via this method, then the
|
||||
* `getReturnType()` method will try to infer the type from the
|
||||
* result types of the `then()` and `else() `calls.
|
||||
*
|
||||
* @param string $type The type name to use.
|
||||
* @return $this
|
||||
*/
|
||||
public function setReturnType(string $type)
|
||||
{
|
||||
$this->returnType = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available data for the given clause.
|
||||
*
|
||||
* ### Available clauses
|
||||
*
|
||||
* The following clause names are available:
|
||||
*
|
||||
* * `value`: The case value for a `CASE case_value WHEN ...` expression.
|
||||
* * `when`: An array of `WHEN ... THEN ...` expressions.
|
||||
* * `else`: The `ELSE` result value.
|
||||
*
|
||||
* @param string $clause The name of the clause to obtain.
|
||||
* @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null
|
||||
* @throws \InvalidArgumentException In case the given clause name is invalid.
|
||||
*/
|
||||
public function clause(string $clause): mixed
|
||||
{
|
||||
if (!in_array($clause, $this->validClauseNames, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
|
||||
implode('`, `', $this->validClauseNames),
|
||||
$clause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->{$clause};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
if ($this->whenBuffer !== null) {
|
||||
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
||||
}
|
||||
|
||||
if (!$this->when) {
|
||||
throw new LogicException('Case expression must have at least one when statement.');
|
||||
}
|
||||
|
||||
$value = '';
|
||||
if ($this->isSimpleVariant) {
|
||||
$value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' ';
|
||||
}
|
||||
|
||||
$whenThenExpressions = [];
|
||||
foreach ($this->when as $whenThen) {
|
||||
$whenThenExpressions[] = $whenThen->sql($binder);
|
||||
}
|
||||
$whenThen = implode(' ', $whenThenExpressions);
|
||||
|
||||
$else = $this->compileNullableValue($binder, $this->else, $this->elseType);
|
||||
|
||||
return "CASE {$value}{$whenThen} ELSE {$else} END";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->whenBuffer !== null) {
|
||||
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
||||
}
|
||||
|
||||
if ($this->value instanceof ExpressionInterface) {
|
||||
$callback($this->value);
|
||||
$this->value->traverse($callback);
|
||||
}
|
||||
|
||||
foreach ($this->when as $when) {
|
||||
$callback($when);
|
||||
$when->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->else instanceof ExpressionInterface) {
|
||||
$callback($this->else);
|
||||
$this->else->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the inner expression objects.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->whenBuffer !== null) {
|
||||
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
||||
}
|
||||
|
||||
if ($this->value instanceof ExpressionInterface) {
|
||||
$this->value = clone $this->value;
|
||||
}
|
||||
|
||||
foreach ($this->when as $key => $when) {
|
||||
$this->when[$key] = clone $this->when[$key];
|
||||
}
|
||||
|
||||
if ($this->else instanceof ExpressionInterface) {
|
||||
$this->else = clone $this->else;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?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 4.1.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An expression that represents a common table expression definition.
|
||||
*/
|
||||
class CommonTableExpression implements ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* The CTE name.
|
||||
*
|
||||
* @var \Cake\Database\Expression\IdentifierExpression
|
||||
*/
|
||||
protected IdentifierExpression $name;
|
||||
|
||||
/**
|
||||
* The field names to use for the CTE.
|
||||
*
|
||||
* @var array<\Cake\Database\Expression\IdentifierExpression>
|
||||
*/
|
||||
protected array $fields = [];
|
||||
|
||||
/**
|
||||
* The CTE query definition.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|null
|
||||
*/
|
||||
protected ?ExpressionInterface $query = null;
|
||||
|
||||
/**
|
||||
* Whether the CTE is materialized or not materialized.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $materialized = null;
|
||||
|
||||
/**
|
||||
* Whether the CTE is recursive.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $recursive = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name The CTE name.
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|null $query CTE query
|
||||
*/
|
||||
public function __construct(string $name = '', ExpressionInterface|Closure|null $query = null)
|
||||
{
|
||||
$this->name = new IdentifierExpression($name);
|
||||
if ($query) {
|
||||
$this->query($query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of this CTE.
|
||||
*
|
||||
* This is the named you used to reference the expression
|
||||
* in select, insert, etc queries.
|
||||
*
|
||||
* @param string $name The CTE name.
|
||||
* @return $this
|
||||
*/
|
||||
public function name(string $name)
|
||||
{
|
||||
$this->name = new IdentifierExpression($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the query for this CTE.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
|
||||
* @return $this
|
||||
*/
|
||||
public function query(ExpressionInterface|Closure $query)
|
||||
{
|
||||
if ($query instanceof Closure) {
|
||||
$query = $query();
|
||||
if (!($query instanceof ExpressionInterface)) {
|
||||
throw new DatabaseException(
|
||||
'You must return an `ExpressionInterface` from a Closure passed to `query()`.',
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more fields (arguments) to the CTE.
|
||||
*
|
||||
* @param \Cake\Database\Expression\IdentifierExpression|array<string>|array<\Cake\Database\Expression\IdentifierExpression>|string $fields Field names
|
||||
* @return $this
|
||||
*/
|
||||
public function field(IdentifierExpression|array|string $fields)
|
||||
{
|
||||
$fields = (array)$fields;
|
||||
/** @var array<string|\Cake\Database\Expression\IdentifierExpression> $fields */
|
||||
foreach ($fields as &$field) {
|
||||
if (!($field instanceof IdentifierExpression)) {
|
||||
$field = new IdentifierExpression($field);
|
||||
}
|
||||
}
|
||||
/** @var array<\Cake\Database\Expression\IdentifierExpression> $mergedFields */
|
||||
$mergedFields = array_merge($this->fields, $fields);
|
||||
$this->fields = $mergedFields;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this CTE as materialized.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function materialized()
|
||||
{
|
||||
$this->materialized = 'MATERIALIZED';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this CTE as not materialized.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function notMaterialized()
|
||||
{
|
||||
$this->materialized = 'NOT MATERIALIZED';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this CTE is recursive.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRecursive(): bool
|
||||
{
|
||||
return $this->recursive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this CTE as recursive.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function recursive()
|
||||
{
|
||||
$this->recursive = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$fields = '';
|
||||
if ($this->fields) {
|
||||
$expressions = array_map(fn(IdentifierExpression $e) => $e->sql($binder), $this->fields);
|
||||
$fields = sprintf('(%s)', implode(', ', $expressions));
|
||||
}
|
||||
|
||||
$suffix = $this->materialized ? $this->materialized . ' ' : '';
|
||||
|
||||
return sprintf(
|
||||
'%s%s AS %s(%s)',
|
||||
$this->name->sql($binder),
|
||||
$fields,
|
||||
$suffix,
|
||||
$this->query ? $this->query->sql($binder) : '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
$callback($this->name);
|
||||
foreach ($this->fields as $field) {
|
||||
$callback($field);
|
||||
$field->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->query) {
|
||||
$callback($this->query);
|
||||
$this->query->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the inner expression objects.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->name = clone $this->name;
|
||||
if ($this->query) {
|
||||
$this->query = clone $this->query;
|
||||
}
|
||||
|
||||
foreach ($this->fields as $key => $field) {
|
||||
$this->fields[$key] = clone $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* A Comparison is a type of query expression that represents an operation
|
||||
* involving a field an operator and a value. In its most common form the
|
||||
* string representation of a comparison is `field = value`
|
||||
*/
|
||||
class ComparisonExpression implements ExpressionInterface, FieldInterface
|
||||
{
|
||||
use ExpressionTypeCasterTrait;
|
||||
use FieldTrait;
|
||||
|
||||
/**
|
||||
* The value to be used in the right hand side of the operation
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $_value;
|
||||
|
||||
/**
|
||||
* The type to be used for casting the value to a database representation
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $_type = null;
|
||||
|
||||
/**
|
||||
* The operator used for comparing field and value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_operator = '=';
|
||||
|
||||
/**
|
||||
* Whether the value in this expression is a traversable
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_isMultiple = false;
|
||||
|
||||
/**
|
||||
* A cached list of ExpressionInterface objects that were
|
||||
* found in the value for this expression.
|
||||
*
|
||||
* @var array<\Cake\Database\ExpressionInterface>
|
||||
*/
|
||||
protected array $_valueExpressions = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value
|
||||
* @param mixed $value The value to be used in comparison
|
||||
* @param string|null $type the type name used to cast the value
|
||||
* @param string $operator the operator used for comparing field and value
|
||||
*/
|
||||
public function __construct(
|
||||
ExpressionInterface|string $field,
|
||||
mixed $value,
|
||||
?string $type = null,
|
||||
string $operator = '=',
|
||||
) {
|
||||
$this->_type = $type;
|
||||
$this->setField($field);
|
||||
$this->setValue($value);
|
||||
$this->_operator = $operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value
|
||||
*
|
||||
* @param mixed $value The value to compare
|
||||
* @return void
|
||||
*/
|
||||
public function setValue(mixed $value): void
|
||||
{
|
||||
$value = $this->_castToExpression($value, $this->_type);
|
||||
|
||||
$isMultiple = $this->_type && str_contains($this->_type, '[]');
|
||||
if ($isMultiple) {
|
||||
[$value, $this->_valueExpressions] = $this->_collectExpressions($value);
|
||||
}
|
||||
|
||||
$this->_isMultiple = $isMultiple;
|
||||
$this->_value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value used for comparison
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(): mixed
|
||||
{
|
||||
return $this->_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the operator to use for the comparison
|
||||
*
|
||||
* @param string $operator The operator to be used for the comparison.
|
||||
* @return void
|
||||
*/
|
||||
public function setOperator(string $operator): void
|
||||
{
|
||||
$this->_operator = $operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operator used for comparison
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(): string
|
||||
{
|
||||
return $this->_operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$field = $this->_field;
|
||||
|
||||
if ($field instanceof ExpressionInterface) {
|
||||
$field = $field->sql($binder);
|
||||
}
|
||||
|
||||
if ($this->_value instanceof IdentifierExpression) {
|
||||
$template = '%s %s %s';
|
||||
$value = $this->_value->sql($binder);
|
||||
} elseif ($this->_value instanceof ExpressionInterface) {
|
||||
$template = '%s %s (%s)';
|
||||
$value = $this->_value->sql($binder);
|
||||
} else {
|
||||
[$template, $value] = $this->_stringExpression($binder);
|
||||
}
|
||||
assert(is_string($field));
|
||||
|
||||
return sprintf($template, $field, $this->_operator, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->_field instanceof ExpressionInterface) {
|
||||
$callback($this->_field);
|
||||
$this->_field->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->_value instanceof ExpressionInterface) {
|
||||
$callback($this->_value);
|
||||
$this->_value->traverse($callback);
|
||||
}
|
||||
|
||||
foreach ($this->_valueExpressions as $v) {
|
||||
$callback($v);
|
||||
$v->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep clone.
|
||||
*
|
||||
* Clones the field and value if they are expression objects.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach (['_value', '_field'] as $prop) {
|
||||
if ($this->{$prop} instanceof ExpressionInterface) {
|
||||
$this->{$prop} = clone $this->{$prop};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a template and a placeholder for the value after registering it
|
||||
* with the placeholder $binder
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use.
|
||||
* @return array First position containing the template and the second a placeholder
|
||||
*/
|
||||
protected function _stringExpression(ValueBinder $binder): array
|
||||
{
|
||||
$template = '%s ';
|
||||
|
||||
if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) {
|
||||
$template = '(%s) ';
|
||||
}
|
||||
|
||||
if ($this->_isMultiple) {
|
||||
$template .= '%s (%s)';
|
||||
$type = $this->_type;
|
||||
if ($type !== null) {
|
||||
$type = str_replace('[]', '', $type);
|
||||
}
|
||||
$value = $this->_flattenValue($this->_value, $binder, $type);
|
||||
|
||||
// To avoid SQL errors when comparing a field to a list of empty values,
|
||||
// better just throw an exception here
|
||||
if ($value === '') {
|
||||
$field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
|
||||
/** @var string $field */
|
||||
throw new DatabaseException(
|
||||
"Impossible to generate condition with empty list of values for field ({$field})",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$template .= '%s %s';
|
||||
$value = $this->_bindValue($this->_value, $binder, $this->_type);
|
||||
}
|
||||
|
||||
return [$template, $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a value in the placeholder generator and returns the generated placeholder
|
||||
*
|
||||
* @param mixed $value The value to bind
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use
|
||||
* @param string|null $type The type of $value
|
||||
* @return string generated placeholder
|
||||
*/
|
||||
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type = null): string
|
||||
{
|
||||
$placeholder = $binder->placeholder('c');
|
||||
$binder->bind($placeholder, $value, $type);
|
||||
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a traversable value into a set of placeholders generated by
|
||||
* $binder and separated by `,`
|
||||
*
|
||||
* @param iterable $value the value to flatten
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use
|
||||
* @param string|null $type the type to cast values to
|
||||
* @return string
|
||||
*/
|
||||
protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
|
||||
{
|
||||
$parts = [];
|
||||
if (is_array($value)) {
|
||||
foreach ($this->_valueExpressions as $k => $v) {
|
||||
$parts[$k] = $v->sql($binder);
|
||||
unset($value[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($value) {
|
||||
$parts += $binder->generateManyNamed($value, $type);
|
||||
}
|
||||
|
||||
return implode(',', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the original $values in the first position
|
||||
* and all ExpressionInterface objects that could be found in the second
|
||||
* position.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert
|
||||
* @return array
|
||||
*/
|
||||
protected function _collectExpressions(ExpressionInterface|iterable $values): array
|
||||
{
|
||||
if ($values instanceof ExpressionInterface) {
|
||||
return [$values, []];
|
||||
}
|
||||
$expressions = [];
|
||||
$result = [];
|
||||
$isArray = is_array($values);
|
||||
|
||||
if ($isArray) {
|
||||
$result = (array)$values;
|
||||
}
|
||||
|
||||
foreach ($values as $k => $v) {
|
||||
if ($v instanceof ExpressionInterface) {
|
||||
$expressions[$k] = $v;
|
||||
}
|
||||
|
||||
if ($isArray) {
|
||||
$result[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return [$result, $expressions];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
|
||||
/**
|
||||
* Describes a getter and a setter for the a field property. Useful for expressions
|
||||
* that contain an identifier to compare against.
|
||||
*/
|
||||
interface FieldInterface
|
||||
{
|
||||
/**
|
||||
* Sets the field name
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
|
||||
* @return void
|
||||
*/
|
||||
public function setField(ExpressionInterface|array|string $field): void;
|
||||
|
||||
/**
|
||||
* Returns the field name
|
||||
*
|
||||
* @return \Cake\Database\ExpressionInterface|array|string
|
||||
*/
|
||||
public function getField(): ExpressionInterface|array|string;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
|
||||
/**
|
||||
* Contains the field property with a getter and a setter for it
|
||||
*/
|
||||
trait FieldTrait
|
||||
{
|
||||
/**
|
||||
* The field name or expression to be used in the left hand side of the operator
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|array|string
|
||||
*/
|
||||
protected ExpressionInterface|array|string $_field;
|
||||
|
||||
/**
|
||||
* Sets the field name
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
|
||||
* @return void
|
||||
*/
|
||||
public function setField(ExpressionInterface|array|string $field): void
|
||||
{
|
||||
$this->_field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field name
|
||||
*
|
||||
* @return \Cake\Database\ExpressionInterface|array|string
|
||||
*/
|
||||
public function getField(): ExpressionInterface|array|string
|
||||
{
|
||||
return $this->_field;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\TypedResultInterface;
|
||||
use Cake\Database\TypedResultTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
|
||||
/**
|
||||
* This class represents a function call string in a SQL statement. Calls can be
|
||||
* constructed by passing the name of the function and a list of params.
|
||||
* For security reasons, all params passed are quoted by default unless
|
||||
* explicitly told otherwise.
|
||||
*/
|
||||
class FunctionExpression extends QueryExpression implements TypedResultInterface
|
||||
{
|
||||
use ExpressionTypeCasterTrait;
|
||||
use TypedResultTrait;
|
||||
|
||||
/**
|
||||
* The name of the function to be constructed when generating the SQL string
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_name;
|
||||
|
||||
/**
|
||||
* Constructor. Takes a name for the function to be invoked and a list of params
|
||||
* to be passed into the function. Optionally you can pass a list of types to
|
||||
* be used for each bound param.
|
||||
*
|
||||
* By default, all params that are passed will be quoted. If you wish to use
|
||||
* literal arguments, you need to explicitly hint this function.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* `$f = new FunctionExpression('CONCAT', ['CakePHP', ' rules']);`
|
||||
*
|
||||
* Previous line will generate `CONCAT('CakePHP', ' rules')`
|
||||
*
|
||||
* `$f = new FunctionExpression('CONCAT', ['name' => 'literal', ' rules']);`
|
||||
*
|
||||
* Will produce `CONCAT(name, ' rules')`
|
||||
*
|
||||
* @param string $name the name of the function to be constructed
|
||||
* @param array $params list of arguments to be passed to the function
|
||||
* If associative the key would be used as argument when value is 'literal'
|
||||
* @param array<string, string>|array<string|null> $types Associative array of types to be associated with the
|
||||
* passed arguments
|
||||
* @param string $returnType The return type of this expression
|
||||
*/
|
||||
public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string')
|
||||
{
|
||||
$this->_name = $name;
|
||||
$this->_returnType = $returnType;
|
||||
parent::__construct($params, $types, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the SQL function to be invoke in this expression.
|
||||
*
|
||||
* @param string $name The name of the function
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->_name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the SQL function to be invoke in this expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more arguments for the function call.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $conditions list of arguments to be passed to the function
|
||||
* If associative the key would be used as argument when value is 'literal'
|
||||
* @param array<string, string> $types Associative array of types to be associated with the
|
||||
* passed arguments
|
||||
* @param bool $prepend Whether to prepend or append to the list of arguments
|
||||
* @see \Cake\Database\Expression\FunctionExpression::__construct() for more details.
|
||||
* @return $this
|
||||
*/
|
||||
public function add(ExpressionInterface|array|string $conditions, array $types = [], bool $prepend = false)
|
||||
{
|
||||
$put = $prepend ? 'array_unshift' : 'array_push';
|
||||
$typeMap = $this->getTypeMap()->setTypes($types);
|
||||
/** @var array $conditions */
|
||||
foreach ($conditions as $k => $p) {
|
||||
if ($p === 'literal') {
|
||||
$put($this->_conditions, $k);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($p === 'identifier') {
|
||||
$put($this->_conditions, new IdentifierExpression($k));
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $typeMap->type($k);
|
||||
|
||||
if ($type !== null && !$p instanceof ExpressionInterface) {
|
||||
$p = $this->_castToExpression($p, $type);
|
||||
}
|
||||
|
||||
if ($p instanceof ExpressionInterface) {
|
||||
$put($this->_conditions, $p);
|
||||
continue;
|
||||
}
|
||||
|
||||
$put($this->_conditions, ['value' => $p, 'type' => $type]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($this->_conditions as $condition) {
|
||||
if ($condition instanceof Query) {
|
||||
$condition = sprintf('(%s)', $condition->sql($binder));
|
||||
} elseif ($condition instanceof ExpressionInterface) {
|
||||
$condition = $condition->sql($binder);
|
||||
} elseif (is_array($condition)) {
|
||||
$p = $binder->placeholder('param');
|
||||
$binder->bind($p, $condition['value'], $condition['type']);
|
||||
$condition = $p;
|
||||
}
|
||||
$parts[] = $condition;
|
||||
}
|
||||
|
||||
return $this->_name . sprintf('(%s)', implode(
|
||||
$this->_conjunction . ' ',
|
||||
$parts,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the function is in itself an expression to generate, thus
|
||||
* always adding 1 to the amount of expressions stored in this object.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return 1 + count($this->_conditions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Represents a single identifier name in the database.
|
||||
*
|
||||
* Identifier values are unsafe with user supplied data.
|
||||
* Values will be quoted when identifier quoting is enabled.
|
||||
*
|
||||
* @see \Cake\Database\Query::identifier()
|
||||
*/
|
||||
class IdentifierExpression implements ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* Holds the identifier string
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_identifier;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $collation = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $identifier The identifier this expression represents
|
||||
* @param string|null $collation The identifier collation
|
||||
*/
|
||||
public function __construct(string $identifier, ?string $collation = null)
|
||||
{
|
||||
$this->_identifier = $identifier;
|
||||
$this->collation = $collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identifier this expression represents
|
||||
*
|
||||
* @param string $identifier The identifier
|
||||
* @return void
|
||||
*/
|
||||
public function setIdentifier(string $identifier): void
|
||||
{
|
||||
$this->_identifier = $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier this expression represents
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->_identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the collation.
|
||||
*
|
||||
* @param string $collation Identifier collation
|
||||
* @return void
|
||||
*/
|
||||
public function setCollation(string $collation): void
|
||||
{
|
||||
$this->collation = $collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collation.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCollation(): ?string
|
||||
{
|
||||
return $this->collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$sql = $this->_identifier;
|
||||
if ($this->collation) {
|
||||
$sql .= ' COLLATE ' . $this->collation;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\TypeMap;
|
||||
use Cake\Database\ValueBinder;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* An expression object for ORDER BY clauses
|
||||
*/
|
||||
class OrderByExpression extends QueryExpression
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $conditions The sort columns
|
||||
* @param \Cake\Database\TypeMap|array<string, string> $types The types for each column.
|
||||
* @param string $conjunction The glue used to join conditions together.
|
||||
*/
|
||||
public function __construct(
|
||||
ExpressionInterface|array|string $conditions = [],
|
||||
TypeMap|array $types = [],
|
||||
string $conjunction = '',
|
||||
) {
|
||||
parent::__construct($conditions, $types, $conjunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$order = [];
|
||||
foreach ($this->_conditions as $k => $direction) {
|
||||
if ($direction instanceof ExpressionInterface) {
|
||||
$direction = $direction->sql($binder);
|
||||
}
|
||||
$order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
|
||||
}
|
||||
|
||||
return sprintf('ORDER BY %s', implode(', ', $order));
|
||||
}
|
||||
|
||||
/**
|
||||
* Auxiliary function used for decomposing a nested array of conditions and
|
||||
* building a tree structure inside this object to represent the full SQL expression.
|
||||
*
|
||||
* New order by expressions are merged to existing ones
|
||||
*
|
||||
* @param array $conditions list of order by expressions
|
||||
* @param array $types list of types associated on fields referenced in $conditions
|
||||
* @return void
|
||||
*/
|
||||
protected function _addConditions(array $conditions, array $types): void
|
||||
{
|
||||
foreach ($conditions as $key => $val) {
|
||||
if (
|
||||
is_string($key) &&
|
||||
is_string($val) &&
|
||||
!in_array(strtoupper($val), ['ASC', 'DESC'], true)
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
"Passing extra expressions by associative array (`'%s' => '%s'`) " .
|
||||
'is not allowed to avoid potential SQL injection. ' .
|
||||
'Use QueryExpression or numeric array instead.',
|
||||
$key,
|
||||
$val,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_conditions = array_merge($this->_conditions, $conditions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An expression object for complex ORDER BY clauses
|
||||
*/
|
||||
class OrderClauseExpression implements ExpressionInterface, FieldInterface
|
||||
{
|
||||
use FieldTrait;
|
||||
|
||||
/**
|
||||
* The direction of sorting.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_direction;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
|
||||
* @param string $direction The direction to sort on.
|
||||
*/
|
||||
public function __construct(ExpressionInterface|string $field, string $direction)
|
||||
{
|
||||
$this->_field = $field;
|
||||
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$field = $this->_field;
|
||||
if ($field instanceof Query) {
|
||||
$field = sprintf('(%s)', $field->sql($binder));
|
||||
} elseif ($field instanceof ExpressionInterface) {
|
||||
$field = $field->sql($binder);
|
||||
}
|
||||
assert(is_string($field));
|
||||
|
||||
return sprintf('%s %s', $field, $this->_direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->_field instanceof ExpressionInterface) {
|
||||
$callback($this->_field);
|
||||
$this->_field->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deep clone of the order clause.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->_field instanceof ExpressionInterface) {
|
||||
$this->_field = clone $this->_field;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,788 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\TypeMap;
|
||||
use Cake\Database\TypeMapTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use Countable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a SQL Query expression. Internally it stores a tree of
|
||||
* expressions that can be compiled by converting this object to string
|
||||
* and will contain a correctly parenthesized and nested expression.
|
||||
*/
|
||||
class QueryExpression implements ExpressionInterface, Countable
|
||||
{
|
||||
use TypeMapTrait;
|
||||
|
||||
/**
|
||||
* String to be used for joining each of the internal expressions
|
||||
* this object internally stores for example "AND", "OR", etc.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_conjunction;
|
||||
|
||||
/**
|
||||
* A list of strings or other expression objects that represent the "branches" of
|
||||
* the expression tree. For example one key of the array might look like "sum > :value"
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $_conditions = [];
|
||||
|
||||
/**
|
||||
* Constructor. A new expression object can be created without any params and
|
||||
* be built dynamically. Otherwise, it is possible to pass an array of conditions
|
||||
* containing either a tree-like array structure to be parsed and/or other
|
||||
* expression objects. Optionally, you can set the conjunction keyword to be used
|
||||
* for joining each part of this level of the expression tree.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $conditions Tree like array structure
|
||||
* containing all the conditions to be added or nested inside this expression object.
|
||||
* @param \Cake\Database\TypeMap|array $types Associative array of types to be associated with the values
|
||||
* passed in $conditions.
|
||||
* @param string $conjunction the glue that will join all the string conditions at this
|
||||
* level of the expression tree. For example "AND", "OR", "XOR"...
|
||||
* @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types
|
||||
*/
|
||||
public function __construct(
|
||||
ExpressionInterface|array|string $conditions = [],
|
||||
TypeMap|array $types = [],
|
||||
string $conjunction = 'AND',
|
||||
) {
|
||||
$this->setTypeMap($types);
|
||||
$this->setConjunction(strtoupper($conjunction));
|
||||
if ($conditions) {
|
||||
$this->add($conditions, $this->getTypeMap()->getTypes());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the conjunction for the conditions at this level of the expression tree.
|
||||
*
|
||||
* @param string $conjunction Value to be used for joining conditions
|
||||
* @return $this
|
||||
*/
|
||||
public function setConjunction(string $conjunction)
|
||||
{
|
||||
$this->_conjunction = strtoupper($conjunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently configured conjunction for the conditions at this level of the expression tree.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getConjunction(): string
|
||||
{
|
||||
return $this->_conjunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more conditions to this expression object. Conditions can be
|
||||
* expressed in a one dimensional array, that will cause all conditions to
|
||||
* be added directly at this level of the tree or they can be nested arbitrarily
|
||||
* making it create more expression objects that will be nested inside and
|
||||
* configured to use the specified conjunction.
|
||||
*
|
||||
* If the type passed for any of the fields is expressed "type[]" (note braces)
|
||||
* then it will cause the placeholder to be re-written dynamically so if the
|
||||
* value is an array, it will create as many placeholders as values are in it.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $conditions single or multiple conditions to
|
||||
* be added. When using an array and the key is 'OR' or 'AND' a new expression
|
||||
* object will be created with that conjunction and internal array value passed
|
||||
* as conditions.
|
||||
* @param array<int|string, string> $types Associative array of fields pointing to the type of the
|
||||
* values that are being passed. Used for correctly binding values to statements.
|
||||
* @see \Cake\Database\Query::where() for examples on conditions
|
||||
* @return $this
|
||||
*/
|
||||
public function add(ExpressionInterface|array|string $conditions, array $types = [])
|
||||
{
|
||||
if (is_string($conditions) || $conditions instanceof ExpressionInterface) {
|
||||
$this->_conditions[] = $conditions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->_addConditions($conditions, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field = value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* If it is suffixed with "[]" and the value is an array then multiple placeholders
|
||||
* will be created, one per each value in the array.
|
||||
* @return $this
|
||||
*/
|
||||
public function eq(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '='));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field != value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* If it is suffixed with "[]" and the value is an array then multiple placeholders
|
||||
* will be created, one per each value in the array.
|
||||
* @return $this
|
||||
*/
|
||||
public function notEq(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '!='));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field > value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function gt(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '>'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field < value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function lt(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '<'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field >= value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function gte(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '>='));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field <= value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function lte(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, '<='));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field IS NULL".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field database field to be
|
||||
* tested for null
|
||||
* @return $this
|
||||
*/
|
||||
public function isNull(ExpressionInterface|string $field)
|
||||
{
|
||||
if (!($field instanceof ExpressionInterface)) {
|
||||
$field = new IdentifierExpression($field);
|
||||
}
|
||||
|
||||
return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field IS NOT NULL".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field database field to be
|
||||
* tested for not null
|
||||
* @return $this
|
||||
*/
|
||||
public function isNotNull(ExpressionInterface|string $field)
|
||||
{
|
||||
if (!($field instanceof ExpressionInterface)) {
|
||||
$field = new IdentifierExpression($field);
|
||||
}
|
||||
|
||||
return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field LIKE value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function like(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "field NOT LIKE value".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param mixed $value The value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function notLike(ExpressionInterface|string $field, mixed $value, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form
|
||||
* "field IN (value1, value2)".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function in(
|
||||
ExpressionInterface|string $field,
|
||||
ExpressionInterface|array|string $values,
|
||||
?string $type = null,
|
||||
) {
|
||||
$type ??= $this->_calculateType($field);
|
||||
$type = $type ?: 'string';
|
||||
$type .= '[]';
|
||||
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $values, $type, 'IN'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new case expression object.
|
||||
*
|
||||
* When a value is set, the syntax generated is
|
||||
* `CASE case_value WHEN when_value ... END` (simple case),
|
||||
* where the `when_value`'s are compared against the
|
||||
* `case_value`.
|
||||
*
|
||||
* When no value is set, the syntax generated is
|
||||
* `CASE WHEN when_conditions ... END` (searched case),
|
||||
* where the conditions hold the comparisons.
|
||||
*
|
||||
* Note that `null` is a valid case value, and thus should
|
||||
* only be passed if you actually want to create the simple
|
||||
* case expression variant!
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
|
||||
* @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
|
||||
* from the value.
|
||||
* @return \Cake\Database\Expression\CaseStatementExpression
|
||||
*/
|
||||
public function case(mixed $value = null, ?string $type = null): CaseStatementExpression
|
||||
{
|
||||
if (func_num_args() > 0) {
|
||||
$expression = new CaseStatementExpression($value, $type);
|
||||
} else {
|
||||
$expression = new CaseStatementExpression();
|
||||
}
|
||||
|
||||
return $expression->setTypeMap($this->getTypeMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form
|
||||
* "field NOT IN (value1, value2)".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function notIn(
|
||||
ExpressionInterface|string $field,
|
||||
ExpressionInterface|array|string $values,
|
||||
?string $type = null,
|
||||
) {
|
||||
$type ??= $this->_calculateType($field);
|
||||
$type = $type ?: 'string';
|
||||
$type .= '[]';
|
||||
$values = $values instanceof ExpressionInterface ? $values : (array)$values;
|
||||
|
||||
return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form
|
||||
* "(field NOT IN (value1, value2) OR field IS NULL".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Database field to be compared against value
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $values the value to be bound to $field for comparison
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function notInOrNull(
|
||||
ExpressionInterface|string $field,
|
||||
ExpressionInterface|array|string $values,
|
||||
?string $type = null,
|
||||
) {
|
||||
$or = new static([], [], 'OR');
|
||||
$or
|
||||
->notIn($field, $values, $type)
|
||||
->isNull($field);
|
||||
|
||||
return $this->add($or);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "EXISTS (...)".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface $expression the inner query
|
||||
* @return $this
|
||||
*/
|
||||
public function exists(ExpressionInterface $expression)
|
||||
{
|
||||
return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form "NOT EXISTS (...)".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface $expression the inner query
|
||||
* @return $this
|
||||
*/
|
||||
public function notExists(ExpressionInterface $expression)
|
||||
{
|
||||
return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new condition to the expression object in the form
|
||||
* "field BETWEEN from AND to".
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values in between the range.
|
||||
* @param mixed $from The initial value of the range.
|
||||
* @param mixed $to The ending value in the comparison range.
|
||||
* @param string|null $type the type name for $value as configured using the Type map.
|
||||
* @return $this
|
||||
*/
|
||||
public function between(ExpressionInterface|string $field, mixed $from, mixed $to, ?string $type = null)
|
||||
{
|
||||
$type ??= $this->_calculateType($field);
|
||||
|
||||
return $this->add(new BetweenExpression($field, $from, $to, $type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new QueryExpression object containing all the conditions passed
|
||||
* and set up the conjunction to be "AND"
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with AND
|
||||
* @param array<string, string> $types Associative array of fields pointing to the type of the
|
||||
* values that are being passed. Used for correctly binding values to statements.
|
||||
* @return static
|
||||
*/
|
||||
public function and(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
|
||||
{
|
||||
if ($conditions instanceof Closure) {
|
||||
return $conditions(new static([], $this->getTypeMap()->setTypes($types)));
|
||||
}
|
||||
|
||||
return new static($conditions, $this->getTypeMap()->setTypes($types));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new QueryExpression object containing all the conditions passed
|
||||
* and set up the conjunction to be "OR"
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be joined with OR
|
||||
* @param array<string, string> $types Associative array of fields pointing to the type of the
|
||||
* values that are being passed. Used for correctly binding values to statements.
|
||||
* @return static
|
||||
*/
|
||||
public function or(ExpressionInterface|Closure|array|string $conditions, array $types = []): static
|
||||
{
|
||||
if ($conditions instanceof Closure) {
|
||||
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
|
||||
}
|
||||
|
||||
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new set of conditions to this level of the tree and negates
|
||||
* the final result by prepending a NOT, it will look like
|
||||
* "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
|
||||
* currently configured for this object.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions to be added and negated
|
||||
* @param array<string, string> $types Associative array of fields pointing to the type of the
|
||||
* values that are being passed. Used for correctly binding values to statements.
|
||||
* @return $this
|
||||
*/
|
||||
public function not(ExpressionInterface|Closure|array|string $conditions, array $types = [])
|
||||
{
|
||||
return $this->add(['NOT' => $conditions], $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of internal conditions that are stored in this expression.
|
||||
* Useful to determine if this expression object is void or it will generate
|
||||
* a non-empty string when compiled
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->_conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds equal condition or assignment with identifier wrapping.
|
||||
*
|
||||
* @param string $leftField Left join condition field name.
|
||||
* @param string $rightField Right join condition field name.
|
||||
* @return $this
|
||||
*/
|
||||
public function equalFields(string $leftField, string $rightField)
|
||||
{
|
||||
$wrapIdentifier = function ($field): ExpressionInterface {
|
||||
if ($field instanceof ExpressionInterface) {
|
||||
return $field;
|
||||
}
|
||||
|
||||
return new IdentifierExpression($field);
|
||||
};
|
||||
|
||||
return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$len = $this->count();
|
||||
if ($len === 0) {
|
||||
return '';
|
||||
}
|
||||
$conjunction = $this->_conjunction;
|
||||
$template = $len === 1 ? '%s' : '(%s)';
|
||||
$parts = [];
|
||||
foreach ($this->_conditions as $part) {
|
||||
if ($part instanceof Query) {
|
||||
$part = '(' . $part->sql($binder) . ')';
|
||||
} elseif ($part instanceof ExpressionInterface) {
|
||||
$part = $part->sql($binder);
|
||||
}
|
||||
if ($part !== '') {
|
||||
$parts[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf($template, implode(" {$conjunction} ", $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
foreach ($this->_conditions as $c) {
|
||||
if ($c instanceof ExpressionInterface) {
|
||||
$callback($c);
|
||||
$c->traverse($callback);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a callback for each of the parts that form this expression.
|
||||
*
|
||||
* The callback is required to return a value with which the currently
|
||||
* visited part will be replaced. If the callback returns null then
|
||||
* the part will be discarded completely from this expression.
|
||||
*
|
||||
* The callback function will receive each of the conditions as first param and
|
||||
* the key as second param. It is possible to declare the second parameter as
|
||||
* passed by reference, this will enable you to change the key under which the
|
||||
* modified part is stored.
|
||||
*
|
||||
* @param \Closure $callback The callback to run for each part
|
||||
* @return $this
|
||||
*/
|
||||
public function iterateParts(Closure $callback)
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($this->_conditions as $k => $c) {
|
||||
$key = &$k;
|
||||
$part = $callback($c, $key);
|
||||
if ($part !== null) {
|
||||
$parts[$key] = $part;
|
||||
}
|
||||
}
|
||||
$this->_conditions = $parts;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this expression contains any other nested
|
||||
* ExpressionInterface objects
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNestedExpression(): bool
|
||||
{
|
||||
foreach ($this->_conditions as $c) {
|
||||
if ($c instanceof ExpressionInterface) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auxiliary function used for decomposing a nested array of conditions and build
|
||||
* a tree structure inside this object to represent the full SQL expression.
|
||||
* String conditions are stored directly in the conditions, while any other
|
||||
* representation is wrapped around an adequate instance or of this class.
|
||||
*
|
||||
* @param array $conditions list of conditions to be stored in this object
|
||||
* @param array<int|string, string> $types list of types associated on fields referenced in $conditions
|
||||
* @return void
|
||||
*/
|
||||
protected function _addConditions(array $conditions, array $types): void
|
||||
{
|
||||
$operators = ['and', 'or', 'xor'];
|
||||
|
||||
$typeMap = $this->getTypeMap()->setTypes($types);
|
||||
|
||||
foreach ($conditions as $k => $c) {
|
||||
$numericKey = is_numeric($k);
|
||||
|
||||
if ($c instanceof Closure) {
|
||||
$expr = new static([], $typeMap);
|
||||
$c = $c($expr, $this);
|
||||
}
|
||||
|
||||
if ($numericKey && empty($c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isArray = is_array($c);
|
||||
$isOperator = false;
|
||||
$isNot = false;
|
||||
if (!$numericKey) {
|
||||
$normalizedKey = strtolower($k);
|
||||
$isOperator = in_array($normalizedKey, $operators);
|
||||
$isNot = $normalizedKey === 'not';
|
||||
}
|
||||
|
||||
if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($numericKey && $c instanceof ExpressionInterface) {
|
||||
$this->_conditions[] = $c;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($numericKey && is_string($c)) {
|
||||
$this->_conditions[] = $c;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($numericKey && $isArray || $isOperator) {
|
||||
$this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isNot) {
|
||||
$this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$numericKey) {
|
||||
$this->_conditions[] = $this->_parseCondition($k, $c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string conditions by trying to extract the operator inside it if any
|
||||
* and finally returning either an adequate QueryExpression object or a plain
|
||||
* string representation of the condition. This function is responsible for
|
||||
* generating the placeholders and replacing the values by them, while storing
|
||||
* the value elsewhere for future binding.
|
||||
*
|
||||
* @param string $condition The value from which the actual field and operator will
|
||||
* be extracted.
|
||||
* @param mixed $value The value to be bound to a placeholder for the field
|
||||
* @return \Cake\Database\ExpressionInterface|string
|
||||
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
|
||||
*/
|
||||
protected function _parseCondition(string $condition, mixed $value): ExpressionInterface|string
|
||||
{
|
||||
$expression = trim($condition);
|
||||
$operator = '=';
|
||||
|
||||
$spaces = substr_count($expression, ' ');
|
||||
// Handle expression values that contain multiple spaces, such as
|
||||
// operators with a space in them like `field IS NOT` and
|
||||
// `field NOT LIKE`, or combinations with function expressions
|
||||
// like `CONCAT(first_name, ' ', last_name) IN`.
|
||||
if ($spaces > 1) {
|
||||
$parts = explode(' ', $expression);
|
||||
if (preg_match('/(is not|not \w+)$/i', $expression)) {
|
||||
$last = array_pop($parts);
|
||||
$second = array_pop($parts);
|
||||
$parts[] = "{$second} {$last}";
|
||||
}
|
||||
$operator = array_pop($parts);
|
||||
$expression = implode(' ', $parts);
|
||||
} elseif ($spaces == 1) {
|
||||
$parts = explode(' ', $expression, 2);
|
||||
[$expression, $operator] = $parts;
|
||||
}
|
||||
$operator = strtoupper(trim($operator));
|
||||
|
||||
$type = $this->getTypeMap()->type($expression);
|
||||
$typeMultiple = (is_string($type) && str_contains($type, '[]'));
|
||||
if (in_array($operator, ['IN', 'NOT IN']) || $typeMultiple) {
|
||||
$type = $type ?: 'string';
|
||||
if (!$typeMultiple) {
|
||||
$type .= '[]';
|
||||
}
|
||||
$operator = $operator === '=' ? 'IN' : $operator;
|
||||
$operator = $operator === '!=' ? 'NOT IN' : $operator;
|
||||
$typeMultiple = true;
|
||||
}
|
||||
|
||||
if ($typeMultiple) {
|
||||
$value = $value instanceof ExpressionInterface ? $value : (array)$value;
|
||||
}
|
||||
|
||||
if ($operator === 'IS' && $value === null) {
|
||||
return new UnaryExpression(
|
||||
'IS NULL',
|
||||
new IdentifierExpression($expression),
|
||||
UnaryExpression::POSTFIX,
|
||||
);
|
||||
}
|
||||
|
||||
if ($operator === 'IS NOT' && $value === null) {
|
||||
return new UnaryExpression(
|
||||
'IS NOT NULL',
|
||||
new IdentifierExpression($expression),
|
||||
UnaryExpression::POSTFIX,
|
||||
);
|
||||
}
|
||||
|
||||
if ($operator === 'IS' && $value !== null) {
|
||||
$operator = '=';
|
||||
}
|
||||
|
||||
if ($operator === 'IS NOT' && $value !== null) {
|
||||
$operator = '!=';
|
||||
}
|
||||
|
||||
if ($value === null && $this->_conjunction !== ',') {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Expression `%s` has invalid `null` value.'
|
||||
. ' If `null` is a valid value, operator (IS, IS NOT) is missing.',
|
||||
$expression,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new ComparisonExpression($expression, $value, $type, $operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type name for the passed field if it was stored in the typeMap
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field name to get a type for.
|
||||
* @return string|null The computed type or null, if the type is unknown.
|
||||
*/
|
||||
protected function _calculateType(ExpressionInterface|string $field): ?string
|
||||
{
|
||||
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
|
||||
if (!is_string($field)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getTypeMap()->type($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this object and its subtree of expressions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->_conditions as $i => $condition) {
|
||||
if ($condition instanceof ExpressionInterface) {
|
||||
$this->_conditions[$i] = clone $condition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?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 4.2.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* String expression with collation.
|
||||
*/
|
||||
class StringExpression implements ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $string;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $collation;
|
||||
|
||||
/**
|
||||
* @param string $string String value
|
||||
* @param string $collation String collation
|
||||
*/
|
||||
public function __construct(string $string, string $collation)
|
||||
{
|
||||
$this->string = $string;
|
||||
$this->collation = $collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string collation.
|
||||
*
|
||||
* @param string $collation String collation
|
||||
* @return void
|
||||
*/
|
||||
public function setCollation(string $collation): void
|
||||
{
|
||||
$this->collation = $collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string collation.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCollation(): string
|
||||
{
|
||||
return $this->collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$placeholder = $binder->placeholder('c');
|
||||
$binder->bind($placeholder, $this->string, 'string');
|
||||
|
||||
return $placeholder . ' COLLATE ' . $this->collation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* This expression represents SQL fragments that are used for comparing one tuple
|
||||
* to another, one tuple to a set of other tuples or one tuple to an expression
|
||||
*/
|
||||
class TupleComparison extends ComparisonExpression
|
||||
{
|
||||
/**
|
||||
* The type to be used for casting the value to a database representation
|
||||
*
|
||||
* @var array<string|null>
|
||||
*/
|
||||
protected array $types;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $fields the fields to use to form a tuple
|
||||
* @param \Cake\Database\ExpressionInterface|array $values the values to use to form a tuple
|
||||
* @param array<string|null> $types the types names to use for casting each of the values, only
|
||||
* one type per position in the value array in needed
|
||||
* @param string $conjunction the operator used for comparing field and value
|
||||
*/
|
||||
public function __construct(
|
||||
ExpressionInterface|array|string $fields,
|
||||
ExpressionInterface|array $values,
|
||||
array $types = [],
|
||||
string $conjunction = '=',
|
||||
) {
|
||||
$this->types = $types;
|
||||
$this->setField($fields);
|
||||
$this->_operator = $conjunction;
|
||||
$this->setValue($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type to be used for casting the value to a database representation
|
||||
*
|
||||
* @return array<string|null>
|
||||
*/
|
||||
public function getType(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value
|
||||
*
|
||||
* @param mixed $value The value to compare
|
||||
* @return void
|
||||
*/
|
||||
public function setValue(mixed $value): void
|
||||
{
|
||||
if ($this->isMulti()) {
|
||||
if (is_array($value) && !is_array(current($value))) {
|
||||
throw new InvalidArgumentException(
|
||||
'Multi-tuple comparisons require a multi-tuple value, single-tuple given.',
|
||||
);
|
||||
}
|
||||
} elseif (is_array($value) && is_array(current($value))) {
|
||||
throw new InvalidArgumentException(
|
||||
'Single-tuple comparisons require a single-tuple value, multi-tuple given.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$template = '(%s) %s (%s)';
|
||||
$fields = [];
|
||||
$originalFields = $this->getField();
|
||||
|
||||
if (!is_array($originalFields)) {
|
||||
$originalFields = [$originalFields];
|
||||
}
|
||||
|
||||
foreach ($originalFields as $field) {
|
||||
$fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field;
|
||||
}
|
||||
|
||||
$values = $this->_stringifyValues($binder);
|
||||
|
||||
$field = implode(', ', $fields);
|
||||
|
||||
return sprintf($template, $field, $this->_operator, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with the values as placeholders in a string to be used
|
||||
* for the SQL version of this expression
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with.
|
||||
* @return string
|
||||
*/
|
||||
protected function _stringifyValues(ValueBinder $binder): string
|
||||
{
|
||||
$values = [];
|
||||
$parts = $this->getValue();
|
||||
|
||||
if ($parts instanceof ExpressionInterface) {
|
||||
return $parts->sql($binder);
|
||||
}
|
||||
|
||||
foreach ($parts as $i => $value) {
|
||||
if ($value instanceof ExpressionInterface) {
|
||||
$values[] = $value->sql($binder);
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->types;
|
||||
$isMultiOperation = $this->isMulti();
|
||||
if (!$type) {
|
||||
$type = null;
|
||||
}
|
||||
|
||||
if ($isMultiOperation) {
|
||||
$bound = [];
|
||||
foreach ($value as $k => $val) {
|
||||
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
|
||||
assert($valType === null || is_scalar($valType));
|
||||
$bound[] = $this->_bindValue($val, $binder, $valType);
|
||||
}
|
||||
|
||||
$values[] = sprintf('(%s)', implode(',', $bound));
|
||||
continue;
|
||||
}
|
||||
|
||||
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
|
||||
assert($valType === null || is_scalar($valType));
|
||||
$values[] = $this->_bindValue($value, $binder, $valType);
|
||||
}
|
||||
|
||||
return implode(', ', $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _bindValue(mixed $value, ValueBinder $binder, ?string $type = null): string
|
||||
{
|
||||
$placeholder = $binder->placeholder('tuple');
|
||||
$binder->bind($placeholder, $value, $type);
|
||||
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
$fields = (array)$this->getField();
|
||||
foreach ($fields as $field) {
|
||||
$this->_traverseValue($field, $callback);
|
||||
}
|
||||
|
||||
$value = $this->getValue();
|
||||
if ($value instanceof ExpressionInterface) {
|
||||
$callback($value);
|
||||
$value->traverse($callback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
foreach ($value as $val) {
|
||||
if ($this->isMulti()) {
|
||||
foreach ($val as $v) {
|
||||
$this->_traverseValue($v, $callback);
|
||||
}
|
||||
} else {
|
||||
$this->_traverseValue($val, $callback);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally executes the callback for the passed value if
|
||||
* it is an ExpressionInterface
|
||||
*
|
||||
* @param mixed $value The value to traverse
|
||||
* @param \Closure $callback The callback to use when traversing
|
||||
* @return void
|
||||
*/
|
||||
protected function _traverseValue(mixed $value, Closure $callback): void
|
||||
{
|
||||
if ($value instanceof ExpressionInterface) {
|
||||
$callback($value);
|
||||
$value->traverse($callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if each of the values in this expressions is a tuple in
|
||||
* itself
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMulti(): bool
|
||||
{
|
||||
return in_array(strtolower($this->_operator), ['in', 'not in']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An expression object that represents an expression with only a single operand.
|
||||
*/
|
||||
class UnaryExpression implements ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* Indicates that the operation is in pre-order
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const PREFIX = 0;
|
||||
|
||||
/**
|
||||
* Indicates that the operation is in post-order
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const POSTFIX = 1;
|
||||
|
||||
/**
|
||||
* The operator this unary expression represents
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_operator;
|
||||
|
||||
/**
|
||||
* Holds the value which the unary expression operates
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected mixed $_value;
|
||||
|
||||
/**
|
||||
* Where to place the operator
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $position;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $operator The operator to used for the expression
|
||||
* @param mixed $value the value to use as the operand for the expression
|
||||
* @param int $position either UnaryExpression::PREFIX or UnaryExpression::POSTFIX
|
||||
*/
|
||||
public function __construct(string $operator, mixed $value, int $position = self::PREFIX)
|
||||
{
|
||||
$this->_operator = $operator;
|
||||
$this->_value = $value;
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$operand = $this->_value;
|
||||
if ($operand instanceof ExpressionInterface) {
|
||||
$operand = $operand->sql($binder);
|
||||
}
|
||||
|
||||
if ($this->position === self::POSTFIX) {
|
||||
return '(' . $operand . ') ' . $this->_operator;
|
||||
}
|
||||
|
||||
return $this->_operator . ' (' . $operand . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->_value instanceof ExpressionInterface) {
|
||||
$callback($this->_value);
|
||||
$this->_value->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a deep clone of the inner expression.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->_value instanceof ExpressionInterface) {
|
||||
$this->_value = clone $this->_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?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 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\TypeMap;
|
||||
use Cake\Database\TypeMapTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An expression object to contain values being inserted.
|
||||
*
|
||||
* Helps generate SQL with the correct number of placeholders and bind
|
||||
* values correctly into the statement.
|
||||
*/
|
||||
class ValuesExpression implements ExpressionInterface
|
||||
{
|
||||
use ExpressionTypeCasterTrait;
|
||||
use TypeMapTrait;
|
||||
|
||||
/**
|
||||
* Array of values to insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $_values = [];
|
||||
|
||||
/**
|
||||
* List of columns to ensure are part of the insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $_columns = [];
|
||||
|
||||
/**
|
||||
* The Query object to use as a values expression
|
||||
*
|
||||
* @var \Cake\Database\Query|null
|
||||
*/
|
||||
protected ?Query $_query = null;
|
||||
|
||||
/**
|
||||
* Whether values have been casted to expressions
|
||||
* already.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_castedExpressions = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $columns The list of columns that are going to be part of the values.
|
||||
* @param \Cake\Database\TypeMap $typeMap A dictionary of column -> type names
|
||||
*/
|
||||
public function __construct(array $columns, TypeMap $typeMap)
|
||||
{
|
||||
$this->_columns = $columns;
|
||||
$this->setTypeMap($typeMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a row of data to be inserted.
|
||||
*
|
||||
* @param \Cake\Database\Query|array $values Array of data to append into the insert, or
|
||||
* a query for doing INSERT INTO .. SELECT style commands
|
||||
* @return void
|
||||
* @throws \Cake\Database\Exception\DatabaseException When mixing array and Query data types.
|
||||
*/
|
||||
public function add(Query|array $values): void
|
||||
{
|
||||
if (
|
||||
(
|
||||
count($this->_values) &&
|
||||
$values instanceof Query
|
||||
) ||
|
||||
(
|
||||
$this->_query &&
|
||||
is_array($values)
|
||||
)
|
||||
) {
|
||||
throw new DatabaseException(
|
||||
'You cannot mix subqueries and array values in inserts.',
|
||||
);
|
||||
}
|
||||
if ($values instanceof Query) {
|
||||
$this->setQuery($values);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->_values[] = $values;
|
||||
$this->_castedExpressions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the columns to be inserted.
|
||||
*
|
||||
* @param array $columns Array with columns to be inserted.
|
||||
* @return $this
|
||||
*/
|
||||
public function setColumns(array $columns)
|
||||
{
|
||||
$this->_columns = $columns;
|
||||
$this->_castedExpressions = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the columns to be inserted.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns(): array
|
||||
{
|
||||
return $this->_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bare column names.
|
||||
*
|
||||
* Because column names could be identifier quoted, we
|
||||
* need to strip the identifiers off of the columns.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function _columnNames(): array
|
||||
{
|
||||
$columns = [];
|
||||
foreach ($this->_columns as $col) {
|
||||
if (is_string($col)) {
|
||||
$col = trim($col, '`[]"');
|
||||
}
|
||||
$columns[] = $col;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values to be inserted.
|
||||
*
|
||||
* @param array $values Array with values to be inserted.
|
||||
* @return $this
|
||||
*/
|
||||
public function setValues(array $values)
|
||||
{
|
||||
$this->_values = $values;
|
||||
$this->_castedExpressions = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values to be inserted.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getValues(): array
|
||||
{
|
||||
if (!$this->_castedExpressions) {
|
||||
$this->_processExpressions();
|
||||
}
|
||||
|
||||
return $this->_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the query object to be used as the values expression to be evaluated
|
||||
* to insert records in the table.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to set
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(Query $query)
|
||||
{
|
||||
$this->_query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the query object to be used as the values expression to be evaluated
|
||||
* to insert records in the table.
|
||||
*
|
||||
* @return \Cake\Database\Query|null
|
||||
*/
|
||||
public function getQuery(): ?Query
|
||||
{
|
||||
return $this->_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
if (!$this->_values && $this->_query === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->_castedExpressions) {
|
||||
$this->_processExpressions();
|
||||
}
|
||||
|
||||
$columns = $this->_columnNames();
|
||||
$defaults = array_fill_keys($columns, null);
|
||||
$placeholders = [];
|
||||
|
||||
$types = [];
|
||||
$typeMap = $this->getTypeMap();
|
||||
foreach ($defaults as $col => $v) {
|
||||
$types[$col] = $typeMap->type($col);
|
||||
}
|
||||
|
||||
foreach ($this->_values as $row) {
|
||||
$row += $defaults;
|
||||
$rowPlaceholders = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$value = $row[$column];
|
||||
|
||||
if ($value instanceof ExpressionInterface) {
|
||||
$rowPlaceholders[] = '(' . $value->sql($binder) . ')';
|
||||
continue;
|
||||
}
|
||||
|
||||
$placeholder = $binder->placeholder('c');
|
||||
$rowPlaceholders[] = $placeholder;
|
||||
$binder->bind($placeholder, $value, $types[$column]);
|
||||
}
|
||||
|
||||
$placeholders[] = implode(', ', $rowPlaceholders);
|
||||
}
|
||||
|
||||
$query = $this->getQuery();
|
||||
if ($query) {
|
||||
return ' ' . $query->sql($binder);
|
||||
}
|
||||
|
||||
return sprintf(' VALUES (%s)', implode('), (', $placeholders));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->_query) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!$this->_castedExpressions) {
|
||||
$this->_processExpressions();
|
||||
}
|
||||
|
||||
foreach ($this->_values as $v) {
|
||||
if ($v instanceof ExpressionInterface) {
|
||||
$v->traverse($callback);
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($v as $field) {
|
||||
if ($field instanceof ExpressionInterface) {
|
||||
$callback($field);
|
||||
$field->traverse($callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts values that need to be casted to expressions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _processExpressions(): void
|
||||
{
|
||||
$types = [];
|
||||
$typeMap = $this->getTypeMap();
|
||||
|
||||
$columns = $this->_columnNames();
|
||||
foreach ($columns as $c) {
|
||||
if (!is_string($c) && !is_int($c)) {
|
||||
continue;
|
||||
}
|
||||
$types[$c] = $typeMap->type($c);
|
||||
}
|
||||
|
||||
$types = $this->_requiresToExpressionCasting($types);
|
||||
|
||||
if (!$types) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->_values as $row => $values) {
|
||||
foreach ($types as $col => $type) {
|
||||
/** @var \Cake\Database\Type\ExpressionTypeInterface $type */
|
||||
$this->_values[$row][$col] = $type->toExpression($values[$col]);
|
||||
}
|
||||
}
|
||||
$this->_castedExpressions = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?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 4.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||||
use Cake\Database\TypeMap;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Represents a SQL when/then clause with a fluid API
|
||||
*/
|
||||
class WhenThenExpression implements ExpressionInterface
|
||||
{
|
||||
use CaseExpressionTrait;
|
||||
use ExpressionTypeCasterTrait;
|
||||
|
||||
/**
|
||||
* The names of the clauses that are valid for use with the
|
||||
* `clause()` method.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $validClauseNames = [
|
||||
'when',
|
||||
'then',
|
||||
];
|
||||
|
||||
/**
|
||||
* The type map to use when using an array of conditions for the
|
||||
* `WHEN` value.
|
||||
*
|
||||
* @var \Cake\Database\TypeMap
|
||||
*/
|
||||
protected TypeMap $_typeMap;
|
||||
|
||||
/**
|
||||
* Then `WHEN` value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected mixed $when = null;
|
||||
|
||||
/**
|
||||
* The `WHEN` value type.
|
||||
*
|
||||
* @var array|string|null
|
||||
*/
|
||||
protected array|string|null $whenType = null;
|
||||
|
||||
/**
|
||||
* The `THEN` value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected mixed $then = null;
|
||||
|
||||
/**
|
||||
* Whether the `THEN` value has been defined, eg whether `then()`
|
||||
* has been invoked.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $hasThenBeenDefined = false;
|
||||
|
||||
/**
|
||||
* The `THEN` result type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $thenType = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Cake\Database\TypeMap|null $typeMap The type map to use when using an array of conditions for the `WHEN`
|
||||
* value.
|
||||
*/
|
||||
public function __construct(?TypeMap $typeMap = null)
|
||||
{
|
||||
$this->_typeMap = $typeMap ?? new TypeMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `WHEN` value.
|
||||
*
|
||||
* @param object|array|string|float|int|bool $when The `WHEN` value. When using an array of
|
||||
* conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is _not_
|
||||
* completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If you
|
||||
* plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to be
|
||||
* a non-array, and then always binds the data), use a conditions array where the user data is only passed on the
|
||||
* value side of the array entries, or custom bindings!
|
||||
* @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
|
||||
* conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException In case the `$when` argument is an empty array.
|
||||
* @throws \InvalidArgumentException In case the `$when` argument is an array, and the `$type` argument is neither
|
||||
* an array, nor null.
|
||||
* @throws \InvalidArgumentException In case the `$when` argument is a non-array value, and the `$type` argument is
|
||||
* neither a string, nor null.
|
||||
* @see CaseStatementExpression::when() for a more detailed usage explanation.
|
||||
*/
|
||||
public function when(object|array|string|float|int|bool $when, array|string|null $type = null)
|
||||
{
|
||||
if (is_array($when)) {
|
||||
if (!$when) {
|
||||
throw new InvalidArgumentException('The `$when` argument must be a non-empty array');
|
||||
}
|
||||
|
||||
if (
|
||||
$type !== null &&
|
||||
!is_array($type)
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'When using an array for the `$when` argument, the `$type` argument must be an ' .
|
||||
'array too, `%s` given.',
|
||||
get_debug_type($type),
|
||||
));
|
||||
}
|
||||
|
||||
// avoid dirtying the type map for possible consecutive `when()` calls
|
||||
$typeMap = clone $this->_typeMap;
|
||||
if (
|
||||
is_array($type) &&
|
||||
$type !== []
|
||||
) {
|
||||
$typeMap = $typeMap->setTypes($type);
|
||||
}
|
||||
|
||||
$when = new QueryExpression($when, $typeMap);
|
||||
} else {
|
||||
if (
|
||||
$type !== null &&
|
||||
!is_string($type)
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'When using a non-array value for the `$when` argument, the `$type` argument must ' .
|
||||
'be a string, `%s` given.',
|
||||
get_debug_type($type),
|
||||
));
|
||||
}
|
||||
|
||||
if (
|
||||
$type === null &&
|
||||
!($when instanceof ExpressionInterface)
|
||||
) {
|
||||
$type = $this->inferType($when);
|
||||
}
|
||||
}
|
||||
|
||||
$this->when = $when;
|
||||
$this->whenType = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `THEN` result value.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
|
||||
* @param string|null $type The result type. If no type is provided, the type will be inferred from the given
|
||||
* result value.
|
||||
* @return $this
|
||||
*/
|
||||
public function then(mixed $result, ?string $type = null)
|
||||
{
|
||||
if (
|
||||
$result !== null &&
|
||||
!is_scalar($result) &&
|
||||
!(is_object($result) && !($result instanceof Closure))
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'The `$result` argument must be either `null`, a scalar value, an object, ' .
|
||||
'or an instance of `\%s`, `%s` given.',
|
||||
ExpressionInterface::class,
|
||||
get_debug_type($result),
|
||||
));
|
||||
}
|
||||
|
||||
$this->then = $result;
|
||||
|
||||
$this->thenType = $type ?? $this->inferType($result);
|
||||
|
||||
$this->hasThenBeenDefined = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expression's result value type.
|
||||
*
|
||||
* @return string|null
|
||||
* @see WhenThenExpression::then()
|
||||
*/
|
||||
public function getResultType(): ?string
|
||||
{
|
||||
return $this->thenType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available data for the given clause.
|
||||
*
|
||||
* ### Available clauses
|
||||
*
|
||||
* The following clause names are available:
|
||||
*
|
||||
* * `when`: The `WHEN` value.
|
||||
* * `then`: The `THEN` result value.
|
||||
*
|
||||
* @param string $clause The name of the clause to obtain.
|
||||
* @return \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
* @throws \InvalidArgumentException In case the given clause name is invalid.
|
||||
*/
|
||||
public function clause(string $clause): mixed
|
||||
{
|
||||
if (!in_array($clause, $this->validClauseNames, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
|
||||
implode('`, `', $this->validClauseNames),
|
||||
$clause,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->{$clause};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
if ($this->when === null) {
|
||||
throw new LogicException('Case expression has incomplete when clause. Missing `when()`.');
|
||||
}
|
||||
|
||||
if (!$this->hasThenBeenDefined) {
|
||||
throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
||||
}
|
||||
|
||||
$when = $this->when;
|
||||
if (
|
||||
is_string($this->whenType) &&
|
||||
!($when instanceof ExpressionInterface)
|
||||
) {
|
||||
$when = $this->_castToExpression($when, $this->whenType);
|
||||
}
|
||||
if ($when instanceof Query) {
|
||||
$when = sprintf('(%s)', $when->sql($binder));
|
||||
} elseif ($when instanceof ExpressionInterface) {
|
||||
$when = $when->sql($binder);
|
||||
} else {
|
||||
$placeholder = $binder->placeholder('c');
|
||||
if (is_string($this->whenType)) {
|
||||
$whenType = $this->whenType;
|
||||
} else {
|
||||
$whenType = null;
|
||||
}
|
||||
$binder->bind($placeholder, $when, $whenType);
|
||||
$when = $placeholder;
|
||||
}
|
||||
|
||||
$then = $this->compileNullableValue($binder, $this->then, $this->thenType);
|
||||
|
||||
return "WHEN {$when} THEN {$then}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
if ($this->when instanceof ExpressionInterface) {
|
||||
$callback($this->when);
|
||||
$this->when->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->then instanceof ExpressionInterface) {
|
||||
$callback($this->then);
|
||||
$this->then->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the inner expression objects.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->when instanceof ExpressionInterface) {
|
||||
$this->when = clone $this->when;
|
||||
}
|
||||
|
||||
if ($this->then instanceof ExpressionInterface) {
|
||||
$this->then = clone $this->then;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?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 4.1.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* This represents a SQL window expression used by aggregate and window functions.
|
||||
*/
|
||||
class WindowExpression implements ExpressionInterface, WindowInterface
|
||||
{
|
||||
/**
|
||||
* @var \Cake\Database\Expression\IdentifierExpression
|
||||
*/
|
||||
protected IdentifierExpression $name;
|
||||
|
||||
/**
|
||||
* @var array<\Cake\Database\ExpressionInterface>
|
||||
*/
|
||||
protected array $partitions = [];
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Expression\OrderByExpression|null
|
||||
*/
|
||||
protected ?OrderByExpression $order = null;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected ?array $frame = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $exclusion = null;
|
||||
|
||||
/**
|
||||
* @param string $name Window name
|
||||
*/
|
||||
public function __construct(string $name = '')
|
||||
{
|
||||
$this->name = new IdentifierExpression($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether is only a named window expression.
|
||||
*
|
||||
* These window expressions only specify a named window and do not
|
||||
* specify their own partitions, frame or order.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNamedOnly(): bool
|
||||
{
|
||||
return $this->name->getIdentifier() && (!$this->partitions && !$this->frame && !$this->order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the window name.
|
||||
*
|
||||
* @param string $name Window name
|
||||
* @return $this
|
||||
*/
|
||||
public function name(string $name)
|
||||
{
|
||||
$this->name = new IdentifierExpression($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function partition(ExpressionInterface|Closure|array|string $partitions)
|
||||
{
|
||||
if (!$partitions) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($partitions instanceof Closure) {
|
||||
$partitions = $partitions(new QueryExpression([], [], ''));
|
||||
}
|
||||
|
||||
if (!is_array($partitions)) {
|
||||
$partitions = [$partitions];
|
||||
}
|
||||
|
||||
foreach ($partitions as &$partition) {
|
||||
if (is_string($partition)) {
|
||||
$partition = new IdentifierExpression($partition);
|
||||
}
|
||||
}
|
||||
|
||||
$this->partitions = array_merge($this->partitions, $partitions);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function order(ExpressionInterface|Closure|array|string $fields)
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.0.0',
|
||||
'WindowExpression::order() is deprecated. Use WindowExpression::orderBy() instead.',
|
||||
);
|
||||
|
||||
return $this->orderBy($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderBy(ExpressionInterface|Closure|array|string $fields)
|
||||
{
|
||||
if (!$fields) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->order ??= new OrderByExpression();
|
||||
|
||||
if ($fields instanceof Closure) {
|
||||
$fields = $fields(new QueryExpression([], [], ''));
|
||||
}
|
||||
|
||||
$this->order->add($fields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0)
|
||||
{
|
||||
return $this->frame(self::RANGE, $start, self::PRECEDING, $end, self::FOLLOWING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rows(?int $start, ?int $end = 0)
|
||||
{
|
||||
return $this->frame(self::ROWS, $start, self::PRECEDING, $end, self::FOLLOWING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function groups(?int $start, ?int $end = 0)
|
||||
{
|
||||
return $this->frame(self::GROUPS, $start, self::PRECEDING, $end, self::FOLLOWING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function frame(
|
||||
string $type,
|
||||
ExpressionInterface|string|int|null $startOffset,
|
||||
string $startDirection,
|
||||
ExpressionInterface|string|int|null $endOffset,
|
||||
string $endDirection,
|
||||
) {
|
||||
$this->frame = [
|
||||
'type' => $type,
|
||||
'start' => [
|
||||
'offset' => $startOffset,
|
||||
'direction' => $startDirection,
|
||||
],
|
||||
'end' => [
|
||||
'offset' => $endOffset,
|
||||
'direction' => $endDirection,
|
||||
],
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeCurrent()
|
||||
{
|
||||
$this->exclusion = 'CURRENT ROW';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeGroup()
|
||||
{
|
||||
$this->exclusion = 'GROUP';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeTies()
|
||||
{
|
||||
$this->exclusion = 'TIES';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$clauses = [];
|
||||
if ($this->name->getIdentifier()) {
|
||||
$clauses[] = $this->name->sql($binder);
|
||||
}
|
||||
|
||||
if ($this->partitions) {
|
||||
$expressions = [];
|
||||
foreach ($this->partitions as $partition) {
|
||||
$expressions[] = $partition->sql($binder);
|
||||
}
|
||||
|
||||
$clauses[] = 'PARTITION BY ' . implode(', ', $expressions);
|
||||
}
|
||||
|
||||
if ($this->order) {
|
||||
$clauses[] = $this->order->sql($binder);
|
||||
}
|
||||
|
||||
if ($this->frame) {
|
||||
$start = $this->buildOffsetSql(
|
||||
$binder,
|
||||
$this->frame['start']['offset'],
|
||||
$this->frame['start']['direction'],
|
||||
);
|
||||
$end = $this->buildOffsetSql(
|
||||
$binder,
|
||||
$this->frame['end']['offset'],
|
||||
$this->frame['end']['direction'],
|
||||
);
|
||||
|
||||
$frameSql = sprintf('%s BETWEEN %s AND %s', $this->frame['type'], $start, $end);
|
||||
|
||||
if ($this->exclusion !== null) {
|
||||
$frameSql .= ' EXCLUDE ' . $this->exclusion;
|
||||
}
|
||||
|
||||
$clauses[] = $frameSql;
|
||||
}
|
||||
|
||||
return implode(' ', $clauses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
$callback($this->name);
|
||||
foreach ($this->partitions as $partition) {
|
||||
$callback($partition);
|
||||
$partition->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->order) {
|
||||
$callback($this->order);
|
||||
$this->order->traverse($callback);
|
||||
}
|
||||
|
||||
if ($this->frame !== null) {
|
||||
$offset = $this->frame['start']['offset'];
|
||||
if ($offset instanceof ExpressionInterface) {
|
||||
$callback($offset);
|
||||
$offset->traverse($callback);
|
||||
}
|
||||
$offset = $this->frame['end']['offset'] ?? null;
|
||||
if ($offset instanceof ExpressionInterface) {
|
||||
$callback($offset);
|
||||
$offset->traverse($callback);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds frame offset sql.
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder
|
||||
* @param \Cake\Database\ExpressionInterface|string|int|null $offset Frame offset
|
||||
* @param string $direction Frame offset direction
|
||||
* @return string
|
||||
*/
|
||||
protected function buildOffsetSql(
|
||||
ValueBinder $binder,
|
||||
ExpressionInterface|string|int|null $offset,
|
||||
string $direction,
|
||||
): string {
|
||||
if ($offset === 0) {
|
||||
return 'CURRENT ROW';
|
||||
}
|
||||
|
||||
if ($offset instanceof ExpressionInterface) {
|
||||
$offset = $offset->sql($binder);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s %s',
|
||||
$offset ?? 'UNBOUNDED',
|
||||
$direction,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this object and its subtree of expressions.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->name = clone $this->name;
|
||||
foreach ($this->partitions as $i => $partition) {
|
||||
$this->partitions[$i] = clone $partition;
|
||||
}
|
||||
if ($this->order !== null) {
|
||||
$this->order = clone $this->order;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?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 4.1.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* This defines the functions used for building window expressions.
|
||||
*/
|
||||
interface WindowInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const PRECEDING = 'PRECEDING';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const FOLLOWING = 'FOLLOWING';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const RANGE = 'RANGE';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const ROWS = 'ROWS';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const GROUPS = 'GROUPS';
|
||||
|
||||
/**
|
||||
* Adds one or more partition expressions to the window.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $partitions Partition expressions
|
||||
* @return $this
|
||||
*/
|
||||
public function partition(ExpressionInterface|Closure|array|string $partitions);
|
||||
|
||||
/**
|
||||
* Adds one or more order by clauses to the window.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
|
||||
* @return $this
|
||||
* @deprecated 5.0.0 Use orderBy() instead.
|
||||
*/
|
||||
public function order(ExpressionInterface|Closure|array|string $fields);
|
||||
|
||||
/**
|
||||
* Adds one or more order by clauses to the window.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
|
||||
* @return $this
|
||||
*/
|
||||
public function orderBy(ExpressionInterface|Closure|array|string $fields);
|
||||
|
||||
/**
|
||||
* Adds a simple range frame to the window.
|
||||
*
|
||||
* `$start`:
|
||||
* - `0` - 'CURRENT ROW'
|
||||
* - `null` - 'UNBOUNDED PRECEDING'
|
||||
* - offset - 'offset PRECEDING'
|
||||
*
|
||||
* `$end`:
|
||||
* - `0` - 'CURRENT ROW'
|
||||
* - `null` - 'UNBOUNDED FOLLOWING'
|
||||
* - offset - 'offset FOLLOWING'
|
||||
*
|
||||
* If you need to use 'FOLLOWING' with frame start or
|
||||
* 'PRECEDING' with frame end, use `frame()` instead.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string|int|null $start Frame start
|
||||
* @param \Cake\Database\ExpressionInterface|string|int|null $end Frame end
|
||||
* If not passed in, only frame start SQL will be generated.
|
||||
* @return $this
|
||||
*/
|
||||
public function range(ExpressionInterface|string|int|null $start, ExpressionInterface|string|int|null $end = 0);
|
||||
|
||||
/**
|
||||
* Adds a simple rows frame to the window.
|
||||
*
|
||||
* See `range()` for details.
|
||||
*
|
||||
* @param int|null $start Frame start
|
||||
* @param int|null $end Frame end
|
||||
* If not passed in, only frame start SQL will be generated.
|
||||
* @return $this
|
||||
*/
|
||||
public function rows(?int $start, ?int $end = 0);
|
||||
|
||||
/**
|
||||
* Adds a simple groups frame to the window.
|
||||
*
|
||||
* See `range()` for details.
|
||||
*
|
||||
* @param int|null $start Frame start
|
||||
* @param int|null $end Frame end
|
||||
* If not passed in, only frame start SQL will be generated.
|
||||
* @return $this
|
||||
*/
|
||||
public function groups(?int $start, ?int $end = 0);
|
||||
|
||||
/**
|
||||
* Adds a frame to the window.
|
||||
*
|
||||
* Use the `range()`, `rows()` or `groups()` helpers if you need simple
|
||||
* 'BETWEEN offset PRECEDING and offset FOLLOWING' frames.
|
||||
*
|
||||
* You can specify any direction for both frame start and frame end.
|
||||
*
|
||||
* With both `$startOffset` and `$endOffset`:
|
||||
* - `0` - 'CURRENT ROW'
|
||||
* - `null` - 'UNBOUNDED'
|
||||
*
|
||||
* @param string $type Frame type
|
||||
* @param \Cake\Database\ExpressionInterface|string|int|null $startOffset Frame start offset
|
||||
* @param string $startDirection Frame start direction
|
||||
* @param \Cake\Database\ExpressionInterface|string|int|null $endOffset Frame end offset
|
||||
* @param string $endDirection Frame end direction
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException WHen offsets are negative.
|
||||
* @phpstan-param self::RANGE|self::ROWS|self::GROUPS $type
|
||||
* @phpstan-param self::PRECEDING|self::FOLLOWING $startDirection
|
||||
* @phpstan-param self::PRECEDING|self::FOLLOWING $endDirection
|
||||
*/
|
||||
public function frame(
|
||||
string $type,
|
||||
ExpressionInterface|string|int|null $startOffset,
|
||||
string $startDirection,
|
||||
ExpressionInterface|string|int|null $endOffset,
|
||||
string $endDirection,
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds current row frame exclusion.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function excludeCurrent();
|
||||
|
||||
/**
|
||||
* Adds group frame exclusion.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function excludeGroup();
|
||||
|
||||
/**
|
||||
* Adds ties frame exclusion.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function excludeTies();
|
||||
}
|
||||
Reference in New Issue
Block a user