init
This commit is contained in:
+833
@@ -0,0 +1,833 @@
|
||||
<?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;
|
||||
|
||||
use Cake\Cache\Cache;
|
||||
use Cake\Core\App;
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Core\Retry\CommandRetry;
|
||||
use Cake\Database\Exception\MissingDriverException;
|
||||
use Cake\Database\Exception\MissingExtensionException;
|
||||
use Cake\Database\Exception\NestedTransactionRollbackException;
|
||||
use Cake\Database\Query\DeleteQuery;
|
||||
use Cake\Database\Query\InsertQuery;
|
||||
use Cake\Database\Query\QueryFactory;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\Query\UpdateQuery;
|
||||
use Cake\Database\Retry\ReconnectStrategy;
|
||||
use Cake\Database\Schema\CachedCollection;
|
||||
use Cake\Database\Schema\Collection as SchemaCollection;
|
||||
use Cake\Database\Schema\CollectionInterface as SchemaCollectionInterface;
|
||||
use Cake\Datasource\ConnectionInterface;
|
||||
use Cake\Log\Log;
|
||||
use Closure;
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Throwable;
|
||||
use function Cake\Core\env;
|
||||
|
||||
/**
|
||||
* Represents a connection with a database server.
|
||||
*/
|
||||
class Connection implements ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Contains the configuration params for this connection.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_config;
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected Driver $readDriver;
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected Driver $writeDriver;
|
||||
|
||||
/**
|
||||
* Contains how many nested transactions have been started.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $_transactionLevel = 0;
|
||||
|
||||
/**
|
||||
* Whether a transaction is active in this connection.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_transactionStarted = false;
|
||||
|
||||
/**
|
||||
* Whether this connection can and should use savepoints for nested
|
||||
* transactions.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useSavePoints = false;
|
||||
|
||||
/**
|
||||
* Cacher object instance.
|
||||
*
|
||||
* @var \Psr\SimpleCache\CacheInterface|null
|
||||
*/
|
||||
protected ?CacheInterface $cacher = null;
|
||||
|
||||
/**
|
||||
* The schema collection object
|
||||
*
|
||||
* @var \Cake\Database\Schema\CollectionInterface|null
|
||||
*/
|
||||
protected ?SchemaCollectionInterface $_schemaCollection = null;
|
||||
|
||||
/**
|
||||
* NestedTransactionRollbackException object instance, will be stored if
|
||||
* the rollback method is called in some nested transaction.
|
||||
*
|
||||
* @var \Cake\Database\Exception\NestedTransactionRollbackException|null
|
||||
*/
|
||||
protected ?NestedTransactionRollbackException $nestedTransactionRollbackException = null;
|
||||
|
||||
protected QueryFactory $queryFactory;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* ### Available options:
|
||||
*
|
||||
* - `driver` Sort name or FQCN for driver.
|
||||
* - `log` Boolean indicating whether to use query logging.
|
||||
* - `name` Connection name.
|
||||
* - `cacheMetaData` Boolean indicating whether metadata (datasource schemas) should be cached.
|
||||
* If set to a string it will be used as the name of cache config to use.
|
||||
* - `cacheKeyPrefix` Custom prefix to use when generation cache keys. Defaults to connection name.
|
||||
*
|
||||
* @param array<string, mixed> $config Configuration array.
|
||||
* @throws \Cake\Database\Exception\MissingDriverException when the driver class cannot be found
|
||||
* @throws \Cake\Database\Exception\MissingExtensionException when the database extension is not enabled
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->_config = $config;
|
||||
[self::ROLE_READ => $this->readDriver, self::ROLE_WRITE => $this->writeDriver] = $this->createDrivers($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates read and write drivers.
|
||||
*
|
||||
* @param array<string, mixed> $config Connection config
|
||||
* @return array<string, \Cake\Database\Driver>
|
||||
* @phpstan-return array{read: \Cake\Database\Driver, write: \Cake\Database\Driver}
|
||||
*/
|
||||
protected function createDrivers(array $config): array
|
||||
{
|
||||
$driver = $config['driver'] ?? '';
|
||||
if (!is_string($driver)) {
|
||||
assert($driver instanceof Driver);
|
||||
if (!$driver->enabled()) {
|
||||
throw new MissingExtensionException(['driver' => $driver::class, 'name' => $this->configName()]);
|
||||
}
|
||||
|
||||
// Legacy support for setting instance instead of driver class
|
||||
return [self::ROLE_READ => $driver, self::ROLE_WRITE => $driver];
|
||||
}
|
||||
|
||||
/** @var class-string<\Cake\Database\Driver>|null $driverClass */
|
||||
$driverClass = App::className($driver, 'Database/Driver');
|
||||
if ($driverClass === null) {
|
||||
throw new MissingDriverException(['driver' => $driver, 'connection' => $this->configName()]);
|
||||
}
|
||||
|
||||
$sharedConfig = array_diff_key($config, array_flip([
|
||||
'name',
|
||||
'className',
|
||||
'driver',
|
||||
'cacheMetaData',
|
||||
'cacheKeyPrefix',
|
||||
'read',
|
||||
'write',
|
||||
]));
|
||||
|
||||
$writeConfig = ($config['write'] ?? []) + $sharedConfig;
|
||||
$readConfig = ($config['read'] ?? []) + $sharedConfig;
|
||||
if (array_key_exists('write', $config) || array_key_exists('read', $config)) {
|
||||
$readDriver = new $driverClass(['_role' => self::ROLE_READ] + $readConfig);
|
||||
$writeDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
|
||||
} else {
|
||||
$readDriver = new $driverClass(['_role' => self::ROLE_WRITE] + $writeConfig);
|
||||
$writeDriver = $readDriver;
|
||||
}
|
||||
|
||||
if (!$writeDriver->enabled()) {
|
||||
throw new MissingExtensionException(['driver' => $writeDriver::class, 'name' => $this->configName()]);
|
||||
}
|
||||
|
||||
return [self::ROLE_READ => $readDriver, self::ROLE_WRITE => $writeDriver];
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* Disconnects the driver to release the connection.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->_transactionStarted && class_exists(Log::class)) {
|
||||
$message = 'The connection is going to be closed but there is an active transaction.';
|
||||
|
||||
$requestUrl = env('REQUEST_URI');
|
||||
if ($requestUrl) {
|
||||
$message .= "\nRequest URL: " . $requestUrl;
|
||||
}
|
||||
|
||||
$clientIp = env('REMOTE_ADDR');
|
||||
if ($clientIp) {
|
||||
$message .= "\nClient IP: " . $clientIp;
|
||||
}
|
||||
|
||||
Log::warning($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function config(): array
|
||||
{
|
||||
return $this->_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function configName(): string
|
||||
{
|
||||
return $this->_config['name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection role: read or write.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function role(): string
|
||||
{
|
||||
return preg_match('/:read$/', $this->configName()) === 1 ? static::ROLE_READ : static::ROLE_WRITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the retry wrapper object that is allows recovery from server disconnects
|
||||
* while performing certain database actions, such as executing a query.
|
||||
*
|
||||
* @return \Cake\Core\Retry\CommandRetry The retry wrapper
|
||||
*/
|
||||
public function getDisconnectRetry(): CommandRetry
|
||||
{
|
||||
return new CommandRetry(new ReconnectStrategy($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role-specific driver instance.
|
||||
*
|
||||
* @param string $role Connection role ('read' or 'write')
|
||||
* @return \Cake\Database\Driver
|
||||
*/
|
||||
public function getDriver(string $role = self::ROLE_WRITE): Driver
|
||||
{
|
||||
assert($role === self::ROLE_READ || $role === self::ROLE_WRITE);
|
||||
|
||||
return $role === self::ROLE_READ ? $this->getReadDriver() : $this->getWriteDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the read-role driver instance.
|
||||
*
|
||||
* @return \Cake\Database\Driver
|
||||
*/
|
||||
public function getReadDriver(): Driver
|
||||
{
|
||||
return $this->readDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the write-role driver instance.
|
||||
*
|
||||
* @return \Cake\Database\Driver
|
||||
*/
|
||||
public function getWriteDriver(): Driver
|
||||
{
|
||||
return $this->writeDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query using $params for interpolating values and $types as a hint for each
|
||||
* those params.
|
||||
*
|
||||
* @param string $sql SQL to be executed and interpolated with $params
|
||||
* @param array $params list or associative array of params to be interpolated in $sql as values
|
||||
* @param array $types list or associative array of types to be used for casting values in query
|
||||
* @return \Cake\Database\StatementInterface executed statement
|
||||
*/
|
||||
public function execute(string $sql, array $params = [], array $types = []): StatementInterface
|
||||
{
|
||||
return $this->getDisconnectRetry()->run(fn() => $this->getWriteDriver()->execute($sql, $params, $types));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the provided query after compiling it for the specific driver
|
||||
* dialect and returns the executed Statement object.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to be executed
|
||||
* @return \Cake\Database\StatementInterface executed statement
|
||||
*/
|
||||
public function run(Query $query): StatementInterface
|
||||
{
|
||||
return $this->getDisconnectRetry()->run(fn() => $this->getDriver($query->getConnectionRole())->run($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query factory instance.
|
||||
*
|
||||
* @return \Cake\Database\Query\QueryFactory
|
||||
*/
|
||||
public function queryFactory(): QueryFactory
|
||||
{
|
||||
return $this->queryFactory ??= new QueryFactory($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SelectQuery instance for this connection.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields Fields/columns list for the query.
|
||||
* @param array|string $table The table or list of tables to query.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\SelectQuery<mixed>
|
||||
*/
|
||||
public function selectQuery(
|
||||
ExpressionInterface|Closure|array|string|float|int $fields = [],
|
||||
array|string $table = [],
|
||||
array $types = [],
|
||||
): SelectQuery {
|
||||
return $this->queryFactory()->select($fields, $table, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new InsertQuery instance for this connection.
|
||||
*
|
||||
* @param string|null $table The table to insert rows into.
|
||||
* @param array $values Associative array of column => value to be inserted.
|
||||
* @param array<int|string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\InsertQuery
|
||||
*/
|
||||
public function insertQuery(?string $table = null, array $values = [], array $types = []): InsertQuery
|
||||
{
|
||||
return $this->queryFactory()->insert($table, $values, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new UpdateQuery instance for this connection.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
|
||||
* @param array $values Values to be updated.
|
||||
* @param array $conditions Conditions to be set for the update statement.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\UpdateQuery
|
||||
*/
|
||||
public function updateQuery(
|
||||
ExpressionInterface|string|null $table = null,
|
||||
array $values = [],
|
||||
array $conditions = [],
|
||||
array $types = [],
|
||||
): UpdateQuery {
|
||||
return $this->queryFactory()->update($table, $values, $conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DeleteQuery instance for this connection.
|
||||
*
|
||||
* @param string|null $table The table to delete rows from.
|
||||
* @param array $conditions Conditions to be set for the delete statement.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\DeleteQuery
|
||||
*/
|
||||
public function deleteQuery(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
|
||||
{
|
||||
return $this->queryFactory()->delete($table, $conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Schema\Collection object for this connection.
|
||||
*
|
||||
* @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object
|
||||
* @return $this
|
||||
*/
|
||||
public function setSchemaCollection(SchemaCollectionInterface $collection)
|
||||
{
|
||||
$this->_schemaCollection = $collection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Schema\Collection object for this connection.
|
||||
*
|
||||
* @return \Cake\Database\Schema\CollectionInterface
|
||||
*/
|
||||
public function getSchemaCollection(): SchemaCollectionInterface
|
||||
{
|
||||
if ($this->_schemaCollection !== null) {
|
||||
return $this->_schemaCollection;
|
||||
}
|
||||
|
||||
if (!empty($this->_config['cacheMetadata'])) {
|
||||
return $this->_schemaCollection = new CachedCollection(
|
||||
new SchemaCollection($this),
|
||||
empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'],
|
||||
$this->getCacher(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->_schemaCollection = new SchemaCollection($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an INSERT query on the specified table.
|
||||
*
|
||||
* @param string $table the table to insert values in
|
||||
* @param array $values values to be inserted
|
||||
* @param array<string, string> $types Array containing the types to be used for casting
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function insert(string $table, array $values, array $types = []): StatementInterface
|
||||
{
|
||||
return $this->insertQuery($table, $values, $types)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an UPDATE statement on the specified table.
|
||||
*
|
||||
* @param string $table the table to update rows from
|
||||
* @param array $values values to be updated
|
||||
* @param array $conditions conditions to be set for update statement
|
||||
* @param array<string, string> $types list of associative array containing the types to be used for casting
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface
|
||||
{
|
||||
return $this->updateQuery($table, $values, $conditions, $types)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a DELETE statement on the specified table.
|
||||
*
|
||||
* @param string $table the table to delete rows from
|
||||
* @param array $conditions conditions to be set for delete statement
|
||||
* @param array<string, string> $types list of associative array containing the types to be used for casting
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function delete(string $table, array $conditions = [], array $types = []): StatementInterface
|
||||
{
|
||||
return $this->deleteQuery($table, $conditions, $types)->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new transaction.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function begin(): void
|
||||
{
|
||||
if (!$this->_transactionStarted) {
|
||||
$this->getDisconnectRetry()->run(function (): void {
|
||||
$this->getWriteDriver()->beginTransaction();
|
||||
});
|
||||
|
||||
$this->_transactionLevel = 0;
|
||||
$this->_transactionStarted = true;
|
||||
$this->nestedTransactionRollbackException = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_transactionLevel++;
|
||||
if ($this->isSavePointsEnabled()) {
|
||||
$this->createSavePoint((string)$this->_transactionLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits current transaction.
|
||||
*
|
||||
* @return bool true on success, false otherwise
|
||||
* @throws \Cake\Database\Exception\NestedTransactionRollbackException when a nested transaction was rolled back
|
||||
*/
|
||||
public function commit(): bool
|
||||
{
|
||||
if (!$this->_transactionStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->_transactionLevel === 0) {
|
||||
if ($this->wasNestedTransactionRolledback()) {
|
||||
$e = $this->nestedTransactionRollbackException;
|
||||
assert($e !== null);
|
||||
$this->nestedTransactionRollbackException = null;
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->_transactionStarted = false;
|
||||
$this->nestedTransactionRollbackException = null;
|
||||
|
||||
return $this->getWriteDriver()->commitTransaction();
|
||||
}
|
||||
if ($this->isSavePointsEnabled()) {
|
||||
$this->releaseSavePoint((string)$this->_transactionLevel);
|
||||
}
|
||||
|
||||
$this->_transactionLevel--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback current transaction.
|
||||
*
|
||||
* @param bool|null $toBeginning Whether the transaction should be rolled back to the
|
||||
* beginning of it. Defaults to false if using savepoints, or true if not.
|
||||
* @return bool
|
||||
*/
|
||||
public function rollback(?bool $toBeginning = null): bool
|
||||
{
|
||||
if (!$this->_transactionStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$useSavePoint = $this->isSavePointsEnabled();
|
||||
$toBeginning ??= !$useSavePoint;
|
||||
if ($this->_transactionLevel === 0 || $toBeginning) {
|
||||
$this->_transactionLevel = 0;
|
||||
$this->_transactionStarted = false;
|
||||
$this->nestedTransactionRollbackException = null;
|
||||
$this->getWriteDriver()->rollbackTransaction();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$savePoint = $this->_transactionLevel--;
|
||||
if ($useSavePoint) {
|
||||
$this->rollbackSavepoint($savePoint);
|
||||
} else {
|
||||
$this->nestedTransactionRollbackException ??= new NestedTransactionRollbackException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disables the usage of savepoints, enables only if the driver allows it.
|
||||
*
|
||||
* If you are trying to enable this feature, make sure you check
|
||||
* `isSavePointsEnabled()` to verify that savepoints were enabled successfully.
|
||||
*
|
||||
* @param bool $enable Whether save points should be used.
|
||||
* @return $this
|
||||
*/
|
||||
public function enableSavePoints(bool $enable = true)
|
||||
{
|
||||
if ($enable === false) {
|
||||
$this->_useSavePoints = false;
|
||||
} else {
|
||||
$this->_useSavePoints = $this->getWriteDriver()->supports(DriverFeatureEnum::SAVEPOINT);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the usage of savepoints.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableSavePoints()
|
||||
{
|
||||
$this->_useSavePoints = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this connection is using savepoints for nested transactions
|
||||
*
|
||||
* @return bool true if enabled, false otherwise
|
||||
*/
|
||||
public function isSavePointsEnabled(): bool
|
||||
{
|
||||
return $this->_useSavePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new save point for nested transactions.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return void
|
||||
*/
|
||||
public function createSavePoint(string|int $name): void
|
||||
{
|
||||
$this->execute($this->getWriteDriver()->savePointSQL($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a save point by its name.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return void
|
||||
*/
|
||||
public function releaseSavePoint(string|int $name): void
|
||||
{
|
||||
$sql = $this->getWriteDriver()->releaseSavePointSQL($name);
|
||||
if ($sql) {
|
||||
$this->execute($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback a save point by its name.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return void
|
||||
*/
|
||||
public function rollbackSavepoint(string|int $name): void
|
||||
{
|
||||
$this->execute($this->getWriteDriver()->rollbackSavePointSQL($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run driver specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableForeignKeys(): void
|
||||
{
|
||||
$this->getDisconnectRetry()->run(function (): void {
|
||||
$this->execute($this->getWriteDriver()->disableForeignKeySQL());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run driver specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enableForeignKeys(): void
|
||||
{
|
||||
$this->getDisconnectRetry()->run(function (): void {
|
||||
$this->execute($this->getWriteDriver()->enableForeignKeySQL());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a callback inside a transaction, if any exception occurs
|
||||
* while executing the passed callback, the transaction will be rolled back
|
||||
* If the result of the callback is `false`, the transaction will
|
||||
* also be rolled back. Otherwise the transaction is committed after executing
|
||||
* the callback.
|
||||
*
|
||||
* The callback will receive the connection instance as its first argument.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $connection->transactional(function ($connection) {
|
||||
* $connection->deleteQuery('users')->execute();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param \Closure $callback The callback to execute within a transaction.
|
||||
* @return mixed The return value of the callback.
|
||||
* @throws \Exception Will re-throw any exception raised in $callback after
|
||||
* rolling back the transaction.
|
||||
*/
|
||||
public function transactional(Closure $callback): mixed
|
||||
{
|
||||
$this->begin();
|
||||
|
||||
try {
|
||||
$result = $callback($this);
|
||||
} catch (Throwable $e) {
|
||||
$this->rollback(false);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
$this->rollback(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->commit();
|
||||
} catch (NestedTransactionRollbackException $e) {
|
||||
$this->rollback(false);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether some nested transaction has been already rolled back.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function wasNestedTransactionRolledback(): bool
|
||||
{
|
||||
return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an operation with constraints disabled.
|
||||
*
|
||||
* Constraints should be re-enabled after the callback succeeds/fails.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $connection->disableConstraints(function ($connection) {
|
||||
* $connection->insertQuery('users')->execute();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param \Closure $callback Callback to run with constraints disabled
|
||||
* @return mixed The return value of the callback.
|
||||
* @throws \Exception Will re-throw any exception raised in $callback after
|
||||
* rolling back the transaction.
|
||||
*/
|
||||
public function disableConstraints(Closure $callback): mixed
|
||||
{
|
||||
return $this->getDisconnectRetry()->run(function () use ($callback) {
|
||||
$this->disableForeignKeys();
|
||||
|
||||
try {
|
||||
$result = $callback($this);
|
||||
} finally {
|
||||
$this->enableForeignKeys();
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a transaction is running.
|
||||
*
|
||||
* @return bool True if a transaction is running else false.
|
||||
*/
|
||||
public function inTransaction(): bool
|
||||
{
|
||||
return $this->_transactionStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables metadata caching for this connection
|
||||
*
|
||||
* Changing this setting will not modify existing schema collections objects.
|
||||
*
|
||||
* @param string|bool $cache Either boolean false to disable metadata caching, or
|
||||
* true to use `_cake_model_` or the name of the cache config to use.
|
||||
* @return void
|
||||
*/
|
||||
public function cacheMetadata(string|bool $cache): void
|
||||
{
|
||||
$this->_schemaCollection = null;
|
||||
$this->_config['cacheMetadata'] = $cache;
|
||||
if (is_string($cache)) {
|
||||
$this->cacher = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setCacher(CacheInterface $cacher)
|
||||
{
|
||||
$this->cacher = $cacher;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCacher(): CacheInterface
|
||||
{
|
||||
if ($this->cacher !== null) {
|
||||
return $this->cacher;
|
||||
}
|
||||
|
||||
$configName = $this->_config['cacheMetadata'] ?? '_cake_model_';
|
||||
if (!is_string($configName)) {
|
||||
$configName = '_cake_model_';
|
||||
}
|
||||
|
||||
if (!class_exists(Cache::class)) {
|
||||
throw new CakeException(
|
||||
'To use caching you must either set a cacher using Connection::setCacher()' .
|
||||
' or require the cakephp/cache package in your composer config.',
|
||||
);
|
||||
}
|
||||
|
||||
return $this->cacher = Cache::pool($configName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array that can be used to describe the internal state of this
|
||||
* object.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
$secrets = [
|
||||
'password' => '*****',
|
||||
'username' => '*****',
|
||||
'host' => '*****',
|
||||
'database' => '*****',
|
||||
'port' => '*****',
|
||||
];
|
||||
$replace = array_intersect_key($secrets, $this->_config);
|
||||
$config = $replace + $this->_config;
|
||||
|
||||
if (isset($config['read'])) {
|
||||
$config['read'] = array_intersect_key($secrets, $config['read']) + $config['read'];
|
||||
}
|
||||
if (isset($config['write'])) {
|
||||
$config['write'] = array_intersect_key($secrets, $config['write']) + $config['write'];
|
||||
}
|
||||
|
||||
return [
|
||||
'config' => $config,
|
||||
'readDriver' => $this->readDriver,
|
||||
'writeDriver' => $this->writeDriver,
|
||||
'transactionLevel' => $this->_transactionLevel,
|
||||
'transactionStarted' => $this->_transactionStarted,
|
||||
'useSavePoints' => $this->_useSavePoints,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
use Cake\Datasource\ConnectionInterface;
|
||||
|
||||
/**
|
||||
* Defines the interface for a fixture that needs to manage constraints.
|
||||
*
|
||||
* @deprecated 5.2.5 This interface is no longer used.
|
||||
*/
|
||||
interface ConstraintsInterface
|
||||
{
|
||||
/**
|
||||
* Build and execute SQL queries necessary to create the constraints for the
|
||||
* fixture
|
||||
*
|
||||
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
|
||||
* into which the constraints will be created.
|
||||
* @return bool on success or if there are no constraints to create, or false on failure
|
||||
*/
|
||||
public function createConstraints(ConnectionInterface $connection): bool;
|
||||
|
||||
/**
|
||||
* Build and execute SQL queries necessary to drop the constraints for the
|
||||
* fixture
|
||||
*
|
||||
* @param \Cake\Datasource\ConnectionInterface $connection An instance of the database
|
||||
* into which the constraints will be dropped.
|
||||
* @return bool on success or if there are no constraints to drop, or false on failure
|
||||
*/
|
||||
public function dropConstraints(ConnectionInterface $connection): bool;
|
||||
}
|
||||
+1080
File diff suppressed because it is too large
Load Diff
+337
@@ -0,0 +1,337 @@
|
||||
<?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\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\Schema\MysqlSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\StatementInterface;
|
||||
use PDO;
|
||||
use Pdo\Mysql as PdoMysql;
|
||||
|
||||
/**
|
||||
* MySQL Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 256;
|
||||
|
||||
/**
|
||||
* Server type MySQL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const SERVER_TYPE_MYSQL = 'mysql';
|
||||
|
||||
/**
|
||||
* Server type MariaDB
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const SERVER_TYPE_MARIADB = 'mariadb';
|
||||
|
||||
/**
|
||||
* Base configuration settings for MySQL driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'port' => '3306',
|
||||
'flags' => [],
|
||||
'encoding' => 'utf8mb4',
|
||||
'timezone' => null,
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '`';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '`';
|
||||
|
||||
/**
|
||||
* Server type.
|
||||
*
|
||||
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
|
||||
* after `version()` method is called.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $serverType = self::SERVER_TYPE_MYSQL;
|
||||
|
||||
/**
|
||||
* Mapping of feature to db server version for feature availability checks.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
protected array $featureVersions = [
|
||||
'mysql' => [
|
||||
'json' => '5.7.0',
|
||||
'cte' => '8.0.0',
|
||||
'window' => '8.0.0',
|
||||
'intersect' => '8.0.31',
|
||||
'intersect-all' => '8.0.31',
|
||||
'check-constraints' => '8.0.16',
|
||||
],
|
||||
'mariadb' => [
|
||||
'json' => '10.2.7',
|
||||
'cte' => '10.2.1',
|
||||
'window' => '10.2.0',
|
||||
'intersect' => '10.3.0',
|
||||
'intersect-all' => '10.5.0',
|
||||
'check-constraints' => '10.2.1',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
|
||||
if ($config['timezone'] === 'UTC') {
|
||||
$config['timezone'] = '+0:00';
|
||||
}
|
||||
|
||||
if (!empty($config['timezone'])) {
|
||||
$config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
|
||||
}
|
||||
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
$this->attrUseBufferedQueryId() => true,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
|
||||
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
|
||||
$config['flags'][$this->attrSslKeyId()] = $config['ssl_key'];
|
||||
$config['flags'][$this->attrSslCertId()] = $config['ssl_cert'];
|
||||
}
|
||||
if (!empty($config['ssl_ca'])) {
|
||||
$config['flags'][$this->attrSslCaId()] = $config['ssl_ca'];
|
||||
}
|
||||
|
||||
if (empty($config['unix_socket'])) {
|
||||
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
|
||||
} else {
|
||||
$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
|
||||
}
|
||||
|
||||
if (!empty($config['encoding'])) {
|
||||
$dsn .= ";charset={$config['encoding']}";
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function run(Query $query): StatementInterface
|
||||
{
|
||||
$statement = $this->prepare($query);
|
||||
$query->getValueBinder()->attachTo($statement);
|
||||
|
||||
if ($query instanceof SelectQuery) {
|
||||
try {
|
||||
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), $query->isBufferedResultsEnabled());
|
||||
$this->executeStatement($statement);
|
||||
} finally {
|
||||
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), true);
|
||||
}
|
||||
} else {
|
||||
$this->executeStatement($statement);
|
||||
}
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('mysql', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new MysqlSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schema(): string
|
||||
{
|
||||
return $this->_config['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET foreign_key_checks = 0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET foreign_key_checks = 1';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
$versionCompare = function () use ($feature) {
|
||||
return version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$this->serverType][$feature->value],
|
||||
'>=',
|
||||
);
|
||||
};
|
||||
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT => true,
|
||||
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => false,
|
||||
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::JSON,
|
||||
DriverFeatureEnum::WINDOW => $versionCompare(),
|
||||
DriverFeatureEnum::INTERSECT => $versionCompare(),
|
||||
DriverFeatureEnum::INTERSECT_ALL => $versionCompare(),
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => $versionCompare(),
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the connected server is MariaDB.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMariadb(): bool
|
||||
{
|
||||
$this->version();
|
||||
|
||||
return $this->serverType === static::SERVER_TYPE_MARIADB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns connected server version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function version(): string
|
||||
{
|
||||
if ($this->_version === null) {
|
||||
$this->_version = (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
|
||||
if (str_contains($this->_version, 'MariaDB')) {
|
||||
$this->serverType = static::SERVER_TYPE_MARIADB;
|
||||
preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
|
||||
$this->_version = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_KEY id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslKeyId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_KEY : PdoMysql::ATTR_SSL_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_CERT id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslCertId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CERT : PdoMysql::ATTR_SSL_CERT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_CA id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslCaId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CA : PdoMysql::ATTR_SSL_CA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_USE_BUFFERED_QUERY id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrUseBufferedQueryId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : PdoMysql::ATTR_USE_BUFFERED_QUERY;
|
||||
}
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
<?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\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\StringExpression;
|
||||
use Cake\Database\PostgresCompiler;
|
||||
use Cake\Database\Query\InsertQuery;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\PostgresSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Class Postgres
|
||||
*/
|
||||
class Postgres extends Driver
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 63;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Postgres driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'schema' => 'public',
|
||||
'port' => 5432,
|
||||
'encoding' => 'utf8',
|
||||
'timezone' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
'ssl_key' => null,
|
||||
'ssl_cert' => null,
|
||||
'ssl_ca' => null,
|
||||
'ssl' => false,
|
||||
'ssl_mode' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
if (empty($config['unix_socket'])) {
|
||||
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
|
||||
} else {
|
||||
$dsn = "pgsql:dbname={$config['database']}";
|
||||
}
|
||||
|
||||
if ($this->_config['ssl']) {
|
||||
if ($this->_config['ssl_mode']) {
|
||||
$dsn .= ';sslmode=' . $this->_config['ssl_mode'];
|
||||
} else {
|
||||
$dsn .= ';sslmode=allow';
|
||||
}
|
||||
|
||||
if ($this->_config['ssl_key']) {
|
||||
$dsn .= ';sslkey=' . $this->_config['ssl_key'];
|
||||
}
|
||||
if ($this->_config['ssl_cert']) {
|
||||
$dsn .= ';sslcert=' . $this->_config['ssl_cert'];
|
||||
}
|
||||
if ($this->_config['ssl_ca']) {
|
||||
$dsn .= ';sslrootcert=' . $this->_config['ssl_ca'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if (!empty($config['encoding'])) {
|
||||
$this->setEncoding($config['encoding']);
|
||||
}
|
||||
|
||||
if (!empty($config['schema'])) {
|
||||
$this->setSchema($config['schema']);
|
||||
}
|
||||
|
||||
if (!empty($config['timezone'])) {
|
||||
$config['init'][] = sprintf('SET timezone = %s', $this->getPdo()->quote($config['timezone']));
|
||||
}
|
||||
|
||||
foreach ($config['init'] as $command) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('pgsql', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new PostgresSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets connection encoding
|
||||
*
|
||||
* @param string $encoding The encoding to use.
|
||||
* @return void
|
||||
*/
|
||||
public function setEncoding(string $encoding): void
|
||||
{
|
||||
$pdo = $this->getPdo();
|
||||
$pdo->exec('SET NAMES ' . $pdo->quote($encoding));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets connection default schema, if any relation defined in a query is not fully qualified
|
||||
* postgres will fallback to looking the relation into defined default schema
|
||||
*
|
||||
* @param string $schema The schema names to set `search_path` to.
|
||||
* @return void
|
||||
*/
|
||||
public function setSchema(string $schema): void
|
||||
{
|
||||
$pdo = $this->getPdo();
|
||||
$pdo->exec('SET search_path TO ' . $pdo->quote($schema));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL DEFERRED';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL IMMEDIATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::JSON,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
|
||||
DriverFeatureEnum::WINDOW => true,
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => true,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _transformDistinct(SelectQuery $query): SelectQuery
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _insertQueryTranslator(InsertQuery $query): InsertQuery
|
||||
{
|
||||
if (!$query->clause('epilog')) {
|
||||
$query->epilog('RETURNING *');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
IdentifierExpression::class => '_transformIdentifierExpression',
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
StringExpression::class => '_transformStringExpression',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes identifer expression into postgresql format.
|
||||
*
|
||||
* @param \Cake\Database\Expression\IdentifierExpression $expression The expression to transform.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformIdentifierExpression(IdentifierExpression $expression): void
|
||||
{
|
||||
$collation = $expression->getCollation();
|
||||
if ($collation) {
|
||||
// use trim() to work around expression being transformed multiple times
|
||||
$expression->setCollation('"' . trim($collation, '"') . '"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
|
||||
* to postgres SQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 || exp2
|
||||
$expression->setName('')->setConjunction(' ||');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$expression
|
||||
->setName('')
|
||||
->setConjunction('-')
|
||||
->iterateParts(function ($p) {
|
||||
if (is_string($p)) {
|
||||
$p = ['value' => [$p => 'literal'], 'type' => null];
|
||||
} else {
|
||||
$p['value'] = [$p['value']];
|
||||
}
|
||||
|
||||
return new FunctionExpression('DATE', $p['value'], [$p['type']]);
|
||||
});
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
|
||||
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
|
||||
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
|
||||
break;
|
||||
case 'RAND':
|
||||
$expression->setName('RANDOM');
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$expression
|
||||
->setName('')
|
||||
->setConjunction(' + INTERVAL')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 1) {
|
||||
return sprintf("'%s'", $p);
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('EXTRACT')
|
||||
->setConjunction(' ')
|
||||
->add(['DOW FROM' => 'literal'], [], true)
|
||||
->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
|
||||
break;
|
||||
case 'JSON_VALUE':
|
||||
$expression->setName('JSONB_PATH_QUERY')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 0) {
|
||||
$p = sprintf('%s::jsonb', $p);
|
||||
} elseif ($key === 1) {
|
||||
$p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value']));
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes string expression into postgresql format.
|
||||
*
|
||||
* @param \Cake\Database\Expression\StringExpression $expression The string expression to transform.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformStringExpression(StringExpression $expression): void
|
||||
{
|
||||
// use trim() to work around expression being transformed multiple times
|
||||
$expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return \Cake\Database\PostgresCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new PostgresCompiler();
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
<?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\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\SqliteSchemaDialect;
|
||||
use Cake\Database\Statement\SqliteStatement;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Class Sqlite
|
||||
*/
|
||||
class Sqlite extends Driver
|
||||
{
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const STATEMENT_CLASS = SqliteStatement::class;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlite driver
|
||||
*
|
||||
* - `mask` The mask used for created database
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => false,
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
'database' => ':memory:',
|
||||
'encoding' => 'utf8',
|
||||
'mask' => 0644,
|
||||
'cache' => null,
|
||||
'mode' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether the connected server supports window functions.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
protected ?bool $_supportsWindowFunctions = null;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* Mapping of date parts.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $_dateParts = [
|
||||
'day' => 'd',
|
||||
'hour' => 'H',
|
||||
'month' => 'm',
|
||||
'minute' => 'M',
|
||||
'second' => 'S',
|
||||
'week' => 'W',
|
||||
'year' => 'Y',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of feature to db server version for feature availability checks.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $featureVersions = [
|
||||
'cte' => '3.8.3',
|
||||
'window' => '3.28.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
if (!is_string($config['database']) || $config['database'] === '') {
|
||||
$name = $config['name'] ?? 'unknown';
|
||||
throw new InvalidArgumentException(
|
||||
"The `database` key for the `{$name}` SQLite connection needs to be a non-empty string.",
|
||||
);
|
||||
}
|
||||
|
||||
$chmodFile = false;
|
||||
if ($config['database'] !== ':memory:' && $config['mode'] !== 'memory') {
|
||||
$chmodFile = !file_exists($config['database']);
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($config['cache']) {
|
||||
$params[] = 'cache=' . $config['cache'];
|
||||
}
|
||||
if ($config['mode']) {
|
||||
$params[] = 'mode=' . $config['mode'];
|
||||
}
|
||||
|
||||
if ($params) {
|
||||
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
|
||||
} else {
|
||||
$dsn = 'sqlite:' . $config['database'];
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if ($chmodFile) {
|
||||
// phpcs:disable
|
||||
@chmod($config['database'], $config['mask']);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('sqlite', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = ON';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => true,
|
||||
|
||||
DriverFeatureEnum::JSON => false,
|
||||
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::WINDOW => version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$feature->value],
|
||||
'>=',
|
||||
),
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => false,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new SqliteSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
TupleComparison::class => '_transformTupleComparison',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 || exp2
|
||||
$expression->setName('')->setConjunction(' ||');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$expression
|
||||
->setName('ROUND')
|
||||
->setConjunction('-')
|
||||
->iterateParts(function ($p) {
|
||||
return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
|
||||
});
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('DATETIME')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'RAND':
|
||||
$expression
|
||||
->setName('ABS')
|
||||
->add(['RANDOM() % 1' => 'literal'], [], true);
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$expression->setName('DATE')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$expression->setName('TIME')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'EXTRACT':
|
||||
$expression
|
||||
->setName('STRFTIME')
|
||||
->setConjunction(' ,')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 0) {
|
||||
$value = rtrim(strtolower($p), 's');
|
||||
if (isset($this->_dateParts[$value])) {
|
||||
$p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$expression
|
||||
->setName('DATE')
|
||||
->setConjunction(',')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 1) {
|
||||
return ['value' => $p, 'type' => null];
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('STRFTIME')
|
||||
->setConjunction(' ')
|
||||
->add(["'%w', " => 'literal'], [], true)
|
||||
->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
|
||||
break;
|
||||
case 'JSON_VALUE':
|
||||
$expression->setName('JSON_EXTRACT');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
<?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\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\OrderByExpression;
|
||||
use Cake\Database\Expression\OrderClauseExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Expression\UnaryExpression;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\SqlserverSchemaDialect;
|
||||
use Cake\Database\SqlserverCompiler;
|
||||
use Cake\Database\Statement\SqlserverStatement;
|
||||
use Cake\Database\StatementInterface;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQLServer driver.
|
||||
*/
|
||||
class Sqlserver extends Driver
|
||||
{
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 128;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const RETRY_ERROR_CODES = [
|
||||
40613, // Azure Sql Database paused
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const STATEMENT_CLASS = SqlserverStatement::class;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlserver driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'host' => 'localhost\SQLEXPRESS',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'port' => '',
|
||||
// PDO::SQLSRV_ENCODING_UTF8
|
||||
'encoding' => 65001,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
'settings' => [],
|
||||
'attributes' => [],
|
||||
'app' => null,
|
||||
'connectionPooling' => null,
|
||||
'failoverPartner' => null,
|
||||
'loginTimeout' => null,
|
||||
'multiSubnetFailover' => null,
|
||||
'encrypt' => null,
|
||||
'trustServerCertificate' => null,
|
||||
'accessToken' => null,
|
||||
'authentication' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '[';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = ']';
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server.
|
||||
*
|
||||
* Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
|
||||
* the SQL Server PHP PDO drivers. As a result you cannot use the
|
||||
* persistent config option when connecting to a SQL Server (for more
|
||||
* information see: https://github.com/Microsoft/msphpsql/issues/65).
|
||||
*
|
||||
* @throws \InvalidArgumentException if an unsupported setting is in the driver config
|
||||
* @return void
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
|
||||
if (isset($config['persistent']) && $config['persistent']) {
|
||||
throw new InvalidArgumentException(
|
||||
'Config setting "persistent" cannot be set to true, '
|
||||
. 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT',
|
||||
);
|
||||
}
|
||||
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
|
||||
if (!empty($config['encoding'])) {
|
||||
$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
|
||||
}
|
||||
$port = '';
|
||||
if ($config['port']) {
|
||||
$port = ',' . $config['port'];
|
||||
}
|
||||
|
||||
$dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
|
||||
if ($config['app'] !== null) {
|
||||
$dsn .= ";APP={$config['app']}";
|
||||
}
|
||||
if ($config['connectionPooling'] !== null) {
|
||||
$dsn .= ";ConnectionPooling={$config['connectionPooling']}";
|
||||
}
|
||||
if ($config['failoverPartner'] !== null) {
|
||||
$dsn .= ";Failover_Partner={$config['failoverPartner']}";
|
||||
}
|
||||
if ($config['loginTimeout'] !== null) {
|
||||
$dsn .= ";LoginTimeout={$config['loginTimeout']}";
|
||||
}
|
||||
if ($config['multiSubnetFailover'] !== null) {
|
||||
$dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
|
||||
}
|
||||
if ($config['encrypt'] !== null) {
|
||||
$dsn .= ";Encrypt={$config['encrypt']}";
|
||||
}
|
||||
if ($config['trustServerCertificate'] !== null) {
|
||||
$dsn .= ";TrustServerCertificate={$config['trustServerCertificate']}";
|
||||
}
|
||||
if ($config['accessToken'] !== null) {
|
||||
$dsn .= ";AccessToken={$config['accessToken']}";
|
||||
}
|
||||
if ($config['authentication'] !== null) {
|
||||
$dsn .= ";Authentication={$config['authentication']}";
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
if (!empty($config['settings']) && is_array($config['settings'])) {
|
||||
foreach ($config['settings'] as $key => $value) {
|
||||
$this->pdo->exec("SET {$key} {$value}");
|
||||
}
|
||||
}
|
||||
if (!empty($config['attributes']) && is_array($config['attributes'])) {
|
||||
foreach ($config['attributes'] as $key => $value) {
|
||||
$this->pdo->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether PHP is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function prepare(Query|string $query): StatementInterface
|
||||
{
|
||||
$options = [
|
||||
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
|
||||
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
|
||||
];
|
||||
|
||||
$sql = $query;
|
||||
if ($query instanceof Query) {
|
||||
$sql = $query->sql();
|
||||
if (count($query->getValueBinder()->bindings()) > 2100) {
|
||||
throw new InvalidArgumentException(
|
||||
'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
|
||||
'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
|
||||
'for each value in the array. ' .
|
||||
'If using an Association, try changing the `strategy` from select to subquery.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($query instanceof SelectQuery && !$query->isBufferedResultsEnabled()) {
|
||||
$options = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string $sql */
|
||||
$statement = $this->getPdo()->prepare(
|
||||
$sql,
|
||||
$options,
|
||||
);
|
||||
|
||||
/** @var \Cake\Database\StatementInterface */
|
||||
return new (static::STATEMENT_CLASS)($statement, $this, $this->getResultSetDecorators($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function savePointSQL($name): string
|
||||
{
|
||||
return 'SAVE TRANSACTION t' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function releaseSavePointSQL($name): string
|
||||
{
|
||||
// SQLServer has no release save point operation.
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rollbackSavePointSQL($name): string
|
||||
{
|
||||
return 'ROLLBACK TRANSACTION t' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
|
||||
DriverFeatureEnum::WINDOW => true,
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => false,
|
||||
DriverFeatureEnum::JSON => false,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ??= new SqlserverSchemaDialect($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return \Cake\Database\SqlserverCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new SqlserverCompiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _selectQueryTranslator(SelectQuery $query): SelectQuery
|
||||
{
|
||||
$limit = $query->clause('limit');
|
||||
$offset = $query->clause('offset');
|
||||
|
||||
if ($limit && $offset === null) {
|
||||
$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
|
||||
}
|
||||
|
||||
if ($offset !== null && !$query->clause('order')) {
|
||||
$query->orderBy($query->expr()->add('(SELECT NULL)'));
|
||||
}
|
||||
|
||||
if ($this->version() < 11 && $offset !== null) {
|
||||
return $this->_pagingSubquery($query, $limit, $offset);
|
||||
}
|
||||
|
||||
return $this->_transformDistinct($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a paging subquery for older versions of SQLserver.
|
||||
*
|
||||
* Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
|
||||
* be used.
|
||||
*
|
||||
* @param \Cake\Database\Query\SelectQuery<mixed> $original The query to wrap in a subquery.
|
||||
* @param int|null $limit The number of rows to fetch.
|
||||
* @param int|null $offset The number of rows to offset.
|
||||
* @return \Cake\Database\Query\SelectQuery<mixed> Modified query object.
|
||||
*/
|
||||
protected function _pagingSubquery(SelectQuery $original, ?int $limit, ?int $offset): SelectQuery
|
||||
{
|
||||
$field = '_cake_paging_._cake_page_rownum_';
|
||||
|
||||
/** @var \Cake\Database\Expression\OrderByExpression $originalOrder */
|
||||
$originalOrder = $original->clause('order');
|
||||
if ($originalOrder) {
|
||||
// SQL server does not support column aliases in OVER clauses. But
|
||||
// the only practical way to specify the use of calculated columns
|
||||
// is with their alias. So substitute the select SQL in place of
|
||||
// any column aliases for those entries in the order clause.
|
||||
$select = $original->clause('select');
|
||||
$order = new OrderByExpression();
|
||||
$originalOrder
|
||||
->iterateParts(function ($direction, $orderBy) use ($select, $order) {
|
||||
$key = $orderBy;
|
||||
if (
|
||||
isset($select[$orderBy]) &&
|
||||
$select[$orderBy] instanceof ExpressionInterface
|
||||
) {
|
||||
$order->add(new OrderClauseExpression($select[$orderBy], $direction));
|
||||
} else {
|
||||
$order->add([$key => $direction]);
|
||||
}
|
||||
|
||||
// Leave original order clause unchanged.
|
||||
return $orderBy;
|
||||
});
|
||||
} else {
|
||||
$order = new OrderByExpression('(SELECT NULL)');
|
||||
}
|
||||
|
||||
$query = clone $original;
|
||||
$query->select([
|
||||
'_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
|
||||
])->limit(null)
|
||||
->offset(null)
|
||||
->orderBy([], true);
|
||||
|
||||
$outer = $query->getConnection()->selectQuery();
|
||||
$outer->select('*')
|
||||
->from(['_cake_paging_' => $query]);
|
||||
|
||||
if ($offset) {
|
||||
$outer->where(["{$field} > " . $offset]);
|
||||
}
|
||||
if ($limit) {
|
||||
$value = (int)$offset + $limit;
|
||||
$outer->where(["{$field} <= {$value}"]);
|
||||
}
|
||||
|
||||
// Decorate the original query as that is what the
|
||||
// end developer will be calling execute() on originally.
|
||||
$original->decorateResults(function ($row) {
|
||||
if (isset($row['_cake_page_rownum_'])) {
|
||||
unset($row['_cake_page_rownum_']);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $outer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _transformDistinct(SelectQuery $query): SelectQuery
|
||||
{
|
||||
if (!is_array($query->clause('distinct'))) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$original = $query;
|
||||
$query = clone $original;
|
||||
|
||||
$distinct = $query->clause('distinct');
|
||||
$query->distinct(false);
|
||||
|
||||
$order = new OrderByExpression($distinct);
|
||||
$query
|
||||
->select(function (Query $q) use ($distinct, $order) {
|
||||
$over = $q->expr('ROW_NUMBER() OVER')
|
||||
->add('(PARTITION BY')
|
||||
->add($q->expr()->add($distinct)->setConjunction(','))
|
||||
->add($order)
|
||||
->add(')')
|
||||
->setConjunction(' ');
|
||||
|
||||
return [
|
||||
'_cake_distinct_pivot_' => $over,
|
||||
];
|
||||
})
|
||||
->limit(null)
|
||||
->offset(null)
|
||||
->orderBy([], true);
|
||||
|
||||
$outer = new SelectQuery($query->getConnection());
|
||||
$outer->select('*')
|
||||
->from(['_cake_distinct_' => $query])
|
||||
->where(['_cake_distinct_pivot_' => 1]);
|
||||
|
||||
// Decorate the original query as that is what the
|
||||
// end developer will be calling execute() on originally.
|
||||
$original->decorateResults(function ($row) {
|
||||
if (isset($row['_cake_distinct_pivot_'])) {
|
||||
unset($row['_cake_distinct_pivot_']);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $outer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
TupleComparison::class => '_transformTupleComparison',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 + exp2
|
||||
$expression->setName('')->setConjunction(' +');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$hasDay = false;
|
||||
$visitor = function ($value) use (&$hasDay) {
|
||||
if ($value === 'day') {
|
||||
$hasDay = true;
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
$expression->iterateParts($visitor);
|
||||
|
||||
if (!$hasDay) {
|
||||
$expression->add(['day' => 'literal'], [], true);
|
||||
}
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$time = new FunctionExpression('GETUTCDATE');
|
||||
$expression->setName('CONVERT')->add(['date' => 'literal', $time]);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$time = new FunctionExpression('GETUTCDATE');
|
||||
$expression->setName('CONVERT')->add(['time' => 'literal', $time]);
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('GETUTCDATE');
|
||||
break;
|
||||
case 'EXTRACT':
|
||||
$expression->setName('DATEPART')->setConjunction(' ,');
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$params = [];
|
||||
$visitor = function ($p, $key) use (&$params) {
|
||||
if ($key === 0) {
|
||||
$params[2] = $p;
|
||||
} else {
|
||||
$valueUnit = explode(' ', $p);
|
||||
$params[0] = rtrim($valueUnit[1], 's');
|
||||
$params[1] = $valueUnit[0];
|
||||
}
|
||||
|
||||
return $p;
|
||||
};
|
||||
$manipulator = function ($p, $key) use (&$params) {
|
||||
return $params[$key];
|
||||
};
|
||||
|
||||
$expression
|
||||
->setName('DATEADD')
|
||||
->setConjunction(',')
|
||||
->iterateParts($visitor)
|
||||
->iterateParts($manipulator)
|
||||
->add([$params[2] => 'literal']);
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('DATEPART')
|
||||
->setConjunction(' ')
|
||||
->add(['weekday, ' => 'literal'], [], true);
|
||||
break;
|
||||
case 'SUBSTR':
|
||||
$expression->setName('SUBSTRING');
|
||||
if (count($expression) < 4) {
|
||||
$params = [];
|
||||
$expression
|
||||
->iterateParts(function ($p) use (&$params) {
|
||||
return $params[] = $p;
|
||||
})
|
||||
->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?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\Driver;
|
||||
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Provides a translator method for tuple comparisons
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait TupleComparisonTranslatorTrait
|
||||
{
|
||||
/**
|
||||
* Receives a TupleExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* It transforms expressions looking like '(a, b) IN ((c, d), (e, f))' into an
|
||||
* equivalent expression of the form '((a = c) AND (b = d)) OR ((a = e) AND (b = f))'.
|
||||
*
|
||||
* It can also transform expressions where the right hand side is a query
|
||||
* selecting the same amount of columns as the elements in the left hand side of
|
||||
* the expression:
|
||||
*
|
||||
* (a, b) IN (SELECT c, d FROM a_table) is transformed into
|
||||
*
|
||||
* 1 = (SELECT 1 FROM a_table WHERE (a = c) AND (b = d))
|
||||
*
|
||||
* @param \Cake\Database\Expression\TupleComparison $expression The expression to transform
|
||||
* @param \Cake\Database\Query $query The query to update.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformTupleComparison(TupleComparison $expression, Query $query): void
|
||||
{
|
||||
$fields = $expression->getField();
|
||||
|
||||
if (!is_array($fields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operator = strtoupper($expression->getOperator());
|
||||
if (!in_array($operator, ['IN', '='])) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
|
||||
$operator,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$value = $expression->getValue();
|
||||
$true = new QueryExpression('1');
|
||||
|
||||
if ($value instanceof SelectQuery) {
|
||||
/** @var array<string> $selected */
|
||||
$selected = array_values($value->clause('select'));
|
||||
foreach ($fields as $i => $field) {
|
||||
$value->andWhere([$field => new IdentifierExpression($selected[$i])]);
|
||||
}
|
||||
$value->select($true, true);
|
||||
$expression->setField($true);
|
||||
$expression->setOperator('=');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $expression->getType();
|
||||
if ($type) {
|
||||
/** @var array<string, string> $typeMap */
|
||||
$typeMap = array_combine($fields, $type) ?: [];
|
||||
} else {
|
||||
$typeMap = [];
|
||||
}
|
||||
|
||||
$surrogate = $query->getConnection()
|
||||
->selectQuery()
|
||||
->select($true);
|
||||
|
||||
if (!is_array(current($value))) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
$conditions = ['OR' => []];
|
||||
foreach ($value as $tuple) {
|
||||
$item = [];
|
||||
foreach (array_values($tuple) as $i => $value2) {
|
||||
$item[] = [$fields[$i] => $value2];
|
||||
}
|
||||
$conditions['OR'][] = $item;
|
||||
}
|
||||
$surrogate->where($conditions, $typeMap);
|
||||
|
||||
$expression->setField($true);
|
||||
$expression->setValue($surrogate);
|
||||
$expression->setOperator('=');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
enum DriverFeatureEnum: string
|
||||
{
|
||||
/**
|
||||
* Common Table Expressions (with clause) support.
|
||||
*/
|
||||
case CTE = 'cte';
|
||||
|
||||
/**
|
||||
* Disabling constraints without being in transaction support.
|
||||
*/
|
||||
case DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
|
||||
|
||||
/**
|
||||
* Native JSON data type support.
|
||||
*/
|
||||
case JSON = 'json';
|
||||
|
||||
/**
|
||||
* Transaction savepoint support.
|
||||
*/
|
||||
case SAVEPOINT = 'savepoint';
|
||||
|
||||
/**
|
||||
* Truncate with foreign keys attached support.
|
||||
*/
|
||||
case TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
|
||||
|
||||
/**
|
||||
* Window function support (all or partial clauses).
|
||||
*/
|
||||
case WINDOW = 'window';
|
||||
|
||||
/**
|
||||
* Intersect feature support
|
||||
*/
|
||||
case INTERSECT = 'intersect';
|
||||
|
||||
/**
|
||||
* Intersect all feature support
|
||||
*/
|
||||
case INTERSECT_ALL = 'intersect-all';
|
||||
|
||||
/**
|
||||
* Support for order by in set operations (union, intersect)
|
||||
*/
|
||||
case SET_OPERATIONS_ORDER_BY = 'set-operations-order-by';
|
||||
|
||||
/**
|
||||
* Support for optimizer hints in comment form after statement keyword (SELECT <hint>, etc)
|
||||
*/
|
||||
case OPTIMIZER_HINT_COMMENT = 'optimizer-hint-comment';
|
||||
|
||||
/**
|
||||
* Support for CHECK constraints.
|
||||
*/
|
||||
case CHECK_CONSTRAINTS = 'check-constraints';
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Exception;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
|
||||
/**
|
||||
* Exception for the database package.
|
||||
*/
|
||||
class DatabaseException extends CakeException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected string $_messageTemplate = '%s';
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Exception;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
|
||||
/**
|
||||
* Class MissingConnectionException
|
||||
*/
|
||||
class MissingConnectionException extends CakeException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected string $_messageTemplate = 'Connection to %s could not be established: %s';
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Exception;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
|
||||
/**
|
||||
* Class MissingDriverException
|
||||
*/
|
||||
class MissingDriverException extends CakeException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected string $_messageTemplate = 'Could not find driver `%s` for connection `%s`.';
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Exception;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
|
||||
/**
|
||||
* Class MissingExtensionException
|
||||
*/
|
||||
class MissingExtensionException extends CakeException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
// phpcs:ignore Generic.Files.LineLength
|
||||
protected string $_messageTemplate = 'Database driver `%s` cannot be used due to a missing PHP extension or unmet dependency. Requested by connection `%s`';
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?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.4.3
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Exception;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class NestedTransactionRollbackException
|
||||
*/
|
||||
class NestedTransactionRollbackException extends CakeException
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|null $message If no message is given, a default message will be used.
|
||||
* @param int|null $code Status code, defaults to 500.
|
||||
* @param \Throwable|null $previous the previous exception.
|
||||
*/
|
||||
public function __construct(?string $message = null, ?int $code = 500, ?Throwable $previous = null)
|
||||
{
|
||||
$message ??= 'Cannot commit transaction - rollback() has been already called in the nested transaction';
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.1.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Exception;
|
||||
|
||||
use Cake\Database\Log\LoggedQuery;
|
||||
use PDOException;
|
||||
|
||||
class QueryException extends PDOException
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\Log\LoggedQuery|string $query
|
||||
* @param \PDOException $previous
|
||||
*/
|
||||
public function __construct(protected LoggedQuery|string $query, PDOException $previous)
|
||||
{
|
||||
$message = $previous->getMessage() . "\nQuery: " . $this->getQueryString();
|
||||
|
||||
parent::__construct($message, (int)$previous->getCode(), $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query string that caused this exception.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryString(): string
|
||||
{
|
||||
if ($this->query instanceof LoggedQuery) {
|
||||
return (string)$this->query;
|
||||
}
|
||||
|
||||
return $this->query;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* An interface used by Expression objects.
|
||||
*/
|
||||
interface ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* Converts the Node into a SQL string fragment.
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder Parameter binder
|
||||
* @return string
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string;
|
||||
|
||||
/**
|
||||
* Iterates over each part of the expression recursively for every
|
||||
* level of the expressions tree and executes the callback,
|
||||
* passing as first parameter the instance of the expression currently
|
||||
* being iterated.
|
||||
*
|
||||
* @param \Closure $callback The callback to run for all nodes.
|
||||
* @return $this
|
||||
*/
|
||||
public function traverse(Closure $callback);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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.2.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
use Cake\Database\Type\BatchCastingInterface;
|
||||
use Cake\Database\Type\OptionalConvertInterface;
|
||||
|
||||
/**
|
||||
* An invokable class to be used for processing each of the rows in a statement
|
||||
* result, so that the values are converted to the right PHP types.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FieldTypeConverter
|
||||
{
|
||||
/**
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected Driver $driver;
|
||||
|
||||
/**
|
||||
* Maps type names to conversion settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $conversions = [];
|
||||
|
||||
/**
|
||||
* Builds the type map
|
||||
*
|
||||
* @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results
|
||||
* @param \Cake\Database\Driver $driver The driver to use for the type conversion
|
||||
*/
|
||||
public function __construct(TypeMap $typeMap, Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
|
||||
$types = TypeFactory::buildAll();
|
||||
foreach ($typeMap->toArray() as $field => $typeName) {
|
||||
$type = $types[$typeName] ?? null;
|
||||
if (!$type || ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->conversions[$typeName] ??= [
|
||||
'type' => $type,
|
||||
'hasBatch' => $type instanceof BatchCastingInterface,
|
||||
'fields' => [],
|
||||
];
|
||||
$this->conversions[$typeName]['fields'][] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts each of the fields in the array that are present in the type map
|
||||
* using the corresponding Type class.
|
||||
*
|
||||
* @param mixed $row The array with the fields to be casted
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke(mixed $row): mixed
|
||||
{
|
||||
if (!is_array($row)) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
foreach ($this->conversions as $conversion) {
|
||||
/** @var \Cake\Database\TypeInterface $type */
|
||||
$type = $conversion['type'];
|
||||
if ($conversion['hasBatch']) {
|
||||
/** @var \Cake\Database\Type\BatchCastingInterface $type */
|
||||
$row = $type->manyToPHP($row, $conversion['fields'], $this->driver);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($conversion['fields'] as $field) {
|
||||
$row[$field] = $type->toPHP($row[$field], $this->driver);
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
<?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;
|
||||
|
||||
use Cake\Database\Expression\AggregateExpression;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Contains methods related to generating FunctionExpression objects
|
||||
* with most commonly used SQL functions.
|
||||
* This acts as a factory for FunctionExpression objects.
|
||||
*/
|
||||
class FunctionsBuilder
|
||||
{
|
||||
/**
|
||||
* Returns a FunctionExpression representing a call to SQL RAND function.
|
||||
*
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function rand(): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression('RAND', [], [], 'float');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a AggregateExpression representing a call to SQL SUM function.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function sum(ExpressionInterface|string $expression, array $types = []): AggregateExpression
|
||||
{
|
||||
$returnType = 'float';
|
||||
if (current($types) === 'integer') {
|
||||
$returnType = 'integer';
|
||||
}
|
||||
|
||||
return $this->aggregate('SUM', $this->toLiteralParam($expression), $types, $returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a AggregateExpression representing a call to SQL AVG function.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function avg(ExpressionInterface|string $expression, array $types = []): AggregateExpression
|
||||
{
|
||||
return $this->aggregate('AVG', $this->toLiteralParam($expression), $types, 'float');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a AggregateExpression representing a call to SQL MAX function.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function max(ExpressionInterface|string $expression, array $types = []): AggregateExpression
|
||||
{
|
||||
return $this->aggregate('MAX', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a AggregateExpression representing a call to SQL MIN function.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function min(ExpressionInterface|string $expression, array $types = []): AggregateExpression
|
||||
{
|
||||
return $this->aggregate('MIN', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a AggregateExpression representing a call to SQL COUNT function.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function count(ExpressionInterface|string $expression, array $types = []): AggregateExpression
|
||||
{
|
||||
return $this->aggregate('COUNT', $this->toLiteralParam($expression), $types, 'integer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a string concatenation
|
||||
*
|
||||
* @param array $args List of strings or expressions to concatenate
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function concat(array $args, array $types = []): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression('CONCAT', $args, $types, 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a call to SQL COALESCE function.
|
||||
*
|
||||
* @param array $args List of expressions to evaluate as function parameters
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function coalesce(array $args, array $types = []): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression('COALESCE', $args, $types, current($types) ?: 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a SQL CAST.
|
||||
*
|
||||
* The `$type` parameter is a SQL type. The return type for the returned expression
|
||||
* is the default type name. Use `setReturnType()` to update it.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field Field or expression to cast.
|
||||
* @param string $dataType The SQL data type
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function cast(ExpressionInterface|string $field, string $dataType): FunctionExpression
|
||||
{
|
||||
$expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
|
||||
|
||||
return $expression->setConjunction(' AS')->add([$dataType => 'literal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing the difference in days between
|
||||
* two dates.
|
||||
*
|
||||
* @param array $args List of expressions to obtain the difference in days.
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function dateDiff(array $args, array $types = []): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression('DATEDIFF', $args, $types, 'integer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified date part from the SQL expression.
|
||||
*
|
||||
* @param string $part Part of the date to return.
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function datePart(
|
||||
string $part,
|
||||
ExpressionInterface|string $expression,
|
||||
array $types = [],
|
||||
): FunctionExpression {
|
||||
return $this->extract($part, $expression, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified date part from the SQL expression.
|
||||
*
|
||||
* @param string $part Part of the date to return.
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function extract(string $part, ExpressionInterface|string $expression, array $types = []): FunctionExpression
|
||||
{
|
||||
$expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
|
||||
|
||||
return $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the time unit to the date expression
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression Expression to obtain the date part from.
|
||||
* @param string|int $value Value to be added. Use negative to subtract.
|
||||
* @param string $unit Unit of the value e.g. hour or day.
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function dateAdd(
|
||||
ExpressionInterface|string $expression,
|
||||
string|int $value,
|
||||
string $unit,
|
||||
array $types = [],
|
||||
): FunctionExpression {
|
||||
if (!is_numeric($value)) {
|
||||
$value = 0;
|
||||
}
|
||||
$interval = $value . ' ' . $unit;
|
||||
$expression = new FunctionExpression('DATE_ADD', $this->toLiteralParam($expression), $types, 'datetime');
|
||||
|
||||
return $expression->setConjunction(', INTERVAL')->add([$interval => 'literal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a call to SQL WEEKDAY function.
|
||||
* 1 - Sunday, 2 - Monday, 3 - Tuesday...
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function dayOfWeek(ExpressionInterface|string $expression, array $types = []): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression('DAYOFWEEK', $this->toLiteralParam($expression), $types, 'integer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a call to SQL WEEKDAY function.
|
||||
* 1 - Sunday, 2 - Monday, 3 - Tuesday...
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression the function argument
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function weekday(ExpressionInterface|string $expression, array $types = []): FunctionExpression
|
||||
{
|
||||
return $this->dayOfWeek($expression, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing a call that will return the current
|
||||
* date and time. By default it returns both date and time, but you can also
|
||||
* make it generate only the date or only the time.
|
||||
*
|
||||
* @param string $type (datetime|date|time)
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function now(string $type = 'datetime'): FunctionExpression
|
||||
{
|
||||
return match ($type) {
|
||||
'datetime' => new FunctionExpression('NOW', [], [], 'datetime'),
|
||||
'date' => new FunctionExpression('CURRENT_DATE', [], [], 'date'),
|
||||
'time' => new FunctionExpression('CURRENT_TIME', [], [], 'time'),
|
||||
default => throw new InvalidArgumentException('Invalid argument for FunctionsBuilder::now(): ' . $type),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AggregateExpression representing call to SQL ROW_NUMBER().
|
||||
*
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function rowNumber(): AggregateExpression
|
||||
{
|
||||
return (new AggregateExpression('ROW_NUMBER', [], [], 'integer'))->over();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AggregateExpression representing call to SQL LAG().
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
|
||||
* @param int $offset The row offset
|
||||
* @param mixed $default The default value if offset doesn't exist
|
||||
* @param string|null $type The output type of the lag expression. Defaults to float.
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function lag(
|
||||
ExpressionInterface|string $expression,
|
||||
int $offset,
|
||||
mixed $default = null,
|
||||
?string $type = null,
|
||||
): AggregateExpression {
|
||||
$params = $this->toLiteralParam($expression) + [$offset => 'literal'];
|
||||
if ($default !== null) {
|
||||
$params[] = $default;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
if ($type !== null) {
|
||||
$types = [$type, 'integer', $type];
|
||||
}
|
||||
|
||||
return (new AggregateExpression('LAG', $params, $types, $type ?? 'float'))->over();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an AggregateExpression representing call to SQL LEAD().
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
|
||||
* @param int $offset The row offset
|
||||
* @param mixed $default The default value if offset doesn't exist
|
||||
* @param string|null $type The output type of the lead expression. Defaults to float.
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function lead(
|
||||
ExpressionInterface|string $expression,
|
||||
int $offset,
|
||||
mixed $default = null,
|
||||
?string $type = null,
|
||||
): AggregateExpression {
|
||||
$params = $this->toLiteralParam($expression) + [$offset => 'literal'];
|
||||
if ($default !== null) {
|
||||
$params[] = $default;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
if ($type !== null) {
|
||||
$types = [$type, 'integer', $type];
|
||||
}
|
||||
|
||||
return (new AggregateExpression('LEAD', $params, $types, $type ?? 'float'))->over();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionExpression representing the Json Value
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression The Json value or json field
|
||||
* @param string $jsonPath A valid JSON PATH Query
|
||||
* @param array $types list of types to bind to the arguments
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function jsonValue(
|
||||
ExpressionInterface|string $expression,
|
||||
string $jsonPath,
|
||||
array $types = [],
|
||||
): FunctionExpression {
|
||||
$params = $this->toLiteralParam($expression) + [$jsonPath];
|
||||
|
||||
return new FunctionExpression('JSON_VALUE', $params, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create arbitrary SQL aggregate function calls.
|
||||
*
|
||||
* @param string $name The SQL aggregate function name
|
||||
* @param array $params Array of arguments to be passed to the function.
|
||||
* Can be an associative array with the literal value or identifier:
|
||||
* `['value' => 'literal']` or `['value' => 'identifier']
|
||||
* @param array $types Array of types that match the names used in `$params`:
|
||||
* `['name' => 'type']`
|
||||
* @param string $return Return type of the entire expression. Defaults to float.
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function aggregate(
|
||||
string $name,
|
||||
array $params = [],
|
||||
array $types = [],
|
||||
string $return = 'float',
|
||||
): AggregateExpression {
|
||||
return new AggregateExpression($name, $params, $types, $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method dispatcher to create custom SQL function calls
|
||||
*
|
||||
* @param string $name the SQL function name to construct
|
||||
* @param array $args list with up to 3 arguments, first one being an array with
|
||||
* parameters for the SQL function, the second one a list of types to bind to those
|
||||
* params, and the third one the return type of the function
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function __call(string $name, array $args): FunctionExpression
|
||||
{
|
||||
return new FunctionExpression($name, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates function parameter array from expression or string literal.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $expression function argument
|
||||
* @return array<\Cake\Database\ExpressionInterface|string>
|
||||
*/
|
||||
protected function toLiteralParam(ExpressionInterface|string $expression): array
|
||||
{
|
||||
if (is_string($expression)) {
|
||||
return [$expression => 'literal'];
|
||||
}
|
||||
|
||||
return [$expression];
|
||||
}
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
<?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;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\Expression\FieldInterface;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\OrderByExpression;
|
||||
use Cake\Database\Query\DeleteQuery;
|
||||
use Cake\Database\Query\InsertQuery;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\Query\UpdateQuery;
|
||||
|
||||
/**
|
||||
* Contains all the logic related to quoting identifiers in a Query object
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IdentifierQuoter
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $startQuote String used to start a database identifier quoting to make it safe.
|
||||
* @param string $endQuote String used to end a database identifier quoting to make it safe.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $startQuote,
|
||||
protected string $endQuote,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a database identifier (a column name, table name, etc..) to
|
||||
* be used safely in queries without the risk of using reserved words
|
||||
*
|
||||
* @param string $identifier The identifier to quote.
|
||||
* @return string
|
||||
*/
|
||||
public function quoteIdentifier(string $identifier): string
|
||||
{
|
||||
$identifier = trim($identifier);
|
||||
|
||||
if ($identifier === '*' || $identifier === '') {
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
// string
|
||||
if (preg_match('/^[\w-]+$/u', $identifier)) {
|
||||
return $this->startQuote . $identifier . $this->endQuote;
|
||||
}
|
||||
|
||||
// string.string
|
||||
if (preg_match('/^[\w-]+\.[^ \*]*$/u', $identifier)) {
|
||||
$items = explode('.', $identifier);
|
||||
|
||||
return $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote;
|
||||
}
|
||||
|
||||
// string.*
|
||||
if (preg_match('/^[\w-]+\.\*$/u', $identifier)) {
|
||||
return $this->startQuote . str_replace('.*', $this->endQuote . '.*', $identifier);
|
||||
}
|
||||
|
||||
// Functions
|
||||
if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) {
|
||||
return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
|
||||
}
|
||||
|
||||
// Alias.field AS thing
|
||||
if (preg_match('/^([\w-]+(\.[\w\s-]+|\(.*\))*)\s+AS\s*([\w-]+)$/ui', $identifier, $matches)) {
|
||||
return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]);
|
||||
}
|
||||
|
||||
// string.string with spaces
|
||||
if (preg_match('/^([\w-]+\.[\w][\w\s-]*[\w])(.*)/u', $identifier, $matches)) {
|
||||
$items = explode('.', $matches[1]);
|
||||
$field = implode($this->endQuote . '.' . $this->startQuote, $items);
|
||||
|
||||
return $this->startQuote . $field . $this->endQuote . $matches[2];
|
||||
}
|
||||
|
||||
if (preg_match('/^[\w\s-]*[\w-]+/u', $identifier)) {
|
||||
return $this->startQuote . $identifier . $this->endQuote;
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over each of the clauses in a query looking for identifiers and
|
||||
* quotes them
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to have its identifiers quoted
|
||||
* @return \Cake\Database\Query
|
||||
*/
|
||||
public function quote(Query $query): Query
|
||||
{
|
||||
$binder = $query->getValueBinder();
|
||||
$query->setValueBinder(null);
|
||||
|
||||
match (true) {
|
||||
$query instanceof InsertQuery => $this->_quoteInsert($query),
|
||||
$query instanceof SelectQuery => $this->_quoteSelect($query),
|
||||
$query instanceof UpdateQuery => $this->_quoteUpdate($query),
|
||||
$query instanceof DeleteQuery => $this->_quoteDelete($query),
|
||||
default =>
|
||||
throw new DatabaseException(sprintf(
|
||||
'Instance of SelectQuery, UpdateQuery, InsertQuery, DeleteQuery expected. Found `%s` instead.',
|
||||
get_debug_type($query),
|
||||
))
|
||||
};
|
||||
|
||||
$query->traverseExpressions($this->quoteExpression(...));
|
||||
|
||||
return $query->setValueBinder($binder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes identifiers inside expression objects
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote.
|
||||
* @return void
|
||||
*/
|
||||
public function quoteExpression(ExpressionInterface $expression): void
|
||||
{
|
||||
match (true) {
|
||||
$expression instanceof FieldInterface => $this->_quoteComparison($expression),
|
||||
$expression instanceof OrderByExpression => $this->_quoteOrderBy($expression),
|
||||
$expression instanceof IdentifierExpression => $this->_quoteIdentifierExpression($expression),
|
||||
default => null // Nothing to do if there is no match
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes all identifiers in each of the clauses/parts of a query
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to quote.
|
||||
* @param array<string> $parts Query clauses.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteParts(Query $query, array $parts): void
|
||||
{
|
||||
foreach ($parts as $part) {
|
||||
$contents = $query->clause($part);
|
||||
|
||||
if (!is_array($contents)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->_basicQuoter($contents);
|
||||
if ($result) {
|
||||
$part = match ($part) {
|
||||
'group' => 'groupBy',
|
||||
'order' => 'orderBy',
|
||||
default => $part,
|
||||
};
|
||||
|
||||
$query->{$part}($result, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic identifier quoting function used for various parts of the query
|
||||
*
|
||||
* @param array<string, mixed> $part the part of the query to quote
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function _basicQuoter(array $part): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($part as $alias => $value) {
|
||||
$value = is_string($value) ? $this->quoteIdentifier($value) : $value;
|
||||
$alias = is_numeric($alias) ? $alias : $this->quoteIdentifier($alias);
|
||||
$result[$alias] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes both the table and alias for an array of joins as stored in a Query
|
||||
* object
|
||||
*
|
||||
* @param array<array> $joins The joins to quote.
|
||||
* @return array<string, array>
|
||||
*/
|
||||
protected function _quoteJoins(array $joins): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($joins as $value) {
|
||||
$alias = '';
|
||||
if (!empty($value['alias'])) {
|
||||
$alias = $this->quoteIdentifier($value['alias']);
|
||||
$value['alias'] = $alias;
|
||||
}
|
||||
|
||||
if (is_string($value['table'])) {
|
||||
$value['table'] = $this->quoteIdentifier($value['table']);
|
||||
}
|
||||
|
||||
$result[$alias] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes all identifiers in each of the clauses of a SELECT query
|
||||
*
|
||||
* @param \Cake\Database\Query\SelectQuery<mixed> $query The query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteSelect(SelectQuery $query): void
|
||||
{
|
||||
$this->_quoteParts($query, ['select', 'distinct', 'from', 'group']);
|
||||
|
||||
$joins = $query->clause('join');
|
||||
if ($joins) {
|
||||
$joins = $this->_quoteJoins($joins);
|
||||
$query->join($joins, [], true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes all identifiers in each of the clauses of a DELETE query
|
||||
*
|
||||
* @param \Cake\Database\Query\DeleteQuery $query The query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteDelete(DeleteQuery $query): void
|
||||
{
|
||||
$this->_quoteParts($query, ['from']);
|
||||
|
||||
$joins = $query->clause('join');
|
||||
if ($joins) {
|
||||
$joins = $this->_quoteJoins($joins);
|
||||
$query->join($joins, [], true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the table name and columns for an insert query
|
||||
*
|
||||
* @param \Cake\Database\Query\InsertQuery $query The insert query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteInsert(InsertQuery $query): void
|
||||
{
|
||||
/** @var array{0?: string, 1?: array} $insert */
|
||||
$insert = $query->clause('insert');
|
||||
if (!isset($insert[0]) || !isset($insert[1])) {
|
||||
return;
|
||||
}
|
||||
[$table, $columns] = $insert;
|
||||
$table = $this->quoteIdentifier($table);
|
||||
foreach ($columns as &$column) {
|
||||
if (is_scalar($column)) {
|
||||
$column = $this->quoteIdentifier((string)$column);
|
||||
}
|
||||
}
|
||||
$query->insert($columns)->into($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the table name for an update query
|
||||
*
|
||||
* @param \Cake\Database\Query\UpdateQuery $query The update query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteUpdate(UpdateQuery $query): void
|
||||
{
|
||||
$table = $query->clause('update')[0];
|
||||
|
||||
if (is_string($table)) {
|
||||
$query->update($this->quoteIdentifier($table));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes identifiers in expression objects implementing the field interface
|
||||
*
|
||||
* @param \Cake\Database\Expression\FieldInterface $expression The expression to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteComparison(FieldInterface $expression): void
|
||||
{
|
||||
$field = $expression->getField();
|
||||
if (is_string($field)) {
|
||||
$expression->setField($this->quoteIdentifier($field));
|
||||
} elseif (is_array($field)) {
|
||||
$quoted = [];
|
||||
foreach ($field as $f) {
|
||||
$quoted[] = $this->quoteIdentifier($f);
|
||||
}
|
||||
$expression->setField($quoted);
|
||||
} else {
|
||||
$this->quoteExpression($field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes identifiers in "order by" expression objects
|
||||
*
|
||||
* Strings with spaces are treated as literal expressions
|
||||
* and will not have identifiers quoted.
|
||||
*
|
||||
* @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteOrderBy(OrderByExpression $expression): void
|
||||
{
|
||||
$expression->iterateParts(function ($part, &$field) {
|
||||
if (is_string($field)) {
|
||||
$field = $this->quoteIdentifier($field);
|
||||
|
||||
return $part;
|
||||
}
|
||||
if (is_string($part) && !str_contains($part, ' ')) {
|
||||
return $this->quoteIdentifier($part);
|
||||
}
|
||||
|
||||
return $part;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes identifiers in "order by" expression objects
|
||||
*
|
||||
* @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteIdentifierExpression(IdentifierExpression $expression): void
|
||||
{
|
||||
$expression->setIdentifier(
|
||||
$this->quoteIdentifier($expression->getIdentifier()),
|
||||
);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
|
||||
Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
<?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\Log;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Driver\Sqlserver;
|
||||
use Exception;
|
||||
use JsonSerializable;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Contains a query string, the params used to executed it, time taken to do it
|
||||
* and the number of rows found or affected by its execution.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LoggedQuery implements JsonSerializable, Stringable
|
||||
{
|
||||
/**
|
||||
* Driver executing the query
|
||||
*
|
||||
* @var \Cake\Database\Driver|null
|
||||
*/
|
||||
protected ?Driver $driver = null;
|
||||
|
||||
/**
|
||||
* Query string that was executed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $query = '';
|
||||
|
||||
/**
|
||||
* Number of milliseconds this query took to complete
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected float $took = 0;
|
||||
|
||||
/**
|
||||
* Associative array with the params bound to the query string
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $params = [];
|
||||
|
||||
/**
|
||||
* Number of rows affected or returned by the query execution
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $numRows = 0;
|
||||
|
||||
/**
|
||||
* The exception that was thrown by the execution of this query
|
||||
*
|
||||
* @var \Exception|null
|
||||
*/
|
||||
protected ?Exception $error = null;
|
||||
|
||||
/**
|
||||
* Helper function used to replace query placeholders by the real
|
||||
* params used to execute the query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function interpolate(): string
|
||||
{
|
||||
$params = array_map(function ($p) {
|
||||
if ($p === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (is_bool($p)) {
|
||||
if ($this->driver instanceof Sqlserver) {
|
||||
return $p ? '1' : '0';
|
||||
}
|
||||
|
||||
return $p ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
if (is_string($p)) {
|
||||
// Likely binary data like a blob or binary uuid.
|
||||
// pattern matches ascii control chars.
|
||||
if (preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $p) !== $p) {
|
||||
$p = bin2hex($p);
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
'$' => '\\$',
|
||||
'\\' => '\\\\\\\\',
|
||||
"'" => "''",
|
||||
];
|
||||
|
||||
$p = strtr($p, $replacements);
|
||||
|
||||
return "'{$p}'";
|
||||
}
|
||||
|
||||
return $p;
|
||||
}, $this->params);
|
||||
|
||||
$keys = [];
|
||||
$limit = is_int(key($params)) ? 1 : -1;
|
||||
foreach ($params as $key => $param) {
|
||||
$keys[] = is_string($key) ? "/:{$key}\b/" : '/[?]/';
|
||||
}
|
||||
|
||||
return (string)preg_replace($keys, $params, $this->query, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the logging context data for a query.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getContext(): array
|
||||
{
|
||||
return [
|
||||
'query' => $this->query,
|
||||
'numRows' => $this->numRows,
|
||||
'took' => $this->took,
|
||||
'role' => $this->driver ? $this->driver->getRole() : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set logging context for this query.
|
||||
*
|
||||
* @param array $context Context data.
|
||||
* @return void
|
||||
*/
|
||||
public function setContext(array $context): void
|
||||
{
|
||||
foreach ($context as $key => $val) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->{$key} = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data that will be serialized as JSON
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$error = $this->error;
|
||||
if ($error !== null) {
|
||||
$error = [
|
||||
'class' => $error::class,
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $this->query,
|
||||
'numRows' => $this->numRows,
|
||||
'params' => $this->params,
|
||||
'took' => $this->took,
|
||||
'error' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of this logged query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->params) {
|
||||
return $this->interpolate();
|
||||
}
|
||||
|
||||
return $this->query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Log;
|
||||
|
||||
use Cake\Log\Engine\BaseLog;
|
||||
use Cake\Log\Log;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* This class is a bridge used to write LoggedQuery objects into a real log.
|
||||
* by default this class use the built-in CakePHP Log class to accomplish this
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class QueryLogger extends BaseLog
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<string, mixed> $config Configuration array
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->_defaultConfig['scopes'] = ['queriesLog', 'cake.database.queries'];
|
||||
$this->_defaultConfig['connection'] = '';
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log($level, string|Stringable $message, array $context = []): void
|
||||
{
|
||||
$context += [
|
||||
'scope' => $this->scopes() ?: ['queriesLog', 'cake.database.queries'],
|
||||
'connection' => $this->getConfig('connection'),
|
||||
'query' => null,
|
||||
];
|
||||
|
||||
if ($context['query'] instanceof LoggedQuery) {
|
||||
$context = $context['query']->getContext() + $context;
|
||||
$message = 'connection={connection} role={role} duration={took} rows={numRows} ' . $message;
|
||||
}
|
||||
Log::write('debug', (string)$message, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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.0.3
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
|
||||
/**
|
||||
* Responsible for compiling a Query object into its SQL representation
|
||||
* for Postgres
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PostgresCompiler extends QueryCompiler
|
||||
{
|
||||
/**
|
||||
* Always quote aliases in SELECT clause.
|
||||
*
|
||||
* Postgres auto converts unquoted identifiers to lower case.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_quotedSelectAliases = true;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s',
|
||||
'order' => ' %s',
|
||||
'limit' => ' LIMIT %s',
|
||||
'offset' => ' OFFSET %s',
|
||||
'epilog' => ' %s',
|
||||
'comment' => '/* %s */ ',
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a HAVING clause,
|
||||
* it constructs the field list taking care of aliasing and
|
||||
* converting expression objects to string.
|
||||
*
|
||||
* @param array $parts list of fields to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildHavingPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$selectParts = $query->clause('select');
|
||||
|
||||
foreach ($selectParts as $selectKey => $selectPart) {
|
||||
if (!$selectPart instanceof FunctionExpression) {
|
||||
continue;
|
||||
}
|
||||
foreach ($parts as $k => $p) {
|
||||
if (!is_string($p)) {
|
||||
continue;
|
||||
}
|
||||
preg_match_all(
|
||||
'/\b' . trim($selectKey, '"') . '\b/i',
|
||||
$p,
|
||||
$matches,
|
||||
);
|
||||
|
||||
if (empty($matches[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[$k] = preg_replace(
|
||||
['/"/', '/\b' . trim($selectKey, '"') . '\b/i'],
|
||||
['', $selectPart->sql($binder)],
|
||||
$p,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf(' HAVING %s', implode(', ', $parts));
|
||||
}
|
||||
}
|
||||
+1896
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
<?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.5.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Query;
|
||||
|
||||
use Cake\Database\Query;
|
||||
|
||||
/**
|
||||
* This class is used to generate DELETE queries for the relational database.
|
||||
*/
|
||||
class DeleteQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_type = self::TYPE_DELETE;
|
||||
|
||||
/**
|
||||
* List of SQL parts that will be used to build this query.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_parts = [
|
||||
'comment' => null,
|
||||
'with' => [],
|
||||
'delete' => true,
|
||||
'optimizerHint' => [],
|
||||
'modifier' => [],
|
||||
'from' => [],
|
||||
'join' => [],
|
||||
'where' => null,
|
||||
'order' => null,
|
||||
'limit' => null,
|
||||
'epilog' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a delete query.
|
||||
*
|
||||
* Can be combined with from(), where() and other methods to
|
||||
* create delete queries with specific conditions.
|
||||
*
|
||||
* @param string|null $table The table to use when deleting.
|
||||
* @return $this
|
||||
*/
|
||||
public function delete(?string $table = null)
|
||||
{
|
||||
$this->_dirty();
|
||||
if ($table !== null) {
|
||||
$this->from($table);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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.5.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Query;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\Expression\ValuesExpression;
|
||||
use Cake\Database\Query;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* This class is used to generate INSERT queries for the relational database.
|
||||
*/
|
||||
class InsertQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_type = self::TYPE_INSERT;
|
||||
|
||||
/**
|
||||
* List of SQL parts that will be used to build this query.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_parts = [
|
||||
'comment' => null,
|
||||
'with' => [],
|
||||
'insert' => [],
|
||||
'optimizerHint' => [],
|
||||
'modifier' => [],
|
||||
'values' => [],
|
||||
'epilog' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an insert query.
|
||||
*
|
||||
* Note calling this method will reset any data previously set
|
||||
* with Query::values().
|
||||
*
|
||||
* @param array $columns The columns to insert into.
|
||||
* @param array<int|string, string> $types A map between columns & their datatypes.
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException When there are 0 columns.
|
||||
*/
|
||||
public function insert(array $columns, array $types = [])
|
||||
{
|
||||
if (!$columns) {
|
||||
throw new InvalidArgumentException('At least 1 column is required to perform an insert.');
|
||||
}
|
||||
$this->_dirty();
|
||||
$this->_parts['insert'][1] = $columns;
|
||||
if (!$this->_parts['values']) {
|
||||
$this->_parts['values'] = new ValuesExpression($columns, $this->getTypeMap()->setTypes($types));
|
||||
} else {
|
||||
/** @var \Cake\Database\Expression\ValuesExpression $valuesExpr */
|
||||
$valuesExpr = $this->_parts['values'];
|
||||
$valuesExpr->setColumns($columns);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table name for insert queries.
|
||||
*
|
||||
* @param string $table The table name to insert into.
|
||||
* @return $this
|
||||
*/
|
||||
public function into(string $table)
|
||||
{
|
||||
$this->_dirty();
|
||||
$this->_parts['insert'][0] = $table;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values for an insert query.
|
||||
*
|
||||
* Multi inserts can be performed by calling values() more than one time,
|
||||
* or by providing an array of value sets. Additionally $data can be a Query
|
||||
* instance to insert data from another SELECT statement.
|
||||
*
|
||||
* @param \Cake\Database\Expression\ValuesExpression|\Cake\Database\Query|array $data The data to insert.
|
||||
* @return $this
|
||||
* @throws \Cake\Database\Exception\DatabaseException if you try to set values before declaring columns.
|
||||
* Or if you try to set values on non-insert queries.
|
||||
*/
|
||||
public function values(ValuesExpression|Query|array $data)
|
||||
{
|
||||
if (empty($this->_parts['insert'])) {
|
||||
throw new DatabaseException(
|
||||
'You cannot add values before defining columns to use.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_dirty();
|
||||
if ($data instanceof ValuesExpression) {
|
||||
$this->_parts['values'] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @var \Cake\Database\Expression\ValuesExpression $valuesExpr */
|
||||
$valuesExpr = $this->_parts['values'];
|
||||
$valuesExpr->add($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Query;
|
||||
|
||||
use Cake\Database\Connection;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Factory class for generating instances of Select, Insert, Update, Delete queries.
|
||||
*/
|
||||
class QueryFactory
|
||||
{
|
||||
/**
|
||||
* Constructor/
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection Connection instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SelectQuery instance.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields Fields/columns list for the query.
|
||||
* @param array|string $table List of tables to query.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\SelectQuery
|
||||
*/
|
||||
public function select(
|
||||
ExpressionInterface|Closure|array|string|float|int $fields = [],
|
||||
array|string $table = [],
|
||||
array $types = [],
|
||||
): SelectQuery {
|
||||
$query = new SelectQuery($this->connection);
|
||||
|
||||
$query
|
||||
->select($fields)
|
||||
->from($table)
|
||||
->setDefaultTypes($types);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new InsertQuery instance.
|
||||
*
|
||||
* @param string|null $table The table to insert rows into.
|
||||
* @param array $values Associative array of column => value to be inserted.
|
||||
* @param array<int|string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\InsertQuery
|
||||
*/
|
||||
public function insert(?string $table = null, array $values = [], array $types = []): InsertQuery
|
||||
{
|
||||
$query = new InsertQuery($this->connection);
|
||||
|
||||
if ($table) {
|
||||
$query->into($table);
|
||||
}
|
||||
|
||||
if ($values) {
|
||||
$columns = array_keys($values);
|
||||
$query
|
||||
->insert($columns, $types)
|
||||
->values($values);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new UpdateQuery instance.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string|null $table The table to update rows of.
|
||||
* @param array $values Values to be updated.
|
||||
* @param array $conditions Conditions to be set for the update statement.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\UpdateQuery
|
||||
*/
|
||||
public function update(
|
||||
ExpressionInterface|string|null $table = null,
|
||||
array $values = [],
|
||||
array $conditions = [],
|
||||
array $types = [],
|
||||
): UpdateQuery {
|
||||
$query = new UpdateQuery($this->connection);
|
||||
|
||||
if ($table) {
|
||||
$query->update($table);
|
||||
}
|
||||
if ($values) {
|
||||
$query->set($values, $types);
|
||||
}
|
||||
if ($conditions) {
|
||||
$query->where($conditions, $types);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DeleteQuery instance.
|
||||
*
|
||||
* @param string|null $table The table to delete rows from.
|
||||
* @param array $conditions Conditions to be set for the delete statement.
|
||||
* @param array<string, string> $types Associative array containing the types to be used for casting.
|
||||
* @return \Cake\Database\Query\DeleteQuery
|
||||
*/
|
||||
public function delete(?string $table = null, array $conditions = [], array $types = []): DeleteQuery
|
||||
{
|
||||
$query = (new DeleteQuery($this->connection))
|
||||
->delete($table);
|
||||
|
||||
if ($conditions) {
|
||||
$query->where($conditions, $types);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
<?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.5.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Query;
|
||||
|
||||
use ArrayIterator;
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\Connection;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\WindowExpression;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\StatementInterface;
|
||||
use Cake\Database\TypeMap;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* This class is used to generate SELECT queries for the relational database.
|
||||
*
|
||||
* @template T of mixed
|
||||
* @implements \IteratorAggregate<T>
|
||||
*/
|
||||
class SelectQuery extends Query implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* Type of this query.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_type = self::TYPE_SELECT;
|
||||
|
||||
/**
|
||||
* List of SQL parts that will be used to build this query.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_parts = [
|
||||
'comment' => null,
|
||||
'with' => [],
|
||||
'select' => [],
|
||||
'optimizerHint' => [],
|
||||
'modifier' => [],
|
||||
'distinct' => false,
|
||||
'from' => [],
|
||||
'join' => [],
|
||||
'where' => null,
|
||||
'group' => [],
|
||||
'having' => null,
|
||||
'window' => [],
|
||||
'order' => null,
|
||||
'limit' => null,
|
||||
'offset' => null,
|
||||
'union' => [],
|
||||
'epilog' => null,
|
||||
'intersect' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of callbacks to be called to alter each row from resulting
|
||||
* statement upon retrieval. Each one of the callback function will receive
|
||||
* the row array as first argument.
|
||||
*
|
||||
* @var array<\Closure>
|
||||
*/
|
||||
protected array $_resultDecorators = [];
|
||||
|
||||
/**
|
||||
* Result set from executed SELECT query.
|
||||
*
|
||||
* @var iterable|null
|
||||
*/
|
||||
protected ?iterable $_results = null;
|
||||
|
||||
/**
|
||||
* Boolean for tracking whether buffered results
|
||||
* are enabled.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $bufferedResults = true;
|
||||
|
||||
/**
|
||||
* The Type map for fields in the select clause
|
||||
*
|
||||
* @var \Cake\Database\TypeMap|null
|
||||
*/
|
||||
protected ?TypeMap $_selectTypeMap = null;
|
||||
|
||||
/**
|
||||
* Tracking flag to disable casting
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $typeCastEnabled = true;
|
||||
|
||||
/**
|
||||
* Executes query and returns set of decorated results.
|
||||
*
|
||||
* The results are cached until the query is modified and marked dirty.
|
||||
*
|
||||
* @return iterable
|
||||
* @throws \Cake\Core\Exception\CakeException When query is not a SELECT query.
|
||||
*/
|
||||
public function all(): iterable
|
||||
{
|
||||
if ($this->_results === null || $this->_dirty) {
|
||||
$this->_results = $this->execute()->fetchAll(StatementInterface::FETCH_TYPE_ASSOC);
|
||||
}
|
||||
|
||||
return $this->_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new fields to be returned by a `SELECT` statement when this query is
|
||||
* executed. Fields can be passed as an array of strings, array of expression
|
||||
* objects, a single expression or a single string.
|
||||
*
|
||||
* If an array is passed, keys will be used to alias fields using the value as the
|
||||
* real field to be aliased. It is possible to alias strings, Expression objects or
|
||||
* even other Query objects.
|
||||
*
|
||||
* If a callback is passed, the returning array of the function will
|
||||
* be used as the list of fields.
|
||||
*
|
||||
* By default this function will append any passed argument to the list of fields
|
||||
* to be selected, unless the second argument is set to true.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* $query->select(['id', 'title']); // Produces SELECT id, title
|
||||
* $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author
|
||||
* $query->select('id', true); // Resets the list: SELECT id
|
||||
* $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total
|
||||
* $query->select(function ($query) {
|
||||
* return ['article_id', 'total' => $query->func()->count('*')];
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append
|
||||
* fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields
|
||||
* from the table.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|float|int $fields fields to be added to the list.
|
||||
* @param bool $overwrite whether to reset fields with passed list or not
|
||||
* @return $this
|
||||
*/
|
||||
public function select(ExpressionInterface|Closure|array|string|float|int $fields = [], bool $overwrite = false)
|
||||
{
|
||||
if (!is_string($fields) && $fields instanceof Closure) {
|
||||
$fields = $fields($this);
|
||||
}
|
||||
|
||||
if (!is_array($fields)) {
|
||||
$fields = [$fields];
|
||||
}
|
||||
|
||||
if ($overwrite) {
|
||||
$this->_parts['select'] = $fields;
|
||||
} else {
|
||||
$this->_parts['select'] = array_merge($this->_parts['select'], $fields);
|
||||
}
|
||||
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `DISTINCT` clause to the query to remove duplicates from the result set.
|
||||
* This clause can only be used for select statements.
|
||||
*
|
||||
* If you wish to filter duplicates based of those rows sharing a particular field
|
||||
* or set of fields, you may pass an array of fields to filter on. Beware that
|
||||
* this option might not be fully supported in all database systems.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* // Filters products with the same name and city
|
||||
* $query->select(['name', 'city'])->from('products')->distinct();
|
||||
*
|
||||
* // Filters products in the same city
|
||||
* $query->distinct(['city']);
|
||||
* $query->distinct('city');
|
||||
*
|
||||
* // Filter products with the same name
|
||||
* $query->distinct(['name'], true);
|
||||
* $query->distinct('name', true);
|
||||
* ```
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string|bool $on Enable/disable distinct class
|
||||
* or list of fields to be filtered on
|
||||
* @param bool $overwrite whether to reset fields with passed list or not
|
||||
* @return $this
|
||||
*/
|
||||
public function distinct(ExpressionInterface|array|string|bool $on = [], bool $overwrite = false)
|
||||
{
|
||||
if ($on === []) {
|
||||
$on = true;
|
||||
} elseif (is_string($on)) {
|
||||
$on = [$on];
|
||||
}
|
||||
|
||||
if (is_array($on)) {
|
||||
$merge = [];
|
||||
if (is_array($this->_parts['distinct'])) {
|
||||
$merge = $this->_parts['distinct'];
|
||||
}
|
||||
$on = $overwrite ? array_values($on) : array_merge($merge, array_values($on));
|
||||
}
|
||||
|
||||
$this->_parts['distinct'] = $on;
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
|
||||
* Fields can be passed as an array of strings, array of expression
|
||||
* objects, a single expression or a single string.
|
||||
*
|
||||
* By default this function will append any passed argument to the list of fields
|
||||
* to be grouped, unless the second argument is set to true.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* // Produces GROUP BY id, title
|
||||
* $query->groupBy(['id', 'title']);
|
||||
*
|
||||
* // Produces GROUP BY title
|
||||
* $query->groupBy('title');
|
||||
* ```
|
||||
*
|
||||
* Group fields are not suitable for use with user supplied data as they are
|
||||
* not sanitized by the query builder.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
|
||||
* @param bool $overwrite whether to reset fields with passed list or not
|
||||
* @return $this
|
||||
* @deprecated 5.0.0 Use groupBy() instead now that CollectionInterface methods are no longer proxied.
|
||||
*/
|
||||
public function group(ExpressionInterface|array|string $fields, bool $overwrite = false)
|
||||
{
|
||||
deprecationWarning('5.0.0', 'SelectQuery::group() is deprecated. Use SelectQuery::groupBy() instead.');
|
||||
|
||||
return $this->groupBy($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single or multiple fields to be used in the GROUP BY clause for this query.
|
||||
* Fields can be passed as an array of strings, array of expression
|
||||
* objects, a single expression or a single string.
|
||||
*
|
||||
* By default this function will append any passed argument to the list of fields
|
||||
* to be grouped, unless the second argument is set to true.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* // Produces GROUP BY id, title
|
||||
* $query->groupBy(['id', 'title']);
|
||||
*
|
||||
* // Produces GROUP BY title
|
||||
* $query->groupBy('title');
|
||||
* ```
|
||||
*
|
||||
* Group fields are not suitable for use with user supplied data as they are
|
||||
* not sanitized by the query builder.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $fields fields to be added to the list
|
||||
* @param bool $overwrite whether to reset fields with passed list or not
|
||||
* @return $this
|
||||
*/
|
||||
public function groupBy(ExpressionInterface|array|string $fields, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['group'] = [];
|
||||
}
|
||||
|
||||
if (!is_array($fields)) {
|
||||
$fields = [$fields];
|
||||
}
|
||||
|
||||
$this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields));
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition or set of conditions to be used in the `HAVING` clause for this
|
||||
* query. This method operates in exactly the same way as the method `where()`
|
||||
* does. Please refer to its documentation for an insight on how to using each
|
||||
* parameter.
|
||||
*
|
||||
* Having fields are not suitable for use with user supplied data as they are
|
||||
* not sanitized by the query builder.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The having conditions.
|
||||
* @param array<string, string> $types Associative array of type names used to bind values to query
|
||||
* @param bool $overwrite whether to reset conditions with passed list or not
|
||||
* @see \Cake\Database\Query::where()
|
||||
* @return $this
|
||||
*/
|
||||
public function having(
|
||||
ExpressionInterface|Closure|array|string|null $conditions = null,
|
||||
array $types = [],
|
||||
bool $overwrite = false,
|
||||
) {
|
||||
if ($overwrite) {
|
||||
$this->_parts['having'] = $this->expr();
|
||||
}
|
||||
$this->_conjugate('having', $conditions, 'AND', $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects any previously defined set of conditions to the provided list
|
||||
* using the AND operator in the HAVING clause. This method operates in exactly
|
||||
* the same way as the method `andWhere()` does. Please refer to its
|
||||
* documentation for an insight on how to using each parameter.
|
||||
*
|
||||
* Having fields are not suitable for use with user supplied data as they are
|
||||
* not sanitized by the query builder.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array|string $conditions The AND conditions for HAVING.
|
||||
* @param array<string, string> $types Associative array of type names used to bind values to query
|
||||
* @see \Cake\Database\Query::andWhere()
|
||||
* @return $this
|
||||
*/
|
||||
public function andHaving(ExpressionInterface|Closure|array|string $conditions, array $types = [])
|
||||
{
|
||||
$this->_conjugate('having', $conditions, 'AND', $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a named window expression.
|
||||
*
|
||||
* You are responsible for adding windows in the order your database requires.
|
||||
*
|
||||
* @param string $name Window name
|
||||
* @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression
|
||||
* @param bool $overwrite Clear all previous query window expressions
|
||||
* @return $this
|
||||
*/
|
||||
public function window(string $name, WindowExpression|Closure $window, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['window'] = [];
|
||||
}
|
||||
|
||||
if ($window instanceof Closure) {
|
||||
$window = $window(new WindowExpression(), $this);
|
||||
if (!($window instanceof WindowExpression)) {
|
||||
throw new CakeException('You must return a `WindowExpression` from a Closure passed to `window()`.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->_parts['window'][] = ['name' => new IdentifierExpression($name), 'window' => $window];
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the page of results you want.
|
||||
*
|
||||
* This method provides an easier to use interface to set the limit + offset
|
||||
* in the record set you want as results. If empty the limit will default to
|
||||
* the existing limit clause, and if that too is empty, then `25` will be used.
|
||||
*
|
||||
* Pages must start at 1.
|
||||
*
|
||||
* @param int $num The page number you want.
|
||||
* @param int|null $limit The number of rows you want in the page. If null
|
||||
* the current limit clause will be used.
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException If page number < 1.
|
||||
*/
|
||||
public function page(int $num, ?int $limit = null)
|
||||
{
|
||||
if ($num < 1) {
|
||||
throw new InvalidArgumentException('Pages must start at 1.');
|
||||
}
|
||||
if ($limit !== null) {
|
||||
$this->limit($limit);
|
||||
}
|
||||
$limit = $this->clause('limit');
|
||||
if ($limit === null) {
|
||||
$limit = 25;
|
||||
$this->limit($limit);
|
||||
}
|
||||
$offset = ($num - 1) * $limit;
|
||||
if (PHP_INT_MAX <= $offset) {
|
||||
$offset = PHP_INT_MAX;
|
||||
}
|
||||
$this->offset((int)$offset);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a complete query to be used in conjunction with an UNION operator with
|
||||
* this query. This is used to combine the result set of this query with the one
|
||||
* that will be returned by the passed query. You can add as many queries as you
|
||||
* required by calling multiple times this method with different queries.
|
||||
*
|
||||
* By default, the UNION operator will remove duplicate rows, if you wish to include
|
||||
* every row for all queries, use unionAll().
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```
|
||||
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
|
||||
* $query->select(['id', 'name'])->from(['d' => 'things'])->union($union);
|
||||
* ```
|
||||
*
|
||||
* Will produce:
|
||||
*
|
||||
* `SELECT id, name FROM things d UNION SELECT id, title FROM articles a`
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
|
||||
* @param bool $overwrite whether to reset the list of queries to be operated or not
|
||||
* @return $this
|
||||
*/
|
||||
public function union(Query|string $query, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['union'] = [];
|
||||
}
|
||||
$this->_parts['union'][] = [
|
||||
'all' => false,
|
||||
'query' => $query,
|
||||
];
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a complete query to be used in conjunction with the UNION ALL operator with
|
||||
* this query. This is used to combine the result set of this query with the one
|
||||
* that will be returned by the passed query. You can add as many queries as you
|
||||
* required by calling multiple times this method with different queries.
|
||||
*
|
||||
* Unlike UNION, UNION ALL will not remove duplicate rows.
|
||||
*
|
||||
* ```
|
||||
* $union = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
|
||||
* $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union);
|
||||
* ```
|
||||
*
|
||||
* Will produce:
|
||||
*
|
||||
* `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a`
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query full SQL query to be used in UNION operator
|
||||
* @param bool $overwrite whether to reset the list of queries to be operated or not
|
||||
* @return $this
|
||||
*/
|
||||
public function unionAll(Query|string $query, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['union'] = [];
|
||||
}
|
||||
$this->_parts['union'][] = [
|
||||
'all' => true,
|
||||
'query' => $query,
|
||||
];
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a complete query to be used in conjunction with an INTERSECT operator with
|
||||
* this query. This is used to combine the result set of this query with the one
|
||||
* that will be returned by the passed query. You can add as many queries as you
|
||||
* required by calling multiple times this method with different queries.
|
||||
*
|
||||
* By default, the INTERSECT operator will remove duplicate rows, if you wish to include
|
||||
* every row for all queries, use intersectAll().
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```
|
||||
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
|
||||
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersect($intersect);
|
||||
* ```
|
||||
*
|
||||
* Will produce:
|
||||
*
|
||||
* `SELECT id, name FROM things d INTERSECT SELECT id, title FROM articles a`
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
|
||||
* @param bool $overwrite whether to reset the list of queries to be operated or not
|
||||
* @return $this
|
||||
*/
|
||||
public function intersect(Query|string $query, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['intersect'] = [];
|
||||
}
|
||||
$this->_parts['intersect'][] = [
|
||||
'all' => false,
|
||||
'query' => $query,
|
||||
];
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a complete query to be used in conjunction with the INTERSECT ALL operator with
|
||||
* this query. This is used to combine the result set of this query with the one
|
||||
* that will be returned by the passed query. You can add as many queries as you
|
||||
* required by calling multiple times this method with different queries.
|
||||
*
|
||||
* Unlike INTERSECT, INTERSECT ALL will not remove duplicate rows.
|
||||
*
|
||||
* ```
|
||||
* $intersect = (new SelectQuery($conn))->select(['id', 'title'])->from(['a' => 'articles']);
|
||||
* $query->select(['id', 'name'])->from(['d' => 'things'])->intersectAll($intersect);
|
||||
* ```
|
||||
*
|
||||
* Will produce:
|
||||
*
|
||||
* `SELECT id, name FROM things d INTERSECT ALL SELECT id, title FROM articles a`
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query full SQL query to be used in INTERSECT operator
|
||||
* @param bool $overwrite whether to reset the list of queries to be operated or not
|
||||
* @return $this
|
||||
*/
|
||||
public function intersectAll(Query|string $query, bool $overwrite = false)
|
||||
{
|
||||
if ($overwrite) {
|
||||
$this->_parts['intersect'] = [];
|
||||
}
|
||||
$this->_parts['intersect'][] = [
|
||||
'all' => true,
|
||||
'query' => $query,
|
||||
];
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes this query and returns a results iterator. This function is required
|
||||
* for implementing the IteratorAggregate interface and allows the query to be
|
||||
* iterated without having to call all() manually, thus making it look like
|
||||
* a result set instead of the query itself.
|
||||
*
|
||||
* @return \Traversable
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
if ($this->bufferedResults) {
|
||||
/** @var \Traversable|array $results */
|
||||
$results = $this->all();
|
||||
if (is_array($results)) {
|
||||
return new ArrayIterator($results);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
return $this->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be executed for each result that is fetched from the
|
||||
* result set, the callback function will receive as first parameter an array with
|
||||
* the raw data from the database for every row that is fetched and must return the
|
||||
* row with any possible modifications.
|
||||
*
|
||||
* Callbacks will be executed lazily, if only 3 rows are fetched for database it will
|
||||
* be called 3 times, event though there might be more rows to be fetched in the cursor.
|
||||
*
|
||||
* Callbacks are stacked in the order they are registered, if you wish to reset the stack
|
||||
* the call this function with the second parameter set to true.
|
||||
*
|
||||
* If you wish to remove all decorators from the stack, set the first parameter
|
||||
* to null and the second to true.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* $query->decorateResults(function ($row) {
|
||||
* $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']);
|
||||
* return $row;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param \Closure|null $callback The callback to invoke when results are fetched.
|
||||
* @param bool $overwrite Whether this should append or replace all existing decorators.
|
||||
* @return $this
|
||||
*/
|
||||
public function decorateResults(?Closure $callback, bool $overwrite = false)
|
||||
{
|
||||
$this->_dirty();
|
||||
if ($overwrite) {
|
||||
$this->_resultDecorators = [];
|
||||
}
|
||||
|
||||
if ($callback !== null) {
|
||||
$this->_resultDecorators[] = $callback;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get result decorators.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getResultDecorators(): array
|
||||
{
|
||||
return $this->_resultDecorators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables buffered results.
|
||||
*
|
||||
* When enabled the results returned by this query will be
|
||||
* buffered. This enables you to iterate a result set multiple times, or
|
||||
* both cache and iterate it.
|
||||
*
|
||||
* When disabled it will consume less memory as fetched results are not
|
||||
* remembered for future iterations.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function enableBufferedResults()
|
||||
{
|
||||
$this->_dirty();
|
||||
$this->bufferedResults = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables buffered results.
|
||||
*
|
||||
* Disabling buffering will consume less memory as fetched results are not
|
||||
* remembered for future iterations.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableBufferedResults()
|
||||
{
|
||||
$this->_dirty();
|
||||
$this->bufferedResults = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether buffered results are enabled/disabled.
|
||||
*
|
||||
* When enabled the results returned by this query will be
|
||||
* buffered. This enables you to iterate a result set multiple times, or
|
||||
* both cache and iterate it.
|
||||
*
|
||||
* When disabled it will consume less memory as fetched results are not
|
||||
* remembered for future iterations.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isBufferedResultsEnabled(): bool
|
||||
{
|
||||
return $this->bufferedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TypeMap class where the types for each of the fields in the
|
||||
* select clause are stored.
|
||||
*
|
||||
* @param \Cake\Database\TypeMap|array $typeMap Creates a TypeMap if array, otherwise sets the given TypeMap.
|
||||
* @return $this
|
||||
*/
|
||||
public function setSelectTypeMap(TypeMap|array $typeMap)
|
||||
{
|
||||
$this->_selectTypeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap;
|
||||
$this->_dirty();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TypeMap class where the types for each of the fields in the
|
||||
* select clause are stored.
|
||||
*
|
||||
* @return \Cake\Database\TypeMap
|
||||
*/
|
||||
public function getSelectTypeMap(): TypeMap
|
||||
{
|
||||
return $this->_selectTypeMap ??= new TypeMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables result casting.
|
||||
*
|
||||
* When disabled, the fields will be returned as received from the database
|
||||
* driver (which in most environments means they are being returned as
|
||||
* strings), which can improve performance with larger datasets.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableResultsCasting()
|
||||
{
|
||||
$this->typeCastEnabled = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables result casting.
|
||||
*
|
||||
* When enabled, the fields in the results returned by this Query will be
|
||||
* cast to their corresponding PHP data type.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function enableResultsCasting()
|
||||
{
|
||||
$this->typeCastEnabled = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether result casting is enabled/disabled.
|
||||
*
|
||||
* When enabled, the fields in the results returned by this Query will be
|
||||
* casted to their corresponding PHP data type.
|
||||
*
|
||||
* When disabled, the fields will be returned as received from the database
|
||||
* driver (which in most environments means they are being returned as
|
||||
* strings), which can improve performance with larger datasets.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isResultsCastingEnabled(): bool
|
||||
{
|
||||
return $this->typeCastEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clearing iterator and cloning all expressions and value binders.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
|
||||
$this->_results = null;
|
||||
if ($this->_selectTypeMap !== null) {
|
||||
$this->_selectTypeMap = clone $this->_selectTypeMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array that can be used to describe the internal state of this
|
||||
* object.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
$return = parent::__debugInfo();
|
||||
$return['decorators'] = count($this->_resultDecorators);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection role.
|
||||
*
|
||||
* @param string $role Connection role ('read' or 'write')
|
||||
* @return $this
|
||||
*/
|
||||
public function setConnectionRole(string $role)
|
||||
{
|
||||
assert($role === Connection::ROLE_READ || $role === Connection::ROLE_WRITE);
|
||||
$this->connectionRole = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection role to read.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function useReadRole()
|
||||
{
|
||||
return $this->setConnectionRole(Connection::ROLE_READ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the connection role to write.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function useWriteRole()
|
||||
{
|
||||
return $this->setConnectionRole(Connection::ROLE_WRITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?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.5.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Query;
|
||||
|
||||
use Cake\Database\Expression\ComparisonExpression;
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* This class is used to generate UPDATE queries for the relational database.
|
||||
*/
|
||||
class UpdateQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_type = self::TYPE_UPDATE;
|
||||
|
||||
/**
|
||||
* List of SQL parts that will be used to build this query.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_parts = [
|
||||
'comment' => null,
|
||||
'with' => [],
|
||||
'update' => [],
|
||||
'optimizerHint' => [],
|
||||
'modifier' => [],
|
||||
'join' => [],
|
||||
'set' => [],
|
||||
'where' => null,
|
||||
'order' => null,
|
||||
'limit' => null,
|
||||
'epilog' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an update query.
|
||||
*
|
||||
* Can be combined with set() and where() methods to create update queries.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $table The table you want to update.
|
||||
* @return $this
|
||||
*/
|
||||
public function update(ExpressionInterface|string $table)
|
||||
{
|
||||
$this->_dirty();
|
||||
$this->_parts['update'][0] = $table;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one or many fields to update.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* Passing a string:
|
||||
*
|
||||
* ```
|
||||
* $query->update('articles')->set('title', 'The Title');
|
||||
* ```
|
||||
*
|
||||
* Passing an array:
|
||||
*
|
||||
* ```
|
||||
* $query->update('articles')->set(['title' => 'The Title'], ['title' => 'string']);
|
||||
* ```
|
||||
*
|
||||
* Passing a callback:
|
||||
*
|
||||
* ```
|
||||
* $query->update('articles')->set(function (ExpressionInterface $exp) {
|
||||
* return $exp->eq('title', 'The title', 'string');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param \Cake\Database\Expression\QueryExpression|\Closure|array|string $key The column name or array of keys
|
||||
* + values to set. This can also be a QueryExpression containing a SQL fragment.
|
||||
* It can also be a Closure, that is required to return an expression object.
|
||||
* @param mixed $value The value to update $key to. Can be null if $key is an
|
||||
* array or QueryExpression. When $key is an array, this parameter will be
|
||||
* used as $types instead.
|
||||
* @param array<string, string>|string $types The column types to treat data as.
|
||||
* @return $this
|
||||
*/
|
||||
public function set(QueryExpression|Closure|array|string $key, mixed $value = null, array|string $types = [])
|
||||
{
|
||||
if (empty($this->_parts['set'])) {
|
||||
$this->_parts['set'] = $this->expr()->setConjunction(',');
|
||||
}
|
||||
|
||||
if ($key instanceof Closure) {
|
||||
$exp = $this->expr()->setConjunction(',');
|
||||
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
|
||||
$setExpr = $this->_parts['set'];
|
||||
$setExpr->add($key($exp));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_array($key) && !isset($key[0])) {
|
||||
$typeMap = $this->getTypeMap()->setTypes($value ?? []);
|
||||
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
|
||||
$setExpr = $this->_parts['set'];
|
||||
foreach ($key as $k => $v) {
|
||||
$setExpr->add(new ComparisonExpression($k, $v, $typeMap->type($k)));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_array($key) || $key instanceof ExpressionInterface) {
|
||||
$types = (array)$value;
|
||||
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
|
||||
$setExpr = $this->_parts['set'];
|
||||
$setExpr->add($key, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!is_string($types)) {
|
||||
$types = null;
|
||||
}
|
||||
/** @var \Cake\Database\Expression\QueryExpression $setExpr */
|
||||
$setExpr = $this->_parts['set'];
|
||||
$setExpr->eq($key, $value, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
<?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;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Closure;
|
||||
use Countable;
|
||||
|
||||
/**
|
||||
* Responsible for compiling a Query object into its SQL representation
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class QueryCompiler
|
||||
{
|
||||
/**
|
||||
* List of sprintf templates that will be used for compiling the SQL for
|
||||
* this query. There are some clauses that can be built as just as the
|
||||
* direct concatenation of the internal parts, those are listed here.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s ',
|
||||
'having' => ' HAVING %s ',
|
||||
'order' => ' %s',
|
||||
'limit' => ' LIMIT %s',
|
||||
'offset' => ' OFFSET %s',
|
||||
'epilog' => ' %s',
|
||||
'comment' => '/* %s */ ',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating a SELECT statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_selectParts = [
|
||||
'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
|
||||
'limit', 'offset', 'union', 'epilog', 'intersect',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating an UPDATE statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_updateParts = ['comment', 'with', 'update', 'set', 'where', 'epilog'];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating a DELETE statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_deleteParts = ['comment', 'with', 'delete', 'optimizerHint', 'modifier', 'from', 'where',
|
||||
'epilog'];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating an INSERT statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_insertParts = ['comment', 'with', 'insert', 'values', 'epilog'];
|
||||
|
||||
/**
|
||||
* Indicate whether aliases in SELECT clause need to be always quoted.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_quotedSelectAliases = false;
|
||||
|
||||
/**
|
||||
* Returns the SQL representation of the provided query after generating
|
||||
* the placeholders for the bound values using the provided generator
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholders
|
||||
* @return string
|
||||
*/
|
||||
public function compile(Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$sql = '';
|
||||
$type = $query->type();
|
||||
$query->traverseParts(
|
||||
$this->_sqlCompiler($sql, $query, $binder),
|
||||
$this->{"_{$type}Parts"},
|
||||
);
|
||||
|
||||
// Propagate bound parameters from sub-queries if the
|
||||
// placeholders can be found in the SQL statement. Only
|
||||
// add new placeholders, as sub-queries may have been executed already.
|
||||
if ($query->getValueBinder() !== $binder) {
|
||||
$existing = $binder->bindings();
|
||||
foreach ($query->getValueBinder()->bindings() as $binding) {
|
||||
$placeholder = ':' . $binding['placeholder'];
|
||||
if (!isset($existing[$placeholder]) && preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) {
|
||||
$binder->bind($placeholder, $binding['value'], $binding['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a closure that can be used to compile a SQL string representation
|
||||
* of this query.
|
||||
*
|
||||
* @param string $sql initial sql string to append to
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return \Closure
|
||||
*/
|
||||
protected function _sqlCompiler(string &$sql, Query $query, ValueBinder $binder): Closure
|
||||
{
|
||||
return function ($part, $partName) use (&$sql, $query, $binder): void {
|
||||
if (
|
||||
$part === null ||
|
||||
($part === []) ||
|
||||
($part instanceof Countable && count($part) === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($part instanceof ExpressionInterface) {
|
||||
$part = [$part->sql($binder)];
|
||||
}
|
||||
if (isset($this->_templates[$partName])) {
|
||||
$part = $this->_stringifyExpressions((array)$part, $binder);
|
||||
$sql .= sprintf($this->_templates[$partName], implode(', ', $part));
|
||||
|
||||
return;
|
||||
}
|
||||
$sql .= $this->{'_build' . $partName . 'Part'}($part, $query, $binder);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a `WITH` clause,
|
||||
* it constructs the CTE definitions list and generates the `RECURSIVE`
|
||||
* keyword when required.
|
||||
*
|
||||
* @param array<\Cake\Database\Expression\CommonTableExpression> $parts List of CTEs to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$recursive = false;
|
||||
$expressions = [];
|
||||
foreach ($parts as $cte) {
|
||||
$recursive = $recursive || $cte->isRecursive();
|
||||
$expressions[] = $cte->sql($binder);
|
||||
}
|
||||
|
||||
$recursive = $recursive ? 'RECURSIVE ' : '';
|
||||
|
||||
return sprintf('WITH %s%s ', $recursive, implode(', ', $expressions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a SELECT clause,
|
||||
* it constructs the field list taking care of aliasing and
|
||||
* converting expression objects to string. This function also constructs the
|
||||
* DISTINCT clause for the query.
|
||||
*
|
||||
* @param array $parts list of fields to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$driver = $query->getDriver();
|
||||
$select = 'SELECT%s%s %s%s';
|
||||
if (
|
||||
($query->clause('union') || $query->clause('intersect')) &&
|
||||
$driver->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY)
|
||||
) {
|
||||
$select = '(SELECT%s%s %s%s';
|
||||
}
|
||||
|
||||
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
$quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases;
|
||||
$normalized = [];
|
||||
$parts = $this->_stringifyExpressions($parts, $binder);
|
||||
foreach ($parts as $k => $p) {
|
||||
if (!is_numeric($k)) {
|
||||
$p .= ' AS ';
|
||||
if ($quoteIdentifiers) {
|
||||
$p .= $driver->quoteIdentifier($k);
|
||||
} else {
|
||||
$p .= $k;
|
||||
}
|
||||
}
|
||||
$normalized[] = $p;
|
||||
}
|
||||
|
||||
$distinct = $query->clause('distinct');
|
||||
if ($distinct === true) {
|
||||
$distinct = 'DISTINCT ';
|
||||
} elseif (is_array($distinct)) {
|
||||
$distinct = $this->_stringifyExpressions($distinct, $binder);
|
||||
$distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct));
|
||||
}
|
||||
|
||||
return sprintf($select, $hint, $modifiers, $distinct, implode(', ', $normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a FROM clause,
|
||||
* it constructs the tables list taking care of aliasing and
|
||||
* converting expression objects to string.
|
||||
*
|
||||
* @param array $parts list of tables to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildFromPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$select = ' FROM %s';
|
||||
$normalized = [];
|
||||
$parts = $this->_stringifyExpressions($parts, $binder);
|
||||
foreach ($parts as $k => $p) {
|
||||
if (!is_numeric($k)) {
|
||||
$p = $p . ' ' . $k;
|
||||
}
|
||||
$normalized[] = $p;
|
||||
}
|
||||
|
||||
return sprintf($select, implode(', ', $normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of multiple JOIN clauses,
|
||||
* it constructs the joins list taking care of aliasing and converting
|
||||
* expression objects to string in both the table to be joined and the conditions
|
||||
* to be used.
|
||||
*
|
||||
* @param array $parts list of joins to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildJoinPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$joins = '';
|
||||
foreach ($parts as $join) {
|
||||
if (!isset($join['table'])) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Could not compile join clause for alias `%s`. No table was specified. ' .
|
||||
'Use the `table` key to define a table.',
|
||||
$join['alias'],
|
||||
));
|
||||
}
|
||||
if ($join['table'] instanceof ExpressionInterface) {
|
||||
$join['table'] = '(' . $join['table']->sql($binder) . ')';
|
||||
}
|
||||
|
||||
$joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']);
|
||||
|
||||
$condition = '';
|
||||
if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) {
|
||||
$condition = $join['conditions']->sql($binder);
|
||||
}
|
||||
if ($condition === '') {
|
||||
$joins .= ' ON 1 = 1';
|
||||
} else {
|
||||
$joins .= " ON {$condition}";
|
||||
}
|
||||
}
|
||||
|
||||
return $joins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to build the string representation of a window clause.
|
||||
*
|
||||
* @param array $parts List of windows to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildWindowPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$windows = [];
|
||||
foreach ($parts as $window) {
|
||||
/** @var \Cake\Database\Expression\IdentifierExpression $expr */
|
||||
$expr = $window['name'];
|
||||
/** @var \Cake\Database\Expression\IdentifierExpression $windowExpr */
|
||||
$windowExpr = $window['window'];
|
||||
$windows[] = $expr->sql($binder) . ' AS (' . $windowExpr->sql($binder) . ')';
|
||||
}
|
||||
|
||||
return ' WINDOW ' . implode(', ', $windows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate SQL for SET expressions.
|
||||
*
|
||||
* @param array $parts List of keys and values to set.
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildSetPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$set = [];
|
||||
foreach ($parts as $part) {
|
||||
if ($part instanceof ExpressionInterface) {
|
||||
$part = $part->sql($binder);
|
||||
}
|
||||
if (str_starts_with($part, '(')) {
|
||||
$part = substr($part, 1, -1);
|
||||
}
|
||||
$set[] = $part;
|
||||
}
|
||||
|
||||
return ' SET ' . implode('', $set);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL string for all the `operation` clauses in this query, when dealing
|
||||
* with query objects it will also transform them using their configured SQL
|
||||
* dialect.
|
||||
*
|
||||
* @param string $operation
|
||||
* @param array $parts
|
||||
* @param \Cake\Database\Query $query
|
||||
* @param \Cake\Database\ValueBinder $binder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildSetOperationPart(
|
||||
string $operation,
|
||||
array $parts,
|
||||
Query $query,
|
||||
ValueBinder $binder,
|
||||
): string {
|
||||
$setOperationsOrderBy = $query
|
||||
->getConnection()
|
||||
->getDriver($query->getConnectionRole())
|
||||
->supports(DriverFeatureEnum::SET_OPERATIONS_ORDER_BY);
|
||||
|
||||
$parts = array_map(function (array $p) use ($binder, $setOperationsOrderBy) {
|
||||
/** @var \Cake\Database\Expression\IdentifierExpression $expr */
|
||||
$expr = $p['query'];
|
||||
$p['query'] = $expr->sql($binder);
|
||||
$p['query'] = str_starts_with($p['query'], '(') ? trim($p['query'], '()') : $p['query'];
|
||||
$prefix = $p['all'] ? 'ALL ' : '';
|
||||
if ($setOperationsOrderBy) {
|
||||
return "{$prefix}({$p['query']})";
|
||||
}
|
||||
|
||||
return $prefix . $p['query'];
|
||||
}, $parts);
|
||||
|
||||
if ($setOperationsOrderBy) {
|
||||
return sprintf(")\n{$operation} %s", implode("\n{$operation} ", $parts));
|
||||
}
|
||||
|
||||
return sprintf("\n{$operation} %s", implode("\n{$operation} ", $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL string for all the INTERSECT clauses in this query, when dealing
|
||||
* with query objects it will also transform them using their configured SQL
|
||||
* dialect.
|
||||
*
|
||||
* @param array $parts list of queries to be operated with INTERSECT
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildIntersectPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
return $this->_buildSetOperationPart('INTERSECT', $parts, $query, $binder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL string for all the UNION clauses in this query, when dealing
|
||||
* with query objects it will also transform them using their configured SQL
|
||||
* dialect.
|
||||
*
|
||||
* @param array $parts list of queries to be operated with UNION
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildUnionPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
return $this->_buildSetOperationPart('UNION', $parts, $query, $binder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL fragment for INSERT INTO.
|
||||
*
|
||||
* @param array $parts The insert parts.
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
if (!isset($parts[0])) {
|
||||
throw new DatabaseException(
|
||||
'Could not compile insert query. No table was specified. ' .
|
||||
'Use `into()` to define a table.',
|
||||
);
|
||||
}
|
||||
$table = $parts[0];
|
||||
$columns = $this->_stringifyExpressions($parts[1], $binder);
|
||||
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
return sprintf('INSERT%s%s INTO %s (%s)', $hint, $modifiers, $table, implode(', ', $columns));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL fragment for INSERT INTO.
|
||||
*
|
||||
* @param array $parts The values parts.
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
protected function _buildValuesPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
return implode('', $this->_stringifyExpressions($parts, $binder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL fragment for UPDATE.
|
||||
*
|
||||
* @param array $parts The update parts.
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
protected function _buildUpdatePart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$table = $this->_stringifyExpressions($parts, $binder);
|
||||
$hint = $this->_buildOptimizerHintPart($query->clause('optimizerHint'), $query, $binder);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
return sprintf('UPDATE%s%s %s', $hint, $modifiers, implode(',', $table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the optimizer hint comment part.
|
||||
*
|
||||
* @param list<string> $parts The optmizer hints
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string Optimizer hint comment
|
||||
*/
|
||||
protected function _buildOptimizerHintPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
if ($parts === [] || !$query->getDriver()->supports(DriverFeatureEnum::OPTIMIZER_HINT_COMMENT)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(' /*+ %s */', implode(' ', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQL modifier fragment
|
||||
*
|
||||
* @param array $parts The query modifier parts
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
protected function _buildModifierPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
if ($parts === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' ' . implode(' ', $this->_stringifyExpressions($parts, $binder, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to covert ExpressionInterface objects inside an array
|
||||
* into their string representation.
|
||||
*
|
||||
* @param array $expressions list of strings and ExpressionInterface objects
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @param bool $wrap Whether to wrap each expression object with parenthesis
|
||||
* @return array
|
||||
*/
|
||||
protected function _stringifyExpressions(array $expressions, ValueBinder $binder, bool $wrap = true): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($expressions as $k => $expression) {
|
||||
if ($expression instanceof ExpressionInterface) {
|
||||
$value = $expression->sql($binder);
|
||||
$expression = $wrap ? '(' . $value . ')' : $value;
|
||||
}
|
||||
$result[$k] = $expression;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
[](https://packagist.org/packages/cakephp/database)
|
||||
[](LICENSE.txt)
|
||||
|
||||
# A flexible and lightweight Database Library for PHP
|
||||
|
||||
This library abstracts and provides help with most aspects of dealing with relational
|
||||
databases such as keeping connections to the server, building queries,
|
||||
preventing SQL injections, inspecting and altering schemas, and with debugging and
|
||||
profiling queries sent to the database.
|
||||
|
||||
It adopts the API from the native PDO extension in PHP for familiarity, but solves many of the
|
||||
inconsistencies PDO has, while also providing several features that extend PDO's capabilities.
|
||||
|
||||
A distinguishing factor of this library when compared to similar database connection packages,
|
||||
is that it takes the concept of "data types" to its core. It lets you work with complex PHP objects
|
||||
or structures that can be passed as query conditions or to be inserted in the database.
|
||||
|
||||
The typing system will intelligently convert the PHP structures when passing them to the database, and
|
||||
convert them back when retrieving.
|
||||
|
||||
|
||||
## Connecting to the database
|
||||
|
||||
This library is able to work with the following databases:
|
||||
|
||||
* MySQL
|
||||
* Postgres
|
||||
* SQLite
|
||||
* Microsoft SQL Server (2008 and above)
|
||||
|
||||
The first thing you need to do when using this library is create a connection object.
|
||||
Before performing any operations with the connection, you need to specify a driver
|
||||
to use:
|
||||
|
||||
```php
|
||||
use Cake\Database\Connection;
|
||||
use Cake\Database\Driver\Mysql;
|
||||
use Cake\Database\Driver\Sqlite;
|
||||
|
||||
$connection = new Connection([
|
||||
'driver' => Mysql::class,
|
||||
'database' => 'test',
|
||||
'username' => 'root',
|
||||
'password' => 'secret',
|
||||
]);
|
||||
|
||||
$connection2 = new Connection([
|
||||
'driver' => Sqlite::class,
|
||||
'database' => '/path/to/file.db'
|
||||
]);
|
||||
```
|
||||
|
||||
Drivers are classes responsible for actually executing the commands to the database and
|
||||
correctly building the SQL according to the database specific dialect.
|
||||
|
||||
### Connection options
|
||||
|
||||
This is a list of possible options that can be passed when creating a connection:
|
||||
|
||||
* `driver`: Driver class name
|
||||
* `persistent`: Creates a persistent connection
|
||||
* `host`: The server host
|
||||
* `database`: The database name
|
||||
* `username`: Login credential
|
||||
* `password`: Connection secret
|
||||
* `encoding`: The connection encoding (or charset)
|
||||
* `timezone`: The connection timezone or time offset
|
||||
|
||||
## Using connections
|
||||
|
||||
After creating a connection, you can immediately interact with the database. You can choose
|
||||
either to use the shorthand methods `execute()`, `insert()`, `update()`, `delete()` or use
|
||||
one of `selectQuery()`, `updateQuery()`, `insertQuery()` or `deleteQuery()`
|
||||
to get a query builder for particular type of query.
|
||||
|
||||
The easiest way of executing queries is by using the `execute()` method, it will return a
|
||||
`Cake\Database\StatementInterface` that you can use to get the data back:
|
||||
|
||||
```php
|
||||
$statement = $connection->execute('SELECT * FROM articles');
|
||||
|
||||
while($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
|
||||
echo $row['title'] . PHP_EOL;
|
||||
}
|
||||
```
|
||||
Binding values to parametrized arguments is also possible with the execute function:
|
||||
|
||||
```php
|
||||
$statement = $connection->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1], ['id' => 'integer']);
|
||||
$results = $statement->fetch(\PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
The third parameter is the types the passed values should be converted to when passed to the database. If
|
||||
no types are passed, all arguments will be interpreted as a string.
|
||||
|
||||
Alternatively you can construct a statement manually and then fetch rows from it:
|
||||
|
||||
```php
|
||||
$statement = $connection->prepare('SELECT * from articles WHERE id != :id');
|
||||
$statement->bind(['id' => 1], ['id' => 'integer']);
|
||||
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
The default types that are understood by this library and can be passed to the `bind()` function or to `execute()`
|
||||
are:
|
||||
|
||||
* biginteger
|
||||
* binary
|
||||
* date
|
||||
* float
|
||||
* decimal
|
||||
* integer
|
||||
* time
|
||||
* datetime
|
||||
* timestamp
|
||||
* uuid
|
||||
|
||||
More types can be added dynamically in a bit.
|
||||
|
||||
Statements can be reused by binding new values to the parameters in the query:
|
||||
|
||||
```php
|
||||
$statement = $connection->prepare('SELECT * from articles WHERE id = :id');
|
||||
$statement->bind(['id' => 1], ['id' => 'integer']);
|
||||
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$statement->bind(['id' => 1], ['id' => 'integer']);
|
||||
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
### Updating Rows
|
||||
|
||||
Updating can be done using the `update()` function in the connection object. In the following
|
||||
example we will update the title of the article with id = 1:
|
||||
|
||||
```php
|
||||
$connection->update('articles', ['title' => 'New title'], ['id' => 1]);
|
||||
```
|
||||
|
||||
The concept of data types is central to this library, so you can use the last parameter of the function
|
||||
to specify what types should be used:
|
||||
|
||||
```php
|
||||
$connection->update(
|
||||
'articles',
|
||||
['title' => 'New title'],
|
||||
['created >=' => new DateTime('-3 day'), 'created <' => new DateTime('now')],
|
||||
['created' => 'datetime']
|
||||
);
|
||||
```
|
||||
|
||||
The example above will execute the following SQL:
|
||||
|
||||
```sql
|
||||
UPDATE articles SET title = 'New Title' WHERE created >= '2014-10-10 00:00:00' AND created < '2014-10-13 00:00:00';
|
||||
```
|
||||
|
||||
More on creating complex where conditions or more complex update queries later.
|
||||
|
||||
### Deleting Rows
|
||||
|
||||
Similarly, the `delete()` method is used to delete rows from the database:
|
||||
|
||||
```php
|
||||
$connection->delete('articles', ['created <' => DateTime('now')], ['created' => 'date']);
|
||||
```
|
||||
|
||||
Will generate the following SQL
|
||||
|
||||
```sql
|
||||
DELETE FROM articles where created < '2014-10-10'
|
||||
```
|
||||
|
||||
### Inserting Rows
|
||||
|
||||
Rows can be inserted using the `insert()` method:
|
||||
|
||||
```php
|
||||
$connection->insert(
|
||||
'articles',
|
||||
['title' => 'My Title', 'body' => 'Some paragraph', 'created' => new DateTime()],
|
||||
['created' => 'datetime']
|
||||
);
|
||||
```
|
||||
|
||||
More complex updates, deletes and insert queries can be generated using the `Query` class.
|
||||
|
||||
## Query Builder
|
||||
|
||||
One of the goals of this library is to allow the generation of both simple and complex queries with
|
||||
ease. The query builder can be accessed by getting a new instance of a query:
|
||||
|
||||
```php
|
||||
$query = $connection->selectQuery();
|
||||
```
|
||||
|
||||
### Selecting Fields
|
||||
|
||||
Adding fields to the `SELECT` clause:
|
||||
|
||||
```php
|
||||
$query->select(['id', 'title', 'body']);
|
||||
|
||||
// Results in SELECT id AS pk, title AS aliased_title, body ...
|
||||
$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']);
|
||||
|
||||
// Use a closure
|
||||
$query->select(function ($query) {
|
||||
return ['id', 'title', 'body'];
|
||||
});
|
||||
```
|
||||
|
||||
### Where Conditions
|
||||
|
||||
Generating conditions:
|
||||
|
||||
```php
|
||||
// WHERE id = 1
|
||||
$query->where(['id' => 1]);
|
||||
|
||||
// WHERE id > 1
|
||||
$query->where(['id >' => 1]);
|
||||
```
|
||||
|
||||
As you can see you can use any operator by placing it with a space after the field name.
|
||||
Adding multiple conditions is easy as well:
|
||||
|
||||
```php
|
||||
$query->where(['id >' => 1])->andWhere(['title' => 'My Title']);
|
||||
|
||||
// Equivalent to
|
||||
$query->where(['id >' => 1, 'title' => 'My title']);
|
||||
```
|
||||
|
||||
It is possible to generate `OR` conditions as well
|
||||
|
||||
```php
|
||||
$query->where(['OR' => ['id >' => 1, 'title' => 'My title']]);
|
||||
```
|
||||
|
||||
For even more complex conditions you can use closures and expression objects:
|
||||
|
||||
```php
|
||||
$query->where(function (ExpressionInterface $exp) {
|
||||
return $exp
|
||||
->eq('author_id', 2)
|
||||
->eq('published', true)
|
||||
->notEq('spam', true)
|
||||
->gt('view_count', 10);
|
||||
});
|
||||
```
|
||||
|
||||
Which results in:
|
||||
|
||||
```sql
|
||||
SELECT * FROM articles
|
||||
WHERE
|
||||
author_id = 2
|
||||
AND published = 1
|
||||
AND spam != 1
|
||||
AND view_count > 10
|
||||
```
|
||||
|
||||
Combining expressions is also possible:
|
||||
|
||||
```php
|
||||
$query->where(function (ExpressionInterface $exp) {
|
||||
$orConditions = $exp->or(['author_id' => 2])
|
||||
->eq('author_id', 5);
|
||||
return $exp
|
||||
->not($orConditions)
|
||||
->lte('view_count', 10);
|
||||
});
|
||||
```
|
||||
|
||||
That generates:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM articles
|
||||
WHERE
|
||||
NOT (author_id = 2 OR author_id = 5)
|
||||
AND view_count <= 10
|
||||
```
|
||||
|
||||
When using the expression objects you can use the following methods to create conditions:
|
||||
|
||||
* `eq()` Creates an equality condition.
|
||||
* `notEq()` Create an inequality condition
|
||||
* `like()` Create a condition using the LIKE operator.
|
||||
* `notLike()` Create a negated LIKE condition.
|
||||
* `in()` Create a condition using IN.
|
||||
* `notIn()` Create a negated condition using IN.
|
||||
* `gt()` Create a > condition.
|
||||
* `gte()` Create a >= condition.
|
||||
* `lt()` Create a < condition.
|
||||
* `lte()` Create a <= condition.
|
||||
* `isNull()` Create an IS NULL condition.
|
||||
* `isNotNull()` Create a negated IS NULL condition.
|
||||
|
||||
### Aggregates and SQL Functions
|
||||
|
||||
```php
|
||||
// Results in SELECT COUNT(*) count FROM ...
|
||||
$query->select(['count' => $query->func()->count('*')]);
|
||||
```
|
||||
|
||||
A number of commonly used functions can be created with the func() method:
|
||||
|
||||
* `sum()` Calculate a sum. The arguments will be treated as literal values.
|
||||
* `avg()` Calculate an average. The arguments will be treated as literal values.
|
||||
* `min()` Calculate the min of a column. The arguments will be treated as literal values.
|
||||
* `max()` Calculate the max of a column. The arguments will be treated as literal values.
|
||||
* `count()` Calculate the count. The arguments will be treated as literal values.
|
||||
* `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal.
|
||||
* `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal.
|
||||
* `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal.
|
||||
* `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date.
|
||||
|
||||
When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Literal
|
||||
parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions.
|
||||
For example:
|
||||
|
||||
```php
|
||||
$concat = $query->func()->concat([
|
||||
'title' => 'literal',
|
||||
' NEW'
|
||||
]);
|
||||
$query->select(['title' => $concat]);
|
||||
```
|
||||
|
||||
The above generates:
|
||||
|
||||
```sql
|
||||
SELECT CONCAT(title, :c0) ...;
|
||||
```
|
||||
|
||||
### Other SQL Clauses
|
||||
|
||||
Read of all other SQL clauses that the builder is capable of generating in the [official API docs](https://api.cakephp.org/4.x/class-Cake.Database.Query.html)
|
||||
|
||||
### Getting Results out of a Query
|
||||
|
||||
Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this:
|
||||
|
||||
```php
|
||||
// Iterate the query
|
||||
foreach ($query as $row) {
|
||||
// Do stuff.
|
||||
}
|
||||
|
||||
// Get the statement and fetch all results
|
||||
$results = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
|
||||
```
|
||||
|
||||
## Official API
|
||||
|
||||
You can read the official [official API docs](https://api.cakephp.org/5.x/namespace-Cake.Database.html) to learn more of what this library
|
||||
has to offer.
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Retry;
|
||||
|
||||
use Cake\Core\Retry\RetryStrategyInterface;
|
||||
use Exception;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* Implements retry strategy based on db error codes and wait interval.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ErrorCodeWaitStrategy implements RetryStrategyInterface
|
||||
{
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
protected array $errorCodes;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $retryInterval;
|
||||
|
||||
/**
|
||||
* @param array<int> $errorCodes DB-specific error codes that allow retrying
|
||||
* @param int $retryInterval Seconds to wait before allowing next retry, 0 for no wait.
|
||||
*/
|
||||
public function __construct(array $errorCodes, int $retryInterval)
|
||||
{
|
||||
$this->errorCodes = $errorCodes;
|
||||
$this->retryInterval = $retryInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function shouldRetry(Exception $exception, int $retryCount): bool
|
||||
{
|
||||
if (
|
||||
$exception instanceof PDOException &&
|
||||
$exception->errorInfo &&
|
||||
in_array($exception->errorInfo[1], $this->errorCodes)
|
||||
) {
|
||||
if ($this->retryInterval > 0) {
|
||||
sleep($this->retryInterval);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?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.6.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Retry;
|
||||
|
||||
use Cake\Core\Retry\RetryStrategyInterface;
|
||||
use Cake\Database\Connection;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Makes sure the connection to the database is alive before authorizing
|
||||
* the retry of an action.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ReconnectStrategy implements RetryStrategyInterface
|
||||
{
|
||||
/**
|
||||
* The list of error strings to match when looking for a disconnection error.
|
||||
*
|
||||
* This is a static variable to enable opcache to inline the values.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected static array $causes = [
|
||||
'gone away',
|
||||
'Lost connection',
|
||||
'Transaction() on null',
|
||||
'closed the connection unexpectedly',
|
||||
'closed unexpectedly',
|
||||
'deadlock avoided',
|
||||
'decryption failed or bad record mac',
|
||||
'is dead or not enabled',
|
||||
'no connection to the server',
|
||||
'query_wait_timeout',
|
||||
'reset by peer',
|
||||
'terminate due to client_idle_limit',
|
||||
'while sending',
|
||||
'writing data to the connection',
|
||||
];
|
||||
|
||||
/**
|
||||
* The connection to check for validity
|
||||
*
|
||||
* @var \Cake\Database\Connection
|
||||
*/
|
||||
protected Connection $connection;
|
||||
|
||||
/**
|
||||
* Creates the ReconnectStrategy object by storing a reference to the
|
||||
* passed connection. This reference will be used to automatically
|
||||
* reconnect to the server in case of failure.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to check
|
||||
*/
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Checks whether the exception was caused by a lost connection,
|
||||
* and returns true if it was able to successfully reconnect.
|
||||
*/
|
||||
public function shouldRetry(Exception $exception, int $retryCount): bool
|
||||
{
|
||||
$message = $exception->getMessage();
|
||||
|
||||
foreach (static::$causes as $cause) {
|
||||
if (str_contains($message, $cause)) {
|
||||
return $this->reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to re-establish the connection to the server, if it is safe to do so
|
||||
*
|
||||
* @return bool Whether the connection was re-established
|
||||
*/
|
||||
protected function reconnect(): bool
|
||||
{
|
||||
if ($this->connection->inTransaction()) {
|
||||
// It is not safe to blindly reconnect in the middle of a transaction
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Make sure we free any resources associated with the old connection
|
||||
$this->connection->getDriver()->disconnect();
|
||||
} catch (Exception) {
|
||||
}
|
||||
|
||||
try {
|
||||
$this->connection->getDriver()->connect();
|
||||
$this->connection->getDriver()->log(
|
||||
'connection={connection} [RECONNECT]',
|
||||
['connection' => $this->connection->configName()],
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (Exception) {
|
||||
// If there was an error connecting again, don't report it back,
|
||||
// let the retry handler do it.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?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\Schema;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
/**
|
||||
* Decorates a schema collection and adds caching
|
||||
*/
|
||||
class CachedCollection implements CollectionInterface
|
||||
{
|
||||
/**
|
||||
* Cacher instance.
|
||||
*
|
||||
* @var \Psr\SimpleCache\CacheInterface
|
||||
*/
|
||||
protected CacheInterface $cacher;
|
||||
|
||||
/**
|
||||
* The decorated schema collection
|
||||
*
|
||||
* @var \Cake\Database\Schema\CollectionInterface
|
||||
*/
|
||||
protected CollectionInterface $collection;
|
||||
|
||||
/**
|
||||
* The cache key prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $prefix;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Cake\Database\Schema\CollectionInterface $collection The collection to wrap.
|
||||
* @param string $prefix The cache key prefix to use. Typically the connection name.
|
||||
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher instance.
|
||||
*/
|
||||
public function __construct(CollectionInterface $collection, string $prefix, CacheInterface $cacher)
|
||||
{
|
||||
$this->collection = $collection;
|
||||
$this->prefix = $prefix;
|
||||
$this->cacher = $cacher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function listTablesWithoutViews(): array
|
||||
{
|
||||
return $this->collection->listTablesWithoutViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function listTables(): array
|
||||
{
|
||||
return $this->collection->listTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column metadata for a table.
|
||||
*
|
||||
* The name can include a database schema name in the form 'schema.table'.
|
||||
*
|
||||
* Caching will be applied if `cacheMetadata` key is present in the Connection
|
||||
* configuration options. Defaults to _cake_model_ when true.
|
||||
*
|
||||
* ### Options
|
||||
*
|
||||
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @param string $name The name of the table to describe.
|
||||
* @param array<string, mixed> $options The options to use, see above.
|
||||
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
|
||||
*/
|
||||
public function describe(string $name, array $options = []): TableSchemaInterface
|
||||
{
|
||||
$options += ['forceRefresh' => false];
|
||||
$cacheKey = $this->cacheKey($name);
|
||||
|
||||
if (!$options['forceRefresh']) {
|
||||
$cached = $this->cacher->get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$table = $this->collection->describe($name, $options);
|
||||
$this->cacher->set($cacheKey, $table);
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache key for a given name.
|
||||
*
|
||||
* @param string $name The name to get a cache key for.
|
||||
* @return string The cache key.
|
||||
*/
|
||||
public function cacheKey(string $name): string
|
||||
{
|
||||
return $this->prefix . '_' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cacher.
|
||||
*
|
||||
* @param \Psr\SimpleCache\CacheInterface $cacher Cacher object
|
||||
* @return $this
|
||||
*/
|
||||
public function setCacher(CacheInterface $cacher)
|
||||
{
|
||||
$this->cacher = $cacher;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cacher.
|
||||
*
|
||||
* @return \Psr\SimpleCache\CacheInterface $cacher Cacher object
|
||||
*/
|
||||
public function getCacher(): CacheInterface
|
||||
{
|
||||
return $this->cacher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Check constraint value object
|
||||
*
|
||||
* Models a check constraint.
|
||||
*/
|
||||
class CheckConstraint extends Constraint
|
||||
{
|
||||
protected string $type = self::CHECK;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name Constraint name.
|
||||
* @param string $expression The check constraint expression (e.g., "age >= 18")
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected string $expression,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the check constraint expression.
|
||||
*
|
||||
* @param string $expression The SQL expression for the check constraint
|
||||
* @return $this
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setExpression(string $expression)
|
||||
{
|
||||
if (trim($expression) === '') {
|
||||
throw new InvalidArgumentException('Check constraint expression cannot be empty');
|
||||
}
|
||||
|
||||
$this->expression = trim($expression);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the check constraint expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExpression(): string
|
||||
{
|
||||
return $this->expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a constraint to an array that is compatible
|
||||
* with the constructor.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'expression' => $this->expression,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\Schema;
|
||||
|
||||
use Cake\Database\Connection;
|
||||
|
||||
/**
|
||||
* Represents a database schema collection
|
||||
*
|
||||
* Gives a simple high-level schema reflection API that can be
|
||||
* decorated or extended with additional behavior like caching.
|
||||
*
|
||||
* @see \Cake\Database\Schema\SchemaDialect For lower level schema reflection API
|
||||
*/
|
||||
class Collection implements CollectionInterface
|
||||
{
|
||||
/**
|
||||
* Connection object
|
||||
*
|
||||
* @var \Cake\Database\Connection
|
||||
*/
|
||||
protected Connection $_connection;
|
||||
|
||||
/**
|
||||
* Schema dialect instance.
|
||||
*
|
||||
* @var \Cake\Database\Schema\SchemaDialect|null
|
||||
*/
|
||||
protected ?SchemaDialect $_dialect = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection instance.
|
||||
*/
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->_connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of tables, excluding any views, available in the current connection.
|
||||
*
|
||||
* @return array<string> The list of tables in the connected database/schema.
|
||||
*/
|
||||
public function listTablesWithoutViews(): array
|
||||
{
|
||||
return $this->getDialect()->listTablesWithoutViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of tables and views available in the current connection.
|
||||
*
|
||||
* @return array<string> The list of tables and views in the connected database/schema.
|
||||
*/
|
||||
public function listTables(): array
|
||||
{
|
||||
return $this->getDialect()->listTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column metadata for a table.
|
||||
*
|
||||
* The name can include a database schema name in the form 'schema.table'.
|
||||
*
|
||||
* @param string $name The name of the table to describe.
|
||||
* @param array<string, mixed> $options Unused
|
||||
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
|
||||
*/
|
||||
public function describe(string $name, array $options = []): TableSchemaInterface
|
||||
{
|
||||
return $this->getDialect()->describe($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups the schema dialect to be used for this collection.
|
||||
*
|
||||
* @return \Cake\Database\Schema\SchemaDialect
|
||||
*/
|
||||
protected function getDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_dialect ??= $this->_connection->getWriteDriver()->schemaDialect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
/**
|
||||
* Represents a database schema collection
|
||||
*
|
||||
* Used to access information about the tables,
|
||||
* and other data in a database.
|
||||
*
|
||||
* @method array<string> listTablesWithoutViews() Get the list of tables available in the current connection.
|
||||
* This will exclude any views in the schema.
|
||||
*/
|
||||
interface CollectionInterface
|
||||
{
|
||||
/**
|
||||
* Get the list of tables available in the current connection.
|
||||
*
|
||||
* @return array<string> The list of tables in the connected database/schema.
|
||||
*/
|
||||
public function listTables(): array;
|
||||
|
||||
/**
|
||||
* Get the column metadata for a table.
|
||||
*
|
||||
* Caching will be applied if `cacheMetadata` key is present in the Connection
|
||||
* configuration options. Defaults to _cake_model_ when true.
|
||||
*
|
||||
* ### Options
|
||||
*
|
||||
* - `forceRefresh` - Set to true to force rebuilding the cached metadata.
|
||||
* Defaults to false.
|
||||
*
|
||||
* @param string $name The name of the table to describe.
|
||||
* @param array<string, mixed> $options The options to use, see above.
|
||||
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
|
||||
*/
|
||||
public function describe(string $name, array $options = []): TableSchemaInterface;
|
||||
}
|
||||
+595
@@ -0,0 +1,595 @@
|
||||
<?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)
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc.
|
||||
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use Cake\Database\TypeFactory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Schema metadata for a single column
|
||||
*
|
||||
* Used by `TableSchema` when reflecting schema or creating tables.
|
||||
*/
|
||||
class Column
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name Name of the column
|
||||
* @param string $type Type of the column
|
||||
* @param bool $null Whether the column allows null values
|
||||
* @param mixed $default Default value for the column
|
||||
* @param int|null $length Length of the column
|
||||
* @param bool $identity Whether the column is an identity column
|
||||
* @param string|null $generated Postgres identity option (always|default)
|
||||
* @param int|null $precision Precision for decimal or float columns
|
||||
* @param int|null $increment Increment for identity columns
|
||||
* @param string|null $after Name of the column to add this column after
|
||||
* @param string|null $onUpdate MySQL 'ON UPDATE' function
|
||||
* @param string|null $comment Comment for the column
|
||||
* @param bool|null $unsigned Whether the column is unsigned
|
||||
* @param string|null $collate Collation for the column
|
||||
* @param int|null $srid SRID for geometry fields
|
||||
* @param string|null $baseType The basic schema type if the column type is a complex/custom type.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected string $type,
|
||||
protected ?bool $null = null,
|
||||
protected mixed $default = null,
|
||||
protected ?int $length = null,
|
||||
protected bool $identity = false,
|
||||
protected ?string $generated = null,
|
||||
protected ?int $precision = null,
|
||||
protected ?int $increment = null,
|
||||
protected ?string $after = null,
|
||||
protected ?string $onUpdate = null,
|
||||
protected ?string $comment = null,
|
||||
protected ?bool $unsigned = null,
|
||||
protected ?string $collate = null,
|
||||
protected ?int $srid = null,
|
||||
protected ?string $baseType = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column name.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column name.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base type if defined. Will fallback to `type` if not set.
|
||||
*
|
||||
* Used to get the base type of a column when the column type is a complex/custom type.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBaseType(): ?string
|
||||
{
|
||||
if (isset($this->baseType)) {
|
||||
return $this->baseType;
|
||||
}
|
||||
$type = $this->type;
|
||||
if (TypeFactory::getMapped($type)) {
|
||||
$type = TypeFactory::build($type)->getBaseType();
|
||||
}
|
||||
|
||||
return $this->baseType = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base type of the column.
|
||||
*
|
||||
* Used to set the base type of a column when the column type is a complex/custom type.
|
||||
*
|
||||
* @param string|null $baseType Base type
|
||||
* @return $this
|
||||
*/
|
||||
public function setBaseType(?string $baseType)
|
||||
{
|
||||
$this->baseType = $baseType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column type.
|
||||
*
|
||||
* Type names are not validated, as drivers and dialects may implement
|
||||
* platform specific types that are not known by cakephp.
|
||||
*
|
||||
* Drivers are expected to handle unknown types gracefully.
|
||||
*
|
||||
* @param string $type Column type
|
||||
* @return $this
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column length.
|
||||
*
|
||||
* @param int|null $length Length
|
||||
* @return $this
|
||||
*/
|
||||
public function setLength(?int $length)
|
||||
{
|
||||
$this->length = $length;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column length.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getLength(): ?int
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the column allows nulls.
|
||||
*
|
||||
* @param bool $null Null
|
||||
* @return $this
|
||||
*/
|
||||
public function setNull(bool $null)
|
||||
{
|
||||
$this->null = $null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the column allows nulls.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getNull(): ?bool
|
||||
{
|
||||
return $this->null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the column allow nulls?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNull(): bool
|
||||
{
|
||||
return $this->getNull() === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default column value.
|
||||
*
|
||||
* @param mixed $default Default
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefault(mixed $default)
|
||||
{
|
||||
$this->default = $default;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default column value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets generated option for identity columns. Ignored otherwise.
|
||||
*
|
||||
* @param string|null $generated Generated option
|
||||
* @return $this
|
||||
*/
|
||||
public function setGenerated(?string $generated)
|
||||
{
|
||||
$this->generated = $generated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets generated option for identity columns. Null otherwise
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getGenerated(): ?string
|
||||
{
|
||||
return $this->generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the column is an identity column.
|
||||
*
|
||||
* @param bool $identity Identity
|
||||
* @return $this
|
||||
*/
|
||||
public function setIdentity(bool $identity)
|
||||
{
|
||||
$this->identity = $identity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the column is an identity column.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIdentity(): bool
|
||||
{
|
||||
return $this->identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the column an identity column?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isIdentity(): bool
|
||||
{
|
||||
return $this->getIdentity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the column to add this column after.
|
||||
*
|
||||
* @param string $after After
|
||||
* @return $this
|
||||
*/
|
||||
public function setAfter(string $after)
|
||||
{
|
||||
$this->after = $after;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the column to add this column after.
|
||||
*
|
||||
* Used by MySQL and MariaDB in ALTER TABLE statements.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAfter(): ?string
|
||||
{
|
||||
return $this->after;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'ON UPDATE' mysql column function.
|
||||
*
|
||||
* Used by MySQL and MariaDB in ALTER TABLE statements.
|
||||
*
|
||||
* @param string $update On Update function
|
||||
* @return $this
|
||||
*/
|
||||
public function setOnUpdate(string $update)
|
||||
{
|
||||
$this->onUpdate = $update;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the ON UPDATE column function.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getOnUpdate(): ?string
|
||||
{
|
||||
return $this->onUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number precision for decimal or float column.
|
||||
*
|
||||
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
|
||||
* and the column could store value from -999.99 to 999.99.
|
||||
*
|
||||
* @param int|null $precision Number precision
|
||||
* @return $this
|
||||
*/
|
||||
public function setPrecision(?int $precision)
|
||||
{
|
||||
$this->precision = $precision;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number precision for decimal or float column.
|
||||
*
|
||||
* For example `DECIMAL(5,2)`, 5 is the length and 2 is the precision,
|
||||
* and the column could store value from -999.99 to 999.99.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getPrecision(): ?int
|
||||
{
|
||||
return $this->precision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column identity increment.
|
||||
*
|
||||
* @param int $increment Number increment
|
||||
* @return $this
|
||||
*/
|
||||
public function setIncrement(int $increment)
|
||||
{
|
||||
$this->increment = $increment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column identity increment.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getIncrement(): ?int
|
||||
{
|
||||
return $this->increment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column comment.
|
||||
*
|
||||
* @param string|null $comment Comment
|
||||
* @return $this
|
||||
*/
|
||||
public function setComment(?string $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column comment.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment(): ?string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether field should be unsigned.
|
||||
*
|
||||
* @param bool $unsigned Signed
|
||||
* @return $this
|
||||
*/
|
||||
public function setUnsigned(bool $unsigned)
|
||||
{
|
||||
$this->unsigned = $unsigned;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether field should be unsigned.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getUnsigned(): ?bool
|
||||
{
|
||||
return $this->unsigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the column be signed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSigned(): bool
|
||||
{
|
||||
return !$this->getUnsigned();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the column be unsigned?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isUnsigned(): bool
|
||||
{
|
||||
return $this->getUnsigned() === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column collation.
|
||||
*
|
||||
* @param string $collation Collation
|
||||
* @return $this
|
||||
*/
|
||||
public function setCollate(string $collation)
|
||||
{
|
||||
$this->collate = $collation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column collation.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCollate(): ?string
|
||||
{
|
||||
return $this->collate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column SRID for geometry fields.
|
||||
*
|
||||
* @param int $srid SRID
|
||||
* @return $this
|
||||
*/
|
||||
public function setSrid(int $srid)
|
||||
{
|
||||
$this->srid = $srid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the column SRID from geometry fields.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getSrid(): ?int
|
||||
{
|
||||
return $this->srid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all allowed options. Each option must have a corresponding `setFoo` method.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getValidOptions(): array
|
||||
{
|
||||
return [
|
||||
'name',
|
||||
'length',
|
||||
'precision',
|
||||
'default',
|
||||
'null',
|
||||
'identity',
|
||||
'after',
|
||||
'onUpdate',
|
||||
'comment',
|
||||
'unsigned',
|
||||
'type',
|
||||
'properties',
|
||||
'collate',
|
||||
'srid',
|
||||
'increment',
|
||||
'generated',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that maps an array of column attributes to this object's methods.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Attributes
|
||||
* @throws \RuntimeException
|
||||
* @return $this
|
||||
*/
|
||||
public function setAttributes(array $attributes)
|
||||
{
|
||||
$validOptions = $this->getValidOptions();
|
||||
if (isset($attributes['identity']) && $attributes['identity'] && !isset($attributes['null'])) {
|
||||
$attributes['null'] = false;
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute => $value) {
|
||||
if (!in_array($attribute, $validOptions, true)) {
|
||||
throw new RuntimeException(sprintf('"%s" is not a valid column option.', $attribute));
|
||||
}
|
||||
|
||||
$method = 'set' . ucfirst($attribute);
|
||||
$this->$method($value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an index into an array that is compatible with the Column constructor.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$type = $this->getType();
|
||||
$length = $this->getLength();
|
||||
$precision = $this->getPrecision();
|
||||
if ($precision !== null && $precision > 0) {
|
||||
if ($type === TableSchemaInterface::TYPE_TIMESTAMP) {
|
||||
$type = 'timestampfractional';
|
||||
} elseif ($type === TableSchemaInterface::TYPE_DATETIME) {
|
||||
$type = 'datetimefractional';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $this->getName(),
|
||||
'baseType' => $this->getBaseType(),
|
||||
'type' => $type,
|
||||
'length' => $length,
|
||||
'null' => $this->getNull(),
|
||||
'default' => $this->getDefault(),
|
||||
'generated' => $this->getGenerated(),
|
||||
'unsigned' => $this->getUnsigned(),
|
||||
'onUpdate' => $this->getOnUpdate(),
|
||||
'collate' => $this->getCollate(),
|
||||
'precision' => $precision,
|
||||
'srid' => $this->getSrid(),
|
||||
'comment' => $this->getComment(),
|
||||
'autoIncrement' => $this->getIdentity(),
|
||||
'identity' => $this->getIdentity(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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)
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc.
|
||||
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
/**
|
||||
* Constraint base class object
|
||||
*
|
||||
* Models a database constraint like a unique or primary key.
|
||||
*/
|
||||
class Constraint
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const PRIMARY = TableSchema::CONSTRAINT_PRIMARY;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const UNIQUE = TableSchema::CONSTRAINT_UNIQUE;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const FOREIGN = TableSchema::CONSTRAINT_FOREIGN;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const CHECK = TableSchema::CONSTRAINT_CHECK;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name The name of the constraint.
|
||||
* @param array<string> $columns The columns to constraint.
|
||||
* @param string $type The type of constraint, e.g. 'unique', 'primary'.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected array $columns,
|
||||
protected string $type,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint columns.
|
||||
*
|
||||
* @param array<string>|string $columns Columns
|
||||
* @return $this
|
||||
*/
|
||||
public function setColumns(string|array $columns)
|
||||
{
|
||||
$this->columns = (array)$columns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint columns.
|
||||
*
|
||||
* @return ?array<string>
|
||||
*/
|
||||
public function getColumns(): ?array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint type.
|
||||
*
|
||||
* @param string $type Type
|
||||
* @return $this
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint name.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint name.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a constraint to an array that is compatible
|
||||
* with the constructor.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'columns' => $this->columns,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?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)
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc.
|
||||
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* ForeignKey metadata object
|
||||
*
|
||||
* Models a database foreign key constraint
|
||||
*/
|
||||
class ForeignKey extends Constraint
|
||||
{
|
||||
public const CASCADE = 'cascade';
|
||||
public const RESTRICT = 'restrict';
|
||||
public const SET_NULL = 'setNull';
|
||||
public const NO_ACTION = 'noAction';
|
||||
public const SET_DEFAULT = 'setDefault';
|
||||
public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED';
|
||||
public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE';
|
||||
public const NOT_DEFERRED = 'NOT DEFERRABLE';
|
||||
|
||||
/**
|
||||
* An allow list of valid actions
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $validActions = [
|
||||
self::CASCADE,
|
||||
self::RESTRICT,
|
||||
self::SET_NULL,
|
||||
self::NO_ACTION,
|
||||
self::SET_DEFAULT,
|
||||
];
|
||||
|
||||
/**
|
||||
* The action to take when the referenced row is deleted.
|
||||
*/
|
||||
protected ?string $delete = null;
|
||||
|
||||
/**
|
||||
* The action to take when the referenced row is updated.
|
||||
*/
|
||||
protected ?string $update = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $deferrable = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name The name of the index.
|
||||
* @param array<string> $columns The columns to index.
|
||||
* @param ?string $referencedTable The columns to index.
|
||||
* @param array<string> $referencedColumns The columns in $referencedTable that this key references.
|
||||
* @param ?string $delete The action to take when the referenced row is deleted.
|
||||
* @param ?string $update The action to take when the referenced row is updated.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected array $columns,
|
||||
protected ?string $referencedTable = null,
|
||||
protected array $referencedColumns = [],
|
||||
?string $delete = null,
|
||||
?string $update = null,
|
||||
?string $deferrable = null,
|
||||
) {
|
||||
$this->type = self::FOREIGN;
|
||||
$this->delete = $this->normalizeAction($delete ?? self::NO_ACTION);
|
||||
$this->update = $this->normalizeAction($update ?? self::NO_ACTION);
|
||||
if ($deferrable) {
|
||||
$this->deferrable = $this->normalizeDeferrable($deferrable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the foreign key referenced table.
|
||||
*
|
||||
* @param string $table The table this KEY is pointing to
|
||||
* @return $this
|
||||
*/
|
||||
public function setReferencedTable(string $table)
|
||||
{
|
||||
$this->referencedTable = $table;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the foreign key referenced table.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getReferencedTable(): ?string
|
||||
{
|
||||
return $this->referencedTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the foreign key referenced columns.
|
||||
*
|
||||
* @param array<string>|string $referencedColumns Referenced columns
|
||||
* @return $this
|
||||
*/
|
||||
public function setReferencedColumns(array|string $referencedColumns)
|
||||
{
|
||||
$referencedColumns = is_string($referencedColumns) ? [$referencedColumns] : $referencedColumns;
|
||||
$this->referencedColumns = $referencedColumns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the foreign key referenced columns.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getReferencedColumns(): array
|
||||
{
|
||||
return $this->referencedColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the foreign key to an array that is compatible
|
||||
* with the constructor.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'columns' => $this->columns,
|
||||
'referencedTable' => $this->referencedTable,
|
||||
'referencedColumns' => $this->referencedColumns,
|
||||
'delete' => $this->delete,
|
||||
'update' => $this->update,
|
||||
'deferrable' => $this->deferrable,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ON DELETE action for the foreign key.
|
||||
*
|
||||
* @param string $delete On Delete
|
||||
* @return $this
|
||||
*/
|
||||
public function setDelete(string $delete)
|
||||
{
|
||||
$this->delete = $this->normalizeAction($delete);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ON DELETE action for the foreign key.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDelete(): ?string
|
||||
{
|
||||
return $this->delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ON UPDATE action for the foreign key.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getUpdate(): ?string
|
||||
{
|
||||
return $this->update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ON UPDATE action for the foreign key.
|
||||
*
|
||||
* @param string $update On Update
|
||||
* @return $this
|
||||
*/
|
||||
public function setUpdate(string $update)
|
||||
{
|
||||
$this->update = $this->normalizeAction($update);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* From passed value checks if it's correct and fixes if needed
|
||||
*
|
||||
* @param string $action Action
|
||||
* @throws \InvalidArgumentException
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeAction(string $action): string
|
||||
{
|
||||
if (in_array($action, $this->validActions, true)) {
|
||||
return $action;
|
||||
}
|
||||
throw new InvalidArgumentException('Unknown action passed: ' . $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets deferrable mode for the foreign key.
|
||||
*
|
||||
* @param string $deferrable Constraint
|
||||
* @return $this
|
||||
*/
|
||||
public function setDeferrable(string $deferrable)
|
||||
{
|
||||
$this->deferrable = $this->normalizeDeferrable($deferrable);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets deferrable mode for the foreign key.
|
||||
*/
|
||||
public function getDeferrable(): ?string
|
||||
{
|
||||
return $this->deferrable;
|
||||
}
|
||||
|
||||
/**
|
||||
* From passed value checks if it's correct and fixes if needed
|
||||
*
|
||||
* @param string $deferrable Deferrable
|
||||
* @throws \InvalidArgumentException
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeDeferrable(string $deferrable): string
|
||||
{
|
||||
$mapping = [
|
||||
'DEFERRED' => ForeignKey::DEFERRED,
|
||||
'IMMEDIATE' => ForeignKey::IMMEDIATE,
|
||||
'NOT DEFERRED' => ForeignKey::NOT_DEFERRED,
|
||||
ForeignKey::DEFERRED => ForeignKey::DEFERRED,
|
||||
ForeignKey::IMMEDIATE => ForeignKey::IMMEDIATE,
|
||||
ForeignKey::NOT_DEFERRED => ForeignKey::NOT_DEFERRED,
|
||||
];
|
||||
$normalized = strtoupper(str_replace('_', ' ', $deferrable));
|
||||
if (array_key_exists($normalized, $mapping)) {
|
||||
return $mapping[$normalized];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown deferrable passed: ' . $deferrable);
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
<?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)
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc.
|
||||
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Index value object
|
||||
*
|
||||
* Models a database index and its attributes.
|
||||
*/
|
||||
class Index
|
||||
{
|
||||
// TODO change the direction of these.
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const INDEX = 'index';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const FULLTEXT = 'fulltext';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name The name of the index.
|
||||
* @param array<string> $columns The columns to index.
|
||||
* @param string $type The type of index, e.g. 'index', 'fulltext'.
|
||||
* @param array<string, int>|int|null $length The length of the index.
|
||||
* @param array<string>|null $order The sort order of the index columns.
|
||||
* @param array<string>|null $include The included columns for covering indexes.
|
||||
* @param ?string $where The where clause for partial indexes.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected array $columns,
|
||||
protected string $type = self::INDEX,
|
||||
protected array|int|null $length = null,
|
||||
protected ?array $order = null,
|
||||
protected ?array $include = null,
|
||||
protected ?string $where = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index columns.
|
||||
*
|
||||
* @param array<string>|string $columns Columns
|
||||
* @return $this
|
||||
*/
|
||||
public function setColumns(string|array $columns)
|
||||
{
|
||||
$this->columns = (array)$columns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index columns.
|
||||
*
|
||||
* @return ?array<string>
|
||||
*/
|
||||
public function getColumns(): ?array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index type.
|
||||
*
|
||||
* @param string $type Type
|
||||
* @return $this
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index name.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index name.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index length.
|
||||
*
|
||||
* In MySQL indexes can have limit clauses to control the number of
|
||||
* characters indexed in text and char columns.
|
||||
*
|
||||
* @param array<string, int>|int $length length value or array of length value
|
||||
* @return $this
|
||||
*/
|
||||
public function setLength(int|array $length)
|
||||
{
|
||||
$this->length = $length;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index length.
|
||||
*
|
||||
* Can be an array of column names and lengths under MySQL.
|
||||
*
|
||||
* @return array<string, int>|int|null
|
||||
*/
|
||||
public function getLength(): array|int|null
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index columns sort order.
|
||||
*
|
||||
* @param array<string> $order column name sort order key value pair
|
||||
* @return $this
|
||||
*/
|
||||
public function setOrder(array $order)
|
||||
{
|
||||
$this->order = $order;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index columns sort order.
|
||||
*
|
||||
* @return ?array<string>
|
||||
*/
|
||||
public function getOrder(): ?array
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index included columns for a 'covering index'.
|
||||
*
|
||||
* In postgres and sqlserver, indexes can define additional non-key
|
||||
* columns to build 'covering indexes'. This feature allows you to
|
||||
* further optimize well-crafted queries that leverage specific
|
||||
* indexes by reading all data from the index.
|
||||
*
|
||||
* @param array<string> $includedColumns Columns
|
||||
* @return $this
|
||||
*/
|
||||
public function setInclude(array $includedColumns)
|
||||
{
|
||||
$this->include = $includedColumns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index included columns.
|
||||
*
|
||||
* @return ?array<string>
|
||||
*/
|
||||
public function getInclude(): ?array
|
||||
{
|
||||
return $this->include;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the where clause for partial indexes.
|
||||
*
|
||||
* @param ?string $where The where clause for partial indexes.
|
||||
* @return $this
|
||||
*/
|
||||
public function setWhere(?string $where)
|
||||
{
|
||||
$this->where = $where;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the where clause for partial indexes.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getWhere(): ?string
|
||||
{
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that maps an array of index options to this object's methods.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Attributes to set.
|
||||
* @throws \RuntimeException
|
||||
* @return $this
|
||||
*/
|
||||
public function setAttributes(array $attributes)
|
||||
{
|
||||
// Valid Options
|
||||
$validOptions = ['columns', 'type', 'name', 'length', 'order', 'include', 'where'];
|
||||
foreach ($attributes as $attr => $value) {
|
||||
if (!in_array($attr, $validOptions, true)) {
|
||||
throw new RuntimeException(sprintf('"%s" is not a valid index option.', $attr));
|
||||
}
|
||||
$method = 'set' . ucfirst($attr);
|
||||
$this->$method($value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an index into an array that is compatible with the Index constructor.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->getName(),
|
||||
'columns' => $this->getColumns(),
|
||||
'type' => $this->getType(),
|
||||
'length' => $this->getLength(),
|
||||
'order' => $this->getOrder(),
|
||||
'include' => $this->getInclude(),
|
||||
'where' => $this->getWhere(),
|
||||
];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,760 @@
|
||||
<?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\Schema;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\Exception\QueryException;
|
||||
use Cake\Database\Type\ColumnSchemaAwareInterface;
|
||||
use Cake\Database\TypeFactory;
|
||||
use InvalidArgumentException;
|
||||
use PDOException;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Base class for schema implementations.
|
||||
*
|
||||
* This class contains methods that are common across
|
||||
* the various SQL dialects.
|
||||
*
|
||||
* Provides methods for performing schema reflection. Results
|
||||
* will be in the form of structured arrays. The structure
|
||||
* of each result will be documented in this class. Subclasses
|
||||
* are free to include *additional* data that is not documented.
|
||||
*
|
||||
* @method array<mixed> listTablesWithoutViewsSql(array $config) Generate the SQL to list the tables, excluding all views.
|
||||
*/
|
||||
abstract class SchemaDialect
|
||||
{
|
||||
/**
|
||||
* The driver instance being used.
|
||||
*
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected Driver $_driver;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This constructor will connect the driver so that methods like columnSql() and others
|
||||
* will fail when the driver has not been connected.
|
||||
*
|
||||
* @param \Cake\Database\Driver $driver The driver to use.
|
||||
*/
|
||||
public function __construct(Driver $driver)
|
||||
{
|
||||
$driver->connect();
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an ON clause for a foreign key.
|
||||
*
|
||||
* @param string $on The on clause
|
||||
* @return string
|
||||
*/
|
||||
protected function _foreignOnClause(string $on): string
|
||||
{
|
||||
if ($on === TableSchema::ACTION_SET_NULL) {
|
||||
return 'SET NULL';
|
||||
}
|
||||
if ($on === TableSchema::ACTION_SET_DEFAULT) {
|
||||
return 'SET DEFAULT';
|
||||
}
|
||||
if ($on === TableSchema::ACTION_CASCADE) {
|
||||
return 'CASCADE';
|
||||
}
|
||||
if ($on === TableSchema::ACTION_RESTRICT) {
|
||||
return 'RESTRICT';
|
||||
}
|
||||
if ($on === TableSchema::ACTION_NO_ACTION) {
|
||||
return 'NO ACTION';
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Invalid value for "on": ' . $on);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string on clauses to the abstract ones.
|
||||
*
|
||||
* @param string $clause The on clause to convert.
|
||||
* @return string
|
||||
*/
|
||||
protected function _convertOnClause(string $clause): string
|
||||
{
|
||||
if ($clause === 'CASCADE' || $clause === 'RESTRICT') {
|
||||
return strtolower($clause);
|
||||
}
|
||||
if ($clause === 'NO ACTION') {
|
||||
return TableSchema::ACTION_NO_ACTION;
|
||||
}
|
||||
|
||||
return TableSchema::ACTION_SET_NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert foreign key constraints references to a valid
|
||||
* stringified list
|
||||
*
|
||||
* @param array<string>|string $references The referenced columns of a foreign key constraint statement
|
||||
* @return string
|
||||
*/
|
||||
protected function _convertConstraintColumns(array|string $references): string
|
||||
{
|
||||
if (is_string($references)) {
|
||||
return $this->_driver->quoteIdentifier($references);
|
||||
}
|
||||
|
||||
return implode(', ', array_map(
|
||||
$this->_driver->quoteIdentifier(...),
|
||||
$references,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to use a matching database type to generate the SQL
|
||||
* fragment for a single column in a table.
|
||||
*
|
||||
* @param string $columnType The column type.
|
||||
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in.
|
||||
* @param string $column The name of the column.
|
||||
* @return string|null An SQL fragment, or `null` in case no corresponding type was found or the type didn't provide
|
||||
* custom column SQL.
|
||||
*/
|
||||
protected function _getTypeSpecificColumnSql(
|
||||
string $columnType,
|
||||
TableSchemaInterface $schema,
|
||||
string $column,
|
||||
): ?string {
|
||||
if (!TypeFactory::getMapped($columnType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = TypeFactory::build($columnType);
|
||||
if (!($type instanceof ColumnSchemaAwareInterface)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $type->getColumnSql($schema, $column, $this->_driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to use a matching database type to convert a SQL column
|
||||
* definition to an abstract type definition.
|
||||
*
|
||||
* @param string $columnType The column type.
|
||||
* @param array $definition The column definition.
|
||||
* @return array<string, mixed>|null Array of column information, or `null`
|
||||
* in case no corresponding type was found or the type didn't provide custom column information.
|
||||
*/
|
||||
protected function _applyTypeSpecificColumnConversion(string $columnType, array $definition): ?array
|
||||
{
|
||||
if (!TypeFactory::getMapped($columnType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = TypeFactory::build($columnType);
|
||||
if (!($type instanceof ColumnSchemaAwareInterface)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $type->convertColumnDefinition($definition, $this->_driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to drop a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema Schema instance
|
||||
* @return array SQL statements to drop a table.
|
||||
*/
|
||||
public function dropTableSql(TableSchema $schema): array
|
||||
{
|
||||
$sql = sprintf(
|
||||
'DROP TABLE %s',
|
||||
$this->_driver->quoteIdentifier($schema->name()),
|
||||
);
|
||||
|
||||
return [$sql];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to list the tables.
|
||||
*
|
||||
* @param array<string, mixed> $config The connection configuration to use for
|
||||
* getting tables from.
|
||||
* @return array An array of (sql, params) to execute.
|
||||
* @deprecated 5.2.0 Use `listTables()` instead.
|
||||
*/
|
||||
abstract public function listTablesSql(array $config): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL to describe a table.
|
||||
*
|
||||
* @param string $tableName The table name to get information on.
|
||||
* @param array<string, mixed> $config The connection configuration.
|
||||
* @return array An array of (sql, params) to execute.
|
||||
* @deprecated 5.2.0 Use `describeColumns()` instead.
|
||||
*/
|
||||
abstract public function describeColumnSql(string $tableName, array $config): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL to describe the indexes in a table.
|
||||
*
|
||||
* @param string $tableName The table name to get information on.
|
||||
* @param array<string, mixed> $config The connection configuration.
|
||||
* @return array An array of (sql, params) to execute.
|
||||
* @deprecated 5.2.0 Use `describeIndexes()` instead.
|
||||
*/
|
||||
abstract public function describeIndexSql(string $tableName, array $config): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL to describe the foreign keys in a table.
|
||||
*
|
||||
* @param string $tableName The table name to get information on.
|
||||
* @param array<string, mixed> $config The connection configuration.
|
||||
* @return array An array of (sql, params) to execute.
|
||||
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
|
||||
*/
|
||||
abstract public function describeForeignKeySql(string $tableName, array $config): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL to describe table options
|
||||
*
|
||||
* @param string $tableName Table name.
|
||||
* @param array<string, mixed> $config The connection configuration.
|
||||
* @return array SQL statements to get options for a table.
|
||||
* @deprecated 5.2.0 Use `describeOptions()` instead.
|
||||
*/
|
||||
public function describeOptionsSql(string $tableName, array $config): array
|
||||
{
|
||||
return ['', ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert field description results into abstract schema fields.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to.
|
||||
* @param array $row The row data from `describeColumnSql`.
|
||||
* @return void
|
||||
* @deprecated 5.2.0 Use `describeColumns()` instead.
|
||||
*/
|
||||
abstract public function convertColumnDescription(TableSchema $schema, array $row): void;
|
||||
|
||||
/**
|
||||
* Convert an index description results into abstract schema indexes or constraints.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
|
||||
* an index or constraint to.
|
||||
* @param array $row The row data from `describeIndexSql`.
|
||||
* @return void
|
||||
* @deprecated 5.2.0 Use `describeIndexes()` instead.
|
||||
*/
|
||||
abstract public function convertIndexDescription(TableSchema $schema, array $row): void;
|
||||
|
||||
/**
|
||||
* Convert a foreign key description into constraints on the Table object.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table object to append
|
||||
* a constraint to.
|
||||
* @param array $row The row data from `describeForeignKeySql`.
|
||||
* @return void
|
||||
* @deprecated 5.2.0 Use `describeForeignKeys()` instead.
|
||||
*/
|
||||
abstract public function convertForeignKeyDescription(TableSchema $schema, array $row): void;
|
||||
|
||||
/**
|
||||
* Convert options data into table options.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
|
||||
* @param array $row The row of data.
|
||||
* @return void
|
||||
* @deprecated 5.2.0 Use `describeOptions()` instead.
|
||||
*/
|
||||
public function convertOptionsDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to create a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
|
||||
* @param array<string> $columns The columns to go inside the table.
|
||||
* @param array<string> $constraints The constraints for the table.
|
||||
* @param array<string> $indexes The indexes for the table.
|
||||
* @return array<string> SQL statements to create a table.
|
||||
*/
|
||||
abstract public function createTableSql(
|
||||
TableSchema $schema,
|
||||
array $columns,
|
||||
array $constraints,
|
||||
array $indexes,
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL fragment for a single column in a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
|
||||
* @param string $name The name of the column.
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
abstract public function columnSql(TableSchema $schema, string $name): string;
|
||||
|
||||
/**
|
||||
* Generate the SQL queries needed to add foreign key constraints to the table
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
|
||||
* @return array SQL fragment.
|
||||
*/
|
||||
abstract public function addConstraintSql(TableSchema $schema): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL queries needed to drop foreign key constraints from the table
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
|
||||
* @return array SQL fragment.
|
||||
*/
|
||||
abstract public function dropConstraintSql(TableSchema $schema): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL fragments for defining table constraints.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
|
||||
* @param string $name The name of the column.
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
abstract public function constraintSql(TableSchema $schema, string $name): string;
|
||||
|
||||
/**
|
||||
* Generate the SQL fragment for a single index in a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table object the column is in.
|
||||
* @param string $name The name of the column.
|
||||
* @return string SQL fragment.
|
||||
*/
|
||||
abstract public function indexSql(TableSchema $schema, string $name): string;
|
||||
|
||||
/**
|
||||
* Generate the SQL to truncate a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema Table instance.
|
||||
* @return array SQL statements to truncate a table.
|
||||
*/
|
||||
abstract public function truncateTableSql(TableSchema $schema): array;
|
||||
|
||||
/**
|
||||
* Create a SQL snippet for a column based on the array shape
|
||||
* that `describeColumns()` creates.
|
||||
*
|
||||
* @param array $column The column metadata
|
||||
* @return string Generated SQL fragment for a column
|
||||
*/
|
||||
public function columnDefinitionSql(array $column): string
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.2.0',
|
||||
'SchemaDialect subclasses need to implement `columnDefinitionSql` before 6.0.0',
|
||||
);
|
||||
$table = new TableSchema('placeholder');
|
||||
$table->addColumn($column['name'], $column);
|
||||
|
||||
return $this->columnSql($table, $column['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of tables, excluding any views, available in the current connection.
|
||||
*
|
||||
* @return array<string> The list of tables in the connected database/schema.
|
||||
*/
|
||||
public function listTablesWithoutViews(): array
|
||||
{
|
||||
[$sql, $params] = $this->listTablesWithoutViewsSql($this->_driver->config());
|
||||
$result = [];
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
while ($row = $statement->fetch()) {
|
||||
$result[] = $row[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of tables and views available in the current connection.
|
||||
*
|
||||
* @param string|null $schema The schema to get the tables for. If null the default schema is used.
|
||||
* @return array<string> The list of tables and views in the connected database/schema.
|
||||
*/
|
||||
public function listTables(?string $schema = null): array
|
||||
{
|
||||
$config = $this->_driver->config();
|
||||
if ($schema !== null) {
|
||||
$config['schema'] = $schema;
|
||||
// Set database for MySQL
|
||||
$config['database'] = $schema;
|
||||
}
|
||||
[$sql, $params] = $this->listTablesSql($config);
|
||||
$result = [];
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
while ($row = $statement->fetch()) {
|
||||
$result[] = $row[0];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column metadata for a table.
|
||||
*
|
||||
* The name can include a database schema name in the form 'schema.table'.
|
||||
*
|
||||
* @param string $name The name of the table to describe.
|
||||
* @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
|
||||
*/
|
||||
public function describe(string $name): TableSchemaInterface
|
||||
{
|
||||
$tableName = $name;
|
||||
if (str_contains($name, '.')) {
|
||||
$tableName = explode('.', $name)[1];
|
||||
}
|
||||
$table = $this->_driver->newTableSchema($tableName);
|
||||
foreach ($this->describeColumns($name) as $column) {
|
||||
$table->addColumn($column['name'], $column);
|
||||
}
|
||||
foreach ($this->describeIndexes($name) as $index) {
|
||||
if (in_array($index['type'], [TableSchema::CONSTRAINT_UNIQUE, TableSchema::CONSTRAINT_PRIMARY])) {
|
||||
$table->addConstraint($index['name'], $index);
|
||||
} else {
|
||||
$table->addIndex($index['name'], $index);
|
||||
}
|
||||
}
|
||||
foreach ($this->describeForeignKeys($name) as $key) {
|
||||
$table->addConstraint($key['name'], $key);
|
||||
}
|
||||
foreach ($this->describeCheckConstraints($name) as $key) {
|
||||
$table->addConstraint($key['name'], $key);
|
||||
}
|
||||
$options = $this->describeOptions($name);
|
||||
if ($options) {
|
||||
$table->setOptions($options);
|
||||
}
|
||||
if ($table->columns() === []) {
|
||||
throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of column metadata as a array
|
||||
*
|
||||
* Each item in the array will contain the following:
|
||||
*
|
||||
* - name : the name of the column.
|
||||
* - type : the abstract type of the column.
|
||||
* - length : the length of the column.
|
||||
* - default : the default value of the column or null.
|
||||
* - null : boolean indicating whether the column can be null.
|
||||
* - comment : the column comment or null.
|
||||
*
|
||||
* Additionaly the `autoIncrement` key will be set for columns that are a primary key.
|
||||
*
|
||||
* @param string $tableName The name of the table to describe columns on.
|
||||
* @return array
|
||||
*/
|
||||
public function describeColumns(string $tableName): array
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.2.0',
|
||||
'SchemaDialect subclasses need to implement `describeColumns` before 6.0.0',
|
||||
);
|
||||
$config = $this->_driver->config();
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$config['schema'], $tableName] = explode('.', $tableName);
|
||||
}
|
||||
/** @var \Cake\Database\Schema\TableSchema $table */
|
||||
$table = $this->_driver->newTableSchema($tableName);
|
||||
|
||||
[$sql, $params] = $this->describeColumnSql($tableName, $config);
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$this->convertColumnDescription($table, $row);
|
||||
}
|
||||
$columns = [];
|
||||
foreach ($table->columns() as $columnName) {
|
||||
$column = $table->getColumn($columnName);
|
||||
$column['name'] = $columnName;
|
||||
$columns[] = $column;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of constraint metadata as a array
|
||||
*
|
||||
* Each item in the array will contain the following:
|
||||
*
|
||||
* - name : The name of the constraint
|
||||
* - type : the type of the constraint. Generally `foreign`.
|
||||
* - columns : the columns in the constraint on the.
|
||||
* - references : A list of the table + all columns in the referenced table
|
||||
* - update : The update action or null
|
||||
* - delete : The delete action or null
|
||||
*
|
||||
* @param string $tableName The name of the table to describe foreign keys on.
|
||||
* @return array
|
||||
*/
|
||||
public function describeForeignKeys(string $tableName): array
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.2.0',
|
||||
'SchemaDialect subclasses need to implement `describeForeignKeys` before 6.0.0',
|
||||
);
|
||||
$config = $this->_driver->config();
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$config['schema'], $tableName] = explode('.', $tableName);
|
||||
}
|
||||
/** @var \Cake\Database\Schema\TableSchema $table */
|
||||
$table = $this->_driver->newTableSchema($tableName);
|
||||
// Add the columns because TableSchema needs them.
|
||||
foreach ($this->describeColumns($tableName) as $column) {
|
||||
$table->addColumn($column['name'], $column);
|
||||
}
|
||||
|
||||
[$sql, $params] = $this->describeForeignKeySql($tableName, $config);
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$this->convertForeignKeyDescription($table, $row);
|
||||
}
|
||||
$keys = [];
|
||||
foreach ($table->constraints() as $name) {
|
||||
$key = $table->getConstraint($name);
|
||||
$key['name'] = $name;
|
||||
$keys[] = $key;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of index metadata as a array
|
||||
*
|
||||
* Each item in the array will contain the following:
|
||||
*
|
||||
* - name : the name of the index.
|
||||
* - type : the type of the index. One of `unique`, `index`, `primary`.
|
||||
* - columns : the columns in the index.
|
||||
* - length : the length of the index if applicable.
|
||||
*
|
||||
* @param string $tableName The name of the table to describe indexes on.
|
||||
* @return array
|
||||
*/
|
||||
public function describeIndexes(string $tableName): array
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.2.0',
|
||||
'SchemaDialect subclasses need to implement `describeIndexes` before 6.0.0',
|
||||
);
|
||||
$config = $this->_driver->config();
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$config['schema'], $tableName] = explode('.', $tableName);
|
||||
}
|
||||
/** @var \Cake\Database\Schema\TableSchema $table */
|
||||
$table = $this->_driver->newTableSchema($tableName);
|
||||
// Add the columns because TableSchema needs them.
|
||||
foreach ($this->describeColumns($tableName) as $column) {
|
||||
$table->addColumn($column['name'], $column);
|
||||
}
|
||||
|
||||
[$sql, $params] = $this->describeIndexSql($tableName, $config);
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$this->convertIndexDescription($table, $row);
|
||||
}
|
||||
$indexes = [];
|
||||
foreach ($table->indexes() as $name) {
|
||||
$index = $table->getIndex($name);
|
||||
$index['name'] = $name;
|
||||
$indexes[] = $index;
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform specific options
|
||||
*
|
||||
* No keys are guaranteed to be present as they are database driver dependent.
|
||||
*
|
||||
* @param string $tableName The name of the table to describe options on.
|
||||
* @return array
|
||||
*/
|
||||
public function describeOptions(string $tableName): array
|
||||
{
|
||||
deprecationWarning(
|
||||
'5.2.0',
|
||||
'SchemaDialect subclasses need to implement `describeOptions` before 6.0.0',
|
||||
);
|
||||
$config = $this->_driver->config();
|
||||
if (str_contains($tableName, '.')) {
|
||||
[$config['schema'], $tableName] = explode('.', $tableName);
|
||||
}
|
||||
/** @var \Cake\Database\Schema\TableSchema $table */
|
||||
$table = $this->_driver->newTableSchema($tableName);
|
||||
|
||||
[$sql, $params] = $this->describeOptionsSql($tableName, $config);
|
||||
if ($sql) {
|
||||
$statement = $this->_driver->execute($sql, $params);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$this->convertOptionsDescription($table, $row);
|
||||
}
|
||||
}
|
||||
|
||||
return $table->getOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of check constraint metadata as an array.
|
||||
*
|
||||
* Each item in the array will contain the following keys:
|
||||
*
|
||||
* - name - The name of the constraint.
|
||||
* - expression - The check constraint expression as a SQL fragment.
|
||||
*
|
||||
* @param string $tableName The name of the table to describe options on.
|
||||
* @return array
|
||||
*/
|
||||
public function describeCheckConstraints(string $tableName): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table has a column with a given name.
|
||||
*
|
||||
* @param string $tableName The name of the table
|
||||
* @param string $columnName The name of the column
|
||||
* @return bool
|
||||
*/
|
||||
public function hasColumn(string $tableName, string $columnName): bool
|
||||
{
|
||||
try {
|
||||
$columns = $this->describeColumns($tableName);
|
||||
} catch (PDOException | DatabaseException) {
|
||||
return false;
|
||||
}
|
||||
foreach ($columns as $column) {
|
||||
if ($column['name'] === $columnName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists
|
||||
*
|
||||
* @param string $tableName The name of the table
|
||||
* @param string|null $schema The schema look for table in. If null the default schema is used.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTable(string $tableName, ?string $schema = null): bool
|
||||
{
|
||||
$tables = $this->listTables($schema);
|
||||
|
||||
return in_array($tableName, $tables, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table has an index with a given name.
|
||||
*
|
||||
* @param string $tableName The name of the table
|
||||
* @param array<string> $columns The columns in the index. Specific
|
||||
* ordering matters.
|
||||
* @param string $name The name of the index to match on. Can be used alone,
|
||||
* or with $columns to match indexes more precisely.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasIndex(string $tableName, array $columns = [], ?string $name = null): bool
|
||||
{
|
||||
try {
|
||||
$indexes = $this->describeIndexes($tableName);
|
||||
} catch (QueryException) {
|
||||
return false;
|
||||
}
|
||||
$found = null;
|
||||
foreach ($indexes as $index) {
|
||||
if ($columns && $index['columns'] === $columns) {
|
||||
$found = $index;
|
||||
break;
|
||||
}
|
||||
if ($columns === [] && $name !== null) {
|
||||
if ($index['name'] === $name) {
|
||||
$found = $index;
|
||||
break;
|
||||
}
|
||||
if (isset($index['constraint']) && $index['constraint'] === $name) {
|
||||
$found = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Both columns and name provided, both must match;
|
||||
if ($columns && $found && $name !== null && $found['name'] !== $name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $found !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table has a foreign key with a given name.
|
||||
*
|
||||
* @param string $tableName The name of the table
|
||||
* @param array<string> $columns The columns in the foriegn key. Specific
|
||||
* ordering matters.
|
||||
* @param string $name The name of the foreign key to match on. Can be used alone,
|
||||
* or with $columns to match keys more precisely.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasForeignKey(string $tableName, array $columns = [], ?string $name = null): bool
|
||||
{
|
||||
try {
|
||||
$keys = $this->describeForeignKeys($tableName);
|
||||
} catch (QueryException) {
|
||||
return false;
|
||||
}
|
||||
$found = null;
|
||||
foreach ($keys as $key) {
|
||||
if ($columns && $key['columns'] === $columns) {
|
||||
$found = $key;
|
||||
break;
|
||||
}
|
||||
if (!$columns && $name !== null && $key['name'] === $name) {
|
||||
$found = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Both columns and name provided, both must match;
|
||||
if ($found !== null && $name !== null && $found['name'] !== $name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $found !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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.5.0
|
||||
* @license https://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use Cake\Database\Connection;
|
||||
|
||||
/**
|
||||
* An interface used by TableSchema objects.
|
||||
*/
|
||||
interface SqlGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* Generate the SQL to create the Table.
|
||||
*
|
||||
* Uses the connection to access the schema dialect
|
||||
* to generate platform specific SQL.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
|
||||
* @return array List of SQL statements to create the table and the
|
||||
* required indexes.
|
||||
*/
|
||||
public function createSql(Connection $connection): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL to drop a table.
|
||||
*
|
||||
* Uses the connection to access the schema dialect to generate platform
|
||||
* specific SQL.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
|
||||
* @return array SQL to drop a table.
|
||||
*/
|
||||
public function dropSql(Connection $connection): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL statements to truncate a table
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
|
||||
* @return array SQL to truncate a table.
|
||||
*/
|
||||
public function truncateSql(Connection $connection): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL statements to add the constraints to the table
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
|
||||
* @return array SQL to add the constraints.
|
||||
*/
|
||||
public function addConstraintSql(Connection $connection): array;
|
||||
|
||||
/**
|
||||
* Generate the SQL statements to drop the constraints to the table
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection to generate SQL for.
|
||||
* @return array SQL to drop a table.
|
||||
*/
|
||||
public function dropConstraintSql(Connection $connection): array;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,971 @@
|
||||
<?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\Schema;
|
||||
|
||||
/**
|
||||
* Schema management/reflection features for SQLServer.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqlserverSchemaDialect extends SchemaDialect
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const DEFAULT_SCHEMA_NAME = 'dbo';
|
||||
|
||||
/**
|
||||
* Generate the SQL to list the tables and views.
|
||||
*
|
||||
* @param array<string, mixed> $config The connection configuration to use for
|
||||
* getting tables from.
|
||||
* @return array An array of (sql, params) to execute.
|
||||
*/
|
||||
public function listTablesSql(array $config): array
|
||||
{
|
||||
$sql = "SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND (TABLE_TYPE = 'BASE TABLE' OR TABLE_TYPE = 'VIEW')
|
||||
ORDER BY TABLE_NAME";
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
|
||||
return [$sql, [$schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to list the tables, excluding all views.
|
||||
*
|
||||
* @param array<string, mixed> $config The connection configuration to use for
|
||||
* getting tables from.
|
||||
* @return array<mixed> An array of (sql, params) to execute.
|
||||
*/
|
||||
public function listTablesWithoutViewsSql(array $config): array
|
||||
{
|
||||
$sql = "SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = ?
|
||||
AND (TABLE_TYPE = 'BASE TABLE')
|
||||
ORDER BY TABLE_NAME";
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
|
||||
return [$sql, [$schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumnSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = $this->describeColumnQuery();
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
|
||||
return [$sql, [$tableName, $schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for creating SQL to describe columns in a table.
|
||||
*
|
||||
* @return string SQL to reflect columns
|
||||
*/
|
||||
private function describeColumnQuery(): string
|
||||
{
|
||||
return 'SELECT DISTINCT
|
||||
AC.column_id AS [column_id],
|
||||
AC.name AS [name],
|
||||
TY.name AS [type],
|
||||
AC.max_length AS [char_length],
|
||||
AC.precision AS [precision],
|
||||
AC.scale AS [scale],
|
||||
AC.is_identity AS [autoincrement],
|
||||
AC.is_nullable AS [null],
|
||||
OBJECT_DEFINITION(AC.default_object_id) AS [default],
|
||||
AC.collation_name AS [collation_name],
|
||||
EP.[value] AS [comment]
|
||||
FROM sys.[objects] T
|
||||
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
|
||||
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id]
|
||||
INNER JOIN sys.[types] TY ON TY.[user_type_id] = AC.[user_type_id]
|
||||
LEFT JOIN sys.[extended_properties] as EP
|
||||
ON T.[object_id] = EP.[major_id]
|
||||
AND AC.[column_id] = EP.[minor_id]
|
||||
AND EP.[name] = \'MS_Description\'
|
||||
WHERE T.[name] = ? AND S.[name] = ?
|
||||
ORDER BY column_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a column definition to the abstract types.
|
||||
*
|
||||
* The returned type will be a type that
|
||||
* Cake\Database\TypeFactory can handle.
|
||||
*
|
||||
* @param string $col The column type
|
||||
* @param int|null $length the column length
|
||||
* @param int|null $precision The column precision
|
||||
* @param int|null $scale The column scale
|
||||
* @return array<string, mixed> Array of column information.
|
||||
* @link https://technet.microsoft.com/en-us/library/ms187752.aspx
|
||||
*/
|
||||
protected function _convertColumn(
|
||||
string $col,
|
||||
?int $length = null,
|
||||
?int $precision = null,
|
||||
?int $scale = null,
|
||||
): array {
|
||||
$col = strtolower($col);
|
||||
|
||||
$type = $this->_applyTypeSpecificColumnConversion(
|
||||
$col,
|
||||
compact('length', 'precision', 'scale'),
|
||||
);
|
||||
if ($type !== null) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (in_array($col, ['date', 'time'])) {
|
||||
return ['type' => $col, 'length' => null];
|
||||
}
|
||||
|
||||
if ($col === 'datetime') {
|
||||
// datetime cannot parse more than 3 digits of precision and isn't accurate
|
||||
return ['type' => TableSchemaInterface::TYPE_DATETIME, 'length' => null];
|
||||
}
|
||||
if (str_contains($col, 'datetime')) {
|
||||
$typeName = TableSchemaInterface::TYPE_DATETIME;
|
||||
if ($scale > 0) {
|
||||
$typeName = TableSchemaInterface::TYPE_DATETIME_FRACTIONAL;
|
||||
}
|
||||
|
||||
return ['type' => $typeName, 'length' => null, 'precision' => $scale];
|
||||
}
|
||||
|
||||
if ($col === 'char') {
|
||||
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
|
||||
if ($col === 'tinyint') {
|
||||
return ['type' => TableSchemaInterface::TYPE_TINYINTEGER, 'length' => $precision ?: 3];
|
||||
}
|
||||
if ($col === 'smallint') {
|
||||
return ['type' => TableSchemaInterface::TYPE_SMALLINTEGER, 'length' => $precision ?: 5];
|
||||
}
|
||||
if ($col === 'int' || $col === 'integer') {
|
||||
return ['type' => TableSchemaInterface::TYPE_INTEGER, 'length' => $precision ?: 10];
|
||||
}
|
||||
if ($col === 'bigint') {
|
||||
return ['type' => TableSchemaInterface::TYPE_BIGINTEGER, 'length' => $precision ?: 20];
|
||||
}
|
||||
if ($col === 'bit') {
|
||||
return ['type' => TableSchemaInterface::TYPE_BOOLEAN, 'length' => null];
|
||||
}
|
||||
if (
|
||||
str_contains($col, 'numeric') ||
|
||||
str_contains($col, 'money') ||
|
||||
str_contains($col, 'decimal')
|
||||
) {
|
||||
return ['type' => TableSchemaInterface::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale];
|
||||
}
|
||||
|
||||
if ($col === 'real' || $col === 'float') {
|
||||
return ['type' => TableSchemaInterface::TYPE_FLOAT, 'length' => null];
|
||||
}
|
||||
// SqlServer schema reflection returns double length for unicode
|
||||
// columns because internally it uses UTF16/UCS2
|
||||
if (in_array($col, ['nvarchar', 'nchar', 'ntext'], true)) {
|
||||
$length /= 2;
|
||||
}
|
||||
if (str_contains($col, 'varchar') && $length < 0) {
|
||||
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
if (str_contains($col, 'varchar')) {
|
||||
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => $length ?: 255];
|
||||
}
|
||||
|
||||
if (str_contains($col, 'char')) {
|
||||
return ['type' => TableSchemaInterface::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
|
||||
if (str_contains($col, 'text')) {
|
||||
return ['type' => TableSchemaInterface::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
if ($col === 'image' || str_contains($col, 'binary')) {
|
||||
// -1 is the value for MAX which we treat as a 'long' binary
|
||||
if ($length == -1) {
|
||||
$length = TableSchema::LENGTH_LONG;
|
||||
}
|
||||
|
||||
return ['type' => TableSchemaInterface::TYPE_BINARY, 'length' => $length];
|
||||
}
|
||||
|
||||
if ($col === 'uniqueidentifier') {
|
||||
return ['type' => TableSchemaInterface::TYPE_UUID];
|
||||
}
|
||||
if ($col === 'geometry') {
|
||||
return ['type' => TableSchemaInterface::TYPE_GEOMETRY];
|
||||
}
|
||||
if ($col === 'geography') {
|
||||
// SQLserver only has one generic geometry type that
|
||||
// we map to point.
|
||||
return ['type' => TableSchemaInterface::TYPE_POINT];
|
||||
}
|
||||
|
||||
return ['type' => TableSchemaInterface::TYPE_STRING, 'length' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertColumnDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$field = $this->_convertColumn(
|
||||
$row['type'],
|
||||
$row['char_length'] !== null ? (int)$row['char_length'] : null,
|
||||
$row['precision'] !== null ? (int)$row['precision'] : null,
|
||||
$row['scale'] !== null ? (int)$row['scale'] : null,
|
||||
);
|
||||
|
||||
if (!empty($row['autoincrement'])) {
|
||||
$field['autoIncrement'] = true;
|
||||
}
|
||||
|
||||
$field += [
|
||||
'null' => $row['null'] === '1',
|
||||
'default' => $this->_defaultValue($field['type'], $row['default']),
|
||||
'collate' => $row['collation_name'],
|
||||
];
|
||||
$schema->addColumn($row['name'], $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a tablename into a tuple of schema, table
|
||||
* If the table does not have a schema name included, the connection
|
||||
* schema will be used.
|
||||
*
|
||||
* @param string $tableName The table name to split
|
||||
* @return array A tuple of [schema, tablename]
|
||||
*/
|
||||
private function splitTablename(string $tableName): array
|
||||
{
|
||||
$config = $this->_driver->config();
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
if (str_contains($tableName, '.')) {
|
||||
return explode('.', $tableName);
|
||||
}
|
||||
|
||||
return [$schema, $tableName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumns(string $tableName): array
|
||||
{
|
||||
[$schema, $name] = $this->splitTablename($tableName);
|
||||
|
||||
$sql = $this->describeColumnQuery();
|
||||
$statement = $this->_driver->execute($sql, [$name, $schema]);
|
||||
$columns = [];
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$field = $this->_convertColumn(
|
||||
$row['type'],
|
||||
$row['char_length'] !== null ? (int)$row['char_length'] : null,
|
||||
$row['precision'] !== null ? (int)$row['precision'] : null,
|
||||
$row['scale'] !== null ? (int)$row['scale'] : null,
|
||||
);
|
||||
|
||||
if (!empty($row['autoincrement'])) {
|
||||
$field['autoIncrement'] = true;
|
||||
}
|
||||
|
||||
$field += [
|
||||
'name' => $row['name'],
|
||||
'null' => $row['null'] === '1',
|
||||
'default' => $this->_defaultValue($field['type'], $row['default']),
|
||||
'comment' => $row['comment'] ?? null,
|
||||
'collate' => $row['collation_name'],
|
||||
];
|
||||
$columns[] = $field;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulate the default value.
|
||||
*
|
||||
* Removes () wrapping default values, extracts strings from
|
||||
* N'' wrappers and collation text and converts NULL strings.
|
||||
*
|
||||
* @param string $type The schema type
|
||||
* @param string|null $default The default value.
|
||||
* @return string|int|null
|
||||
*/
|
||||
protected function _defaultValue(string $type, ?string $default): string|int|null
|
||||
{
|
||||
if ($default === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove () surrounding value (NULL) but leave () at the end of functions
|
||||
// integers might have two ((0)) wrapping value
|
||||
if (preg_match('/^\(+(.*?(\(\))?)\)+$/', $default, $matches)) {
|
||||
$default = $matches[1];
|
||||
}
|
||||
|
||||
if ($default === 'NULL') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === TableSchemaInterface::TYPE_BOOLEAN) {
|
||||
return (int)$default;
|
||||
}
|
||||
|
||||
// Remove quotes
|
||||
if (preg_match("/^\(?N?'(.*)'\)?/", $default, $matches)) {
|
||||
return str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeIndexSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = $this->describeIndexQuery();
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
|
||||
return [$sql, [$tableName, $schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query to describe indexes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function describeIndexQuery(): string
|
||||
{
|
||||
return "SELECT
|
||||
I.[name] AS [index_name],
|
||||
IC.[index_column_id] AS [index_order],
|
||||
AC.[name] AS [column_name],
|
||||
I.[is_unique], I.[is_primary_key],
|
||||
I.[is_unique_constraint],
|
||||
IC.[is_included_column]
|
||||
FROM sys.[tables] AS T
|
||||
INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
|
||||
INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
|
||||
INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id]
|
||||
INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]
|
||||
WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ?
|
||||
ORDER BY I.[index_id], IC.[index_column_id]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertIndexDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$type = TableSchema::INDEX_INDEX;
|
||||
$name = $row['index_name'];
|
||||
if ($row['is_primary_key']) {
|
||||
$name = TableSchema::CONSTRAINT_PRIMARY;
|
||||
$type = TableSchema::CONSTRAINT_PRIMARY;
|
||||
}
|
||||
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
|
||||
$type = TableSchema::CONSTRAINT_UNIQUE;
|
||||
}
|
||||
|
||||
if ($type === TableSchema::INDEX_INDEX) {
|
||||
$existing = $schema->getIndex($name);
|
||||
} else {
|
||||
$existing = $schema->getConstraint($name);
|
||||
}
|
||||
|
||||
$columns = [$row['column_name']];
|
||||
if ($existing) {
|
||||
$columns = array_merge($existing['columns'], $columns);
|
||||
}
|
||||
|
||||
if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
|
||||
$schema->addConstraint($name, [
|
||||
'type' => $type,
|
||||
'columns' => $columns,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
$schema->addIndex($name, [
|
||||
'type' => $type,
|
||||
'columns' => $columns,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeIndexes(string $tableName): array
|
||||
{
|
||||
[$schema, $name] = $this->splitTablename($tableName);
|
||||
$sql = $this->describeIndexQuery();
|
||||
$indexes = [];
|
||||
$statement = $this->_driver->execute($sql, [$name, $schema]);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$type = TableSchema::INDEX_INDEX;
|
||||
$name = $row['index_name'];
|
||||
$constraint = null;
|
||||
if ($row['is_primary_key']) {
|
||||
$constraint = $name;
|
||||
$name = TableSchema::CONSTRAINT_PRIMARY;
|
||||
$type = TableSchema::CONSTRAINT_PRIMARY;
|
||||
}
|
||||
if (($row['is_unique'] || $row['is_unique_constraint']) && $type === TableSchema::INDEX_INDEX) {
|
||||
$type = TableSchema::CONSTRAINT_UNIQUE;
|
||||
}
|
||||
|
||||
if (!isset($indexes[$name])) {
|
||||
$indexes[$name] = [
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'columns' => [],
|
||||
'length' => [],
|
||||
];
|
||||
}
|
||||
if ($row['is_included_column']) {
|
||||
$indexes[$name]['include'][] = $row['column_name'];
|
||||
} else {
|
||||
$indexes[$name]['columns'][] = $row['column_name'];
|
||||
}
|
||||
if ($constraint) {
|
||||
$indexes[$name]['constraint'] = $constraint;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query to describe foreign keys
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function describeForeignKeyQuery(): string
|
||||
{
|
||||
// phpcs:disable Generic.Files.LineLength
|
||||
|
||||
return 'SELECT FK.[name] AS [foreign_key_name],
|
||||
FK.[delete_referential_action_desc] AS [delete_type],
|
||||
FK.[update_referential_action_desc] AS [update_type],
|
||||
C.name AS [column],
|
||||
RT.name AS [reference_table],
|
||||
RC.name AS [reference_column]
|
||||
FROM sys.foreign_keys FK
|
||||
INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id
|
||||
INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id
|
||||
INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id
|
||||
INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id
|
||||
INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id
|
||||
INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id
|
||||
WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ?
|
||||
ORDER BY FKC.constraint_column_id';
|
||||
// phpcs:enable Generic.Files.LineLength
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeForeignKeys(string $tableName): array
|
||||
{
|
||||
[$schema, $name] = $this->splitTablename($tableName);
|
||||
$sql = $this->describeForeignKeyQuery();
|
||||
$keys = [];
|
||||
$statement = $this->_driver->execute($sql, [$name, $schema]);
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$name = $row['foreign_key_name'];
|
||||
if (!isset($keys[$name])) {
|
||||
$keys[$name] = [
|
||||
'name' => $name,
|
||||
'type' => TableSchema::CONSTRAINT_FOREIGN,
|
||||
'columns' => [],
|
||||
'references' => [$row['reference_table'], []],
|
||||
'update' => $this->_convertOnClause($row['update_type']),
|
||||
'delete' => $this->_convertOnClause($row['delete_type']),
|
||||
];
|
||||
}
|
||||
$keys[$name]['columns'][] = $row['column'];
|
||||
$keys[$name]['references'][1][] = $row['reference_column'];
|
||||
}
|
||||
|
||||
foreach ($keys as $id => $key) {
|
||||
// references.1 is the referenced columns. Backwards compat
|
||||
// requires a single column to be a string, but multiple to be an array.
|
||||
if (count($key['references'][1]) === 1) {
|
||||
$keys[$id]['references'][1] = $key['references'][1][0];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeForeignKeySql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = $this->describeForeignKeyQuery();
|
||||
$schema = $config['schema'] ?? static::DEFAULT_SCHEMA_NAME;
|
||||
|
||||
return [$sql, [$tableName, $schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$data = [
|
||||
'type' => TableSchema::CONSTRAINT_FOREIGN,
|
||||
'columns' => [$row['column']],
|
||||
'references' => [$row['reference_table'], $row['reference_column']],
|
||||
'update' => $this->_convertOnClause($row['update_type']),
|
||||
'delete' => $this->_convertOnClause($row['delete_type']),
|
||||
];
|
||||
$name = $row['foreign_key_name'];
|
||||
$schema->addConstraint($name, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeOptions(string $tableName): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _foreignOnClause(string $on): string
|
||||
{
|
||||
$parent = parent::_foreignOnClause($on);
|
||||
|
||||
return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_NO_ACTION) : $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _convertOnClause(string $clause): string
|
||||
{
|
||||
return match ($clause) {
|
||||
'NO_ACTION' => TableSchema::ACTION_NO_ACTION,
|
||||
'CASCADE' => TableSchema::ACTION_CASCADE,
|
||||
'SET_NULL' => TableSchema::ACTION_SET_NULL,
|
||||
'SET_DEFAULT' => TableSchema::ACTION_SET_DEFAULT,
|
||||
default => TableSchema::ACTION_SET_NULL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
$data = $schema->getColumn($name);
|
||||
assert($data !== null);
|
||||
$data['name'] = $name;
|
||||
|
||||
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
|
||||
if ($sql !== null) {
|
||||
return $sql;
|
||||
}
|
||||
$autoIncrementTypes = [
|
||||
TableSchemaInterface::TYPE_TINYINTEGER,
|
||||
TableSchemaInterface::TYPE_SMALLINTEGER,
|
||||
TableSchemaInterface::TYPE_INTEGER,
|
||||
TableSchemaInterface::TYPE_BIGINTEGER,
|
||||
];
|
||||
$primaryKey = $schema->getPrimaryKey();
|
||||
if (
|
||||
in_array($data['type'], $autoIncrementTypes, true) &&
|
||||
$primaryKey === [$name] &&
|
||||
$name === 'id'
|
||||
) {
|
||||
$data['autoIncrement'] = true;
|
||||
}
|
||||
|
||||
return $this->columnDefinitionSql($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnDefinitionSql(array $column): string
|
||||
{
|
||||
$name = $column['name'];
|
||||
$column += [
|
||||
'length' => null,
|
||||
'precision' => null,
|
||||
];
|
||||
$out = $this->_driver->quoteIdentifier($name);
|
||||
$typeMap = [
|
||||
TableSchemaInterface::TYPE_TINYINTEGER => ' TINYINT',
|
||||
TableSchemaInterface::TYPE_SMALLINTEGER => ' SMALLINT',
|
||||
TableSchemaInterface::TYPE_INTEGER => ' INTEGER',
|
||||
TableSchemaInterface::TYPE_BIGINTEGER => ' BIGINT',
|
||||
TableSchemaInterface::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
|
||||
TableSchemaInterface::TYPE_BOOLEAN => ' BIT',
|
||||
TableSchemaInterface::TYPE_CHAR => ' NCHAR',
|
||||
TableSchemaInterface::TYPE_STRING => ' NVARCHAR',
|
||||
TableSchemaInterface::TYPE_FLOAT => ' FLOAT',
|
||||
TableSchemaInterface::TYPE_DECIMAL => ' DECIMAL',
|
||||
TableSchemaInterface::TYPE_DATE => ' DATE',
|
||||
TableSchemaInterface::TYPE_TIME => ' TIME',
|
||||
TableSchemaInterface::TYPE_DATETIME => ' DATETIME2',
|
||||
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL => ' DATETIME2',
|
||||
TableSchemaInterface::TYPE_TIMESTAMP => ' DATETIME2',
|
||||
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2',
|
||||
TableSchemaInterface::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2',
|
||||
TableSchemaInterface::TYPE_UUID => ' UNIQUEIDENTIFIER',
|
||||
TableSchemaInterface::TYPE_NATIVE_UUID => ' UNIQUEIDENTIFIER',
|
||||
TableSchemaInterface::TYPE_JSON => ' NVARCHAR(MAX)',
|
||||
TableSchemaInterface::TYPE_GEOMETRY => ' GEOMETRY',
|
||||
TableSchemaInterface::TYPE_POINT => ' GEOGRAPHY',
|
||||
TableSchemaInterface::TYPE_LINESTRING => ' GEOGRAPHY',
|
||||
TableSchemaInterface::TYPE_POLYGON => ' GEOGRAPHY',
|
||||
];
|
||||
|
||||
$foundType = false;
|
||||
if (isset($typeMap[$column['type']])) {
|
||||
$out .= $typeMap[$column['type']];
|
||||
$foundType = true;
|
||||
}
|
||||
|
||||
$hasLength = [
|
||||
TableSchemaInterface::TYPE_CHAR,
|
||||
TableSchemaInterface::TYPE_STRING,
|
||||
TableSchemaInterface::TYPE_BINARY,
|
||||
];
|
||||
$autoIncrementTypes = [
|
||||
TableSchemaInterface::TYPE_TINYINTEGER,
|
||||
TableSchemaInterface::TYPE_SMALLINTEGER,
|
||||
TableSchemaInterface::TYPE_INTEGER,
|
||||
TableSchemaInterface::TYPE_BIGINTEGER,
|
||||
];
|
||||
$autoIncrement = (bool)($column['autoIncrement'] ?? false);
|
||||
if (in_array($column['type'], $autoIncrementTypes, true) && $autoIncrement) {
|
||||
$out .= ' IDENTITY(1, 1)';
|
||||
$foundType = true;
|
||||
unset($column['default']);
|
||||
}
|
||||
|
||||
if ($column['type'] === TableSchemaInterface::TYPE_STRING && !isset($column['length'])) {
|
||||
$column['length'] = TableSchema::LENGTH_TINY;
|
||||
} elseif (
|
||||
$column['type'] === TableSchemaInterface::TYPE_TEXT &&
|
||||
$column['length'] !== TableSchema::LENGTH_TINY
|
||||
) {
|
||||
$out .= ' NVARCHAR(MAX)';
|
||||
$foundType = true;
|
||||
}
|
||||
|
||||
if ($column['type'] === TableSchemaInterface::TYPE_BINARY) {
|
||||
if (
|
||||
!isset($column['length'])
|
||||
|| in_array($column['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
|
||||
) {
|
||||
$column['length'] = 'MAX';
|
||||
}
|
||||
|
||||
if ($column['length'] === 1) {
|
||||
$out .= ' BINARY';
|
||||
} else {
|
||||
$out .= ' VARBINARY';
|
||||
}
|
||||
$foundType = true;
|
||||
}
|
||||
|
||||
if ($column['type'] === TableSchemaInterface::TYPE_TEXT && $column['length'] === TableSchema::LENGTH_TINY) {
|
||||
$out .= ' NVARCHAR';
|
||||
$hasLength[] = $column['type'];
|
||||
$foundType = true;
|
||||
}
|
||||
if (!$foundType) {
|
||||
$out .= ' ' . strtoupper($column['type']);
|
||||
$hasLength[] = $column['type'];
|
||||
}
|
||||
if (in_array($column['type'], $hasLength, true) && isset($column['length'])) {
|
||||
$out .= '(' . $column['length'] . ')';
|
||||
}
|
||||
|
||||
$hasCollate = [
|
||||
TableSchemaInterface::TYPE_TEXT,
|
||||
TableSchemaInterface::TYPE_STRING,
|
||||
TableSchemaInterface::TYPE_CHAR,
|
||||
];
|
||||
if (in_array($column['type'], $hasCollate, true) && isset($column['collate']) && $column['collate'] !== '') {
|
||||
$out .= ' COLLATE ' . $column['collate'];
|
||||
}
|
||||
|
||||
$precisionTypes = [
|
||||
TableSchemaInterface::TYPE_FLOAT,
|
||||
TableSchemaInterface::TYPE_DATETIME,
|
||||
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchemaInterface::TYPE_TIMESTAMP,
|
||||
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
];
|
||||
if (in_array($column['type'], $precisionTypes, true) && isset($column['precision'])) {
|
||||
$out .= '(' . (int)$column['precision'] . ')';
|
||||
}
|
||||
|
||||
if (
|
||||
$column['type'] === TableSchemaInterface::TYPE_DECIMAL &&
|
||||
(isset($column['length']) || isset($column['precision']))
|
||||
) {
|
||||
$out .= '(' . (int)$column['length'] . ',' . (int)$column['precision'] . ')';
|
||||
}
|
||||
|
||||
if (isset($column['null']) && $column['null'] === false) {
|
||||
$out .= ' NOT NULL';
|
||||
}
|
||||
|
||||
$dateTimeTypes = [
|
||||
TableSchemaInterface::TYPE_DATETIME,
|
||||
TableSchemaInterface::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchemaInterface::TYPE_TIMESTAMP,
|
||||
TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
];
|
||||
$dateTimeDefaults = [
|
||||
'current_timestamp',
|
||||
'getdate()',
|
||||
'getutcdate()',
|
||||
'sysdatetime()',
|
||||
'sysutcdatetime()',
|
||||
'sysdatetimeoffset()',
|
||||
];
|
||||
if (
|
||||
isset($column['default']) &&
|
||||
in_array($column['type'], $dateTimeTypes, true) &&
|
||||
is_string($column['default']) &&
|
||||
in_array(strtolower($column['default']), $dateTimeDefaults, true)
|
||||
) {
|
||||
$out .= ' DEFAULT ' . strtoupper($column['default']);
|
||||
} elseif (isset($column['default'])) {
|
||||
$default = is_bool($column['default'])
|
||||
? (int)$column['default']
|
||||
: $this->_driver->schemaValue($column['default']);
|
||||
$out .= ' DEFAULT ' . $default;
|
||||
} elseif (isset($column['null']) && $column['null'] !== false) {
|
||||
$out .= ' DEFAULT NULL';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addConstraintSql(TableSchema $schema): array
|
||||
{
|
||||
$sqlPattern = 'ALTER TABLE %s ADD %s;';
|
||||
$sql = [];
|
||||
|
||||
foreach ($schema->constraints() as $name) {
|
||||
$constraint = $schema->getConstraint($name);
|
||||
assert($constraint !== null);
|
||||
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
|
||||
$tableName = $this->_driver->quoteIdentifier($schema->name());
|
||||
$sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function dropConstraintSql(TableSchema $schema): array
|
||||
{
|
||||
$sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
|
||||
$sql = [];
|
||||
|
||||
foreach ($schema->constraints() as $name) {
|
||||
$constraint = $schema->getConstraint($name);
|
||||
assert($constraint !== null);
|
||||
if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
|
||||
$tableName = $this->_driver->quoteIdentifier($schema->name());
|
||||
$constraintName = $this->_driver->quoteIdentifier($name);
|
||||
$sql[] = sprintf($sqlPattern, $tableName, $constraintName);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function indexSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
$index = $schema->index($name);
|
||||
$columns = array_map(
|
||||
$this->_driver->quoteIdentifier(...),
|
||||
(array)$index->getColumns(),
|
||||
);
|
||||
$include = '';
|
||||
$included = $index->getInclude();
|
||||
if ($included !== null) {
|
||||
$included = array_map(
|
||||
$this->_driver->quoteIdentifier(...),
|
||||
$included,
|
||||
);
|
||||
$include = sprintf(' INCLUDE (%s)', implode(', ', $included));
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'CREATE INDEX %s ON %s (%s)%s',
|
||||
$this->_driver->quoteIdentifier($name),
|
||||
$this->_driver->quoteIdentifier($schema->name()),
|
||||
implode(', ', $columns),
|
||||
$include,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function constraintSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
$data = $schema->getConstraint($name);
|
||||
assert($data !== null);
|
||||
$out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
|
||||
$out = 'PRIMARY KEY';
|
||||
}
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
|
||||
$out .= ' UNIQUE';
|
||||
}
|
||||
|
||||
return $this->_keySql($out, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for generating key SQL snippets.
|
||||
*
|
||||
* @param string $prefix The key prefix
|
||||
* @param array $data Key data.
|
||||
* @return string
|
||||
*/
|
||||
protected function _keySql(string $prefix, array $data): string
|
||||
{
|
||||
$columns = array_map(
|
||||
$this->_driver->quoteIdentifier(...),
|
||||
$data['columns'],
|
||||
);
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
|
||||
return $prefix . sprintf(
|
||||
' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
|
||||
implode(', ', $columns),
|
||||
$this->_driver->quoteIdentifier($data['references'][0]),
|
||||
$this->_convertConstraintColumns($data['references'][1]),
|
||||
$this->_foreignOnClause($data['update']),
|
||||
$this->_foreignOnClause($data['delete']),
|
||||
);
|
||||
}
|
||||
|
||||
return $prefix . ' (' . implode(', ', $columns) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
|
||||
{
|
||||
$content = array_merge($columns, $constraints);
|
||||
$content = implode(",\n", array_filter($content));
|
||||
$tableName = $this->_driver->quoteIdentifier($schema->name());
|
||||
$out = [];
|
||||
$out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content);
|
||||
foreach ($indexes as $index) {
|
||||
$out[] = $index;
|
||||
}
|
||||
foreach ($schema->columns() as $name) {
|
||||
$column = $schema->getColumn($name);
|
||||
$comment = $column['comment'] ?? null;
|
||||
if ($comment !== null) {
|
||||
$out[] = $this->columnCommentSql($schema, $name, $comment);
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to create a column comment.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table schema.
|
||||
* @param string $name The column name.
|
||||
* @param string $comment The column comment.
|
||||
* @return string
|
||||
*/
|
||||
protected function columnCommentSql(TableSchema $schema, string $name, string $comment): string
|
||||
{
|
||||
$tableName = $this->_driver->quoteIdentifier($schema->name());
|
||||
$columnName = $this->_driver->quoteIdentifier($name);
|
||||
$comment = $this->_driver->schemaValue($comment);
|
||||
|
||||
return sprintf(
|
||||
"EXEC sp_addextendedproperty N'MS_Description', %s, N'SCHEMA', N'dbo', N'TABLE', %s, N'COLUMN', %s;",
|
||||
$comment,
|
||||
$tableName,
|
||||
$columnName,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function truncateTableSql(TableSchema $schema): array
|
||||
{
|
||||
$name = $this->_driver->quoteIdentifier($schema->name());
|
||||
$queries = [
|
||||
sprintf('DELETE FROM %s', $name),
|
||||
];
|
||||
|
||||
// Restart identity sequences
|
||||
$pk = $schema->getPrimaryKey();
|
||||
if (count($pk) === 1) {
|
||||
$column = $schema->getColumn($pk[0]);
|
||||
assert($column !== null);
|
||||
if (in_array($column['type'], ['integer', 'biginteger'])) {
|
||||
$queries[] = sprintf(
|
||||
"IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = '%s' AND " .
|
||||
"last_value IS NOT NULL) DBCC CHECKIDENT('%s', RESEED, 0)",
|
||||
$schema->name(),
|
||||
$schema->name(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
}
|
||||
+1054
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,381 @@
|
||||
<?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.5.0
|
||||
* @license https://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
use Cake\Datasource\SchemaInterface;
|
||||
|
||||
/**
|
||||
* An interface used by database TableSchema objects.
|
||||
*
|
||||
* @method \Cake\Database\Schema\Column column(string $name)
|
||||
* @method \Cake\Database\Schema\Index index(string $name)
|
||||
* @method \Cake\Database\Schema\Constraint constraint(string $name)
|
||||
*/
|
||||
interface TableSchemaInterface extends SchemaInterface
|
||||
{
|
||||
/**
|
||||
* Binary column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_BINARY = 'binary';
|
||||
|
||||
/**
|
||||
* Binary UUID column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_BINARY_UUID = 'binaryuuid';
|
||||
|
||||
/**
|
||||
* Date column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_DATE = 'date';
|
||||
|
||||
/**
|
||||
* Datetime column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_DATETIME = 'datetime';
|
||||
|
||||
/**
|
||||
* Datetime with fractional seconds column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_DATETIME_FRACTIONAL = 'datetimefractional';
|
||||
|
||||
/**
|
||||
* Time column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TIME = 'time';
|
||||
|
||||
/**
|
||||
* Year column type
|
||||
*
|
||||
* Currently only implemented in MySQL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_YEAR = 'year';
|
||||
|
||||
/**
|
||||
* Timestamp column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TIMESTAMP = 'timestamp';
|
||||
|
||||
/**
|
||||
* Timestamp with fractional seconds column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TIMESTAMP_FRACTIONAL = 'timestampfractional';
|
||||
|
||||
/**
|
||||
* Timestamp with time zone column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TIMESTAMP_TIMEZONE = 'timestamptimezone';
|
||||
|
||||
/**
|
||||
* Datetime interval. Only implemented in postgres.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_INTERVAL = 'interval';
|
||||
|
||||
/**
|
||||
* JSON column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_JSON = 'json';
|
||||
|
||||
/**
|
||||
* String column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_STRING = 'string';
|
||||
|
||||
/**
|
||||
* Char column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_CHAR = 'char';
|
||||
|
||||
/**
|
||||
* Case-insensitive text column type.
|
||||
*
|
||||
* Only implemented in postgres
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_CITEXT = 'citext';
|
||||
|
||||
/**
|
||||
* Text column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TEXT = 'text';
|
||||
|
||||
/**
|
||||
* Tiny Integer column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_TINYINTEGER = 'tinyinteger';
|
||||
|
||||
/**
|
||||
* Small Integer column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_SMALLINTEGER = 'smallinteger';
|
||||
|
||||
/**
|
||||
* Integer column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_INTEGER = 'integer';
|
||||
|
||||
/**
|
||||
* Big Integer column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_BIGINTEGER = 'biginteger';
|
||||
|
||||
/**
|
||||
* Float column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_FLOAT = 'float';
|
||||
|
||||
/**
|
||||
* Decimal column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_DECIMAL = 'decimal';
|
||||
|
||||
/**
|
||||
* Boolean column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_BOOLEAN = 'boolean';
|
||||
|
||||
/**
|
||||
* UUID column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_UUID = 'uuid';
|
||||
|
||||
/**
|
||||
* Native UUID column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_NATIVE_UUID = 'nativeuuid';
|
||||
|
||||
/**
|
||||
* Geometry column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_GEOMETRY = 'geometry';
|
||||
|
||||
/**
|
||||
* Point column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_POINT = 'point';
|
||||
|
||||
/**
|
||||
* Linestring column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_LINESTRING = 'linestring';
|
||||
|
||||
/**
|
||||
* Polgon column type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_POLYGON = 'polygon';
|
||||
|
||||
/**
|
||||
* INET type. Only implemented in postgres.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_INET = 'inet';
|
||||
|
||||
/**
|
||||
* CIDR type. Only implemented in postgres.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_CIDR = 'cidr';
|
||||
|
||||
/**
|
||||
* Macaddr type. Only implemented in postgres.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_MACADDR = 'macaddr';
|
||||
|
||||
/**
|
||||
* Geospatial column types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public const GEOSPATIAL_TYPES = [
|
||||
self::TYPE_GEOMETRY,
|
||||
self::TYPE_POINT,
|
||||
self::TYPE_LINESTRING,
|
||||
self::TYPE_POLYGON,
|
||||
];
|
||||
|
||||
/**
|
||||
* Check whether a table has an autoIncrement column defined.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAutoincrement(): bool;
|
||||
|
||||
/**
|
||||
* Sets whether the table is temporary in the database.
|
||||
*
|
||||
* @param bool $temporary Whether the table is to be temporary.
|
||||
* @return $this
|
||||
*/
|
||||
public function setTemporary(bool $temporary);
|
||||
|
||||
/**
|
||||
* Gets whether the table is temporary in the database.
|
||||
*
|
||||
* @return bool The current temporary setting.
|
||||
*/
|
||||
public function isTemporary(): bool;
|
||||
|
||||
/**
|
||||
* Get the column(s) used for the primary key.
|
||||
*
|
||||
* @return array<string> Column name(s) for the primary key. An
|
||||
* empty list will be returned when the table has no primary key.
|
||||
*/
|
||||
public function getPrimaryKey(): array;
|
||||
|
||||
/**
|
||||
* Add an index.
|
||||
*
|
||||
* Used to add indexes, and full text indexes in platforms that support
|
||||
* them.
|
||||
*
|
||||
* ### Attributes
|
||||
*
|
||||
* - `type` The type of index being added.
|
||||
* - `columns` The columns in the index.
|
||||
*
|
||||
* @param string $name The name of the index.
|
||||
* @param array<string, mixed>|string $attrs The attributes for the index.
|
||||
* If string it will be used as `type`.
|
||||
* @return $this
|
||||
* @throws \Cake\Database\Exception\DatabaseException
|
||||
*/
|
||||
public function addIndex(string $name, array|string $attrs);
|
||||
|
||||
/**
|
||||
* Read information about an index based on name.
|
||||
*
|
||||
* @param string $name The name of the index.
|
||||
* @return array<string, mixed>|null Array of index data, or null
|
||||
*/
|
||||
public function getIndex(string $name): ?array;
|
||||
|
||||
/**
|
||||
* Get the names of all the indexes in the table.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function indexes(): array;
|
||||
|
||||
/**
|
||||
* Add a constraint.
|
||||
*
|
||||
* Used to add constraints to a table. For example primary keys, unique
|
||||
* keys, check constraints and foreign keys.
|
||||
*
|
||||
* ### Attributes
|
||||
*
|
||||
* - `type` The type of constraint being added.
|
||||
* - `columns` The columns in the index.
|
||||
* - `references` The table, column a foreign key references.
|
||||
* - `update` The behavior on update. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
|
||||
* - `delete` The behavior on delete. Options are 'restrict', 'setNull', 'cascade', 'noAction'.
|
||||
* - `expression` The SQL expression for check constraints.
|
||||
*
|
||||
* The default for 'update' & 'delete' is 'cascade'.
|
||||
*
|
||||
* @param string $name The name of the constraint.
|
||||
* @param array<string, mixed>|string $attrs The attributes for the constraint.
|
||||
* If string it will be used as `type`.
|
||||
* @return $this
|
||||
* @throws \Cake\Database\Exception\DatabaseException
|
||||
*/
|
||||
public function addConstraint(string $name, array|string $attrs);
|
||||
|
||||
/**
|
||||
* Read information about a constraint based on name.
|
||||
*
|
||||
* @param string $name The name of the constraint.
|
||||
* @return array<string, mixed>|null Array of constraint data, or null
|
||||
*/
|
||||
public function getConstraint(string $name): ?array;
|
||||
|
||||
/**
|
||||
* Remove a constraint.
|
||||
*
|
||||
* @param string $name Name of the constraint to remove
|
||||
* @return $this
|
||||
*/
|
||||
public function dropConstraint(string $name);
|
||||
|
||||
/**
|
||||
* Get the names of all the constraints in the table.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function constraints(): array;
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
<?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)
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc.
|
||||
* (https://github.com/cakephp/migrations/tree/master/LICENSE.txt)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Schema;
|
||||
|
||||
/**
|
||||
* UniqueKey class
|
||||
*
|
||||
* Models a unique key constraint, and provides methods to set driver specific attributes.
|
||||
*/
|
||||
class UniqueKey extends Constraint
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name The name of the constraint.
|
||||
* @param array<string> $columns The columns to constraint.
|
||||
* @param array<string, int>|null $length The length of the columns, if applicable.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected array $columns,
|
||||
protected ?array $length = null,
|
||||
) {
|
||||
$this->type = self::UNIQUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint columns.
|
||||
*
|
||||
* @param array<string>|string $columns Columns
|
||||
* @return $this
|
||||
*/
|
||||
public function setColumns(string|array $columns)
|
||||
{
|
||||
$this->columns = (array)$columns;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint columns.
|
||||
*
|
||||
* @return ?array<string>
|
||||
*/
|
||||
public function getColumns(): ?array
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint type.
|
||||
*
|
||||
* @param string $type Type
|
||||
* @return $this
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint name.
|
||||
*
|
||||
* @param string $name Name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint name.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the constraint length.
|
||||
*
|
||||
* In MySQL unique constraints can have limit clauses to control the number of
|
||||
* characters indexed in text and char columns.
|
||||
*
|
||||
* @param array<string, int> $length array of length values
|
||||
* @return $this
|
||||
*/
|
||||
public function setLength(array $length)
|
||||
{
|
||||
$this->length = $length;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraint length.
|
||||
*
|
||||
* Can be an array of column names and lengths under MySQL.
|
||||
*
|
||||
* @return array<string, int>|null
|
||||
*/
|
||||
public function getLength(): ?array
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a constraint to an array that is compatible
|
||||
* with the constructor.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'columns' => $this->columns,
|
||||
'length' => $this->length,
|
||||
];
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?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.6.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
use Cake\Database\Schema\CachedCollection;
|
||||
|
||||
/**
|
||||
* Schema Cache.
|
||||
*
|
||||
* This tool is intended to be used by deployment scripts so that you
|
||||
* can prevent thundering herd effects on the metadata cache when new
|
||||
* versions of your application are deployed, or when migrations
|
||||
* requiring updated metadata are required.
|
||||
*
|
||||
* @link https://en.wikipedia.org/wiki/Thundering_herd_problem About the thundering herd problem
|
||||
*/
|
||||
class SchemaCache
|
||||
{
|
||||
/**
|
||||
* Schema
|
||||
*
|
||||
* @var \Cake\Database\Schema\CachedCollection
|
||||
*/
|
||||
protected CachedCollection $_schema;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection Connection name to get the schema for or a connection instance
|
||||
*/
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->_schema = $this->getSchema($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build metadata.
|
||||
*
|
||||
* @param string|null $name The name of the table to build cache data for.
|
||||
* @return array<string> Returns a list build table caches
|
||||
*/
|
||||
public function build(?string $name = null): array
|
||||
{
|
||||
if ($name) {
|
||||
$tables = [$name];
|
||||
} else {
|
||||
$tables = $this->_schema->listTables();
|
||||
}
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$this->_schema->describe($table, ['forceRefresh' => true]);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear metadata.
|
||||
*
|
||||
* @param string|null $name The name of the table to clear cache data for.
|
||||
* @return array<string> Returns a list of cleared table caches
|
||||
*/
|
||||
public function clear(?string $name = null): array
|
||||
{
|
||||
if ($name) {
|
||||
$tables = [$name];
|
||||
} else {
|
||||
$tables = $this->_schema->listTables();
|
||||
}
|
||||
|
||||
$cacher = $this->_schema->getCacher();
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$key = $this->_schema->cacheKey($table);
|
||||
$cacher->delete($key);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the schema collection.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection Connection object
|
||||
* @return \Cake\Database\Schema\CachedCollection
|
||||
* @throws \RuntimeException If given connection object is not compatible with schema caching
|
||||
*/
|
||||
public function getSchema(Connection $connection): CachedCollection
|
||||
{
|
||||
$config = $connection->config();
|
||||
if (empty($config['cacheMetadata'])) {
|
||||
$connection->cacheMetadata(true);
|
||||
}
|
||||
|
||||
/** @var \Cake\Database\Schema\CachedCollection */
|
||||
return $connection->getSchemaCollection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?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;
|
||||
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
|
||||
/**
|
||||
* Responsible for compiling a Query object into its SQL representation
|
||||
* for SQL Server
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqlserverCompiler extends QueryCompiler
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s',
|
||||
'order' => ' %s',
|
||||
'offset' => ' OFFSET %s ROWS',
|
||||
'epilog' => ' %s',
|
||||
'comment' => '/* %s */ ',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_selectParts = [
|
||||
'comment', 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
|
||||
'offset', 'limit', 'union', 'epilog', 'intersect',
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a `WITH` clause,
|
||||
* it constructs the CTE definitions list without generating the `RECURSIVE`
|
||||
* keyword that is neither required nor valid.
|
||||
*
|
||||
* @param array<\Cake\Database\Expression\CommonTableExpression> $parts List of CTEs to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$expressions = [];
|
||||
foreach ($parts as $cte) {
|
||||
$expressions[] = $cte->sql($binder);
|
||||
}
|
||||
|
||||
return sprintf('WITH %s ', implode(', ', $expressions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the INSERT part of a SQL query
|
||||
*
|
||||
* To better handle concurrency and low transaction isolation levels,
|
||||
* we also include an OUTPUT clause so we can ensure we get the inserted
|
||||
* row's data back.
|
||||
*
|
||||
* @param array $parts The parts to build
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
if (!isset($parts[0])) {
|
||||
throw new DatabaseException(
|
||||
'Could not compile insert query. No table was specified. ' .
|
||||
'Use `into()` to define a table.',
|
||||
);
|
||||
}
|
||||
$table = $parts[0];
|
||||
$columns = $this->_stringifyExpressions($parts[1], $binder);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
return sprintf(
|
||||
'INSERT%s INTO %s (%s) OUTPUT INSERTED.*',
|
||||
$modifiers,
|
||||
$table,
|
||||
implode(', ', $columns),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the LIMIT part of a SQL query
|
||||
*
|
||||
* @param int $limit the limit clause
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildLimitPart(int $limit, Query $query): string
|
||||
{
|
||||
if ($query->clause('offset') === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(' FETCH FIRST %d ROWS ONLY', $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to build the string representation of a HAVING clause,
|
||||
* it constructs the field list taking care of aliasing and
|
||||
* converting expression objects to string.
|
||||
*
|
||||
* @param array $parts list of fields to be transformed to string
|
||||
* @param \Cake\Database\Query $query The query that is being compiled
|
||||
* @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
|
||||
* @return string
|
||||
*/
|
||||
protected function _buildHavingPart(array $parts, Query $query, ValueBinder $binder): string
|
||||
{
|
||||
$selectParts = $query->clause('select');
|
||||
|
||||
foreach ($selectParts as $selectKey => $selectPart) {
|
||||
if (!$selectPart instanceof FunctionExpression) {
|
||||
continue;
|
||||
}
|
||||
foreach ($parts as $k => $p) {
|
||||
if (!is_string($p)) {
|
||||
continue;
|
||||
}
|
||||
preg_match_all(
|
||||
'/\b' . trim($selectKey, '[]') . '\b/i',
|
||||
$p,
|
||||
$matches,
|
||||
);
|
||||
|
||||
if (empty($matches[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[$k] = preg_replace(
|
||||
['/\[|\]/', '/\b' . trim($selectKey, '[]') . '\b/i'],
|
||||
['', $selectPart->sql($binder)],
|
||||
$p,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf(' HAVING %s', implode(', ', $parts));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Statement;
|
||||
|
||||
/**
|
||||
* Statement class meant to be used by an Sqlite driver
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqliteStatement extends Statement
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
protected ?int $affectedRows = null;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
$this->affectedRows = null;
|
||||
|
||||
return parent::execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
if ($this->affectedRows !== null) {
|
||||
return $this->affectedRows;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->statement->queryString &&
|
||||
preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->statement->queryString)
|
||||
) {
|
||||
$changes = $this->_driver->prepare('SELECT CHANGES()');
|
||||
$changes->execute();
|
||||
$row = $changes->fetch();
|
||||
|
||||
$this->affectedRows = $row ? (int)$row[0] : 0;
|
||||
} else {
|
||||
$this->affectedRows = parent::rowCount();
|
||||
}
|
||||
|
||||
return $this->affectedRows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Statement;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Statement class meant to be used by an Sqlserver driver
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqlserverStatement extends Statement
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function performBind(string|int $column, mixed $value, int $type): void
|
||||
{
|
||||
if ($type === PDO::PARAM_LOB) {
|
||||
$this->statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY);
|
||||
} else {
|
||||
parent::performBind($column, $value, $type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Statement;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\StatementInterface;
|
||||
use Cake\Database\TypeFactory;
|
||||
use Cake\Database\TypeInterface;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
|
||||
class Statement implements StatementInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, int>
|
||||
*/
|
||||
protected const MODE_NAME_MAP = [
|
||||
self::FETCH_TYPE_ASSOC => PDO::FETCH_ASSOC,
|
||||
self::FETCH_TYPE_NUM => PDO::FETCH_NUM,
|
||||
self::FETCH_TYPE_OBJ => PDO::FETCH_OBJ,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected Driver $_driver;
|
||||
|
||||
/**
|
||||
* Cached bound parameters used for logging
|
||||
*
|
||||
* @var array<mixed>
|
||||
*/
|
||||
protected array $params = [];
|
||||
|
||||
/**
|
||||
* @param \PDOStatement $statement PDO statement
|
||||
* @param \Cake\Database\Driver $driver Database driver
|
||||
* @param array<\Closure> $resultDecorators Results decorators
|
||||
*/
|
||||
public function __construct(
|
||||
protected PDOStatement $statement,
|
||||
Driver $driver,
|
||||
protected array $resultDecorators = [],
|
||||
) {
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function bind(array $params, array $types): void
|
||||
{
|
||||
if (!$params) {
|
||||
return;
|
||||
}
|
||||
|
||||
$anonymousParams = is_int(key($params));
|
||||
$offset = 1;
|
||||
foreach ($params as $index => $value) {
|
||||
$type = $types[$index] ?? null;
|
||||
if ($anonymousParams) {
|
||||
$index += $offset;
|
||||
}
|
||||
$this->bindValue($index, $value, $type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function bindValue(string|int $column, mixed $value, string|int|null $type = 'string'): void
|
||||
{
|
||||
$type ??= 'string';
|
||||
if (!is_int($type)) {
|
||||
[$value, $type] = $this->cast($value, $type);
|
||||
}
|
||||
|
||||
$this->params[$column] = $value;
|
||||
$this->performBind($column, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a give value to a suitable database value based on type and
|
||||
* return relevant internal statement type.
|
||||
*
|
||||
* @param mixed $value The value to cast.
|
||||
* @param \Cake\Database\TypeInterface|string|int $type The type name or type instance to use.
|
||||
* @return array List containing converted value and internal type.
|
||||
* @phpstan-return array{0:mixed, 1:int}
|
||||
*/
|
||||
protected function cast(mixed $value, TypeInterface|string|int $type = 'string'): array
|
||||
{
|
||||
if (is_string($type)) {
|
||||
$type = TypeFactory::build($type);
|
||||
}
|
||||
if ($type instanceof TypeInterface) {
|
||||
$value = $type->toDatabase($value, $this->_driver);
|
||||
$type = $type->toStatement($value, $this->_driver);
|
||||
}
|
||||
|
||||
return [$value, $type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getBoundParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $column
|
||||
* @param mixed $value
|
||||
* @param int $type
|
||||
* @return void
|
||||
*/
|
||||
protected function performBind(string|int $column, mixed $value, int $type): void
|
||||
{
|
||||
$this->statement->bindValue($column, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
return $this->statement->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetch(string|int $mode = PDO::FETCH_NUM): mixed
|
||||
{
|
||||
$mode = $this->convertMode($mode);
|
||||
$row = $this->statement->fetch($mode);
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->resultDecorators as $decorator) {
|
||||
$row = $decorator($row);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAssoc(): array
|
||||
{
|
||||
return $this->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchColumn(int $position): mixed
|
||||
{
|
||||
$row = $this->fetch(PDO::FETCH_NUM);
|
||||
if ($row && isset($row[$position])) {
|
||||
return $row[$position];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAll(string|int $mode = PDO::FETCH_NUM): array
|
||||
{
|
||||
$mode = $this->convertMode($mode);
|
||||
$rows = $this->statement->fetchAll($mode);
|
||||
|
||||
foreach ($this->resultDecorators as $decorator) {
|
||||
$rows = array_map($decorator, $rows);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts mode name to PDO constant.
|
||||
*
|
||||
* @param string|int $mode Mode name or PDO constant
|
||||
* @return int
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function convertMode(string|int $mode): int
|
||||
{
|
||||
if (is_int($mode)) {
|
||||
// We don't try to validate the PDO constants
|
||||
return $mode;
|
||||
}
|
||||
|
||||
return static::MODE_NAME_MAP[$mode]
|
||||
??
|
||||
throw new InvalidArgumentException("Invalid fetch mode requested. Expected 'assoc', 'num' or 'obj'.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function closeCursor(): void
|
||||
{
|
||||
$this->statement->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
return $this->statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnCount(): int
|
||||
{
|
||||
return $this->statement->columnCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function errorCode(): string
|
||||
{
|
||||
return $this->statement->errorCode() ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function errorInfo(): array
|
||||
{
|
||||
return $this->statement->errorInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function lastInsertId(?string $table = null, ?string $column = null): string|int
|
||||
{
|
||||
if ($column && $this->columnCount()) {
|
||||
$row = $this->fetch(static::FETCH_TYPE_ASSOC);
|
||||
|
||||
if ($row && isset($row[$column])) {
|
||||
return $row[$column];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_driver->lastInsertId($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns prepared query string stored in PDOStatement.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function queryString(): string
|
||||
{
|
||||
return $this->statement->queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner iterator
|
||||
*
|
||||
* @return \Generator
|
||||
*/
|
||||
public function getIterator(): Generator
|
||||
{
|
||||
$this->statement->setFetchMode(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($this->statement as $row) {
|
||||
foreach ($this->resultDecorators as $decorator) {
|
||||
$row = $decorator($row);
|
||||
}
|
||||
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$this->closeCursor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?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;
|
||||
|
||||
use IteratorAggregate;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* @template-extends \IteratorAggregate<array>
|
||||
*/
|
||||
interface StatementInterface extends IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* Maps to PDO::FETCH_NUM.
|
||||
*
|
||||
* @var string
|
||||
* @link https://www.php.net/manual/en/pdo.constants.php
|
||||
*/
|
||||
public const FETCH_TYPE_NUM = 'num';
|
||||
|
||||
/**
|
||||
* Maps to PDO::FETCH_ASSOC.
|
||||
*
|
||||
* @var string
|
||||
* @link https://www.php.net/manual/en/pdo.constants.php
|
||||
*/
|
||||
public const FETCH_TYPE_ASSOC = 'assoc';
|
||||
|
||||
/**
|
||||
* Maps to PDO::FETCH_OBJ.
|
||||
*
|
||||
* @var string
|
||||
* @link https://www.php.net/manual/en/pdo.constants.php
|
||||
*/
|
||||
public const FETCH_TYPE_OBJ = 'obj';
|
||||
|
||||
/**
|
||||
* Assign a value to a positional or named variable in prepared query. If using
|
||||
* positional variables you need to start with index one, if using named params then
|
||||
* just use the name in any order.
|
||||
*
|
||||
* It is not allowed to combine positional and named variables in the same statement.
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* $statement->bindValue(1, 'a title');
|
||||
* $statement->bindValue('active', true, 'boolean');
|
||||
* $statement->bindValue(5, new \DateTime(), 'date');
|
||||
* ```
|
||||
*
|
||||
* @param string|int $column name or param position to be bound
|
||||
* @param mixed $value The value to bind to variable in query
|
||||
* @param string|int|null $type name of configured Type class
|
||||
* @return void
|
||||
*/
|
||||
public function bindValue(string|int $column, mixed $value, string|int|null $type = 'string'): void;
|
||||
|
||||
/**
|
||||
* Closes the cursor, enabling the statement to be executed again.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::closeCursor()`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCursor(): void;
|
||||
|
||||
/**
|
||||
* Returns the number of columns in the result set.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::columnCount()`.
|
||||
*
|
||||
* @return int
|
||||
* @link https://php.net/manual/en/pdostatement.columncount.php
|
||||
*/
|
||||
public function columnCount(): int;
|
||||
|
||||
/**
|
||||
* Fetch the SQLSTATE associated with the last operation on the statement handle.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::errorCode()`.
|
||||
*
|
||||
* @return string
|
||||
* @link https://www.php.net/manual/en/pdostatement.errorcode.php
|
||||
*/
|
||||
public function errorCode(): string;
|
||||
|
||||
/**
|
||||
* Fetch extended error information associated with the last operation on the statement handle.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::errorInfo()`.
|
||||
*
|
||||
* @return array
|
||||
* @link https://www.php.net/manual/en/pdostatement.errorinfo.php
|
||||
*/
|
||||
public function errorInfo(): array;
|
||||
|
||||
/**
|
||||
* Executes the statement by sending the SQL query to the database. It can optionally
|
||||
* take an array or arguments to be bound to the query variables. Please note
|
||||
* that binding parameters from this method will not perform any custom type conversion
|
||||
* as it would normally happen when calling `bindValue`.
|
||||
*
|
||||
* @param array|null $params list of values to be bound to query
|
||||
* @return bool true on success, false otherwise
|
||||
*/
|
||||
public function execute(?array $params = null): bool;
|
||||
|
||||
/**
|
||||
* Fetches the next row from a result set
|
||||
* and converts fields to types based on TypeMap.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::fetch()`.
|
||||
*
|
||||
* @param string|int $mode PDO::FETCH_* constant or fetch mode name.
|
||||
* Valid names are 'assoc', 'num' or 'obj'.
|
||||
* @return mixed
|
||||
* @throws \InvalidArgumentException
|
||||
* @link https://www.php.net/manual/en/pdo.constants.php
|
||||
*/
|
||||
public function fetch(string|int $mode = PDO::FETCH_NUM): mixed;
|
||||
|
||||
/**
|
||||
* Fetches the remaining rows from a result set
|
||||
* and converts fields to types based on TypeMap.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::fetchAll()`.
|
||||
*
|
||||
* @param string|int $mode PDO::FETCH_* constant or fetch mode name.
|
||||
* Valid names are 'assoc', 'num' or 'obj'.
|
||||
* @return array
|
||||
* @throws \InvalidArgumentException
|
||||
* @link https://www.php.net/manual/en/pdo.constants.php
|
||||
*/
|
||||
public function fetchAll(string|int $mode = PDO::FETCH_NUM): array;
|
||||
|
||||
/**
|
||||
* Fetches the next row from a result set using PDO::FETCH_NUM
|
||||
* and converts fields to types based on TypeMap.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::fetch()` except only
|
||||
* a specific column from the row is returned.
|
||||
*
|
||||
* @param int $position Column index in result row.
|
||||
* @return mixed
|
||||
*/
|
||||
public function fetchColumn(int $position): mixed;
|
||||
|
||||
/**
|
||||
* Fetches the next row from a result set using PDO::FETCH_ASSOC
|
||||
* and converts fields to types based on TypeMap.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::fetch()` except an
|
||||
* empty array is returned instead of false.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc(): array;
|
||||
|
||||
/**
|
||||
* Returns the number of rows affected by the last SQL statement.
|
||||
*
|
||||
* This behaves the same as `PDOStatement::rowCount()`.
|
||||
*
|
||||
* @return int
|
||||
* @link https://www.php.net/manual/en/pdostatement.rowcount.php
|
||||
*/
|
||||
public function rowCount(): int;
|
||||
|
||||
/**
|
||||
* Binds a set of values to statement object with corresponding type.
|
||||
*
|
||||
* @param array $params list of values to be bound
|
||||
* @param array $types list of types to be used, keys should match those in $params
|
||||
* @return void
|
||||
*/
|
||||
public function bind(array $params, array $types): void;
|
||||
|
||||
/**
|
||||
* Returns the latest primary inserted using this statement.
|
||||
*
|
||||
* @param string|null $table table name or sequence to get last insert value from
|
||||
* @param string|null $column the name of the column representing the primary key
|
||||
* @return string|int
|
||||
*/
|
||||
public function lastInsertId(?string $table = null, ?string $column = null): string|int;
|
||||
|
||||
/**
|
||||
* Returns prepared query string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function queryString(): string;
|
||||
|
||||
/**
|
||||
* Get the bound params.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getBoundParams(): array;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\TypeInterface;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Base type class.
|
||||
*/
|
||||
abstract class BaseType implements TypeInterface
|
||||
{
|
||||
/**
|
||||
* Identifier name for this type
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected ?string $_name = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|null $name The name identifying this type
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
$this->_name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getBaseType(): ?string
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
if ($value === null) {
|
||||
return PDO::PARAM_NULL;
|
||||
}
|
||||
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function newId(): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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.6.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
|
||||
/**
|
||||
* Denotes type objects capable of converting many values from their original
|
||||
* database representation to php values.
|
||||
*/
|
||||
interface BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* Returns an array of the values converted to the PHP representation of
|
||||
* this type.
|
||||
*
|
||||
* @param array $values The original array of values containing the fields to be casted
|
||||
* @param array<string> $fields The field keys to cast
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\Driver;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Binary type converter.
|
||||
*
|
||||
* Use to convert binary data between PHP and the database types.
|
||||
*/
|
||||
class BinaryType extends BaseType
|
||||
{
|
||||
/**
|
||||
* Convert binary data into the database format.
|
||||
*
|
||||
* Binary data is not altered before being inserted into the database.
|
||||
* As PDO will handle reading file handles.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return resource|string
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary into resource handles
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return resource|null
|
||||
* @throws \Cake\Core\Exception\CakeException
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return fopen('data:text/plain;base64,' . base64_encode($value), 'rb') ?: null;
|
||||
}
|
||||
if (is_resource($value)) {
|
||||
return $value;
|
||||
}
|
||||
throw new CakeException(sprintf('Unable to convert `%s` into binary.', gettype($value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_LOB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals flat data into PHP objects.
|
||||
*
|
||||
* Most useful for converting request data into PHP objects
|
||||
* that make sense for the rest of the ORM/Database layers.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return mixed Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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.6.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Utility\Text;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Binary UUID type converter.
|
||||
*
|
||||
* Use to convert binary uuid data between PHP and the database types.
|
||||
*/
|
||||
class BinaryUuidType extends BaseType
|
||||
{
|
||||
/**
|
||||
* Convert binary uuid data into the database format.
|
||||
*
|
||||
* Binary data is not altered before being inserted into the database.
|
||||
* As PDO will handle reading file handles.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$length = strlen($value);
|
||||
if ($length !== 36 && $length !== 32) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->convertStringToBinaryUuid($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new binary UUID
|
||||
*
|
||||
* @return string A new primary key value.
|
||||
*/
|
||||
public function newId(): string
|
||||
{
|
||||
return Text::uuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary uuid into resource handles
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return resource|string|null
|
||||
* @throws \Cake\Core\Exception\CakeException
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return $this->convertBinaryUuidToString($value);
|
||||
}
|
||||
if (is_resource($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
throw new CakeException(sprintf('Unable to convert %s into binary uuid.', gettype($value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_LOB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals flat data into PHP objects.
|
||||
*
|
||||
* Most useful for converting request data into PHP objects
|
||||
* that make sense for the rest of the ORM/Database layers.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return mixed Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a binary uuid to a string representation
|
||||
*
|
||||
* @param mixed $binary The value to convert.
|
||||
* @return string Converted value.
|
||||
*/
|
||||
protected function convertBinaryUuidToString(mixed $binary): string
|
||||
{
|
||||
$string = unpack('H*', $binary);
|
||||
assert($string !== false, 'Could not unpack uuid');
|
||||
|
||||
/** @var array<int, string> $string */
|
||||
$string = preg_replace(
|
||||
'/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/',
|
||||
'$1-$2-$3-$4-$5',
|
||||
$string,
|
||||
);
|
||||
|
||||
return $string[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string UUID (36 or 32 char) to a binary representation.
|
||||
*
|
||||
* @param string $string The value to convert.
|
||||
* @return string Converted value.
|
||||
*/
|
||||
protected function convertStringToBinaryUuid(string $string): string
|
||||
{
|
||||
$string = str_replace('-', '', $string);
|
||||
|
||||
return pack('H*', $string);
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?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.1.2
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Bool type converter.
|
||||
*
|
||||
* Use to convert bool data between PHP and the database types.
|
||||
*/
|
||||
class BoolType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* Convert bool data into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return bool|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?bool
|
||||
{
|
||||
if (in_array($value, [true, false, null], true)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (in_array($value, [1, 0, '1', '0'], true)) {
|
||||
return (bool)$value;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value `%s` of type `%s` to bool',
|
||||
print_r($value, true),
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bool values to PHP booleans
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return bool|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?bool
|
||||
{
|
||||
if ($value === null || is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return strtolower($value) === 'true';
|
||||
}
|
||||
|
||||
return !empty($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$value = $values[$field] ?? null;
|
||||
if ($value === null || is_bool($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
$values[$field] = strtolower($value) === 'true';
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$field] = !empty($value);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
if ($value === null) {
|
||||
return PDO::PARAM_NULL;
|
||||
}
|
||||
|
||||
return PDO::PARAM_BOOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into PHP booleans.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return bool|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Schema\TableSchemaInterface;
|
||||
|
||||
interface ColumnSchemaAwareInterface
|
||||
{
|
||||
/**
|
||||
* Generate the SQL fragment for a single column in a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchemaInterface $schema The table schema instance the column is in.
|
||||
* @param string $column The name of the column.
|
||||
* @param \Cake\Database\Driver $driver The driver instance being used.
|
||||
* @return string|null An SQL fragment, or `null` in case the column isn't processed by this type.
|
||||
*/
|
||||
public function getColumnSql(TableSchemaInterface $schema, string $column, Driver $driver): ?string;
|
||||
|
||||
/**
|
||||
* Convert a SQL column definition to an abstract type definition.
|
||||
*
|
||||
* @param array $definition The column definition.
|
||||
* @param \Cake\Database\Driver $driver The driver instance being used.
|
||||
* @return array<string, mixed>|null Array of column information, or `null` in case the column isn't processed by this type.
|
||||
*/
|
||||
public function convertColumnDefinition(array $definition, Driver $driver): ?array;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
/**
|
||||
* Extends DateTimeType with support for fractional seconds up to microseconds.
|
||||
*/
|
||||
class DateTimeFractionalType extends DateTimeType
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected string $_format = 'Y-m-d H:i:s.u';
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
/**
|
||||
* Extends DateTimeType with support for time zones.
|
||||
*/
|
||||
class DateTimeTimezoneType extends DateTimeType
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected string $_format = 'Y-m-d H:i:s.uP';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
|
||||
protected array $_marshalFormats = [
|
||||
'Y-m-d H:i',
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d H:i:sP',
|
||||
'Y-m-d H:i:s.u',
|
||||
'Y-m-d H:i:s.uP',
|
||||
'Y-m-d\TH:i',
|
||||
'Y-m-d\TH:i:s',
|
||||
'Y-m-d\TH:i:sP',
|
||||
'Y-m-d\TH:i:s.u',
|
||||
'Y-m-d\TH:i:s.uP',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Chronos\ChronosDate;
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\I18n\DateTime;
|
||||
use DateTime as NativeDateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Datetime type converter.
|
||||
*
|
||||
* Use to convert datetime instances to strings & back.
|
||||
*/
|
||||
class DateTimeType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* The DateTime format used when converting to string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_format = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* The DateTime formats allowed by `marshal()`.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_marshalFormats = [
|
||||
'Y-m-d H:i',
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d H:i:s.u',
|
||||
'Y-m-d\TH:i',
|
||||
'Y-m-d\TH:i:s',
|
||||
'Y-m-d\TH:i:sP',
|
||||
'Y-m-d\TH:i:s.u',
|
||||
'Y-m-d\TH:i:s.uP',
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useLocaleMarshal = false;
|
||||
|
||||
/**
|
||||
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
|
||||
*
|
||||
* See `Cake\I18n\Time::parseDateTime()` for accepted formats.
|
||||
*
|
||||
* @var array|string|int|null
|
||||
*/
|
||||
protected array|string|int|null $_localeMarshalFormat = null;
|
||||
|
||||
/**
|
||||
* The classname to use when creating objects.
|
||||
*
|
||||
* @var class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
|
||||
*/
|
||||
protected string $_className;
|
||||
|
||||
/**
|
||||
* Database time zone.
|
||||
*
|
||||
* @var \DateTimeZone|null
|
||||
*/
|
||||
protected ?DateTimeZone $dbTimezone = null;
|
||||
|
||||
/**
|
||||
* User time zone.
|
||||
*
|
||||
* @var \DateTimeZone|null
|
||||
*/
|
||||
protected ?DateTimeZone $userTimezone = null;
|
||||
|
||||
/**
|
||||
* Default time zone.
|
||||
*
|
||||
* @var \DateTimeZone
|
||||
*/
|
||||
protected DateTimeZone $defaultTimezone;
|
||||
|
||||
/**
|
||||
* Whether database time zone is kept when converting
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $keepDatabaseTimezone = false;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param string|null $name The name identifying this type
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->defaultTimezone = new DateTimeZone(date_default_timezone_get());
|
||||
$this->_className = class_exists(DateTime::class) ? DateTime::class : DateTimeImmutable::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DateTime instance into strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if ($value === null || is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_int($value) || is_float($value)) {
|
||||
$class = $this->_className;
|
||||
$value = new $class('@' . $value);
|
||||
}
|
||||
|
||||
if ($value instanceof ChronosDate) {
|
||||
return $value->format($this->_format);
|
||||
}
|
||||
|
||||
if (!$value instanceof DateTimeInterface) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->dbTimezone !== null
|
||||
&& $this->dbTimezone->getName() !== $value->getTimezone()->getName()
|
||||
) {
|
||||
if (!$value instanceof DateTimeImmutable) {
|
||||
$value = clone $value;
|
||||
}
|
||||
$value = $value->setTimezone($this->dbTimezone);
|
||||
}
|
||||
|
||||
return $value->format($this->_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set database timezone.
|
||||
*
|
||||
* This is the time zone used when converting database strings to DateTime
|
||||
* instances and converting DateTime instances to database strings.
|
||||
*
|
||||
* @see DateTimeType::setKeepDatabaseTimezone
|
||||
* @param \DateTimeZone|string|null $timezone Database timezone.
|
||||
* @return $this
|
||||
*/
|
||||
public function setDatabaseTimezone(DateTimeZone|string|null $timezone)
|
||||
{
|
||||
if (is_string($timezone)) {
|
||||
$timezone = new DateTimeZone($timezone);
|
||||
}
|
||||
$this->dbTimezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user timezone.
|
||||
*
|
||||
* This is the time zone used when marshaling strings to DateTime instances.
|
||||
*
|
||||
* @param \DateTimeZone|string|null $timezone User timezone.
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserTimezone(DateTimeZone|string|null $timezone)
|
||||
{
|
||||
if (is_string($timezone)) {
|
||||
$timezone = new DateTimeZone($timezone);
|
||||
}
|
||||
$this->userTimezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value Value to be converted to PHP equivalent
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
|
||||
* @return \Cake\I18n\DateTime|\DateTimeImmutable|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): DateTime|DateTimeImmutable|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = $this->_className;
|
||||
if (is_numeric($value)) {
|
||||
$instance = new $class('@' . $value);
|
||||
} elseif (str_starts_with($value, '0000-00-00')) {
|
||||
return null;
|
||||
} else {
|
||||
$instance = new $class($value, $this->dbTimezone);
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->keepDatabaseTimezone
|
||||
&& $instance->getTimezone()
|
||||
&& $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
|
||||
) {
|
||||
return $instance->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether DateTime object created from database string is converted
|
||||
* to default time zone.
|
||||
*
|
||||
* If your database date times are in a specific time zone that you want
|
||||
* to keep in the DateTime instance then set this to true.
|
||||
*
|
||||
* When false, datetime timezones are converted to default time zone.
|
||||
* This is default behavior.
|
||||
*
|
||||
* @param bool $keep If true, database time zone is kept when converting
|
||||
* to DateTime instances.
|
||||
* @return $this
|
||||
*/
|
||||
public function setKeepDatabaseTimezone(bool $keep)
|
||||
{
|
||||
$this->keepDatabaseTimezone = $keep;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $values[$field];
|
||||
|
||||
$class = $this->_className;
|
||||
if (is_int($value)) {
|
||||
$instance = new $class('@' . $value);
|
||||
} elseif (str_starts_with($value, '0000-00-00')) {
|
||||
$values[$field] = null;
|
||||
continue;
|
||||
} else {
|
||||
$instance = new $class($value, $this->dbTimezone);
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->keepDatabaseTimezone
|
||||
&& $instance->getTimezone()
|
||||
&& $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
|
||||
) {
|
||||
$instance = $instance->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
$values[$field] = $instance;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert request data into a datetime object.
|
||||
*
|
||||
* @param mixed $value Request data
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function marshal(mixed $value): ?DateTimeInterface
|
||||
{
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
if ($value instanceof NativeDateTime) {
|
||||
$value = clone $value;
|
||||
}
|
||||
|
||||
/** @var \Datetime|\DateTimeImmutable $value */
|
||||
return $value->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
if ($value instanceof ChronosDate) {
|
||||
return $value->toNative();
|
||||
}
|
||||
|
||||
$class = $this->_className;
|
||||
try {
|
||||
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
|
||||
$dateTime = new $class('@' . $value);
|
||||
|
||||
return $dateTime->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($this->_useLocaleMarshal) {
|
||||
$dateTime = $this->_parseLocaleValue($value);
|
||||
} else {
|
||||
$dateTime = $this->_parseValue($value);
|
||||
}
|
||||
|
||||
if ($dateTime) {
|
||||
return $dateTime->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value += [
|
||||
'year' => null, 'month' => null, 'day' => null,
|
||||
'hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0,
|
||||
];
|
||||
if (
|
||||
!is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day']) ||
|
||||
!is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
|
||||
!is_numeric($value['microsecond'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($value['meridian']) && (int)$value['hour'] === 12) {
|
||||
$value['hour'] = 0;
|
||||
}
|
||||
if (isset($value['meridian'])) {
|
||||
$value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
|
||||
}
|
||||
$format = sprintf(
|
||||
'%d-%02d-%02d %02d:%02d:%02d.%06d',
|
||||
$value['year'],
|
||||
$value['month'],
|
||||
$value['day'],
|
||||
$value['hour'],
|
||||
$value['minute'],
|
||||
$value['second'],
|
||||
$value['microsecond'],
|
||||
);
|
||||
|
||||
$dateTime = new $class($format, $value['timezone'] ?? $this->userTimezone);
|
||||
|
||||
return $dateTime->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to parse strings passed to `marshal()` using
|
||||
* the locale-aware format set by `setLocaleFormat()`.
|
||||
*
|
||||
* @param bool $enable Whether to enable
|
||||
* @return $this
|
||||
*/
|
||||
public function useLocaleParser(bool $enable = true)
|
||||
{
|
||||
if ($enable === false) {
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
if (is_a($this->_className, DateTime::class, true)) {
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
throw new DatabaseException(
|
||||
sprintf('Cannot use locale parsing with the %s class', $this->_className),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale-aware format used by `marshal()` when parsing strings.
|
||||
*
|
||||
* See `Cake\I18n\Time::parseDateTime()` for accepted formats.
|
||||
*
|
||||
* @param array|string $format The locale-aware format
|
||||
* @see \Cake\I18n\Time::parseDateTime()
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocaleFormat(array|string $format)
|
||||
{
|
||||
$this->_localeMarshalFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the classname used for building objects.
|
||||
*
|
||||
* @return class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
|
||||
*/
|
||||
public function getDateTimeClassName(): string
|
||||
{
|
||||
return $this->_className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a DateTime object after parsing it using the locale
|
||||
* aware parser with the format set by `setLocaleFormat()`.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an object.
|
||||
* @return \Cake\I18n\DateTime|null
|
||||
*/
|
||||
protected function _parseLocaleValue(string $value): ?DateTime
|
||||
{
|
||||
/** @var class-string<\Cake\I18n\DateTime> $class */
|
||||
$class = $this->_className;
|
||||
|
||||
return $class::parseDateTime($value, $this->_localeMarshalFormat, $this->userTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a DateTime object after parsing it using the
|
||||
* formats in `_marshalFormats`.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an object.
|
||||
* @return \Cake\I18n\DateTime|\DateTimeImmutable|null
|
||||
*/
|
||||
protected function _parseValue(string $value): DateTime|DateTimeImmutable|null
|
||||
{
|
||||
$class = $this->_className;
|
||||
|
||||
foreach ($this->_marshalFormats as $format) {
|
||||
try {
|
||||
$dateTime = $class::createFromFormat($format, $value, $this->userTimezone);
|
||||
// Check for false in case DateTimeImmutable is used
|
||||
if ($dateTime !== false) {
|
||||
return $dateTime;
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
// Chronos wraps DateTimeImmutable::createFromFormat and throws
|
||||
// exception if parse fails.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Chronos\ChronosDate;
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\I18n\Date;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class DateType
|
||||
*/
|
||||
class DateType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $_format = 'Y-m-d';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $_marshalFormats = [
|
||||
'Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useLocaleMarshal = false;
|
||||
|
||||
/**
|
||||
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
|
||||
*
|
||||
* See `Cake\I18n\Date::parseDate()` for accepted formats.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
protected string|int|null $_localeMarshalFormat = null;
|
||||
|
||||
/**
|
||||
* The classname to use when creating objects.
|
||||
*
|
||||
* @var class-string<\Cake\Chronos\ChronosDate>
|
||||
*/
|
||||
protected string $_className;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->_className = class_exists(Date::class) ? Date::class : ChronosDate::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DateTime instance into strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if ($value === null || is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
$class = $this->_className;
|
||||
$value = new $class('@' . $value);
|
||||
}
|
||||
|
||||
assert(is_object($value) && method_exists($value, 'format'));
|
||||
|
||||
return $value->format($this->_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value Value to be converted to PHP equivalent
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
|
||||
* @return \Cake\Chronos\ChronosDate|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?ChronosDate
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = $this->_className;
|
||||
if (is_int($value)) {
|
||||
$instance = new $class('@' . $value);
|
||||
} elseif (str_starts_with($value, '0000-00-00')) {
|
||||
return null;
|
||||
} else {
|
||||
$instance = new $class($value);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $values[$field];
|
||||
|
||||
$class = $this->_className;
|
||||
if (is_int($value)) {
|
||||
$instance = new $class('@' . $value);
|
||||
} elseif (str_starts_with($value, '0000-00-00')) {
|
||||
$values[$field] = null;
|
||||
continue;
|
||||
} else {
|
||||
$instance = new $class($value);
|
||||
}
|
||||
|
||||
$values[$field] = $instance;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert request data into a datetime object.
|
||||
*
|
||||
* @param mixed $value Request data
|
||||
* @return \Cake\Chronos\ChronosDate|null
|
||||
*/
|
||||
public function marshal(mixed $value): ?ChronosDate
|
||||
{
|
||||
if ($value instanceof $this->_className) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface || $value instanceof ChronosDate) {
|
||||
return new $this->_className($value->format($this->_format));
|
||||
}
|
||||
|
||||
$class = $this->_className;
|
||||
try {
|
||||
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
|
||||
return new $class('@' . $value);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($this->_useLocaleMarshal) {
|
||||
return $this->_parseLocaleValue($value);
|
||||
}
|
||||
|
||||
return $this->_parseValue($value);
|
||||
}
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!is_array($value) ||
|
||||
!isset($value['year'], $value['month'], $value['day']) ||
|
||||
!is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$format = sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
|
||||
|
||||
return new $class($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to parse strings passed to `marshal()` using
|
||||
* the locale-aware format set by `setLocaleFormat()`.
|
||||
*
|
||||
* @param bool $enable Whether to enable
|
||||
* @return $this
|
||||
*/
|
||||
public function useLocaleParser(bool $enable = true)
|
||||
{
|
||||
if ($enable === false) {
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
if (is_a($this->_className, Date::class, true)) {
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
throw new DatabaseException(
|
||||
sprintf('Cannot use locale parsing with %s', $this->_className),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale-aware format used by `marshal()` when parsing strings.
|
||||
*
|
||||
* See `Cake\I18n\Date::parseDate()` for accepted formats.
|
||||
*
|
||||
* @param string|int $format The locale-aware format
|
||||
* @see \Cake\I18n\Date::parseDate()
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocaleFormat(string|int $format)
|
||||
{
|
||||
$this->_localeMarshalFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the classname used for building objects.
|
||||
*
|
||||
* @return class-string<\Cake\Chronos\ChronosDate>
|
||||
*/
|
||||
public function getDateClassName(): string
|
||||
{
|
||||
return $this->_className;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return \Cake\I18n\Date|null
|
||||
*/
|
||||
protected function _parseLocaleValue(string $value): ?Date
|
||||
{
|
||||
/** @var class-string<\Cake\I18n\Date> $class */
|
||||
$class = $this->_className;
|
||||
|
||||
return $class::parseDate($value, $this->_localeMarshalFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a DateTime object after parsing it using the
|
||||
* formats in `_marshalFormats`.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an object.
|
||||
* @return \Cake\Chronos\ChronosDate|null
|
||||
*/
|
||||
protected function _parseValue(string $value): ?ChronosDate
|
||||
{
|
||||
$class = $this->_className;
|
||||
foreach ($this->_marshalFormats as $format) {
|
||||
try {
|
||||
return $class::createFromFormat($format, $value);
|
||||
} catch (InvalidArgumentException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
<?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.3.4
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\I18n\Number;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Decimal type converter.
|
||||
*
|
||||
* Use to convert decimal data between PHP and the database types.
|
||||
*/
|
||||
class DecimalType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* The class to use for representing number objects
|
||||
*
|
||||
* @var class-string<\Cake\I18n\Number>|string
|
||||
*/
|
||||
public static string $numberClass = Number::class;
|
||||
|
||||
/**
|
||||
* Whether numbers should be parsed using a locale aware parser
|
||||
* when marshaling string inputs.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useLocaleParser = false;
|
||||
|
||||
/**
|
||||
* Convert decimal strings into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|float|int|null
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): string|float|int|null
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof Stringable) {
|
||||
$str = (string)$value;
|
||||
|
||||
if (is_numeric($str)) {
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value `%s` of type `%s` to a decimal',
|
||||
print_r($value, true),
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$field] = (string)$values[$field];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshalls request data into decimal strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return string|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value) && $this->_useLocaleParser) {
|
||||
return $this->_parseValue($value);
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to parse numbers passed to the marshal() function
|
||||
* by using a locale aware parser.
|
||||
*
|
||||
* @param bool $enable Whether to enable
|
||||
* @return $this
|
||||
* @throws \Cake\Database\Exception\DatabaseException
|
||||
*/
|
||||
public function useLocaleParser(bool $enable = true)
|
||||
{
|
||||
if ($enable === false) {
|
||||
$this->_useLocaleParser = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
if (
|
||||
static::$numberClass === Number::class ||
|
||||
is_subclass_of(static::$numberClass, Number::class)
|
||||
) {
|
||||
$this->_useLocaleParser = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
throw new DatabaseException(
|
||||
sprintf('Cannot use locale parsing with the %s class', static::$numberClass),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts localized string into a decimal string after parsing it using
|
||||
* the locale aware parser.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an float.
|
||||
* @return string
|
||||
*/
|
||||
protected function _parseValue(string $value): string
|
||||
{
|
||||
$class = static::$numberClass;
|
||||
|
||||
return (string)$class::parseFloat($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.0.3
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
/**
|
||||
* An interface used to clarify that an enum has a label() method instead of having to use
|
||||
* `name` property.
|
||||
*/
|
||||
interface EnumLabelInterface
|
||||
{
|
||||
/**
|
||||
* Label to return as string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function label(): string;
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 5.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use BackedEnum;
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\TypeFactory;
|
||||
use Cake\Utility\Text;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use ReflectionEnum;
|
||||
use ReflectionException;
|
||||
use TypeError;
|
||||
use ValueError;
|
||||
|
||||
/**
|
||||
* Enum type converter.
|
||||
*
|
||||
* Use to convert string data between PHP and the database types.
|
||||
*/
|
||||
class EnumType extends BaseType
|
||||
{
|
||||
/**
|
||||
* The type of the enum which is either string or int
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $backingType;
|
||||
|
||||
/**
|
||||
* The enum classname which is associated to the type instance
|
||||
*
|
||||
* @var class-string<\BackedEnum>
|
||||
*/
|
||||
protected string $enumClassName;
|
||||
|
||||
/**
|
||||
* @param string $name The name identifying this type
|
||||
* @param class-string<\BackedEnum> $enumClassName The associated enum class name
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $enumClassName,
|
||||
) {
|
||||
parent::__construct($name);
|
||||
$this->enumClassName = $enumClassName;
|
||||
|
||||
try {
|
||||
$reflectionEnum = new ReflectionEnum($enumClassName);
|
||||
} catch (ReflectionException $e) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Unable to use `%s` for type `%s`. %s.',
|
||||
$enumClassName,
|
||||
$name,
|
||||
$e->getMessage(),
|
||||
));
|
||||
}
|
||||
|
||||
$namedType = $reflectionEnum->getBackingType();
|
||||
if ($namedType == null) {
|
||||
throw new DatabaseException(
|
||||
sprintf('Unable to use enum `%s` for type `%s`, must be a backed enum.', $enumClassName, $name),
|
||||
);
|
||||
}
|
||||
|
||||
$this->backingType = (string)$namedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert enum instances into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|int|null
|
||||
* @throws \InvalidArgumentException When the given value is not a valid value for the associated enum
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): string|int|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $this->enumClassName) {
|
||||
return $value->value;
|
||||
}
|
||||
|
||||
if ($this->backingType === 'int' && is_string($value)) {
|
||||
$intVal = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($intVal !== false) {
|
||||
$value = $intVal;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->enumClassName::from($value)->value;
|
||||
} catch (ValueError | TypeError $exception) {
|
||||
if ($exception instanceof TypeError) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Given value `%s` of type `%s` does not match associated `%s` backed enum in `%s`',
|
||||
print_r($value, true),
|
||||
get_debug_type($value),
|
||||
$this->backingType,
|
||||
$this->enumClassName,
|
||||
));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'`%s` is not a valid value for `%s`',
|
||||
$value,
|
||||
$this->enumClassName,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform DB value to backed enum instance
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return \BackedEnum|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?BackedEnum
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->backingType === 'int' && is_string($value)) {
|
||||
$intVal = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($intVal !== false) {
|
||||
$value = $intVal;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->enumClassName::from($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
if ($this->backingType === 'int') {
|
||||
return PDO::PARAM_INT;
|
||||
}
|
||||
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return \BackedEnum|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?BackedEnum
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $this->enumClassName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($this->backingType === 'int') {
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->enumClassName::from($value);
|
||||
} catch (ValueError | TypeError) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an `EnumType` that is paired with the provided `$enumClassName`.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* ```
|
||||
* // In a table class
|
||||
* $this->getSchema()->setColumnType('status', EnumType::from(StatusEnum::class));
|
||||
* ```
|
||||
*
|
||||
* @param class-string<\BackedEnum> $enumClassName The enum class name
|
||||
* @return string
|
||||
*/
|
||||
public static function from(string $enumClassName): string
|
||||
{
|
||||
$typeName = 'enum-' . strtolower(Text::slug($enumClassName));
|
||||
$instance = new EnumType($typeName, $enumClassName);
|
||||
TypeFactory::set($typeName, $instance);
|
||||
|
||||
return $typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<\BackedEnum>
|
||||
*/
|
||||
public function getEnumClassName(): string
|
||||
{
|
||||
return $this->enumClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\TypeFactory;
|
||||
|
||||
/**
|
||||
* Offers a method to convert values to ExpressionInterface objects
|
||||
* if the type they should be converted to implements ExpressionTypeInterface
|
||||
*/
|
||||
trait ExpressionTypeCasterTrait
|
||||
{
|
||||
/**
|
||||
* Conditionally converts the passed value to an ExpressionInterface object
|
||||
* if the type class implements the ExpressionTypeInterface. Otherwise,
|
||||
* returns the value unmodified.
|
||||
*
|
||||
* @param mixed $value The value to convert to ExpressionInterface
|
||||
* @param string|null $type The type name
|
||||
* @return mixed
|
||||
*/
|
||||
protected function _castToExpression(mixed $value, ?string $type = null): mixed
|
||||
{
|
||||
if ($type === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$baseType = str_replace('[]', '', $type);
|
||||
$converter = TypeFactory::build($baseType);
|
||||
|
||||
if (!$converter instanceof ExpressionTypeInterface) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$multi = $type !== $baseType;
|
||||
|
||||
if ($multi) {
|
||||
/** @var \Cake\Database\Type\ExpressionTypeInterface&\Cake\Database\TypeInterface $converter */
|
||||
return array_map($converter->toExpression(...), $value);
|
||||
}
|
||||
|
||||
return $converter->toExpression($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the types that require values to
|
||||
* be casted to expressions, out of the list of type names
|
||||
* passed as parameter.
|
||||
*
|
||||
* @param array $types List of type names
|
||||
* @return array
|
||||
*/
|
||||
protected function _requiresToExpressionCasting(array $types): array
|
||||
{
|
||||
$result = [];
|
||||
$types = array_filter($types);
|
||||
foreach ($types as $k => $type) {
|
||||
$object = TypeFactory::build($type);
|
||||
if ($object instanceof ExpressionTypeInterface) {
|
||||
$result[$k] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
|
||||
/**
|
||||
* An interface used by Type objects to signal whether the value should
|
||||
* be converted to an ExpressionInterface instead of a string when sent
|
||||
* to the database.
|
||||
*/
|
||||
interface ExpressionTypeInterface
|
||||
{
|
||||
/**
|
||||
* Returns an ExpressionInterface object for the given value that can
|
||||
* be used in queries.
|
||||
*
|
||||
* @param mixed $value The value to be converted to an expression
|
||||
* @return \Cake\Database\ExpressionInterface
|
||||
*/
|
||||
public function toExpression(mixed $value): ExpressionInterface;
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\I18n\Number;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Float type converter.
|
||||
*
|
||||
* Use to convert float/decimal data between PHP and the database types.
|
||||
*/
|
||||
class FloatType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* The class to use for representing number objects
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static string $numberClass = Number::class;
|
||||
|
||||
/**
|
||||
* Whether numbers should be parsed using a locale aware parser
|
||||
* when marshaling string inputs.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useLocaleParser = false;
|
||||
|
||||
/**
|
||||
* Convert integer data into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return float|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return float|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?float
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$field] = (float)$values[$field];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into PHP floats.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return string|float|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): string|float|null
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value) && $this->_useLocaleParser) {
|
||||
return $this->_parseValue($value);
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (float)$value;
|
||||
}
|
||||
if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to parse numbers passed to the marshal() function
|
||||
* by using a locale aware parser.
|
||||
*
|
||||
* @param bool $enable Whether to enable
|
||||
* @return $this
|
||||
*/
|
||||
public function useLocaleParser(bool $enable = true)
|
||||
{
|
||||
if ($enable === false) {
|
||||
$this->_useLocaleParser = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
if (
|
||||
static::$numberClass === Number::class ||
|
||||
is_subclass_of(static::$numberClass, Number::class)
|
||||
) {
|
||||
$this->_useLocaleParser = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
throw new DatabaseException(
|
||||
sprintf('Cannot use locale parsing with the %s class', static::$numberClass),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a float point after parsing it using the locale
|
||||
* aware parser.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an float.
|
||||
* @return float
|
||||
*/
|
||||
protected function _parseValue(string $value): float
|
||||
{
|
||||
$class = static::$numberClass;
|
||||
|
||||
return $class::parseFloat($value);
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Integer type converter.
|
||||
*
|
||||
* Use to convert integer data between PHP and the database types.
|
||||
*/
|
||||
class IntegerType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* Checks if the value is not a numeric value
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @param mixed $value Value to check
|
||||
* @return void
|
||||
*/
|
||||
protected function checkNumeric(mixed $value): void
|
||||
{
|
||||
if (!is_numeric($value) && !is_bool($value)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value `%s` of type `%s` to int',
|
||||
print_r($value, true),
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert integer data into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return int|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->checkNumeric($value);
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return int|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?int
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkNumeric($values[$field]);
|
||||
|
||||
$values[$field] = (int)$values[$field];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_INT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into PHP integers.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return int|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?int
|
||||
{
|
||||
if ($value === '' || !is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?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.3.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* JSON type converter.
|
||||
*
|
||||
* Used to convert JSON data between PHP and the database types.
|
||||
*/
|
||||
class JsonType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $_encodingOptions = 0;
|
||||
|
||||
/**
|
||||
* Flags for json_decode()
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $_decodingOptions = JSON_OBJECT_AS_ARRAY;
|
||||
|
||||
/**
|
||||
* Convert a value data into a JSON string
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if (is_resource($value)) {
|
||||
throw new InvalidArgumentException('Cannot convert a resource value to JSON');
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_encode($value, JSON_THROW_ON_ERROR | $this->_encodingOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($value, flags: $this->_decodingOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$field] = json_decode($values[$field], flags: $this->_decodingOptions);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into a JSON compatible structure.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return mixed Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set json_encode options.
|
||||
*
|
||||
* @param int $options Encoding flags. Use JSON_* flags. Set `0` to reset.
|
||||
* @return $this
|
||||
* @see https://www.php.net/manual/en/function.json-encode.php
|
||||
*/
|
||||
public function setEncodingOptions(int $options)
|
||||
{
|
||||
$this->_encodingOptions = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set json_decode() options.
|
||||
*
|
||||
* By default, the value is `JSON_OBJECT_AS_ARRAY`.
|
||||
*
|
||||
* @param int $options Decoding flags. Use JSON_* flags. Set `0` to reset.
|
||||
* @return $this
|
||||
*/
|
||||
public function setDecodingOptions(int $options)
|
||||
{
|
||||
$this->_decodingOptions = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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.2.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
/**
|
||||
* An interface used by Type objects to signal whether the casting
|
||||
* is actually required.
|
||||
*/
|
||||
interface OptionalConvertInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether the cast to PHP is required to be invoked, since
|
||||
* it is not a identity function.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function requiresToPhpCast(): bool;
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?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.1.2
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* String type converter.
|
||||
*
|
||||
* Use to convert string data between PHP and the database types.
|
||||
*/
|
||||
class StringType extends BaseType implements OptionalConvertInterface
|
||||
{
|
||||
/**
|
||||
* Convert string data into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if ($value === null || is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof Stringable) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value `%s` of type `%s` to string',
|
||||
print_r($value, true),
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string values to PHP strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into PHP strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return string|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return bool False as database results are returned already as strings
|
||||
*/
|
||||
public function requiresToPhpCast(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Chronos\ChronosTime;
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\Driver;
|
||||
use Cake\I18n\Time;
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Time type converter.
|
||||
*
|
||||
* Use to convert time instances to strings and back.
|
||||
*/
|
||||
class TimeType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* The PHP Time format used when converting to string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_format = 'H:i:s';
|
||||
|
||||
/**
|
||||
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $_useLocaleMarshal = false;
|
||||
|
||||
/**
|
||||
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
|
||||
*
|
||||
* See `Cake\I18n\Time::parseTime()` for accepted formats.
|
||||
*
|
||||
* @var string|int|null
|
||||
*/
|
||||
protected string|int|null $_localeMarshalFormat = null;
|
||||
|
||||
/**
|
||||
* The classname to use when creating objects.
|
||||
*
|
||||
* @var class-string<\Cake\Chronos\ChronosTime>
|
||||
*/
|
||||
protected string $_className;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|null $name The name identifying this type.
|
||||
* @param class-string<\Cake\Chronos\ChronosTime>|null $className Class name for time representation.
|
||||
*/
|
||||
public function __construct(?string $name = null, ?string $className = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
if ($className === null) {
|
||||
$className = class_exists(Time::class) ? Time::class : ChronosTime::class;
|
||||
}
|
||||
|
||||
$this->_className = $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert request data into a datetime object.
|
||||
*
|
||||
* @param mixed $value Request data
|
||||
* @return \Cake\Chronos\ChronosTime|null
|
||||
*/
|
||||
public function marshal(mixed $value): ?ChronosTime
|
||||
{
|
||||
if ($value instanceof $this->_className) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface || $value instanceof ChronosTime) {
|
||||
return new $this->_className($value->format($this->_format));
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($this->_useLocaleMarshal) {
|
||||
return $this->_parseLocalTimeValue($value);
|
||||
}
|
||||
|
||||
return $this->_parseTimeValue($value);
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value += ['hour' => null, 'minute' => null, 'second' => 0, 'microsecond' => 0];
|
||||
if (
|
||||
!is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
|
||||
!is_numeric($value['microsecond'])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($value['meridian']) && (int)$value['hour'] === 12) {
|
||||
$value['hour'] = 0;
|
||||
}
|
||||
if (isset($value['meridian'])) {
|
||||
$value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
|
||||
}
|
||||
$format = sprintf(
|
||||
'%02d:%02d:%02d.%06d',
|
||||
$value['hour'],
|
||||
$value['minute'],
|
||||
$value['second'],
|
||||
$value['microsecond'],
|
||||
);
|
||||
|
||||
return new $this->_className($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, Driver $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $values[$field];
|
||||
$instance = new $this->_className($value);
|
||||
$values[$field] = $instance;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time data into the database time format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): mixed
|
||||
{
|
||||
if ($value === null || is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
assert(method_exists($value, 'format'));
|
||||
|
||||
return $value->format($this->_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time values to PHP time instances
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\Driver $driver The driver instance to convert with.
|
||||
* @return \Cake\Chronos\ChronosTime|null
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): ?ChronosTime
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new $this->_className($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the classname used for building objects.
|
||||
*
|
||||
* @return class-string<\Cake\Chronos\ChronosTime>
|
||||
*/
|
||||
public function getTimeClassName(): string
|
||||
{
|
||||
return $this->_className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a Time object
|
||||
*
|
||||
* @param string $value The value to parse and convert to an object.
|
||||
* @return \Cake\Chronos\ChronosTime|null
|
||||
*/
|
||||
protected function _parseTimeValue(string $value): ?ChronosTime
|
||||
{
|
||||
try {
|
||||
return $this->_className::parse($value);
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string into a Time object after parsing it using the locale
|
||||
* aware parser with the format set by `setLocaleFormat()`.
|
||||
*
|
||||
* @param string $value The value to parse and convert to an object.
|
||||
* @return \Cake\Chronos\ChronosTime|null
|
||||
*/
|
||||
protected function _parseLocalTimeValue(string $value): ?ChronosTime
|
||||
{
|
||||
assert(is_a($this->_className, Time::class, true));
|
||||
|
||||
return $this->_className::parseTime($value, $this->_localeMarshalFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to parse strings passed to `marshal()` using
|
||||
* the locale-aware format set by `setLocaleFormat()`.
|
||||
*
|
||||
* @param bool $enable Whether to enable
|
||||
* @return $this
|
||||
*/
|
||||
public function useLocaleParser(bool $enable = true)
|
||||
{
|
||||
if (
|
||||
$enable &&
|
||||
($this->_className !== Time::class && !is_subclass_of($this->_className, Time::class))
|
||||
) {
|
||||
throw new CakeException('You must install the `cakephp/i18n` package to use locale aware parsing.');
|
||||
}
|
||||
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the locale-aware format used by `marshal()` when parsing strings.
|
||||
*
|
||||
* See `Cake\I18n\Time::parseTime()` for accepted formats.
|
||||
*
|
||||
* @param string|int|null $format The locale-aware format
|
||||
* @see \Cake\I18n\Time::parseTime()
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocaleFormat(string|int|null $format)
|
||||
{
|
||||
$this->_localeMarshalFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Type;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Utility\Text;
|
||||
|
||||
/**
|
||||
* Provides behavior for the UUID type
|
||||
*/
|
||||
class UuidType extends StringType
|
||||
{
|
||||
/**
|
||||
* Casts given value from a PHP type to one acceptable by database
|
||||
*
|
||||
* @param mixed $value value to be converted to database equivalent
|
||||
* @param \Cake\Database\Driver $driver object from which database preferences and configuration will be extracted
|
||||
* @return string|null
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): ?string
|
||||
{
|
||||
if (in_array($value, [null, '', false], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::toDatabase($value, $driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new UUID
|
||||
*
|
||||
* @return string A new primary key value.
|
||||
*/
|
||||
public function newId(): string
|
||||
{
|
||||
return Text::uuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals request data into a PHP string
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return string|null Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '' || is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<?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.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
/**
|
||||
* Factory for building database type classes.
|
||||
*/
|
||||
class TypeFactory
|
||||
{
|
||||
/**
|
||||
* List of supported database types. A human-readable
|
||||
* identifier is used as key and a complete namespaced class name as value
|
||||
* representing the class that will do actual type conversions.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @phpstan-var array<string, class-string<\Cake\Database\TypeInterface>>
|
||||
*/
|
||||
protected static array $_types = [
|
||||
'biginteger' => Type\IntegerType::class,
|
||||
'binary' => Type\BinaryType::class,
|
||||
'binaryuuid' => Type\BinaryUuidType::class,
|
||||
'boolean' => Type\BoolType::class,
|
||||
'char' => Type\StringType::class,
|
||||
'cidr' => Type\StringType::class,
|
||||
'citext' => Type\StringType::class,
|
||||
'date' => Type\DateType::class,
|
||||
'datetime' => Type\DateTimeType::class,
|
||||
'datetimefractional' => Type\DateTimeFractionalType::class,
|
||||
'decimal' => Type\DecimalType::class,
|
||||
'float' => Type\FloatType::class,
|
||||
'geometry' => Type\StringType::class,
|
||||
'integer' => Type\IntegerType::class,
|
||||
'inet' => Type\StringType::class,
|
||||
'json' => Type\JsonType::class,
|
||||
'linestring' => Type\StringType::class,
|
||||
'macaddr' => Type\StringType::class,
|
||||
'nativeuuid' => Type\UuidType::class,
|
||||
'point' => Type\StringType::class,
|
||||
'polygon' => Type\StringType::class,
|
||||
'smallinteger' => Type\IntegerType::class,
|
||||
'string' => Type\StringType::class,
|
||||
'text' => Type\StringType::class,
|
||||
'time' => Type\TimeType::class,
|
||||
'timestamp' => Type\DateTimeType::class,
|
||||
'timestampfractional' => Type\DateTimeFractionalType::class,
|
||||
'timestamptimezone' => Type\DateTimeTimezoneType::class,
|
||||
'tinyinteger' => Type\IntegerType::class,
|
||||
'uuid' => Type\UuidType::class,
|
||||
'year' => Type\IntegerType::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Contains a map of type object instances to be reused if needed.
|
||||
*
|
||||
* @var array<\Cake\Database\TypeInterface>
|
||||
*/
|
||||
protected static array $_builtTypes = [];
|
||||
|
||||
/**
|
||||
* Returns a Type object capable of converting a type identified by name.
|
||||
*
|
||||
* @param string $name type identifier
|
||||
* @return \Cake\Database\TypeInterface
|
||||
*/
|
||||
public static function build(string $name): TypeInterface
|
||||
{
|
||||
if (isset(static::$_builtTypes[$name])) {
|
||||
return static::$_builtTypes[$name];
|
||||
}
|
||||
if (!isset(static::$_types[$name])) {
|
||||
return static::$_builtTypes[$name] = new static::$_types['string']($name);
|
||||
}
|
||||
|
||||
return static::$_builtTypes[$name] = new static::$_types[$name]($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an arrays with all the mapped type objects, indexed by name.
|
||||
*
|
||||
* @return array<\Cake\Database\TypeInterface>
|
||||
*/
|
||||
public static function buildAll(): array
|
||||
{
|
||||
foreach (static::$_types as $name => $type) {
|
||||
static::$_builtTypes[$name] ??= static::build($name);
|
||||
}
|
||||
|
||||
return static::$_builtTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set TypeInterface instance capable of converting a type identified by $name
|
||||
*
|
||||
* @param string $name The type identifier you want to set.
|
||||
* @param \Cake\Database\TypeInterface $instance The type instance you want to set.
|
||||
* @return void
|
||||
*/
|
||||
public static function set(string $name, TypeInterface $instance): void
|
||||
{
|
||||
static::$_builtTypes[$name] = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new type identifier and maps it to a fully namespaced classname.
|
||||
*
|
||||
* @param string $type Name of type to map.
|
||||
* @param string $className The classname to register.
|
||||
* @return void
|
||||
* @phpstan-param class-string<\Cake\Database\TypeInterface> $className
|
||||
*/
|
||||
public static function map(string $type, string $className): void
|
||||
{
|
||||
static::$_types[$type] = $className;
|
||||
unset(static::$_builtTypes[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set type to classname mapping.
|
||||
*
|
||||
* @param array<string, string> $map List of types to be mapped.
|
||||
* @return void
|
||||
* @phpstan-param array<string, class-string<\Cake\Database\TypeInterface>> $map
|
||||
*/
|
||||
public static function setMap(array $map): void
|
||||
{
|
||||
static::$_types = $map;
|
||||
static::$_builtTypes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type mapping array.
|
||||
*
|
||||
* Deprecated 5.3.0: Argument $type has been deprecated.
|
||||
* Use getMap() without arguments to get the full map, or getMapped($type) to get a specific type mapping.
|
||||
*
|
||||
* @param string|null $type Type name to get mapped class for or null to get map array.
|
||||
* @return array<string, class-string<\Cake\Database\TypeInterface>>|string|null Configured class name for given $type or map array.
|
||||
*/
|
||||
public static function getMap(?string $type = null): array|string|null
|
||||
{
|
||||
if ($type === null) {
|
||||
return static::$_types;
|
||||
}
|
||||
|
||||
trigger_error(
|
||||
'Calling getMap() with a type argument is deprecated. Use getMapped() instead.',
|
||||
E_USER_DEPRECATED,
|
||||
);
|
||||
|
||||
return static::$_types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mapped class name for a specific type.
|
||||
*
|
||||
* @param string $type Type name to get mapped class for.
|
||||
* @return string|null Configured class name for given $type or null if not found.
|
||||
* @phpstan-return class-string<\Cake\Database\TypeInterface>|null
|
||||
*/
|
||||
public static function getMapped(string $type): ?string
|
||||
{
|
||||
return static::$_types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears out all created instances and mapped types classes, useful for testing
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clear(): void
|
||||
{
|
||||
static::$_types = [];
|
||||
static::$_builtTypes = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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.2.14
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database;
|
||||
|
||||
/**
|
||||
* Encapsulates all conversion functions for values coming from a database into PHP and
|
||||
* going from PHP into a database.
|
||||
*/
|
||||
interface TypeInterface
|
||||
{
|
||||
/**
|
||||
* Casts given value from a PHP type to one acceptable by a database.
|
||||
*
|
||||
* @param mixed $value Value to be converted to a database equivalent.
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
|
||||
* @return mixed Given PHP type casted to one acceptable by a database.
|
||||
*/
|
||||
public function toDatabase(mixed $value, Driver $driver): mixed;
|
||||
|
||||
/**
|
||||
* Casts given value from a database type to a PHP equivalent.
|
||||
*
|
||||
* @param mixed $value Value to be converted to PHP equivalent
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
|
||||
* @return mixed Given value casted from a database to a PHP equivalent.
|
||||
*/
|
||||
public function toPHP(mixed $value, Driver $driver): mixed;
|
||||
|
||||
/**
|
||||
* Get the binding type to use in a PDO statement.
|
||||
*
|
||||
* @param mixed $value The value being bound.
|
||||
* @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted.
|
||||
* @return int One of PDO::PARAM_* constants.
|
||||
*/
|
||||
public function toStatement(mixed $value, Driver $driver): int;
|
||||
|
||||
/**
|
||||
* Marshals flat data into PHP objects.
|
||||
*
|
||||
* Most useful for converting request data into PHP objects,
|
||||
* that make sense for the rest of the ORM/Database layers.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @return mixed Converted value.
|
||||
*/
|
||||
public function marshal(mixed $value): mixed;
|
||||
|
||||
/**
|
||||
* Returns the base type name that this class is inheriting.
|
||||
*
|
||||
* This is useful when extending base type for adding extra functionality,
|
||||
* but still want the rest of the framework to use the same assumptions it would
|
||||
* do about the base type it inherits from.
|
||||
*
|
||||
* @return string|null The base type name that this class is inheriting.
|
||||
*/
|
||||
public function getBaseType(): ?string;
|
||||
|
||||
/**
|
||||
* Returns type identifier name for this object.
|
||||
*
|
||||
* @return string|null The type identifier name for this object.
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
|
||||
/**
|
||||
* Generate a new primary key value for a given type.
|
||||
*
|
||||
* This method can be used by types to create new primary key values
|
||||
* when entities are inserted.
|
||||
*
|
||||
* @return mixed A new primary key value.
|
||||
* @see \Cake\Database\Type\UuidType
|
||||
*/
|
||||
public function newId(): mixed;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user