Composer Version Change
This commit is contained in:
+1197
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
<?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.
|
||||
*
|
||||
* If an implementation of `Cake\Datasource\FixtureInterface` also implements
|
||||
* this interface, the FixtureManager will use these methods to manage
|
||||
* a fixtures constraints.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
Vendored
+559
@@ -0,0 +1,559 @@
|
||||
<?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\Core\App;
|
||||
use Cake\Core\Retry\CommandRetry;
|
||||
use Cake\Database\Exception\MissingConnectionException;
|
||||
use Cake\Database\Retry\ErrorCodeWaitStrategy;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\TableSchema;
|
||||
use Cake\Database\Statement\PDOStatement;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Represents a database driver containing all specificities for
|
||||
* a database engine including its SQL dialect.
|
||||
*/
|
||||
abstract class Driver implements DriverInterface
|
||||
{
|
||||
/**
|
||||
* @var int|null Maximum alias length or null if no limit
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = null;
|
||||
|
||||
/**
|
||||
* @var array<int> DB-specific error codes that allow connect retry
|
||||
*/
|
||||
protected const RETRY_ERROR_CODES = [];
|
||||
|
||||
/**
|
||||
* Instance of PDO.
|
||||
*
|
||||
* @var \PDO
|
||||
*/
|
||||
protected $_connection;
|
||||
|
||||
/**
|
||||
* Configuration data.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_config;
|
||||
|
||||
/**
|
||||
* Base configuration that is merged into the user
|
||||
* supplied configuration data.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_baseConfig = [];
|
||||
|
||||
/**
|
||||
* Indicates whether the driver is doing automatic identifier quoting
|
||||
* for all queries
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_autoQuoting = false;
|
||||
|
||||
/**
|
||||
* The server version
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $_version;
|
||||
|
||||
/**
|
||||
* The last number of connection retry attempts.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $connectRetries = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array<string, mixed> $config The configuration for the driver.
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
if (empty($config['username']) && !empty($config['login'])) {
|
||||
throw new InvalidArgumentException(
|
||||
'Please pass "username" instead of "login" for connecting to the database'
|
||||
);
|
||||
}
|
||||
$config += $this->_baseConfig;
|
||||
$this->_config = $config;
|
||||
if (!empty($config['quoteIdentifiers'])) {
|
||||
$this->enableAutoQuoting();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration data used to create the driver.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function config(): array
|
||||
{
|
||||
return $this->_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server
|
||||
*
|
||||
* @param string $dsn A Driver-specific PDO-DSN
|
||||
* @param array<string, mixed> $config configuration to be used for creating connection
|
||||
* @return bool true on success
|
||||
*/
|
||||
protected function _connect(string $dsn, array $config): bool
|
||||
{
|
||||
$action = function () use ($dsn, $config) {
|
||||
$this->setConnection(new PDO(
|
||||
$dsn,
|
||||
$config['username'] ?: null,
|
||||
$config['password'] ?: null,
|
||||
$config['flags']
|
||||
));
|
||||
};
|
||||
|
||||
$retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
|
||||
try {
|
||||
$retry->run($action);
|
||||
} catch (PDOException $e) {
|
||||
throw new MissingConnectionException(
|
||||
[
|
||||
'driver' => App::shortName(static::class, 'Database/Driver'),
|
||||
'reason' => $e->getMessage(),
|
||||
],
|
||||
null,
|
||||
$e
|
||||
);
|
||||
} finally {
|
||||
$this->connectRetries = $retry->getRetries();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
abstract public function connect(): bool;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
|
||||
$this->_connection = null;
|
||||
$this->_version = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns connected server version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function version(): string
|
||||
{
|
||||
if ($this->_version === null) {
|
||||
$this->connect();
|
||||
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
return $this->_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the internal PDO connection instance.
|
||||
*
|
||||
* @return \PDO
|
||||
*/
|
||||
public function getConnection()
|
||||
{
|
||||
if ($this->_connection === null) {
|
||||
throw new MissingConnectionException([
|
||||
'driver' => App::shortName(static::class, 'Database/Driver'),
|
||||
'reason' => 'Unknown',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->_connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal PDO connection instance.
|
||||
*
|
||||
* @param \PDO $connection PDO instance.
|
||||
* @return $this
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function setConnection($connection)
|
||||
{
|
||||
$this->_connection = $connection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
abstract public function enabled(): bool;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function prepare($query): StatementInterface
|
||||
{
|
||||
$this->connect();
|
||||
$statement = $this->_connection->prepare($query instanceof Query ? $query->sql() : $query);
|
||||
|
||||
return new PDOStatement($statement, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function beginTransaction(): bool
|
||||
{
|
||||
$this->connect();
|
||||
if ($this->_connection->inTransaction()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->_connection->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function commitTransaction(): bool
|
||||
{
|
||||
$this->connect();
|
||||
if (!$this->_connection->inTransaction()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->_connection->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rollbackTransaction(): bool
|
||||
{
|
||||
$this->connect();
|
||||
if (!$this->_connection->inTransaction()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->_connection->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a transaction is active for connection.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction(): bool
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
return $this->_connection->inTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsSavePoints(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_SAVEPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server supports common table expressions.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
|
||||
*/
|
||||
public function supportsCTEs(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_CTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function quote($value, $type = PDO::PARAM_STR): string
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
return $this->_connection->quote((string)$value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the driver supports quoting, as PDO_ODBC does not support it.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
|
||||
*/
|
||||
public function supportsQuoting(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_QUOTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
abstract public function queryTranslator(string $type): Closure;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
abstract public function schemaDialect(): SchemaDialect;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
abstract public function quoteIdentifier(string $identifier): string;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaValue($value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if ($value === false) {
|
||||
return 'FALSE';
|
||||
}
|
||||
if ($value === true) {
|
||||
return 'TRUE';
|
||||
}
|
||||
if (is_float($value)) {
|
||||
return str_replace(',', '.', (string)$value);
|
||||
}
|
||||
/** @psalm-suppress InvalidArgument */
|
||||
if (
|
||||
(
|
||||
is_int($value) ||
|
||||
$value === '0'
|
||||
) ||
|
||||
(
|
||||
is_numeric($value) &&
|
||||
strpos($value, ',') === false &&
|
||||
substr($value, 0, 1) !== '0' &&
|
||||
strpos($value, 'e') === false
|
||||
)
|
||||
) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return $this->_connection->quote((string)$value, PDO::PARAM_STR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schema(): string
|
||||
{
|
||||
return $this->_config['schema'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function lastInsertId(?string $table = null, ?string $column = null)
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
if ($this->_connection instanceof PDO) {
|
||||
return $this->_connection->lastInsertId($table);
|
||||
}
|
||||
|
||||
return $this->_connection->lastInsertId($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isConnected(): bool
|
||||
{
|
||||
if ($this->_connection === null) {
|
||||
$connected = false;
|
||||
} else {
|
||||
try {
|
||||
$connected = (bool)$this->_connection->query('SELECT 1');
|
||||
} catch (PDOException $e) {
|
||||
$connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableAutoQuoting(bool $enable = true)
|
||||
{
|
||||
$this->_autoQuoting = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disableAutoQuoting()
|
||||
{
|
||||
$this->_autoQuoting = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isAutoQuotingEnabled(): bool
|
||||
{
|
||||
return $this->_autoQuoting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the driver supports the feature.
|
||||
*
|
||||
* Defaults to true for FEATURE_QUOTE and FEATURE_SAVEPOINT.
|
||||
*
|
||||
* @param string $feature Driver feature name
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(string $feature): bool
|
||||
{
|
||||
switch ($feature) {
|
||||
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
|
||||
case static::FEATURE_QUOTE:
|
||||
case static::FEATURE_SAVEPOINT:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function compileQuery(Query $query, ValueBinder $binder): array
|
||||
{
|
||||
$processor = $this->newCompiler();
|
||||
$translator = $this->queryTranslator($query->type());
|
||||
$query = $translator($query);
|
||||
|
||||
return [$query, $processor->compile($query, $binder)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new QueryCompiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function newTableSchema(string $table, array $columns = []): TableSchema
|
||||
{
|
||||
$className = TableSchema::class;
|
||||
if (isset($this->_config['tableSchema'])) {
|
||||
/** @var class-string<\Cake\Database\Schema\TableSchema> $className */
|
||||
$className = $this->_config['tableSchema'];
|
||||
}
|
||||
|
||||
return new $className($table, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum alias length allowed.
|
||||
* This can be different from the maximum identifier length for columns.
|
||||
*
|
||||
* @return int|null Maximum alias length or null if no limit
|
||||
*/
|
||||
public function getMaxAliasLength(): ?int
|
||||
{
|
||||
return static::MAX_ALIAS_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of connection retry attempts made.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getConnectRetries(): int
|
||||
{
|
||||
return $this->connectRetries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection role this driver performs.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRole(): string
|
||||
{
|
||||
return $this->_config['_role'] ?? Connection::ROLE_WRITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
/** @psalm-suppress PossiblyNullPropertyAssignmentValue */
|
||||
$this->_connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array that can be used to describe the internal state of this
|
||||
* object.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return [
|
||||
'connected' => $this->_connection !== null,
|
||||
'role' => $this->getRole(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
<?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\Query;
|
||||
use Cake\Database\Schema\MysqlSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Statement\MysqlStatement;
|
||||
use Cake\Database\StatementInterface;
|
||||
use PDO;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* MySQL Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
use SqlDialectTrait;
|
||||
|
||||
/**
|
||||
* @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 $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'port' => '3306',
|
||||
'flags' => [],
|
||||
'encoding' => 'utf8mb4',
|
||||
'timezone' => null,
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* The schema dialect for this driver
|
||||
*
|
||||
* @var \Cake\Database\Schema\MysqlSchemaDialect|null
|
||||
*/
|
||||
protected $_schemaDialect;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_startQuote = '`';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_endQuote = '`';
|
||||
|
||||
/**
|
||||
* Server type.
|
||||
*
|
||||
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
|
||||
* after `version()` method is called.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $serverType = self::SERVER_TYPE_MYSQL;
|
||||
|
||||
/**
|
||||
* Mapping of feature to db server version for feature availability checks.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
protected $featureVersions = [
|
||||
'mysql' => [
|
||||
'json' => '5.7.0',
|
||||
'cte' => '8.0.0',
|
||||
'window' => '8.0.0',
|
||||
],
|
||||
'mariadb' => [
|
||||
'json' => '10.2.7',
|
||||
'cte' => '10.2.1',
|
||||
'window' => '10.2.0',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->_connection) {
|
||||
return true;
|
||||
}
|
||||
$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'],
|
||||
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
|
||||
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
|
||||
$config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key'];
|
||||
$config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert'];
|
||||
}
|
||||
if (!empty($config['ssl_ca'])) {
|
||||
$config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $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->_connect($dsn, $config);
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
$connection = $this->getConnection();
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$connection->exec($command);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a sql statement to be executed
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query The query to prepare.
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function prepare($query): StatementInterface
|
||||
{
|
||||
$this->connect();
|
||||
$isObject = $query instanceof Query;
|
||||
/**
|
||||
* @psalm-suppress PossiblyInvalidMethodCall
|
||||
* @psalm-suppress PossiblyInvalidArgument
|
||||
*/
|
||||
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
|
||||
$result = new MysqlStatement($statement, $this);
|
||||
/** @psalm-suppress PossiblyInvalidMethodCall */
|
||||
if ($isObject && $query->isBufferedResultsEnabled() === false) {
|
||||
$result->bufferResults(false);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
if ($this->_schemaDialect === null) {
|
||||
$this->_schemaDialect = new MysqlSchemaDialect($this);
|
||||
}
|
||||
|
||||
return $this->_schemaDialect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schema(): string
|
||||
{
|
||||
return $this->_config['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
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(string $feature): bool
|
||||
{
|
||||
switch ($feature) {
|
||||
case static::FEATURE_CTE:
|
||||
case static::FEATURE_JSON:
|
||||
case static::FEATURE_WINDOW:
|
||||
return version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$this->serverType][$feature],
|
||||
'>='
|
||||
);
|
||||
}
|
||||
|
||||
return parent::supports($feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsDynamicConstraints(): bool
|
||||
{
|
||||
return 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->connect();
|
||||
$this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
|
||||
if (strpos($this->_version, 'MariaDB') !== false) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server supports common table expressions.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
|
||||
*/
|
||||
public function supportsCTEs(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_CTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server supports native JSON columns
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_JSON)` instead
|
||||
*/
|
||||
public function supportsNativeJson(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the connected server supports window functions.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
|
||||
*/
|
||||
public function supportsWindowFunctions(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_WINDOW);
|
||||
}
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
<?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\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\StringExpression;
|
||||
use Cake\Database\PostgresCompiler;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\PostgresSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Class Postgres
|
||||
*/
|
||||
class Postgres extends Driver
|
||||
{
|
||||
use SqlDialectTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 63;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Postgres driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'schema' => 'public',
|
||||
'port' => 5432,
|
||||
'encoding' => 'utf8',
|
||||
'timezone' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* The schema dialect class for this driver
|
||||
*
|
||||
* @var \Cake\Database\Schema\PostgresSchemaDialect|null
|
||||
*/
|
||||
protected $_schemaDialect;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->_connection) {
|
||||
return true;
|
||||
}
|
||||
$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']}";
|
||||
}
|
||||
|
||||
$this->_connect($dsn, $config);
|
||||
$this->_connection = $connection = $this->getConnection();
|
||||
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', $connection->quote($config['timezone']));
|
||||
}
|
||||
|
||||
foreach ($config['init'] as $command) {
|
||||
$connection->exec($command);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if ($this->_schemaDialect === null) {
|
||||
$this->_schemaDialect = new PostgresSchemaDialect($this);
|
||||
}
|
||||
|
||||
return $this->_schemaDialect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets connection encoding
|
||||
*
|
||||
* @param string $encoding The encoding to use.
|
||||
* @return void
|
||||
*/
|
||||
public function setEncoding(string $encoding): void
|
||||
{
|
||||
$this->connect();
|
||||
$this->_connection->exec('SET NAMES ' . $this->_connection->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
|
||||
{
|
||||
$this->connect();
|
||||
$this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL DEFERRED';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL IMMEDIATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(string $feature): bool
|
||||
{
|
||||
switch ($feature) {
|
||||
case static::FEATURE_CTE:
|
||||
case static::FEATURE_JSON:
|
||||
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
|
||||
case static::FEATURE_WINDOW:
|
||||
return true;
|
||||
|
||||
case static::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION:
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::supports($feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsDynamicConstraints(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _transformDistinct(Query $query): Query
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _insertQueryTranslator(Query $query): Query
|
||||
{
|
||||
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 tranform.
|
||||
* @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) {
|
||||
$p = 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes string expression into postgresql format.
|
||||
*
|
||||
* @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform.
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
<?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\ComparisonExpression;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\IdentifierQuoter;
|
||||
use Cake\Database\Query;
|
||||
use Closure;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Sql dialect trait
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait SqlDialectTrait
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a callable function that will be used to transform a passed Query object.
|
||||
* This function, in turn, will return an instance of a Query object that has been
|
||||
* transformed to accommodate any specificities of the SQL dialect in use.
|
||||
*
|
||||
* @param string $type the type of query to be transformed
|
||||
* (select, insert, update, delete)
|
||||
* @return \Closure
|
||||
*/
|
||||
public function queryTranslator(string $type): Closure
|
||||
{
|
||||
return function ($query) use ($type) {
|
||||
if ($this->isAutoQuotingEnabled()) {
|
||||
$query = (new IdentifierQuoter($this))->quote($query);
|
||||
}
|
||||
|
||||
/** @var \Cake\ORM\Query $query */
|
||||
$query = $this->{'_' . $type . 'QueryTranslator'}($query);
|
||||
$translators = $this->_expressionTranslators();
|
||||
if (!$translators) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$query->traverseExpressions(function ($expression) use ($translators, $query): void {
|
||||
foreach ($translators as $class => $method) {
|
||||
if ($expression instanceof $class) {
|
||||
$this->{$method}($expression, $query);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $query;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array of methods that will transform Expression
|
||||
* objects to conform with the specific SQL dialect. Keys are class names
|
||||
* and values a method in this class.
|
||||
*
|
||||
* @psalm-return array<class-string, string>
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply translation steps to select queries.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to translate
|
||||
* @return \Cake\Database\Query The modified query
|
||||
*/
|
||||
protected function _selectQueryTranslator(Query $query): Query
|
||||
{
|
||||
return $this->_transformDistinct($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the passed query after rewriting the DISTINCT clause, so that drivers
|
||||
* that do not support the "ON" part can provide the actual way it should be done
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to be transformed
|
||||
* @return \Cake\Database\Query
|
||||
*/
|
||||
protected function _transformDistinct(Query $query): Query
|
||||
{
|
||||
if (is_array($query->clause('distinct'))) {
|
||||
$query->group($query->clause('distinct'), true);
|
||||
$query->distinct(false);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply translation steps to delete queries.
|
||||
*
|
||||
* Chops out aliases on delete query conditions as most database dialects do not
|
||||
* support aliases in delete queries. This also removes aliases
|
||||
* in table names as they frequently don't work either.
|
||||
*
|
||||
* We are intentionally not supporting deletes with joins as they have even poorer support.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to translate
|
||||
* @return \Cake\Database\Query The modified query
|
||||
*/
|
||||
protected function _deleteQueryTranslator(Query $query): Query
|
||||
{
|
||||
$hadAlias = false;
|
||||
$tables = [];
|
||||
foreach ($query->clause('from') as $alias => $table) {
|
||||
if (is_string($alias)) {
|
||||
$hadAlias = true;
|
||||
}
|
||||
$tables[] = $table;
|
||||
}
|
||||
if ($hadAlias) {
|
||||
$query->from($tables, true);
|
||||
}
|
||||
|
||||
if (!$hadAlias) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
return $this->_removeAliasesFromConditions($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply translation steps to update queries.
|
||||
*
|
||||
* Chops out aliases on update query conditions as not all database dialects do support
|
||||
* aliases in update queries.
|
||||
*
|
||||
* Just like for delete queries, joins are currently not supported for update queries.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to translate
|
||||
* @return \Cake\Database\Query The modified query
|
||||
*/
|
||||
protected function _updateQueryTranslator(Query $query): Query
|
||||
{
|
||||
return $this->_removeAliasesFromConditions($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes aliases from the `WHERE` clause of a query.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to process.
|
||||
* @return \Cake\Database\Query The modified query.
|
||||
* @throws \RuntimeException In case the processed query contains any joins, as removing
|
||||
* aliases from the conditions can break references to the joined tables.
|
||||
*/
|
||||
protected function _removeAliasesFromConditions(Query $query): Query
|
||||
{
|
||||
if ($query->clause('join')) {
|
||||
throw new RuntimeException(
|
||||
'Aliases are being removed from conditions for UPDATE/DELETE queries, ' .
|
||||
'this can break references to joined tables.'
|
||||
);
|
||||
}
|
||||
|
||||
$conditions = $query->clause('where');
|
||||
if ($conditions) {
|
||||
$conditions->traverse(function ($expression) {
|
||||
if ($expression instanceof ComparisonExpression) {
|
||||
$field = $expression->getField();
|
||||
if (
|
||||
is_string($field) &&
|
||||
strpos($field, '.') !== false
|
||||
) {
|
||||
[, $unaliasedField] = explode('.', $field, 2);
|
||||
$expression->setField($unaliasedField);
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
if ($expression instanceof IdentifierExpression) {
|
||||
$identifier = $expression->getIdentifier();
|
||||
if (strpos($identifier, '.') !== false) {
|
||||
[, $unaliasedIdentifier] = explode('.', $identifier, 2);
|
||||
$expression->setIdentifier($unaliasedIdentifier);
|
||||
}
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
return $expression;
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply translation steps to insert queries.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to translate
|
||||
* @return \Cake\Database\Query The modified query
|
||||
*/
|
||||
protected function _insertQueryTranslator(Query $query): Query
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL snippet for creating a new transaction savepoint
|
||||
*
|
||||
* @param string|int $name save point name
|
||||
* @return string
|
||||
*/
|
||||
public function savePointSQL($name): string
|
||||
{
|
||||
return 'SAVEPOINT LEVEL' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL snippet for releasing a previously created save point
|
||||
*
|
||||
* @param string|int $name save point name
|
||||
* @return string
|
||||
*/
|
||||
public function releaseSavePointSQL($name): string
|
||||
{
|
||||
return 'RELEASE SAVEPOINT LEVEL' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL snippet for rollbacking a previously created save point
|
||||
*
|
||||
* @param string|int $name save point name
|
||||
* @return string
|
||||
*/
|
||||
public function rollbackSavePointSQL($name): string
|
||||
{
|
||||
return 'ROLLBACK TO SAVEPOINT LEVEL' . $name;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Driver\SqlDialectTrait',
|
||||
'Cake\Database\SqlDialectTrait'
|
||||
);
|
||||
// phpcs:enable
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
<?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\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\SqliteSchemaDialect;
|
||||
use Cake\Database\SqliteCompiler;
|
||||
use Cake\Database\Statement\PDOStatement;
|
||||
use Cake\Database\Statement\SqliteStatement;
|
||||
use Cake\Database\StatementInterface;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Class Sqlite
|
||||
*/
|
||||
class Sqlite extends Driver
|
||||
{
|
||||
use SqlDialectTrait;
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlite driver
|
||||
*
|
||||
* - `mask` The mask used for created database
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_baseConfig = [
|
||||
'persistent' => false,
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
'database' => ':memory:',
|
||||
'encoding' => 'utf8',
|
||||
'mask' => 0644,
|
||||
'cache' => null,
|
||||
'mode' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* The schema dialect class for this driver
|
||||
*
|
||||
* @var \Cake\Database\Schema\SqliteSchemaDialect|null
|
||||
*/
|
||||
protected $_schemaDialect;
|
||||
|
||||
/**
|
||||
* Whether the connected server supports window functions.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $_supportsWindowFunctions;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* Mapping of date parts.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $_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 $featureVersions = [
|
||||
'cte' => '3.8.3',
|
||||
'window' => '3.28.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server
|
||||
*
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->_connection) {
|
||||
return true;
|
||||
}
|
||||
$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) {
|
||||
if (PHP_VERSION_ID < 80100) {
|
||||
throw new RuntimeException('SQLite URI support requires PHP 8.1.');
|
||||
}
|
||||
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
|
||||
} else {
|
||||
$dsn = 'sqlite:' . $config['database'];
|
||||
}
|
||||
|
||||
$this->_connect($dsn, $config);
|
||||
if ($chmodFile) {
|
||||
// phpcs:disable
|
||||
@chmod($config['database'], $config['mask']);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->getConnection()->exec($command);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a sql statement to be executed
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query The query to prepare.
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function prepare($query): StatementInterface
|
||||
{
|
||||
$this->connect();
|
||||
$isObject = $query instanceof Query;
|
||||
/**
|
||||
* @psalm-suppress PossiblyInvalidMethodCall
|
||||
* @psalm-suppress PossiblyInvalidArgument
|
||||
*/
|
||||
$statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
|
||||
$result = new SqliteStatement(new PDOStatement($statement, $this), $this);
|
||||
/** @psalm-suppress PossiblyInvalidMethodCall */
|
||||
if ($isObject && $query->isBufferedResultsEnabled() === false) {
|
||||
$result->bufferResults(false);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = ON';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(string $feature): bool
|
||||
{
|
||||
switch ($feature) {
|
||||
case static::FEATURE_CTE:
|
||||
case static::FEATURE_WINDOW:
|
||||
return version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$feature],
|
||||
'>='
|
||||
);
|
||||
|
||||
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::supports($feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsDynamicConstraints(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
if ($this->_schemaDialect === null) {
|
||||
$this->_schemaDialect = new SqliteSchemaDialect($this);
|
||||
}
|
||||
|
||||
return $this->_schemaDialect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new SqliteCompiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
$p = ['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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server supports common table expressions.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_CTE)` instead
|
||||
*/
|
||||
public function supportsCTEs(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_CTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the connected server supports window functions.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_WINDOW)` instead
|
||||
*/
|
||||
public function supportsWindowFunctions(): bool
|
||||
{
|
||||
deprecationWarning('Feature support checks are now implemented by `supports()` with FEATURE_* constants.');
|
||||
|
||||
return $this->supports(static::FEATURE_WINDOW);
|
||||
}
|
||||
}
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
<?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\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\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 SqlDialectTrait;
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 128;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const RETRY_ERROR_CODES = [
|
||||
40613, // Azure Sql Database paused
|
||||
];
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlserver driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_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,
|
||||
];
|
||||
|
||||
/**
|
||||
* The schema dialect class for this driver
|
||||
*
|
||||
* @var \Cake\Database\Schema\SqlserverSchemaDialect|null
|
||||
*/
|
||||
protected $_schemaDialect;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_startQuote = '[';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_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 bool true on success
|
||||
*/
|
||||
public function connect(): bool
|
||||
{
|
||||
if ($this->_connection) {
|
||||
return true;
|
||||
}
|
||||
$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']}";
|
||||
}
|
||||
$this->_connect($dsn, $config);
|
||||
|
||||
$connection = $this->getConnection();
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$connection->exec($command);
|
||||
}
|
||||
}
|
||||
if (!empty($config['settings']) && is_array($config['settings'])) {
|
||||
foreach ($config['settings'] as $key => $value) {
|
||||
$connection->exec("SET {$key} {$value}");
|
||||
}
|
||||
}
|
||||
if (!empty($config['attributes']) && is_array($config['attributes'])) {
|
||||
foreach ($config['attributes'] as $key => $value) {
|
||||
$connection->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a sql statement to be executed
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query The query to prepare.
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function prepare($query): StatementInterface
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$sql = $query;
|
||||
$options = [
|
||||
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
|
||||
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
|
||||
];
|
||||
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->isBufferedResultsEnabled()) {
|
||||
$options = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyInvalidArgument */
|
||||
$statement = $this->_connection->prepare($sql, $options);
|
||||
|
||||
return new SqlserverStatement($statement, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(string $feature): bool
|
||||
{
|
||||
switch ($feature) {
|
||||
case static::FEATURE_CTE:
|
||||
case static::FEATURE_TRUNCATE_WITH_CONSTRAINTS:
|
||||
case static::FEATURE_WINDOW:
|
||||
return true;
|
||||
|
||||
case static::FEATURE_QUOTE:
|
||||
$this->connect();
|
||||
|
||||
return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc';
|
||||
}
|
||||
|
||||
return parent::supports($feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supportsDynamicConstraints(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
if ($this->_schemaDialect === null) {
|
||||
$this->_schemaDialect = new SqlserverSchemaDialect($this);
|
||||
}
|
||||
|
||||
return $this->_schemaDialect;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return \Cake\Database\SqlserverCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new SqlserverCompiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _selectQueryTranslator(Query $query): Query
|
||||
{
|
||||
$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->order($query->newExpr()->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 $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 Modified query object.
|
||||
*/
|
||||
protected function _pagingSubquery(Query $original, ?int $limit, ?int $offset): Query
|
||||
{
|
||||
$field = '_cake_paging_._cake_page_rownum_';
|
||||
|
||||
if ($original->clause('order')) {
|
||||
// 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();
|
||||
$original
|
||||
->clause('order')
|
||||
->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)
|
||||
->order([], true);
|
||||
|
||||
$outer = new Query($query->getConnection());
|
||||
$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(Query $query): Query
|
||||
{
|
||||
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 ($q) use ($distinct, $order) {
|
||||
$over = $q->newExpr('ROW_NUMBER() OVER')
|
||||
->add('(PARTITION BY')
|
||||
->add($q->newExpr()->add($distinct)->setConjunction(','))
|
||||
->add($order)
|
||||
->add(')')
|
||||
->setConjunction(' ');
|
||||
|
||||
return [
|
||||
'_cake_distinct_pivot_' => $over,
|
||||
];
|
||||
})
|
||||
->limit(null)
|
||||
->offset(null)
|
||||
->order([], true);
|
||||
|
||||
$outer = new Query($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':
|
||||
/** @var bool $hasDay */
|
||||
$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,113 @@
|
||||
<?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 RuntimeException;
|
||||
|
||||
/**
|
||||
* 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 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 RuntimeException(
|
||||
sprintf(
|
||||
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
|
||||
$operator
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$value = $expression->getValue();
|
||||
$true = new QueryExpression('1');
|
||||
|
||||
if ($value instanceof Query) {
|
||||
$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($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('=');
|
||||
}
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
<?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\SchemaDialect;
|
||||
use Cake\Database\Schema\TableSchema;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Interface for database driver.
|
||||
*
|
||||
* @method int|null getMaxAliasLength() Returns the maximum alias length allowed.
|
||||
* @method int getConnectRetries() Returns the number of connection retry attempts made.
|
||||
* @method bool supports(string $feature) Checks whether a feature is supported by the driver.
|
||||
* @method bool inTransaction() Returns whether a transaction is active.
|
||||
* @method array config() Get the configuration data used to create the driver.
|
||||
* @method string getRole() Returns the connection role this driver prforms.
|
||||
*/
|
||||
interface DriverInterface
|
||||
{
|
||||
/**
|
||||
* Common Table Expressions (with clause) support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_CTE = 'cte';
|
||||
|
||||
/**
|
||||
* Disabling constraints without being in transaction support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION = 'disable-constraint-without-transaction';
|
||||
|
||||
/**
|
||||
* Native JSON data type support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_JSON = 'json';
|
||||
|
||||
/**
|
||||
* PDO::quote() support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_QUOTE = 'quote';
|
||||
|
||||
/**
|
||||
* Transaction savepoint support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_SAVEPOINT = 'savepoint';
|
||||
|
||||
/**
|
||||
* Truncate with foreign keys attached support.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_TRUNCATE_WITH_CONSTRAINTS = 'truncate-with-constraints';
|
||||
|
||||
/**
|
||||
* Window function support (all or partial clauses).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FEATURE_WINDOW = 'window';
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server.
|
||||
*
|
||||
* @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public function connect(): bool;
|
||||
|
||||
/**
|
||||
* Disconnects from database server.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Returns correct connection resource or object that is internally used.
|
||||
*
|
||||
* @return object Connection object used internally.
|
||||
*/
|
||||
public function getConnection();
|
||||
|
||||
/**
|
||||
* Set the internal connection object.
|
||||
*
|
||||
* @param object $connection The connection instance.
|
||||
* @return $this
|
||||
*/
|
||||
public function setConnection($connection);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Prepares a sql statement to be executed.
|
||||
*
|
||||
* @param \Cake\Database\Query|string $query The query to turn into a prepared statement.
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function prepare($query): StatementInterface;
|
||||
|
||||
/**
|
||||
* Starts a transaction.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function beginTransaction(): bool;
|
||||
|
||||
/**
|
||||
* Commits a transaction.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function commitTransaction(): bool;
|
||||
|
||||
/**
|
||||
* Rollbacks a transaction.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function rollbackTransaction(): bool;
|
||||
|
||||
/**
|
||||
* Get the SQL for releasing a save point.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return string
|
||||
*/
|
||||
public function releaseSavePointSQL($name): string;
|
||||
|
||||
/**
|
||||
* Get the SQL for creating a save point.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return string
|
||||
*/
|
||||
public function savePointSQL($name): string;
|
||||
|
||||
/**
|
||||
* Get the SQL for rollingback a save point.
|
||||
*
|
||||
* @param string|int $name Save point name or id
|
||||
* @return string
|
||||
*/
|
||||
public function rollbackSavePointSQL($name): string;
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string;
|
||||
|
||||
/**
|
||||
* Get the SQL for enabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function enableForeignKeySQL(): string;
|
||||
|
||||
/**
|
||||
* Returns whether the driver supports adding or dropping constraints
|
||||
* to already created tables.
|
||||
*
|
||||
* @return bool True if driver supports dynamic constraints.
|
||||
* @deprecated 4.3.0 Fixtures no longer dynamically drop and create constraints.
|
||||
*/
|
||||
public function supportsDynamicConstraints(): bool;
|
||||
|
||||
/**
|
||||
* Returns whether this driver supports save points for nested transactions.
|
||||
*
|
||||
* @return bool True if save points are supported, false otherwise.
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_SAVEPOINT)` instead
|
||||
*/
|
||||
public function supportsSavePoints(): bool;
|
||||
|
||||
/**
|
||||
* Returns a value in a safe representation to be used in a query string
|
||||
*
|
||||
* @param mixed $value The value to quote.
|
||||
* @param int $type Must be one of the \PDO::PARAM_* constants
|
||||
* @return string
|
||||
*/
|
||||
public function quote($value, $type): string;
|
||||
|
||||
/**
|
||||
* Checks if the driver supports quoting.
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 4.3.0 Use `supports(DriverInterface::FEATURE_QUOTE)` instead
|
||||
*/
|
||||
public function supportsQuoting(): bool;
|
||||
|
||||
/**
|
||||
* Returns a callable function that will be used to transform a passed Query object.
|
||||
* This function, in turn, will return an instance of a Query object that has been
|
||||
* transformed to accommodate any specificities of the SQL dialect in use.
|
||||
*
|
||||
* @param string $type The type of query to be transformed
|
||||
* (select, insert, update, delete).
|
||||
* @return \Closure
|
||||
*/
|
||||
public function queryTranslator(string $type): Closure;
|
||||
|
||||
/**
|
||||
* Get the schema dialect.
|
||||
*
|
||||
* Used by {@link \Cake\Database\Schema} package to reflect schema and
|
||||
* generate schema.
|
||||
*
|
||||
* If all the tables that use this Driver specify their
|
||||
* own schemas, then this may return null.
|
||||
*
|
||||
* @return \Cake\Database\Schema\SchemaDialect
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect;
|
||||
|
||||
/**
|
||||
* 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 expression to quote.
|
||||
* @return string
|
||||
*/
|
||||
public function quoteIdentifier(string $identifier): string;
|
||||
|
||||
/**
|
||||
* Escapes values for use in schema definitions.
|
||||
*
|
||||
* @param mixed $value The value to escape.
|
||||
* @return string String for use in schema definitions.
|
||||
*/
|
||||
public function schemaValue($value): string;
|
||||
|
||||
/**
|
||||
* Returns the schema name that's being used.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function schema(): string;
|
||||
|
||||
/**
|
||||
* Returns last id generated for a table or sequence in database.
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Checks whether the driver is connected.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected(): bool;
|
||||
|
||||
/**
|
||||
* Sets whether this driver should automatically quote identifiers
|
||||
* in queries.
|
||||
*
|
||||
* @param bool $enable Whether to enable auto quoting
|
||||
* @return $this
|
||||
*/
|
||||
public function enableAutoQuoting(bool $enable = true);
|
||||
|
||||
/**
|
||||
* Disable auto quoting of identifiers in queries.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disableAutoQuoting();
|
||||
|
||||
/**
|
||||
* Returns whether this driver should automatically quote identifiers
|
||||
* in queries.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutoQuotingEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Transforms the passed query to this Driver's dialect and returns an instance
|
||||
* of the transformed query and the full compiled SQL string.
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to compile.
|
||||
* @param \Cake\Database\ValueBinder $binder The value binder to use.
|
||||
* @return array containing 2 entries. The first entity is the transformed query
|
||||
* and the second one the compiled SQL.
|
||||
*/
|
||||
public function compileQuery(Query $query, ValueBinder $binder): array;
|
||||
|
||||
/**
|
||||
* Returns an instance of a QueryCompiler.
|
||||
*
|
||||
* @return \Cake\Database\QueryCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler;
|
||||
|
||||
/**
|
||||
* Constructs new TableSchema.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param array $columns The list of columns for the schema.
|
||||
* @return \Cake\Database\Schema\TableSchema
|
||||
*/
|
||||
public function newTableSchema(string $table, array $columns = []): TableSchema;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.2.0: Cake\Database\Exception is deprecated. ' .
|
||||
'Use Cake\Database\Exception\DatabaseException instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Exception\DatabaseException');
|
||||
@@ -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.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 $_messageTemplate = '%s';
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Exception\DatabaseException',
|
||||
'Cake\Database\Exception'
|
||||
);
|
||||
// phpcs:enable
|
||||
@@ -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 $_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 $_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 $_messageTemplate = 'Database driver %s cannot be used due to a missing PHP extension or unmet dependency. Requested by connection "%s"';
|
||||
}
|
||||
@@ -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.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 meesage 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)
|
||||
{
|
||||
if ($message === null) {
|
||||
$message = 'Cannot commit transaction - rollback() has been already called in the nested transaction';
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?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\ValueBinder;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected $filter;
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Expression\WindowExpression
|
||||
*/
|
||||
protected $window;
|
||||
|
||||
/**
|
||||
* 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($conditions, array $types = [])
|
||||
{
|
||||
if ($this->filter === null) {
|
||||
$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 epression.
|
||||
*
|
||||
* @param string|null $name Window name
|
||||
* @return $this
|
||||
*/
|
||||
public function over(?string $name = null)
|
||||
{
|
||||
if ($this->window === null) {
|
||||
$this->window = new WindowExpression();
|
||||
}
|
||||
if ($name) {
|
||||
// Set name manually in case this was chained from FunctionsBuilder wrapper
|
||||
$this->window->name($name);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function partition($partitions)
|
||||
{
|
||||
$this->over();
|
||||
$this->window->partition($partitions);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function order($fields)
|
||||
{
|
||||
$this->over();
|
||||
$this->window->order($fields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function range($start, $end = 0)
|
||||
{
|
||||
$this->over();
|
||||
$this->window->range($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rows(?int $start, ?int $end = 0)
|
||||
{
|
||||
$this->over();
|
||||
$this->window->rows($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function groups(?int $start, ?int $end = 0)
|
||||
{
|
||||
$this->over();
|
||||
$this->window->groups($start, $end);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function frame(
|
||||
string $type,
|
||||
$startOffset,
|
||||
string $startDirection,
|
||||
$endOffset,
|
||||
string $endDirection
|
||||
) {
|
||||
$this->over();
|
||||
$this->window->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeCurrent()
|
||||
{
|
||||
$this->over();
|
||||
$this->window->excludeCurrent();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeGroup()
|
||||
{
|
||||
$this->over();
|
||||
$this->window->excludeGroup();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function excludeTies()
|
||||
{
|
||||
$this->over();
|
||||
$this->window->excludeTies();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = $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 $_from;
|
||||
|
||||
/**
|
||||
* The second value in the expression
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $_to;
|
||||
|
||||
/**
|
||||
* The data type for the from and to arguments
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $_type;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field name to compare for values inbetween 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($field, $from, $to, $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,
|
||||
];
|
||||
|
||||
/** @var \Cake\Database\ExpressionInterface|string $field */
|
||||
$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);
|
||||
}
|
||||
|
||||
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 $type The type of $value
|
||||
* @return string generated placeholder
|
||||
*/
|
||||
protected function _bindValue($value, $binder, $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,251 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This class represents a SQL Case statement
|
||||
*
|
||||
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
|
||||
*/
|
||||
class CaseExpression implements ExpressionInterface
|
||||
{
|
||||
use ExpressionTypeCasterTrait;
|
||||
|
||||
/**
|
||||
* A list of strings or other expression objects that represent the conditions of
|
||||
* the case statement. For example one key of the array might look like "sum > :value"
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_conditions = [];
|
||||
|
||||
/**
|
||||
* Values that are associated with the conditions in the $_conditions array.
|
||||
* Each value represents the 'true' value for the condition with the corresponding key.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_values = [];
|
||||
|
||||
/**
|
||||
* The `ELSE` value for the case statement. If null then no `ELSE` will be included.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|array|string|null
|
||||
*/
|
||||
protected $_elseValue;
|
||||
|
||||
/**
|
||||
* Constructs the case expression
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
|
||||
* instance, or an array of ExpressionInterface instances.
|
||||
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
|
||||
* conditions passed in $conditions. If there are more $values than $conditions,
|
||||
* the last $value is used as the `ELSE` value.
|
||||
* @param array<string> $types Associative array of types to be associated with the values
|
||||
* passed in $values
|
||||
*/
|
||||
public function __construct($conditions = [], $values = [], $types = [])
|
||||
{
|
||||
$conditions = is_array($conditions) ? $conditions : [$conditions];
|
||||
$values = is_array($values) ? $values : [$values];
|
||||
$types = is_array($types) ? $types : [$types];
|
||||
|
||||
if (!empty($conditions)) {
|
||||
$this->add($conditions, $values, $types);
|
||||
}
|
||||
|
||||
if (count($values) > count($conditions)) {
|
||||
end($values);
|
||||
$key = key($values);
|
||||
$this->elseValue($values[$key], $types[$key] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one or more conditions and their respective true values to the case object.
|
||||
* Conditions must be a one dimensional array or a QueryExpression.
|
||||
* The trueValues must be a similar structure, but may contain a string value.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array $conditions Must be a ExpressionInterface instance,
|
||||
* or an array of ExpressionInterface instances.
|
||||
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values of each condition
|
||||
* @param array<string> $types Associative array of types to be associated with the values
|
||||
* @return $this
|
||||
*/
|
||||
public function add($conditions = [], $values = [], $types = [])
|
||||
{
|
||||
$conditions = is_array($conditions) ? $conditions : [$conditions];
|
||||
$values = is_array($values) ? $values : [$values];
|
||||
$types = is_array($types) ? $types : [$types];
|
||||
|
||||
$this->_addExpressions($conditions, $values, $types);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the passed in conditions and ensures that there is a matching true value for each.
|
||||
* If no matching true value, then it is defaulted to '1'.
|
||||
*
|
||||
* @param array $conditions Array of ExpressionInterface instances.
|
||||
* @param array<mixed> $values Associative array of values of each condition
|
||||
* @param array<string> $types Associative array of types to be associated with the values
|
||||
* @return void
|
||||
*/
|
||||
protected function _addExpressions(array $conditions, array $values, array $types): void
|
||||
{
|
||||
$rawValues = array_values($values);
|
||||
$keyValues = array_keys($values);
|
||||
|
||||
foreach ($conditions as $k => $c) {
|
||||
$numericKey = is_numeric($k);
|
||||
|
||||
if ($numericKey && empty($c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$c instanceof ExpressionInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->_conditions[] = $c;
|
||||
$value = $rawValues[$k] ?? 1;
|
||||
|
||||
if ($value === 'literal') {
|
||||
$value = $keyValues[$k];
|
||||
$this->_values[] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === 'identifier') {
|
||||
/** @var string $identifier */
|
||||
$identifier = $keyValues[$k];
|
||||
$value = new IdentifierExpression($identifier);
|
||||
$this->_values[] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $types[$k] ?? null;
|
||||
|
||||
if ($type !== null && !$value instanceof ExpressionInterface) {
|
||||
$value = $this->_castToExpression($value, $type);
|
||||
}
|
||||
|
||||
if ($value instanceof ExpressionInterface) {
|
||||
$this->_values[] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->_values[] = ['value' => $value, 'type' => $type];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default value
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string|null $value Value to set
|
||||
* @param string|null $type Type of value
|
||||
* @return void
|
||||
*/
|
||||
public function elseValue($value = null, ?string $type = null): void
|
||||
{
|
||||
if (is_array($value)) {
|
||||
end($value);
|
||||
$value = key($value);
|
||||
}
|
||||
|
||||
if ($value !== null && !$value instanceof ExpressionInterface) {
|
||||
$value = $this->_castToExpression($value, $type);
|
||||
}
|
||||
|
||||
if (!$value instanceof ExpressionInterface) {
|
||||
$value = ['value' => $value, 'type' => $type];
|
||||
}
|
||||
|
||||
$this->_elseValue = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the relevant parts into sql
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $part The part to compile
|
||||
* @param \Cake\Database\ValueBinder $binder Sql generator
|
||||
* @return string
|
||||
*/
|
||||
protected function _compile($part, ValueBinder $binder): string
|
||||
{
|
||||
if ($part instanceof ExpressionInterface) {
|
||||
$part = $part->sql($binder);
|
||||
} elseif (is_array($part)) {
|
||||
$placeholder = $binder->placeholder('param');
|
||||
$binder->bind($placeholder, $part['value'], $part['type']);
|
||||
$part = $placeholder;
|
||||
}
|
||||
|
||||
return $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Node into a SQL string fragment.
|
||||
*
|
||||
* @param \Cake\Database\ValueBinder $binder Placeholder generator object
|
||||
* @return string
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
$parts = [];
|
||||
$parts[] = 'CASE';
|
||||
foreach ($this->_conditions as $k => $part) {
|
||||
$value = $this->_values[$k];
|
||||
$parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder);
|
||||
}
|
||||
if ($this->_elseValue !== null) {
|
||||
$parts[] = 'ELSE';
|
||||
$parts[] = $this->_compile($this->_elseValue, $binder);
|
||||
}
|
||||
$parts[] = 'END';
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
foreach (['_conditions', '_values'] as $part) {
|
||||
foreach ($this->{$part} as $c) {
|
||||
if ($c instanceof ExpressionInterface) {
|
||||
$callback($c);
|
||||
$c->traverse($callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->_elseValue instanceof ExpressionInterface) {
|
||||
$callback($this->_elseValue);
|
||||
$this->_elseValue->traverse($callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\Chronos\MutableDate;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\TypedResultInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Trait that holds shared functionality for case related expressions.
|
||||
*
|
||||
* @property \Cake\Database\TypeMap $_typeMap The type map to use when using an array of conditions for the `WHEN`
|
||||
* value.
|
||||
* @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($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 ||
|
||||
$value instanceof MutableDate
|
||||
) {
|
||||
$type = 'date';
|
||||
} elseif ($value instanceof DateTimeInterface) {
|
||||
$type = 'datetime';
|
||||
} elseif (
|
||||
is_object($value) &&
|
||||
method_exists($value, '__toString')
|
||||
) {
|
||||
$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, $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,597 @@
|
||||
<?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;
|
||||
use function Cake\Core\getTypeName;
|
||||
|
||||
/**
|
||||
* 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 $validClauseNames = [
|
||||
'value',
|
||||
'when',
|
||||
'else',
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether this is a simple case expression.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isSimpleVariant = false;
|
||||
|
||||
/**
|
||||
* The case value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected $value = null;
|
||||
|
||||
/**
|
||||
* The case value type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $valueType = null;
|
||||
|
||||
/**
|
||||
* The `WHEN ... THEN ...` expressions.
|
||||
*
|
||||
* @var array<\Cake\Database\Expression\WhenThenExpression>
|
||||
*/
|
||||
protected $when = [];
|
||||
|
||||
/**
|
||||
* Buffer that holds values and types for use with `then()`.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $whenBuffer = null;
|
||||
|
||||
/**
|
||||
* The else part result value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected $else = null;
|
||||
|
||||
/**
|
||||
* The else part result type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $elseType = null;
|
||||
|
||||
/**
|
||||
* The return type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $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($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,
|
||||
getTypeName($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->newExpr()
|
||||
* ->case()
|
||||
* ->when($query->newExpr(':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($when, $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,
|
||||
getTypeName($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($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($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,
|
||||
getTypeName($result)
|
||||
));
|
||||
}
|
||||
|
||||
if ($type === null) {
|
||||
$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)
|
||||
{
|
||||
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 (empty($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,239 @@
|
||||
<?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 RuntimeException;
|
||||
|
||||
/**
|
||||
* An expression that represents a common table expression definition.
|
||||
*/
|
||||
class CommonTableExpression implements ExpressionInterface
|
||||
{
|
||||
/**
|
||||
* The CTE name.
|
||||
*
|
||||
* @var \Cake\Database\Expression\IdentifierExpression
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The field names to use for the CTE.
|
||||
*
|
||||
* @var array<\Cake\Database\Expression\IdentifierExpression>
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* The CTE query definition.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|null
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Whether the CTE is materialized or not materialized.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $materialized = null;
|
||||
|
||||
/**
|
||||
* Whether the CTE is recursive.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $recursive = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $name The CTE name.
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure $query CTE query
|
||||
*/
|
||||
public function __construct(string $name = '', $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($query)
|
||||
{
|
||||
if ($query instanceof Closure) {
|
||||
$query = $query();
|
||||
if (!($query instanceof ExpressionInterface)) {
|
||||
throw new RuntimeException(
|
||||
'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<\Cake\Database\Expression\IdentifierExpression>|array<string>|string $fields Field names
|
||||
* @return $this
|
||||
*/
|
||||
public function field($fields)
|
||||
{
|
||||
$fields = (array)$fields;
|
||||
foreach ($fields as &$field) {
|
||||
if (!($field instanceof IdentifierExpression)) {
|
||||
$field = new IdentifierExpression($field);
|
||||
}
|
||||
}
|
||||
$this->fields = array_merge($this->fields, $fields);
|
||||
|
||||
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(function (IdentifierExpression $e) use ($binder) {
|
||||
return $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,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning('Since 4.1.0: `Comparison` deprecated. Use `ComparisonExpression` instead.');
|
||||
class_exists('Cake\Database\Expression\ComparisonExpression');
|
||||
@@ -0,0 +1,324 @@
|
||||
<?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 $_value;
|
||||
|
||||
/**
|
||||
* The type to be used for casting the value to a database representation
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $_type;
|
||||
|
||||
/**
|
||||
* The operator used for comparing field and value
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_operator = '=';
|
||||
|
||||
/**
|
||||
* Whether the value in this expression is a traversable
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_isMultiple = false;
|
||||
|
||||
/**
|
||||
* A cached list of ExpressionInterface objects that were
|
||||
* found in the value for this expression.
|
||||
*
|
||||
* @var array<\Cake\Database\ExpressionInterface>
|
||||
*/
|
||||
protected $_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($field, $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($value): void
|
||||
{
|
||||
$value = $this->_castToExpression($value, $this->_type);
|
||||
|
||||
$isMultiple = $this->_type && strpos($this->_type, '[]') !== false;
|
||||
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()
|
||||
{
|
||||
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
|
||||
{
|
||||
/** @var \Cake\Database\ExpressionInterface|string $field */
|
||||
$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);
|
||||
}
|
||||
|
||||
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;
|
||||
/** @psalm-suppress PossiblyInvalidCast */
|
||||
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($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 (!empty($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($values): array
|
||||
{
|
||||
if ($values instanceof ExpressionInterface) {
|
||||
return [$values, []];
|
||||
}
|
||||
|
||||
$expressions = $result = [];
|
||||
$isArray = is_array($values);
|
||||
|
||||
if ($isArray) {
|
||||
/** @var array $result */
|
||||
$result = $values;
|
||||
}
|
||||
|
||||
foreach ($values as $k => $v) {
|
||||
if ($v instanceof ExpressionInterface) {
|
||||
$expressions[$k] = $v;
|
||||
}
|
||||
|
||||
if ($isArray) {
|
||||
$result[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return [$result, $expressions];
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Expression\ComparisonExpression',
|
||||
'Cake\Database\Expression\Comparison'
|
||||
);
|
||||
// phpcs:enable
|
||||
@@ -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\Expression;
|
||||
|
||||
/**
|
||||
* 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($field): void;
|
||||
|
||||
/**
|
||||
* Returns the field name
|
||||
*
|
||||
* @return \Cake\Database\ExpressionInterface|array|string
|
||||
*/
|
||||
public function getField();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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 $_field;
|
||||
|
||||
/**
|
||||
* Sets the field name
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array|string $field The field to compare with.
|
||||
* @return void
|
||||
*/
|
||||
public function setField($field): void
|
||||
{
|
||||
$this->_field = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field name
|
||||
*
|
||||
* @return \Cake\Database\ExpressionInterface|array|string
|
||||
*/
|
||||
public function getField()
|
||||
{
|
||||
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 $_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 array $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
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function add($conditions, array $types = [], bool $prepend = false)
|
||||
{
|
||||
$put = $prepend ? 'array_unshift' : 'array_push';
|
||||
$typeMap = $this->getTypeMap()->setTypes($types);
|
||||
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 $_identifier;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $collation;
|
||||
|
||||
/**
|
||||
* 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,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\Expression;
|
||||
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\ValueBinder;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 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($conditions = [], $types = [], $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 RuntimeException(
|
||||
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 $_direction;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|string $field The field to order on.
|
||||
* @param string $direction The direction to sort on.
|
||||
*/
|
||||
public function __construct($field, $direction)
|
||||
{
|
||||
$this->_field = $field;
|
||||
$this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sql(ValueBinder $binder): string
|
||||
{
|
||||
/** @var \Cake\Database\ExpressionInterface|string $field */
|
||||
$field = $this->_field;
|
||||
if ($field instanceof Query) {
|
||||
$field = sprintf('(%s)', $field->sql($binder));
|
||||
} elseif ($field instanceof ExpressionInterface) {
|
||||
$field = $field->sql($binder);
|
||||
}
|
||||
|
||||
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,876 @@
|
||||
<?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\TypeMapTrait;
|
||||
use Cake\Database\ValueBinder;
|
||||
use Closure;
|
||||
use Countable;
|
||||
use InvalidArgumentException;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* 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 $_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 $_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($conditions = [], $types = [], $conjunction = 'AND')
|
||||
{
|
||||
$this->setTypeMap($types);
|
||||
$this->setConjunction(strtoupper($conjunction));
|
||||
if (!empty($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($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($field, $value, ?string $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($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($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $value, $type = null)
|
||||
{
|
||||
if ($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($field, $values, $type = null)
|
||||
{
|
||||
if ($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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new case expression to the expression object
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|array $conditions The conditions to test. Must be a ExpressionInterface
|
||||
* instance, or an array of ExpressionInterface instances.
|
||||
* @param \Cake\Database\ExpressionInterface|array $values Associative array of values to be associated with the
|
||||
* conditions passed in $conditions. If there are more $values than $conditions,
|
||||
* the last $value is used as the `ELSE` value.
|
||||
* @param array<string> $types Associative array of types to be associated with the values
|
||||
* passed in $values
|
||||
* @return $this
|
||||
* @deprecated 4.3.0 Use QueryExpression::case() or CaseStatementExpression instead
|
||||
*/
|
||||
public function addCase($conditions, $values = [], $types = [])
|
||||
{
|
||||
deprecationWarning('QueryExpression::addCase() is deprecated, use case() instead.');
|
||||
|
||||
return $this->add(new CaseExpression($conditions, $values, $types));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($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($field, $values, $type = null)
|
||||
{
|
||||
if ($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($field, $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 inbetween 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($field, $from, $to, $type = null)
|
||||
{
|
||||
if ($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 \Cake\Database\Expression\QueryExpression
|
||||
*/
|
||||
public function and($conditions, $types = [])
|
||||
{
|
||||
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 \Cake\Database\Expression\QueryExpression
|
||||
*/
|
||||
public function or($conditions, $types = [])
|
||||
{
|
||||
if ($conditions instanceof Closure) {
|
||||
return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
|
||||
}
|
||||
|
||||
return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
|
||||
}
|
||||
|
||||
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
|
||||
|
||||
/**
|
||||
* 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 \Cake\Database\Expression\QueryExpression
|
||||
* @deprecated 4.0.0 Use {@link and()} instead.
|
||||
*/
|
||||
public function and_($conditions, $types = [])
|
||||
{
|
||||
deprecationWarning('QueryExpression::and_() is deprecated use and() instead.');
|
||||
|
||||
return $this->and($conditions, $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 \Cake\Database\Expression\QueryExpression
|
||||
* @deprecated 4.0.0 Use {@link or()} instead.
|
||||
*/
|
||||
public function or_($conditions, $types = [])
|
||||
{
|
||||
deprecationWarning('QueryExpression::or_() is deprecated use or() instead.');
|
||||
|
||||
return $this->or($conditions, $types);
|
||||
}
|
||||
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* 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($conditions, $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) {
|
||||
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 callable function for each of the parts that form this expression.
|
||||
*
|
||||
* The callable function is required to return a value with which the currently
|
||||
* visited part will be replaced. If the callable function 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 callable $callback The callable to apply to each part.
|
||||
* @return $this
|
||||
*/
|
||||
public function iterateParts(callable $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a callable is acceptable.
|
||||
*
|
||||
* We don't accept ['class', 'method'] style callbacks,
|
||||
* as they often contain user input and arrays of strings
|
||||
* are easy to sneak in.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|callable|array|string $callable The callable to check.
|
||||
* @return bool Valid callable.
|
||||
* @deprecated 4.2.0 This method is unused.
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isCallable($callable): bool
|
||||
{
|
||||
if (is_string($callable)) {
|
||||
return false;
|
||||
}
|
||||
if (is_object($callable) && is_callable($callable)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_array($callable) && isset($callable[0]) && is_object($callable[0]) && is_callable($callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = $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 $field 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
|
||||
* @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
|
||||
*/
|
||||
protected function _parseCondition(string $field, $value)
|
||||
{
|
||||
$field = trim($field);
|
||||
$operator = '=';
|
||||
$expression = $field;
|
||||
|
||||
$spaces = substr_count($field, ' ');
|
||||
// Handle field 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(' ', $field);
|
||||
if (preg_match('/(is not|not \w+)$/i', $field)) {
|
||||
$last = array_pop($parts);
|
||||
$second = array_pop($parts);
|
||||
$parts[] = "{$second} {$last}";
|
||||
}
|
||||
$operator = array_pop($parts);
|
||||
$expression = implode(' ', $parts);
|
||||
} elseif ($spaces == 1) {
|
||||
$parts = explode(' ', $field, 2);
|
||||
[$expression, $operator] = $parts;
|
||||
}
|
||||
$operator = strtolower(trim($operator));
|
||||
$type = $this->getTypeMap()->type($expression);
|
||||
|
||||
$typeMultiple = (is_string($type) && strpos($type, '[]') !== false);
|
||||
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` is missing operator (IS, IS NOT) with `null` value.', $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($field): ?string
|
||||
{
|
||||
$field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
|
||||
if (is_string($field)) {
|
||||
return $this->getTypeMap()->type($field);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $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>
|
||||
* @psalm-suppress NonInvariantDocblockPropertyType
|
||||
*/
|
||||
protected $_type;
|
||||
|
||||
/**
|
||||
* 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($fields, $values, array $types = [], string $conjunction = '=')
|
||||
{
|
||||
$this->_type = $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->_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value
|
||||
*
|
||||
* @param mixed $value The value to compare
|
||||
* @return void
|
||||
*/
|
||||
public function setValue($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.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (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->_type;
|
||||
$isMultiOperation = $this->isMulti();
|
||||
if (empty($type)) {
|
||||
$type = null;
|
||||
}
|
||||
|
||||
if ($isMultiOperation) {
|
||||
$bound = [];
|
||||
foreach ($value as $k => $val) {
|
||||
/** @var string $valType */
|
||||
$valType = $type && isset($type[$k]) ? $type[$k] : $type;
|
||||
$bound[] = $this->_bindValue($val, $binder, $valType);
|
||||
}
|
||||
|
||||
$values[] = sprintf('(%s)', implode(',', $bound));
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var string $valType */
|
||||
$valType = $type && isset($type[$i]) ? $type[$i] : $type;
|
||||
$values[] = $this->_bindValue($value, $binder, $valType);
|
||||
}
|
||||
|
||||
return implode(', ', $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
|
||||
{
|
||||
$placeholder = $binder->placeholder('tuple');
|
||||
$binder->bind($placeholder, $value, $type);
|
||||
|
||||
return $placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function traverse(Closure $callback)
|
||||
{
|
||||
/** @var array<string> $fields */
|
||||
$fields = $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 callable to use when traversing
|
||||
* @return void
|
||||
*/
|
||||
protected function _traverseValue($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 $_operator;
|
||||
|
||||
/**
|
||||
* Holds the value which the unary expression operates
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $_value;
|
||||
|
||||
/**
|
||||
* Where to place the operator
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $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, $value, $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 $_values = [];
|
||||
|
||||
/**
|
||||
* List of columns to ensure are part of the insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_columns = [];
|
||||
|
||||
/**
|
||||
* The Query object to use as a values expression
|
||||
*
|
||||
* @var \Cake\Database\Query|null
|
||||
*/
|
||||
protected $_query;
|
||||
|
||||
/**
|
||||
* Whether values have been casted to expressions
|
||||
* already.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_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 + Query data types.
|
||||
*/
|
||||
public function add($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 (empty($this->_values) && empty($this->_query)) {
|
||||
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 (empty($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,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.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;
|
||||
use function Cake\Core\getTypeName;
|
||||
|
||||
/**
|
||||
* 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 $validClauseNames = [
|
||||
'when',
|
||||
'then',
|
||||
];
|
||||
|
||||
/**
|
||||
* The type map to use when using an array of conditions for the
|
||||
* `WHEN` value.
|
||||
*
|
||||
* @var \Cake\Database\TypeMap
|
||||
*/
|
||||
protected $_typeMap;
|
||||
|
||||
/**
|
||||
* Then `WHEN` value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected $when = null;
|
||||
|
||||
/**
|
||||
* The `WHEN` value type.
|
||||
*
|
||||
* @var array|string|null
|
||||
*/
|
||||
protected $whenType = null;
|
||||
|
||||
/**
|
||||
* The `THEN` value.
|
||||
*
|
||||
* @var \Cake\Database\ExpressionInterface|object|scalar|null
|
||||
*/
|
||||
protected $then = null;
|
||||
|
||||
/**
|
||||
* Whether the `THEN` value has been defined, eg whether `then()`
|
||||
* has been invoked.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $hasThenBeenDefined = false;
|
||||
|
||||
/**
|
||||
* The `THEN` result type.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $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)
|
||||
{
|
||||
if ($typeMap === null) {
|
||||
$typeMap = new TypeMap();
|
||||
}
|
||||
$this->_typeMap = $typeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `WHEN` value.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|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 \InvalidArgumentException In case the `$when` argument is neither a non-empty array, nor a scalar value,
|
||||
* an object, or an instance of `\Cake\Database\ExpressionInterface`.
|
||||
* @throws \InvalidArgumentException In case the `$type` argument is neither an array, a string, nor null.
|
||||
* @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($when, $type = null)
|
||||
{
|
||||
if (
|
||||
!(is_array($when) && !empty($when)) &&
|
||||
!is_scalar($when) &&
|
||||
!is_object($when)
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'The `$when` argument must be either a non-empty array, a scalar value, an object, ' .
|
||||
'or an instance of `\%s`, `%s` given.',
|
||||
ExpressionInterface::class,
|
||||
is_array($when) ? '[]' : getTypeName($when)
|
||||
));
|
||||
}
|
||||
|
||||
if (
|
||||
$type !== null &&
|
||||
!is_array($type) &&
|
||||
!is_string($type)
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'The `$type` argument must be either an array, a string, or `null`, `%s` given.',
|
||||
getTypeName($type)
|
||||
));
|
||||
}
|
||||
|
||||
if (is_array($when)) {
|
||||
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.',
|
||||
getTypeName($type)
|
||||
));
|
||||
}
|
||||
|
||||
// avoid dirtying the type map for possible consecutive `when()` calls
|
||||
$typeMap = clone $this->_typeMap;
|
||||
if (
|
||||
is_array($type) &&
|
||||
count($type) > 0
|
||||
) {
|
||||
$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.',
|
||||
getTypeName($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($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,
|
||||
getTypeName($result)
|
||||
));
|
||||
}
|
||||
|
||||
$this->then = $result;
|
||||
|
||||
if ($type === null) {
|
||||
$type = $this->inferType($result);
|
||||
}
|
||||
|
||||
$this->thenType = $type;
|
||||
|
||||
$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)
|
||||
{
|
||||
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,335 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* This represents a SQL window expression used by aggregate and window functions.
|
||||
*/
|
||||
class WindowExpression implements ExpressionInterface, WindowInterface
|
||||
{
|
||||
/**
|
||||
* @var \Cake\Database\Expression\IdentifierExpression
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var array<\Cake\Database\ExpressionInterface>
|
||||
*/
|
||||
protected $partitions = [];
|
||||
|
||||
/**
|
||||
* @var \Cake\Database\Expression\OrderByExpression|null
|
||||
*/
|
||||
protected $order;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $frame;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $exclusion;
|
||||
|
||||
/**
|
||||
* @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($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($fields)
|
||||
{
|
||||
if (!$fields) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->order === null) {
|
||||
$this->order = new OrderByExpression();
|
||||
}
|
||||
|
||||
if ($fields instanceof Closure) {
|
||||
$fields = $fields(new QueryExpression([], [], ''));
|
||||
}
|
||||
|
||||
$this->order->add($fields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function range($start, $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,
|
||||
$startOffset,
|
||||
string $startDirection,
|
||||
$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, $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,163 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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($partitions);
|
||||
|
||||
/**
|
||||
* Adds one or more order clauses to the window.
|
||||
*
|
||||
* @param \Cake\Database\ExpressionInterface|\Closure|array<\Cake\Database\ExpressionInterface|string>|string $fields Order expressions
|
||||
* @return $this
|
||||
*/
|
||||
public function order($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($start, $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.
|
||||
* @psalm-param self::RANGE|self::ROWS|self::GROUPS $type
|
||||
* @psalm-param self::PRECEDING|self::FOLLOWING $startDirection
|
||||
* @psalm-param self::PRECEDING|self::FOLLOWING $endDirection
|
||||
*/
|
||||
public function frame(
|
||||
string $type,
|
||||
$startOffset,
|
||||
string $startDirection,
|
||||
$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();
|
||||
}
|
||||
+44
@@ -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 callable
|
||||
* passing as first parameter the instance of the expression currently
|
||||
* being iterated.
|
||||
*
|
||||
* @param \Closure $callback The callable to apply to all nodes.
|
||||
* @return $this
|
||||
*/
|
||||
public function traverse(Closure $callback);
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* A callable 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.
|
||||
*/
|
||||
class FieldTypeConverter
|
||||
{
|
||||
/**
|
||||
* An array containing the name of the fields and the Type objects
|
||||
* each should use when converting them.
|
||||
*
|
||||
* @var array<\Cake\Database\TypeInterface>
|
||||
*/
|
||||
protected $_typeMap;
|
||||
|
||||
/**
|
||||
* An array containing the name of the fields and the Type objects
|
||||
* each should use when converting them using batching.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected $batchingTypeMap;
|
||||
|
||||
/**
|
||||
* An array containing all the types registered in the Type system
|
||||
* at the moment this object is created. Used so that the types list
|
||||
* is not fetched on each single row of the results.
|
||||
*
|
||||
* @var array<\Cake\Database\TypeInterface|\Cake\Database\Type\BatchCastingInterface>
|
||||
*/
|
||||
protected $types;
|
||||
|
||||
/**
|
||||
* The driver object to be used in the type conversion
|
||||
*
|
||||
* @var \Cake\Database\DriverInterface
|
||||
*/
|
||||
protected $_driver;
|
||||
|
||||
/**
|
||||
* Builds the type map
|
||||
*
|
||||
* @param \Cake\Database\TypeMap $typeMap Contains the types to use for converting results
|
||||
* @param \Cake\Database\DriverInterface $driver The driver to use for the type conversion
|
||||
*/
|
||||
public function __construct(TypeMap $typeMap, DriverInterface $driver)
|
||||
{
|
||||
$this->_driver = $driver;
|
||||
$map = $typeMap->toArray();
|
||||
$types = TypeFactory::buildAll();
|
||||
|
||||
$simpleMap = $batchingMap = [];
|
||||
$simpleResult = $batchingResult = [];
|
||||
|
||||
foreach ($types as $k => $type) {
|
||||
if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type instanceof BatchCastingInterface) {
|
||||
$batchingMap[$k] = $type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$simpleMap[$k] = $type;
|
||||
}
|
||||
|
||||
foreach ($map as $field => $type) {
|
||||
if (isset($simpleMap[$type])) {
|
||||
$simpleResult[$field] = $simpleMap[$type];
|
||||
continue;
|
||||
}
|
||||
if (isset($batchingMap[$type])) {
|
||||
$batchingResult[$type][] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
// Using batching when there is only a couple for the type is actually slower,
|
||||
// so, let's check for that case here.
|
||||
foreach ($batchingResult as $type => $fields) {
|
||||
if (count($fields) > 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fields as $f) {
|
||||
$simpleResult[$f] = $batchingMap[$type];
|
||||
}
|
||||
unset($batchingResult[$type]);
|
||||
}
|
||||
|
||||
$this->types = $types;
|
||||
$this->_typeMap = $simpleResult;
|
||||
$this->batchingTypeMap = $batchingResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts each of the fields in the array that are present in the type map
|
||||
* using the corresponding Type class.
|
||||
*
|
||||
* @param array $row The array with the fields to be casted
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __invoke(array $row): array
|
||||
{
|
||||
if (!empty($this->_typeMap)) {
|
||||
foreach ($this->_typeMap as $field => $type) {
|
||||
$row[$field] = $type->toPHP($row[$field], $this->_driver);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->batchingTypeMap)) {
|
||||
foreach ($this->batchingTypeMap as $t => $fields) {
|
||||
/** @psalm-suppress PossiblyUndefinedMethod */
|
||||
$row = $this->types[$t]->manyToPHP($row, $fields, $this->_driver);
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
<?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;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* 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($expression, $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($expression, $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($expression, $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($expression, $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($expression, $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 $type The SQL data type
|
||||
* @return \Cake\Database\Expression\FunctionExpression
|
||||
*/
|
||||
public function cast($field, string $type = ''): FunctionExpression
|
||||
{
|
||||
if (is_array($field)) {
|
||||
deprecationWarning(
|
||||
'Build cast function by FunctionsBuilder::cast(array $args) is deprecated. ' .
|
||||
'Use FunctionsBuilder::cast($field, string $type) instead.'
|
||||
);
|
||||
|
||||
return new FunctionExpression('CAST', $field);
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
throw new InvalidArgumentException('The `$type` in a cast cannot be empty.');
|
||||
}
|
||||
|
||||
$expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
|
||||
$expression->setConjunction(' AS')->add([$type => 'literal']);
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, $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, $expression, array $types = []): FunctionExpression
|
||||
{
|
||||
$expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
|
||||
$expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($expression, $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');
|
||||
$expression->setConjunction(', INTERVAL')->add([$interval => 'literal']);
|
||||
|
||||
return $expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($expression, $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($expression, $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
|
||||
{
|
||||
if ($type === 'datetime') {
|
||||
return new FunctionExpression('NOW', [], [], 'datetime');
|
||||
}
|
||||
if ($type === 'date') {
|
||||
return new FunctionExpression('CURRENT_DATE', [], [], 'date');
|
||||
}
|
||||
if ($type === 'time') {
|
||||
return new FunctionExpression('CURRENT_TIME', [], [], 'time');
|
||||
}
|
||||
|
||||
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 $type The output type of the lag expression. Defaults to float.
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function lag($expression, int $offset, $default = null, $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 $type The output type of the lead expression. Defaults to float.
|
||||
* @return \Cake\Database\Expression\AggregateExpression
|
||||
*/
|
||||
public function lead($expression, int $offset, $default = null, $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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
{
|
||||
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($expression)
|
||||
{
|
||||
if (is_string($expression)) {
|
||||
return [$expression => 'literal'];
|
||||
}
|
||||
|
||||
return [$expression];
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
<?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\FieldInterface;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\OrderByExpression;
|
||||
|
||||
/**
|
||||
* Contains all the logic related to quoting identifiers in a Query object
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class IdentifierQuoter
|
||||
{
|
||||
/**
|
||||
* The driver instance used to do the identifier quoting
|
||||
*
|
||||
* @var \Cake\Database\Driver
|
||||
*/
|
||||
protected $_driver;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\Driver $driver The driver instance used to do the identifier quoting
|
||||
*/
|
||||
public function __construct(Driver $driver)
|
||||
{
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if ($query->type() === 'insert') {
|
||||
$this->_quoteInsert($query);
|
||||
} elseif ($query->type() === 'update') {
|
||||
$this->_quoteUpdate($query);
|
||||
} else {
|
||||
$this->_quoteParts($query);
|
||||
}
|
||||
|
||||
$query->traverseExpressions([$this, 'quoteExpression']);
|
||||
$query->setValueBinder($binder);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if ($expression instanceof FieldInterface) {
|
||||
$this->_quoteComparison($expression);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($expression instanceof OrderByExpression) {
|
||||
$this->_quoteOrderBy($expression);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($expression instanceof IdentifierExpression) {
|
||||
$this->_quoteIdentifierExpression($expression);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes all identifiers in each of the clauses of a query
|
||||
*
|
||||
* @param \Cake\Database\Query $query The query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteParts(Query $query): void
|
||||
{
|
||||
foreach (['distinct', 'select', 'from', 'group'] as $part) {
|
||||
$contents = $query->clause($part);
|
||||
|
||||
if (!is_array($contents)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->_basicQuoter($contents);
|
||||
if (!empty($result)) {
|
||||
$query->{$part}($result, true);
|
||||
}
|
||||
}
|
||||
|
||||
$joins = $query->clause('join');
|
||||
if ($joins) {
|
||||
$joins = $this->_quoteJoins($joins);
|
||||
$query->join($joins, [], 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) ? $value : $this->_driver->quoteIdentifier($value);
|
||||
$alias = is_numeric($alias) ? $alias : $this->_driver->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 $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->_driver->quoteIdentifier($value['alias']);
|
||||
$value['alias'] = $alias;
|
||||
}
|
||||
|
||||
if (is_string($value['table'])) {
|
||||
$value['table'] = $this->_driver->quoteIdentifier($value['table']);
|
||||
}
|
||||
|
||||
$result[$alias] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the table name and columns for an insert query
|
||||
*
|
||||
* @param \Cake\Database\Query $query The insert query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteInsert(Query $query): void
|
||||
{
|
||||
$insert = $query->clause('insert');
|
||||
if (!isset($insert[0]) || !isset($insert[1])) {
|
||||
return;
|
||||
}
|
||||
[$table, $columns] = $insert;
|
||||
$table = $this->_driver->quoteIdentifier($table);
|
||||
foreach ($columns as &$column) {
|
||||
if (is_scalar($column)) {
|
||||
$column = $this->_driver->quoteIdentifier((string)$column);
|
||||
}
|
||||
}
|
||||
$query->insert($columns)->into($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the table name for an update query
|
||||
*
|
||||
* @param \Cake\Database\Query $query The update query to quote.
|
||||
* @return void
|
||||
*/
|
||||
protected function _quoteUpdate(Query $query): void
|
||||
{
|
||||
$table = $query->clause('update')[0];
|
||||
|
||||
if (is_string($table)) {
|
||||
$query->update($this->_driver->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->_driver->quoteIdentifier($field));
|
||||
} elseif (is_array($field)) {
|
||||
$quoted = [];
|
||||
foreach ($field as $f) {
|
||||
$quoted[] = $this->_driver->quoteIdentifier($f);
|
||||
}
|
||||
$expression->setField($quoted);
|
||||
} elseif ($field instanceof ExpressionInterface) {
|
||||
$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->_driver->quoteIdentifier($field);
|
||||
|
||||
return $part;
|
||||
}
|
||||
if (is_string($part) && strpos($part, ' ') === false) {
|
||||
return $this->_driver->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->_driver->quoteIdentifier($expression->getIdentifier())
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+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.
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?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\Sqlserver;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* Driver executing the query
|
||||
*
|
||||
* @var \Cake\Database\DriverInterface|null
|
||||
*/
|
||||
public $driver = null;
|
||||
|
||||
/**
|
||||
* Query string that was executed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $query = '';
|
||||
|
||||
/**
|
||||
* Number of milliseconds this query took to complete
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $took = 0;
|
||||
|
||||
/**
|
||||
* Associative array with the params bound to the query string
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $params = [];
|
||||
|
||||
/**
|
||||
* Number of rows affected or returned by the query execution
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $numRows = 0;
|
||||
|
||||
/**
|
||||
* The exception that was thrown by the execution of this query
|
||||
*
|
||||
* @var \Exception|null
|
||||
*/
|
||||
public $error;
|
||||
|
||||
/**
|
||||
* 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 preg_replace($keys, $params, $this->query, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the logging context data for a query.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getContext(): array
|
||||
{
|
||||
return [
|
||||
'numRows' => $this->numRows,
|
||||
'took' => $this->took,
|
||||
'role' => $this->driver ? $this->driver->getRole() : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data that will be serialized as JSON
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$error = $this->error;
|
||||
if ($error !== null) {
|
||||
$error = [
|
||||
'class' => get_class($error),
|
||||
'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
|
||||
{
|
||||
$sql = $this->query;
|
||||
if (!empty($this->params)) {
|
||||
$sql = $this->interpolate();
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
<?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\Core\Configure;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\Statement\StatementDecorator;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Statement decorator used to
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LoggingStatement extends StatementDecorator
|
||||
{
|
||||
/**
|
||||
* Logger instance responsible for actually doing the logging task
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $_logger;
|
||||
|
||||
/**
|
||||
* Holds bound params
|
||||
*
|
||||
* @var array<array>
|
||||
*/
|
||||
protected $_compiledParams = [];
|
||||
|
||||
/**
|
||||
* Query execution start time.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $startTime = 0.0;
|
||||
|
||||
/**
|
||||
* Logged query
|
||||
*
|
||||
* @var \Cake\Database\Log\LoggedQuery|null
|
||||
*/
|
||||
protected $loggedQuery;
|
||||
|
||||
/**
|
||||
* Wrapper for the execute function to calculate time spent
|
||||
* and log the query afterwards.
|
||||
*
|
||||
* @param array|null $params List of values to be bound to query
|
||||
* @return bool True on success, false otherwise
|
||||
* @throws \Exception Re-throws any exception raised during query execution.
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
$this->startTime = microtime(true);
|
||||
|
||||
$this->loggedQuery = new LoggedQuery();
|
||||
$this->loggedQuery->driver = $this->_driver;
|
||||
$this->loggedQuery->params = $params ?: $this->_compiledParams;
|
||||
|
||||
try {
|
||||
$result = parent::execute($params);
|
||||
$this->loggedQuery->took = (int)round((microtime(true) - $this->startTime) * 1000, 0);
|
||||
} catch (Exception $e) {
|
||||
$this->loggedQuery->error = $e;
|
||||
$this->_log();
|
||||
|
||||
if (Configure::read('Error.convertStatementToDatabaseException', false) === true) {
|
||||
$code = $e->getCode();
|
||||
if (!is_int($code)) {
|
||||
$code = null;
|
||||
}
|
||||
|
||||
throw new DatabaseException([
|
||||
'message' => $e->getMessage(),
|
||||
'queryString' => $this->queryString,
|
||||
], $code, $e);
|
||||
}
|
||||
|
||||
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
|
||||
deprecationWarning(
|
||||
'4.4.12 - Having queryString set on exceptions is deprecated.' .
|
||||
'If you are not using this attribute there is no action to take.' .
|
||||
'Otherwise, enable Error.convertStatementToDatabaseException.'
|
||||
);
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$e->queryString = $this->queryString;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?!SELECT)/i', $this->queryString)) {
|
||||
$this->rowCount();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetch($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
$record = parent::fetch($type);
|
||||
|
||||
if ($this->loggedQuery) {
|
||||
$this->rowCount();
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAll($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
$results = parent::fetchAll($type);
|
||||
|
||||
if ($this->loggedQuery) {
|
||||
$this->rowCount();
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
$result = parent::rowCount();
|
||||
|
||||
if ($this->loggedQuery) {
|
||||
$this->loggedQuery->numRows = $result;
|
||||
$this->_log();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the logging data to the passed LoggedQuery and sends it
|
||||
* to the logging system.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _log(): void
|
||||
{
|
||||
if ($this->loggedQuery === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loggedQuery->query = $this->queryString;
|
||||
$this->getLogger()->debug((string)$this->loggedQuery, ['query' => $this->loggedQuery]);
|
||||
|
||||
$this->loggedQuery = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for bindValue function to gather each parameter to be later used
|
||||
* in the logger function.
|
||||
*
|
||||
* @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 PDO type or name of configured Type class
|
||||
* @return void
|
||||
*/
|
||||
public function bindValue($column, $value, $type = 'string'): void
|
||||
{
|
||||
parent::bindValue($column, $value, $type);
|
||||
|
||||
if ($type === null) {
|
||||
$type = 'string';
|
||||
}
|
||||
if (!ctype_digit($type)) {
|
||||
$value = $this->cast($value, $type)[0];
|
||||
}
|
||||
$this->_compiledParams[$column] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a logger
|
||||
*
|
||||
* @param \Psr\Log\LoggerInterface $logger Logger object
|
||||
* @return void
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->_logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logger object
|
||||
*
|
||||
* @return \Psr\Log\LoggerInterface logger instance
|
||||
*/
|
||||
public function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->_logger;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
$this->_defaultConfig['connection'] = '';
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log($level, $message, array $context = [])
|
||||
{
|
||||
$context['scope'] = $this->scopes() ?: ['queriesLog'];
|
||||
$context['connection'] = $this->getConfig('connection');
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?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 $_quotedSelectAliases = true;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s',
|
||||
'order' => ' %s',
|
||||
'limit' => ' LIMIT %s',
|
||||
'offset' => ' OFFSET %s',
|
||||
'epilog' => ' %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($parts, $query, $binder)
|
||||
{
|
||||
$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));
|
||||
}
|
||||
}
|
||||
Vendored
+2516
File diff suppressed because it is too large
Load Diff
+222
@@ -0,0 +1,222 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Delete Query forward compatibility shim.
|
||||
*/
|
||||
class DeleteQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query (select, insert, update, delete).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_type = 'delete';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function select($fields = [], bool $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
|
||||
|
||||
return parent::select($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function distinct($on = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('distint()');
|
||||
|
||||
return parent::distinct($on, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function modifier($modifiers, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('modifier()');
|
||||
|
||||
return parent::modifier($modifiers, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function order($fields, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('order()');
|
||||
|
||||
return parent::order($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderAsc($field, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('orderAsc()');
|
||||
|
||||
return parent::orderAsc($field, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderDesc($field, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('orderDesc()');
|
||||
|
||||
return parent::orderDesc($field, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function group($fields, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('group()');
|
||||
|
||||
return parent::group($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function having($conditions = null, $types = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('having()');
|
||||
|
||||
return parent::having($conditions, $types, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function andHaving($conditions, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('andHaving()');
|
||||
|
||||
return parent::andHaving($conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function page(int $num, ?int $limit = null)
|
||||
{
|
||||
$this->_deprecatedMethod('page()');
|
||||
|
||||
return parent::page($num, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function limit($limit)
|
||||
{
|
||||
$this->_deprecatedMethod('limit()');
|
||||
|
||||
return parent::limit($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offset($offset)
|
||||
{
|
||||
$this->_deprecatedMethod('offset()');
|
||||
|
||||
return parent::offset($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function union($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('union()');
|
||||
|
||||
return parent::union($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function unionAll($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('unionAll()');
|
||||
|
||||
return parent::unionAll($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function insert(array $columns, array $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
|
||||
|
||||
return parent::insert($columns, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function into(string $table)
|
||||
{
|
||||
$this->_deprecatedMethod('into()');
|
||||
|
||||
return parent::into($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function values($data)
|
||||
{
|
||||
$this->_deprecatedMethod('values()');
|
||||
|
||||
return parent::values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function update($table)
|
||||
{
|
||||
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
|
||||
|
||||
return parent::update($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function set($key, $value = null, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('set()');
|
||||
|
||||
return parent::set($key, $value, $types);
|
||||
}
|
||||
}
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Insert Query forward compatibility shim.
|
||||
*/
|
||||
class InsertQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query (select, insert, update, delete).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_type = 'insert';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete(?string $table = null)
|
||||
{
|
||||
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
|
||||
|
||||
return parent::delete($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function select($fields = [], bool $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
|
||||
|
||||
return parent::select($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function distinct($on = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('distinct()');
|
||||
|
||||
return parent::distinct($on, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function modifier($modifiers, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('modifier()');
|
||||
|
||||
return parent::modifier($modifiers, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function join($tables, $types = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('join()');
|
||||
|
||||
return parent::join($tables, $types, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function removeJoin(string $name)
|
||||
{
|
||||
$this->_deprecatedMethod('removeJoin()');
|
||||
|
||||
return parent::removeJoin($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function leftJoin($table, $conditions = [], $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('leftJoin()');
|
||||
|
||||
return parent::leftJoin($table, $conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rightJoin($table, $conditions = [], $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('rightJoin()');
|
||||
|
||||
return parent::rightJoin($table, $conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function innerJoin($table, $conditions = [], $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('innerJoin()');
|
||||
|
||||
return parent::innerJoin($table, $conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function group($fields, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('group()');
|
||||
|
||||
return parent::group($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function having($conditions = null, $types = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('having()');
|
||||
|
||||
return parent::having($conditions, $types, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function andHaving($conditions, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('andHaving()');
|
||||
|
||||
return parent::andHaving($conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function page(int $num, ?int $limit = null)
|
||||
{
|
||||
$this->_deprecatedMethod('page()');
|
||||
|
||||
return parent::page($num, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offset($offset)
|
||||
{
|
||||
$this->_deprecatedMethod('offset()');
|
||||
|
||||
return parent::offset($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function union($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('union()');
|
||||
|
||||
return parent::union($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function unionAll($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('union()');
|
||||
|
||||
return parent::unionAll($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function from($tables = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('from()', 'Use into() instead.');
|
||||
|
||||
return parent::from($tables, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function update($table)
|
||||
{
|
||||
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
|
||||
|
||||
return parent::update($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function set($key, $value = null, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('set()');
|
||||
|
||||
return parent::set($key, $value, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function where($conditions = null, array $types = [], bool $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('where()');
|
||||
|
||||
return parent::where($conditions, $types, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function whereNotNull($fields)
|
||||
{
|
||||
$this->_deprecatedMethod('whereNotNull()');
|
||||
|
||||
return parent::whereNotNull($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function whereNull($fields)
|
||||
{
|
||||
$this->_deprecatedMethod('whereNull()');
|
||||
|
||||
return parent::whereNull($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function whereInList(string $field, array $values, array $options = [])
|
||||
{
|
||||
$this->_deprecatedMethod('whereInList()');
|
||||
|
||||
return parent::whereInList($field, $values, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function whereNotInList(string $field, array $values, array $options = [])
|
||||
{
|
||||
$this->_deprecatedMethod('whereNotInList()');
|
||||
|
||||
return parent::whereNotInList($field, $values, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function whereNotInListOrNull(string $field, array $values, array $options = [])
|
||||
{
|
||||
$this->_deprecatedMethod('whereNotInListOrNull()');
|
||||
|
||||
return parent::whereNotInListOrNull($field, $values, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function andWhere($conditions, array $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('andWhere()');
|
||||
|
||||
return parent::andWhere($conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function order($fields, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('order()');
|
||||
|
||||
return parent::order($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderAsc($field, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('orderAsc()');
|
||||
|
||||
return parent::orderAsc($field, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function orderDesc($field, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('orderDesc()');
|
||||
|
||||
return parent::orderDesc($field, $overwrite);
|
||||
}
|
||||
}
|
||||
+127
@@ -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\Connection;
|
||||
use Cake\Database\Query;
|
||||
|
||||
/**
|
||||
* Select Query forward compatibility shim.
|
||||
*/
|
||||
class SelectQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query (select, insert, update, delete).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_type = 'select';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete(?string $table = null)
|
||||
{
|
||||
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
|
||||
|
||||
return parent::delete($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function insert(array $columns, array $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
|
||||
|
||||
return parent::insert($columns, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function into(string $table)
|
||||
{
|
||||
$this->_deprecatedMethod('into()', 'Use from() instead.');
|
||||
|
||||
return parent::into($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function values($data)
|
||||
{
|
||||
$this->_deprecatedMethod('values()');
|
||||
|
||||
return parent::values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function update($table)
|
||||
{
|
||||
$this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.');
|
||||
|
||||
return parent::update($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function set($key, $value = null, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('set()');
|
||||
|
||||
return parent::set($key, $value, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Update Query forward compatibility shim.
|
||||
*/
|
||||
class UpdateQuery extends Query
|
||||
{
|
||||
/**
|
||||
* Type of this query (select, insert, update, delete).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_type = 'update';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete(?string $table = null)
|
||||
{
|
||||
$this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.');
|
||||
|
||||
return parent::delete($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function select($fields = [], bool $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.');
|
||||
|
||||
return parent::select($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function distinct($on = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('distinct()');
|
||||
|
||||
return parent::distinct($on, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function modifier($modifiers, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('modifier()');
|
||||
|
||||
return parent::modifier($modifiers, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function group($fields, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('group()');
|
||||
|
||||
return parent::group($fields, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function having($conditions = null, $types = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('having()');
|
||||
|
||||
return parent::having($conditions, $types, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function andHaving($conditions, $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('andHaving()');
|
||||
|
||||
return parent::andHaving($conditions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function page(int $num, ?int $limit = null)
|
||||
{
|
||||
$this->_deprecatedMethod('page()');
|
||||
|
||||
return parent::page($num, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function offset($offset)
|
||||
{
|
||||
$this->_deprecatedMethod('offset()');
|
||||
|
||||
return parent::offset($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function union($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('union()');
|
||||
|
||||
return parent::union($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function unionAll($query, $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('union()');
|
||||
|
||||
return parent::unionAll($query, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function insert(array $columns, array $types = [])
|
||||
{
|
||||
$this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.');
|
||||
|
||||
return parent::insert($columns, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function into(string $table)
|
||||
{
|
||||
$this->_deprecatedMethod('into()', 'Use update() instead.');
|
||||
|
||||
return parent::into($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function values($data)
|
||||
{
|
||||
$this->_deprecatedMethod('values()');
|
||||
|
||||
return parent::values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function from($tables = [], $overwrite = false)
|
||||
{
|
||||
$this->_deprecatedMethod('from()', 'Use update() instead.');
|
||||
|
||||
return parent::from($tables, $overwrite);
|
||||
}
|
||||
}
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
<?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 $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s ',
|
||||
'having' => ' HAVING %s ',
|
||||
'order' => ' %s',
|
||||
'limit' => ' LIMIT %s',
|
||||
'offset' => ' OFFSET %s',
|
||||
'epilog' => ' %s',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating a SELECT statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $_selectParts = [
|
||||
'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
|
||||
'limit', 'offset', 'union', 'epilog',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating an UPDATE statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog'];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating a DELETE statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog'];
|
||||
|
||||
/**
|
||||
* The list of query clauses to traverse for generating an INSERT statement
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $_insertParts = ['with', 'insert', 'values', 'epilog'];
|
||||
|
||||
/**
|
||||
* Indicate whether this query dialect supports ordered unions.
|
||||
*
|
||||
* Overridden in subclasses.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_orderedUnion = true;
|
||||
|
||||
/**
|
||||
* Indicate whether aliases in SELECT clause need to be always quoted.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_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.
|
||||
if ($query->getValueBinder() !== $binder) {
|
||||
foreach ($query->getValueBinder()->bindings() as $binding) {
|
||||
$placeholder = ':' . $binding['placeholder'];
|
||||
if (preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) {
|
||||
$binder->bind($placeholder, $binding['value'], $binding['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a callable object 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) {
|
||||
if (
|
||||
$part === null ||
|
||||
(is_array($part) && empty($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 $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
|
||||
{
|
||||
$select = 'SELECT%s %s%s';
|
||||
if ($this->_orderedUnion && $query->clause('union')) {
|
||||
$select = '(SELECT%s %s%s';
|
||||
}
|
||||
$distinct = $query->clause('distinct');
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
$driver = $query->getConnection()->getDriver($query->getConnectionRole());
|
||||
$quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases;
|
||||
$normalized = [];
|
||||
$parts = $this->_stringifyExpressions($parts, $binder);
|
||||
foreach ($parts as $k => $p) {
|
||||
if (!is_numeric($k)) {
|
||||
$p = $p . ' AS ';
|
||||
if ($quoteIdentifiers) {
|
||||
$p .= $driver->quoteIdentifier($k);
|
||||
} else {
|
||||
$p .= $k;
|
||||
}
|
||||
}
|
||||
$normalized[] = $p;
|
||||
}
|
||||
|
||||
if ($distinct === true) {
|
||||
$distinct = 'DISTINCT ';
|
||||
}
|
||||
|
||||
if (is_array($distinct)) {
|
||||
$distinct = $this->_stringifyExpressions($distinct, $binder);
|
||||
$distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct));
|
||||
}
|
||||
|
||||
return sprintf($select, $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) {
|
||||
$windows[] = $window['name']->sql($binder) . ' AS (' . $window['window']->sql($binder) . ')';
|
||||
}
|
||||
|
||||
return ' WINDOW ' . implode(', ', $windows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate SQL for SET expressions.
|
||||
*
|
||||
* @param array $parts List of keys & 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 ($part[0] === '(') {
|
||||
$part = substr($part, 1, -1);
|
||||
}
|
||||
$set[] = $part;
|
||||
}
|
||||
|
||||
return ' SET ' . implode('', $set);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$parts = array_map(function ($p) use ($binder) {
|
||||
$p['query'] = $p['query']->sql($binder);
|
||||
$p['query'] = $p['query'][0] === '(' ? trim($p['query'], '()') : $p['query'];
|
||||
$prefix = $p['all'] ? 'ALL ' : '';
|
||||
if ($this->_orderedUnion) {
|
||||
return "{$prefix}({$p['query']})";
|
||||
}
|
||||
|
||||
return $prefix . $p['query'];
|
||||
}, $parts);
|
||||
|
||||
if ($this->_orderedUnion) {
|
||||
return sprintf(")\nUNION %s", implode("\nUNION ", $parts));
|
||||
}
|
||||
|
||||
return sprintf("\nUNION %s", implode("\nUNION ", $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
return sprintf('INSERT%s INTO %s (%s)', $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);
|
||||
$modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
|
||||
|
||||
return sprintf('UPDATE%s %s', $modifiers, implode(',', $table));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Vendored
+358
@@ -0,0 +1,358 @@
|
||||
[](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 the
|
||||
`newQuery()` for using a query builder.
|
||||
|
||||
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('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('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('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('assoc');
|
||||
|
||||
$statement->bind(['id' => 1], ['id' => 'integer']);
|
||||
$results = $statement->fetchAll('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->newQuery();
|
||||
```
|
||||
|
||||
### 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 > 2
|
||||
$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 ($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 ($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('assoc');
|
||||
```
|
||||
|
||||
## Official API
|
||||
|
||||
You can read the official [official API docs](https://api.cakephp.org/4.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 $errorCodes;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $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;
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?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 $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;
|
||||
|
||||
/**
|
||||
* 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 (strstr($message, $cause) !== false) {
|
||||
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 $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$this->connection->connect();
|
||||
if ($this->connection->isQueryLoggingEnabled()) {
|
||||
$this->connection->log('[RECONNECT]');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
// If there was an error connecting again, don't report it back,
|
||||
// let the retry handler do it.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\Schema\BaseSchema is deprecated. ' .
|
||||
'Use Cake\Database\Schema\SchemaDialect instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Schema\SchemaDialect');
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
<?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 $cacher;
|
||||
|
||||
/**
|
||||
* The decorated schema collection
|
||||
*
|
||||
* @var \Cake\Database\Schema\CollectionInterface
|
||||
*/
|
||||
protected $collection;
|
||||
|
||||
/**
|
||||
* The cache key prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<?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;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* Represents a database schema collection
|
||||
*
|
||||
* Used to access information about the tables,
|
||||
* and other data in a database.
|
||||
*/
|
||||
class Collection implements CollectionInterface
|
||||
{
|
||||
/**
|
||||
* Connection object
|
||||
*
|
||||
* @var \Cake\Database\Connection
|
||||
*/
|
||||
protected $_connection;
|
||||
|
||||
/**
|
||||
* Schema dialect instance.
|
||||
*
|
||||
* @var \Cake\Database\Schema\SchemaDialect
|
||||
*/
|
||||
protected $_dialect;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Cake\Database\Connection $connection The connection instance.
|
||||
*/
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->_connection = $connection;
|
||||
$this->_dialect = $connection->getDriver()->schemaDialect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->_dialect->listTablesWithoutViewsSql($this->_connection->getDriver()->config());
|
||||
$result = [];
|
||||
$statement = $this->_connection->execute($sql, $params);
|
||||
while ($row = $statement->fetch()) {
|
||||
$result[] = $row[0];
|
||||
}
|
||||
$statement->closeCursor();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
[$sql, $params] = $this->_dialect->listTablesSql($this->_connection->getDriver()->config());
|
||||
$result = [];
|
||||
$statement = $this->_connection->execute($sql, $params);
|
||||
while ($row = $statement->fetch()) {
|
||||
$result[] = $row[0];
|
||||
}
|
||||
$statement->closeCursor();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\TableSchema Object with column metadata.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
|
||||
*/
|
||||
public function describe(string $name, array $options = []): TableSchemaInterface
|
||||
{
|
||||
$config = $this->_connection->getDriver()->config();
|
||||
if (strpos($name, '.')) {
|
||||
[$config['schema'], $name] = explode('.', $name);
|
||||
}
|
||||
$table = $this->_connection->getDriver()->newTableSchema($name);
|
||||
|
||||
$this->_reflect('Column', $name, $config, $table);
|
||||
if (count($table->columns()) === 0) {
|
||||
throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
|
||||
}
|
||||
|
||||
$this->_reflect('Index', $name, $config, $table);
|
||||
$this->_reflect('ForeignKey', $name, $config, $table);
|
||||
$this->_reflect('Options', $name, $config, $table);
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for running each step of the reflection process.
|
||||
*
|
||||
* @param string $stage The stage name.
|
||||
* @param string $name The table name.
|
||||
* @param array<string, mixed> $config The config data.
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table schema instance.
|
||||
* @return void
|
||||
* @throws \Cake\Database\Exception\DatabaseException on query failure.
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::describeColumnSql
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::describeIndexSql
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::describeForeignKeySql
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::describeOptionsSql
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::convertColumnDescription
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::convertIndexDescription
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::convertForeignKeyDescription
|
||||
* @uses \Cake\Database\Schema\SchemaDialect::convertOptionsDescription
|
||||
*/
|
||||
protected function _reflect(string $stage, string $name, array $config, TableSchema $schema): void
|
||||
{
|
||||
$describeMethod = "describe{$stage}Sql";
|
||||
$convertMethod = "convert{$stage}Description";
|
||||
|
||||
[$sql, $params] = $this->_dialect->{$describeMethod}($name, $config);
|
||||
if (empty($sql)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$statement = $this->_connection->execute($sql, $params);
|
||||
} catch (PDOException $e) {
|
||||
throw new DatabaseException($e->getMessage(), 500, $e);
|
||||
}
|
||||
/** @psalm-suppress PossiblyFalseIterator */
|
||||
foreach ($statement->fetchAll('assoc') as $row) {
|
||||
$this->_dialect->{$convertMethod}($schema, $row);
|
||||
}
|
||||
$statement->closeCursor();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\Schema\MysqlSchema is deprecated. ' .
|
||||
'Use Cake\Database\Schema\MysqlSchemaDialect instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Schema\MysqlSchemaDialect');
|
||||
@@ -0,0 +1,669 @@
|
||||
<?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\DriverInterface;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
|
||||
/**
|
||||
* Schema generation/reflection features for MySQL
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MysqlSchemaDialect extends SchemaDialect
|
||||
{
|
||||
/**
|
||||
* The driver instance being used.
|
||||
*
|
||||
* @var \Cake\Database\Driver\Mysql
|
||||
*/
|
||||
protected $_driver;
|
||||
|
||||
/**
|
||||
* 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<mixed> An array of (sql, params) to execute.
|
||||
*/
|
||||
public function listTablesSql(array $config): array
|
||||
{
|
||||
return ['SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return [
|
||||
'SHOW FULL TABLES FROM ' . $this->_driver->quoteIdentifier($config['database'])
|
||||
. ' WHERE TABLE_TYPE = "BASE TABLE"'
|
||||
, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumnSql(string $tableName, array $config): array
|
||||
{
|
||||
return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeIndexSql(string $tableName, array $config): array
|
||||
{
|
||||
return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeOptionsSql(string $tableName, array $config): array
|
||||
{
|
||||
return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertOptionsDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$schema->setOptions([
|
||||
'engine' => $row['Engine'],
|
||||
'collation' => $row['Collation'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MySQL column type into an abstract type.
|
||||
*
|
||||
* The returned type will be a type that Cake\Database\TypeFactory can handle.
|
||||
*
|
||||
* @param string $column The column type + length
|
||||
* @return array<string, mixed> Array of column information.
|
||||
* @throws \Cake\Database\Exception\DatabaseException When column type cannot be parsed.
|
||||
*/
|
||||
protected function _convertColumn(string $column): array
|
||||
{
|
||||
preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches);
|
||||
if (empty($matches)) {
|
||||
throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
|
||||
}
|
||||
|
||||
$col = strtolower($matches[1]);
|
||||
$length = $precision = $scale = null;
|
||||
if (isset($matches[2]) && strlen($matches[2])) {
|
||||
$length = $matches[2];
|
||||
if (strpos($matches[2], ',') !== false) {
|
||||
[$length, $precision] = explode(',', $length);
|
||||
}
|
||||
$length = (int)$length;
|
||||
$precision = (int)$precision;
|
||||
}
|
||||
|
||||
$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 (in_array($col, ['datetime', 'timestamp'])) {
|
||||
$typeName = $col;
|
||||
if ($length > 0) {
|
||||
$typeName = $col . 'fractional';
|
||||
}
|
||||
|
||||
return ['type' => $typeName, 'length' => null, 'precision' => $length];
|
||||
}
|
||||
|
||||
if (($col === 'tinyint' && $length === 1) || $col === 'boolean') {
|
||||
return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
|
||||
}
|
||||
|
||||
$unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned');
|
||||
if (strpos($col, 'bigint') !== false || $col === 'bigint') {
|
||||
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => null, 'unsigned' => $unsigned];
|
||||
}
|
||||
if ($col === 'tinyint') {
|
||||
return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => null, 'unsigned' => $unsigned];
|
||||
}
|
||||
if ($col === 'smallint') {
|
||||
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => null, 'unsigned' => $unsigned];
|
||||
}
|
||||
if (in_array($col, ['int', 'integer', 'mediumint'])) {
|
||||
return ['type' => TableSchema::TYPE_INTEGER, 'length' => null, 'unsigned' => $unsigned];
|
||||
}
|
||||
if ($col === 'char' && $length === 36) {
|
||||
return ['type' => TableSchema::TYPE_UUID, 'length' => null];
|
||||
}
|
||||
if ($col === 'char') {
|
||||
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'char') !== false) {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'text') !== false) {
|
||||
$lengthName = substr($col, 0, -4);
|
||||
$length = TableSchema::$columnLengths[$lengthName] ?? null;
|
||||
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => $length];
|
||||
}
|
||||
if ($col === 'binary' && $length === 16) {
|
||||
return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
|
||||
}
|
||||
if (strpos($col, 'blob') !== false || in_array($col, ['binary', 'varbinary'])) {
|
||||
$lengthName = substr($col, 0, -4);
|
||||
$length = TableSchema::$columnLengths[$lengthName] ?? $length;
|
||||
|
||||
return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) {
|
||||
return [
|
||||
'type' => TableSchema::TYPE_FLOAT,
|
||||
'length' => $length,
|
||||
'precision' => $precision,
|
||||
'unsigned' => $unsigned,
|
||||
];
|
||||
}
|
||||
if (strpos($col, 'decimal') !== false) {
|
||||
return [
|
||||
'type' => TableSchema::TYPE_DECIMAL,
|
||||
'length' => $length,
|
||||
'precision' => $precision,
|
||||
'unsigned' => $unsigned,
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($col, 'json') !== false) {
|
||||
return ['type' => TableSchema::TYPE_JSON, 'length' => null];
|
||||
}
|
||||
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertColumnDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$field = $this->_convertColumn($row['Type']);
|
||||
$field += [
|
||||
'null' => $row['Null'] === 'YES',
|
||||
'default' => $row['Default'],
|
||||
'collate' => $row['Collation'],
|
||||
'comment' => $row['Comment'],
|
||||
];
|
||||
if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') {
|
||||
$field['autoIncrement'] = true;
|
||||
}
|
||||
$schema->addColumn($row['Field'], $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertIndexDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$type = null;
|
||||
$columns = $length = [];
|
||||
|
||||
$name = $row['Key_name'];
|
||||
if ($name === 'PRIMARY') {
|
||||
$name = $type = TableSchema::CONSTRAINT_PRIMARY;
|
||||
}
|
||||
|
||||
if (!empty($row['Column_name'])) {
|
||||
$columns[] = $row['Column_name'];
|
||||
}
|
||||
|
||||
if ($row['Index_type'] === 'FULLTEXT') {
|
||||
$type = TableSchema::INDEX_FULLTEXT;
|
||||
} elseif ((int)$row['Non_unique'] === 0 && $type !== 'primary') {
|
||||
$type = TableSchema::CONSTRAINT_UNIQUE;
|
||||
} elseif ($type !== 'primary') {
|
||||
$type = TableSchema::INDEX_INDEX;
|
||||
}
|
||||
|
||||
if (!empty($row['Sub_part'])) {
|
||||
$length[$row['Column_name']] = $row['Sub_part'];
|
||||
}
|
||||
$isIndex = (
|
||||
$type === TableSchema::INDEX_INDEX ||
|
||||
$type === TableSchema::INDEX_FULLTEXT
|
||||
);
|
||||
if ($isIndex) {
|
||||
$existing = $schema->getIndex($name);
|
||||
} else {
|
||||
$existing = $schema->getConstraint($name);
|
||||
}
|
||||
|
||||
// MySQL multi column indexes come back as multiple rows.
|
||||
if (!empty($existing)) {
|
||||
$columns = array_merge($existing['columns'], $columns);
|
||||
$length = array_merge($existing['length'], $length);
|
||||
}
|
||||
if ($isIndex) {
|
||||
$schema->addIndex($name, [
|
||||
'type' => $type,
|
||||
'columns' => $columns,
|
||||
'length' => $length,
|
||||
]);
|
||||
} else {
|
||||
$schema->addConstraint($name, [
|
||||
'type' => $type,
|
||||
'columns' => $columns,
|
||||
'length' => $length,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeForeignKeySql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = 'SELECT * FROM information_schema.key_column_usage AS kcu
|
||||
INNER JOIN information_schema.referential_constraints AS rc
|
||||
ON (
|
||||
kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
||||
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
||||
)
|
||||
WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ?
|
||||
ORDER BY kcu.ORDINAL_POSITION ASC';
|
||||
|
||||
return [$sql, [$config['database'], $tableName, $tableName]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$data = [
|
||||
'type' => TableSchema::CONSTRAINT_FOREIGN,
|
||||
'columns' => [$row['COLUMN_NAME']],
|
||||
'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']],
|
||||
'update' => $this->_convertOnClause($row['UPDATE_RULE']),
|
||||
'delete' => $this->_convertOnClause($row['DELETE_RULE']),
|
||||
];
|
||||
$name = $row['CONSTRAINT_NAME'];
|
||||
$schema->addConstraint($name, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function truncateTableSql(TableSchema $schema): array
|
||||
{
|
||||
return [sprintf('TRUNCATE TABLE `%s`', $schema->name())];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
|
||||
{
|
||||
$content = implode(",\n", array_merge($columns, $constraints, $indexes));
|
||||
$temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
|
||||
$content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content);
|
||||
$options = $schema->getOptions();
|
||||
if (isset($options['engine'])) {
|
||||
$content .= sprintf(' ENGINE=%s', $options['engine']);
|
||||
}
|
||||
if (isset($options['charset'])) {
|
||||
$content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']);
|
||||
}
|
||||
if (isset($options['collate'])) {
|
||||
$content .= sprintf(' COLLATE=%s', $options['collate']);
|
||||
}
|
||||
|
||||
return [$content];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getColumn($name);
|
||||
|
||||
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
|
||||
if ($sql !== null) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$out = $this->_driver->quoteIdentifier($name);
|
||||
$nativeJson = $this->_driver->supports(DriverInterface::FEATURE_JSON);
|
||||
|
||||
$typeMap = [
|
||||
TableSchema::TYPE_TINYINTEGER => ' TINYINT',
|
||||
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
|
||||
TableSchema::TYPE_INTEGER => ' INTEGER',
|
||||
TableSchema::TYPE_BIGINTEGER => ' BIGINT',
|
||||
TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
|
||||
TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
|
||||
TableSchema::TYPE_FLOAT => ' FLOAT',
|
||||
TableSchema::TYPE_DECIMAL => ' DECIMAL',
|
||||
TableSchema::TYPE_DATE => ' DATE',
|
||||
TableSchema::TYPE_TIME => ' TIME',
|
||||
TableSchema::TYPE_DATETIME => ' DATETIME',
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME',
|
||||
TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMP',
|
||||
TableSchema::TYPE_CHAR => ' CHAR',
|
||||
TableSchema::TYPE_UUID => ' CHAR(36)',
|
||||
TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT',
|
||||
];
|
||||
$specialMap = [
|
||||
'string' => true,
|
||||
'text' => true,
|
||||
'char' => true,
|
||||
'binary' => true,
|
||||
];
|
||||
if (isset($typeMap[$data['type']])) {
|
||||
$out .= $typeMap[$data['type']];
|
||||
}
|
||||
if (isset($specialMap[$data['type']])) {
|
||||
switch ($data['type']) {
|
||||
case TableSchema::TYPE_STRING:
|
||||
$out .= ' VARCHAR';
|
||||
if (!isset($data['length'])) {
|
||||
$data['length'] = 255;
|
||||
}
|
||||
break;
|
||||
case TableSchema::TYPE_TEXT:
|
||||
$isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
|
||||
if (empty($data['length']) || !$isKnownLength) {
|
||||
$out .= ' TEXT';
|
||||
break;
|
||||
}
|
||||
|
||||
/** @var string $length */
|
||||
$length = array_search($data['length'], TableSchema::$columnLengths);
|
||||
$out .= ' ' . strtoupper($length) . 'TEXT';
|
||||
|
||||
break;
|
||||
case TableSchema::TYPE_BINARY:
|
||||
$isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
|
||||
if ($isKnownLength) {
|
||||
/** @var string $length */
|
||||
$length = array_search($data['length'], TableSchema::$columnLengths);
|
||||
$out .= ' ' . strtoupper($length) . 'BLOB';
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($data['length'])) {
|
||||
$out .= ' BLOB';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($data['length'] > 2) {
|
||||
$out .= ' VARBINARY(' . $data['length'] . ')';
|
||||
} else {
|
||||
$out .= ' BINARY(' . $data['length'] . ')';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$hasLength = [
|
||||
TableSchema::TYPE_INTEGER,
|
||||
TableSchema::TYPE_CHAR,
|
||||
TableSchema::TYPE_SMALLINTEGER,
|
||||
TableSchema::TYPE_TINYINTEGER,
|
||||
TableSchema::TYPE_STRING,
|
||||
];
|
||||
if (in_array($data['type'], $hasLength, true) && isset($data['length'])) {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
|
||||
$lengthAndPrecisionTypes = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
|
||||
if (in_array($data['type'], $lengthAndPrecisionTypes, true) && isset($data['length'])) {
|
||||
if (isset($data['precision'])) {
|
||||
$out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
|
||||
} else {
|
||||
$out .= '(' . (int)$data['length'] . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$precisionTypes = [TableSchema::TYPE_DATETIME_FRACTIONAL, TableSchema::TYPE_TIMESTAMP_FRACTIONAL];
|
||||
if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
|
||||
$out .= '(' . (int)$data['precision'] . ')';
|
||||
}
|
||||
|
||||
$hasUnsigned = [
|
||||
TableSchema::TYPE_TINYINTEGER,
|
||||
TableSchema::TYPE_SMALLINTEGER,
|
||||
TableSchema::TYPE_INTEGER,
|
||||
TableSchema::TYPE_BIGINTEGER,
|
||||
TableSchema::TYPE_FLOAT,
|
||||
TableSchema::TYPE_DECIMAL,
|
||||
];
|
||||
if (
|
||||
in_array($data['type'], $hasUnsigned, true) &&
|
||||
isset($data['unsigned']) &&
|
||||
$data['unsigned'] === true
|
||||
) {
|
||||
$out .= ' UNSIGNED';
|
||||
}
|
||||
|
||||
$hasCollate = [
|
||||
TableSchema::TYPE_TEXT,
|
||||
TableSchema::TYPE_CHAR,
|
||||
TableSchema::TYPE_STRING,
|
||||
];
|
||||
if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
|
||||
$out .= ' COLLATE ' . $data['collate'];
|
||||
}
|
||||
|
||||
if (isset($data['null']) && $data['null'] === false) {
|
||||
$out .= ' NOT NULL';
|
||||
}
|
||||
$addAutoIncrement = (
|
||||
$schema->getPrimaryKey() === [$name] &&
|
||||
!$schema->hasAutoincrement() &&
|
||||
!isset($data['autoIncrement'])
|
||||
);
|
||||
if (
|
||||
in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) &&
|
||||
(
|
||||
$data['autoIncrement'] === true ||
|
||||
$addAutoIncrement
|
||||
)
|
||||
) {
|
||||
$out .= ' AUTO_INCREMENT';
|
||||
}
|
||||
|
||||
$timestampTypes = [
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
|
||||
];
|
||||
if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) {
|
||||
$out .= ' NULL';
|
||||
unset($data['default']);
|
||||
}
|
||||
|
||||
$dateTimeTypes = [
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
|
||||
];
|
||||
if (
|
||||
isset($data['default']) &&
|
||||
in_array($data['type'], $dateTimeTypes) &&
|
||||
strpos(strtolower($data['default']), 'current_timestamp') !== false
|
||||
) {
|
||||
$out .= ' DEFAULT CURRENT_TIMESTAMP';
|
||||
if (isset($data['precision'])) {
|
||||
$out .= '(' . $data['precision'] . ')';
|
||||
}
|
||||
unset($data['default']);
|
||||
}
|
||||
if (isset($data['default'])) {
|
||||
$out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
|
||||
unset($data['default']);
|
||||
}
|
||||
if (isset($data['comment']) && $data['comment'] !== '') {
|
||||
$out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function constraintSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getConstraint($name);
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
|
||||
$columns = array_map(
|
||||
[$this->_driver, 'quoteIdentifier'],
|
||||
$data['columns']
|
||||
);
|
||||
|
||||
return sprintf('PRIMARY KEY (%s)', implode(', ', $columns));
|
||||
}
|
||||
|
||||
$out = '';
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
|
||||
$out = 'UNIQUE KEY ';
|
||||
}
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
|
||||
$out = 'CONSTRAINT ';
|
||||
}
|
||||
$out .= $this->_driver->quoteIdentifier($name);
|
||||
|
||||
return $this->_keySql($out, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addConstraintSql(TableSchema $schema): array
|
||||
{
|
||||
$sqlPattern = 'ALTER TABLE %s ADD %s;';
|
||||
$sql = [];
|
||||
|
||||
foreach ($schema->constraints() as $name) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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 FOREIGN KEY %s;';
|
||||
$sql = [];
|
||||
|
||||
foreach ($schema->constraints() as $name) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getIndex($name);
|
||||
$out = '';
|
||||
if ($data['type'] === TableSchema::INDEX_INDEX) {
|
||||
$out = 'KEY ';
|
||||
}
|
||||
if ($data['type'] === TableSchema::INDEX_FULLTEXT) {
|
||||
$out = 'FULLTEXT KEY ';
|
||||
}
|
||||
$out .= $this->_driver->quoteIdentifier($name);
|
||||
|
||||
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']
|
||||
);
|
||||
foreach ($data['columns'] as $i => $column) {
|
||||
if (isset($data['length'][$column])) {
|
||||
$columns[$i] .= sprintf('(%d)', $data['length'][$column]);
|
||||
}
|
||||
}
|
||||
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) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Schema\MysqlSchemaDialect',
|
||||
'Cake\Database\Schema\MysqlSchema'
|
||||
);
|
||||
// phpcs:enable
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\Schema\PostgresSchema is deprecated. ' .
|
||||
'Use Cake\Database\Schema\PostgresSchemaDialect instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Schema\PostgresSchemaDialect');
|
||||
@@ -0,0 +1,704 @@
|
||||
<?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\Exception\DatabaseException;
|
||||
|
||||
/**
|
||||
* Schema management/reflection features for Postgres.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PostgresSchemaDialect extends SchemaDialect
|
||||
{
|
||||
/**
|
||||
* 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 as name FROM information_schema.tables
|
||||
WHERE table_schema = ? ORDER BY name';
|
||||
$schema = empty($config['schema']) ? 'public' : $config['schema'];
|
||||
|
||||
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 as name FROM information_schema.tables
|
||||
WHERE table_schema = ? AND table_type = \'BASE TABLE\' ORDER BY name';
|
||||
$schema = empty($config['schema']) ? 'public' : $config['schema'];
|
||||
|
||||
return [$sql, [$schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumnSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = 'SELECT DISTINCT table_schema AS schema,
|
||||
column_name AS name,
|
||||
data_type AS type,
|
||||
is_nullable AS null, column_default AS default,
|
||||
character_maximum_length AS char_length,
|
||||
c.collation_name,
|
||||
d.description as comment,
|
||||
ordinal_position,
|
||||
c.datetime_precision,
|
||||
c.numeric_precision as column_precision,
|
||||
c.numeric_scale as column_scale,
|
||||
pg_get_serial_sequence(attr.attrelid::regclass::text, attr.attname) IS NOT NULL AS has_serial
|
||||
FROM information_schema.columns c
|
||||
INNER JOIN pg_catalog.pg_namespace ns ON (ns.nspname = table_schema)
|
||||
INNER JOIN pg_catalog.pg_class cl ON (cl.relnamespace = ns.oid AND cl.relname = table_name)
|
||||
LEFT JOIN pg_catalog.pg_index i ON (i.indrelid = cl.oid AND i.indkey[0] = c.ordinal_position)
|
||||
LEFT JOIN pg_catalog.pg_description d on (cl.oid = d.objoid AND d.objsubid = c.ordinal_position)
|
||||
LEFT JOIN pg_catalog.pg_attribute attr ON (cl.oid = attr.attrelid AND column_name = attr.attname)
|
||||
WHERE table_name = ? AND table_schema = ? AND table_catalog = ?
|
||||
ORDER BY ordinal_position';
|
||||
|
||||
$schema = empty($config['schema']) ? 'public' : $config['schema'];
|
||||
|
||||
return [$sql, [$tableName, $schema, $config['database']]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a column definition to the abstract types.
|
||||
*
|
||||
* The returned type will be a type that
|
||||
* Cake\Database\TypeFactory can handle.
|
||||
*
|
||||
* @param string $column The column type + length
|
||||
* @throws \Cake\Database\Exception\DatabaseException when column cannot be parsed.
|
||||
* @return array<string, mixed> Array of column information.
|
||||
*/
|
||||
protected function _convertColumn(string $column): array
|
||||
{
|
||||
preg_match('/([a-z\s]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
|
||||
if (empty($matches)) {
|
||||
throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
|
||||
}
|
||||
|
||||
$col = strtolower($matches[1]);
|
||||
$length = $precision = $scale = null;
|
||||
if (isset($matches[2])) {
|
||||
$length = (int)$matches[2];
|
||||
}
|
||||
|
||||
$type = $this->_applyTypeSpecificColumnConversion(
|
||||
$col,
|
||||
compact('length', 'precision', 'scale')
|
||||
);
|
||||
if ($type !== null) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (in_array($col, ['date', 'time', 'boolean'], true)) {
|
||||
return ['type' => $col, 'length' => null];
|
||||
}
|
||||
if (in_array($col, ['timestamptz', 'timestamp with time zone'], true)) {
|
||||
return ['type' => TableSchema::TYPE_TIMESTAMP_TIMEZONE, 'length' => null];
|
||||
}
|
||||
if (strpos($col, 'timestamp') !== false) {
|
||||
return ['type' => TableSchema::TYPE_TIMESTAMP_FRACTIONAL, 'length' => null];
|
||||
}
|
||||
if (strpos($col, 'time') !== false) {
|
||||
return ['type' => TableSchema::TYPE_TIME, 'length' => null];
|
||||
}
|
||||
if ($col === 'serial' || $col === 'integer') {
|
||||
return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10];
|
||||
}
|
||||
if ($col === 'bigserial' || $col === 'bigint') {
|
||||
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20];
|
||||
}
|
||||
if ($col === 'smallint') {
|
||||
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5];
|
||||
}
|
||||
if ($col === 'inet') {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => 39];
|
||||
}
|
||||
if ($col === 'uuid') {
|
||||
return ['type' => TableSchema::TYPE_UUID, 'length' => null];
|
||||
}
|
||||
if ($col === 'char') {
|
||||
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'character') !== false) {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
|
||||
}
|
||||
// money is 'string' as it includes arbitrary text content
|
||||
// before the number value.
|
||||
if (strpos($col, 'money') !== false || $col === 'string') {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'text') !== false) {
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
if ($col === 'bytea') {
|
||||
return ['type' => TableSchema::TYPE_BINARY, 'length' => null];
|
||||
}
|
||||
if ($col === 'real' || strpos($col, 'double') !== false) {
|
||||
return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
|
||||
}
|
||||
if (
|
||||
strpos($col, 'numeric') !== false ||
|
||||
strpos($col, 'decimal') !== false
|
||||
) {
|
||||
return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null];
|
||||
}
|
||||
|
||||
if (strpos($col, 'json') !== false) {
|
||||
return ['type' => TableSchema::TYPE_JSON, 'length' => null];
|
||||
}
|
||||
|
||||
$length = is_numeric($length) ? $length : null;
|
||||
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertColumnDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$field = $this->_convertColumn($row['type']);
|
||||
|
||||
if ($field['type'] === TableSchema::TYPE_BOOLEAN) {
|
||||
if ($row['default'] === 'true') {
|
||||
$row['default'] = 1;
|
||||
}
|
||||
if ($row['default'] === 'false') {
|
||||
$row['default'] = 0;
|
||||
}
|
||||
}
|
||||
if (!empty($row['has_serial'])) {
|
||||
$field['autoIncrement'] = true;
|
||||
}
|
||||
|
||||
$field += [
|
||||
'default' => $this->_defaultValue($row['default']),
|
||||
'null' => $row['null'] === 'YES',
|
||||
'collate' => $row['collation_name'],
|
||||
'comment' => $row['comment'],
|
||||
];
|
||||
$field['length'] = $row['char_length'] ?: $field['length'];
|
||||
|
||||
if ($field['type'] === 'numeric' || $field['type'] === 'decimal') {
|
||||
$field['length'] = $row['column_precision'];
|
||||
$field['precision'] = $row['column_scale'] ?: null;
|
||||
}
|
||||
|
||||
if ($field['type'] === TableSchema::TYPE_TIMESTAMP_FRACTIONAL) {
|
||||
$field['precision'] = $row['datetime_precision'];
|
||||
if ($field['precision'] === 0) {
|
||||
$field['type'] = TableSchema::TYPE_TIMESTAMP;
|
||||
}
|
||||
}
|
||||
|
||||
if ($field['type'] === TableSchema::TYPE_TIMESTAMP_TIMEZONE) {
|
||||
$field['precision'] = $row['datetime_precision'];
|
||||
}
|
||||
|
||||
$schema->addColumn($row['name'], $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulate the default value.
|
||||
*
|
||||
* Postgres includes sequence data and casting information in default values.
|
||||
* We need to remove those.
|
||||
*
|
||||
* @param string|int|null $default The default value.
|
||||
* @return string|int|null
|
||||
*/
|
||||
protected function _defaultValue($default)
|
||||
{
|
||||
if (is_numeric($default) || $default === null) {
|
||||
return $default;
|
||||
}
|
||||
// Sequences
|
||||
if (strpos($default, 'nextval') === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strpos($default, 'NULL::') === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove quotes and postgres casts
|
||||
return preg_replace(
|
||||
"/^'(.*)'(?:::.*)$/",
|
||||
'$1',
|
||||
$default
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeIndexSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = 'SELECT
|
||||
c2.relname,
|
||||
a.attname,
|
||||
i.indisprimary,
|
||||
i.indisunique
|
||||
FROM pg_catalog.pg_namespace n
|
||||
INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace)
|
||||
INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid)
|
||||
INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid)
|
||||
INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass)
|
||||
WHERE n.nspname = ?
|
||||
AND a.attnum = ANY(i.indkey)
|
||||
AND c.relname = ?
|
||||
ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum';
|
||||
|
||||
$schema = 'public';
|
||||
if (!empty($config['schema'])) {
|
||||
$schema = $config['schema'];
|
||||
}
|
||||
|
||||
return [$sql, [$schema, $tableName]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertIndexDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$type = TableSchema::INDEX_INDEX;
|
||||
$name = $row['relname'];
|
||||
if ($row['indisprimary']) {
|
||||
$name = $type = TableSchema::CONSTRAINT_PRIMARY;
|
||||
}
|
||||
if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) {
|
||||
$type = TableSchema::CONSTRAINT_UNIQUE;
|
||||
}
|
||||
if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
|
||||
$this->_convertConstraint($schema, $name, $type, $row);
|
||||
|
||||
return;
|
||||
}
|
||||
$index = $schema->getIndex($name);
|
||||
if (!$index) {
|
||||
$index = [
|
||||
'type' => $type,
|
||||
'columns' => [],
|
||||
];
|
||||
}
|
||||
$index['columns'][] = $row['attname'];
|
||||
$schema->addIndex($name, $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/update a constraint into the schema object.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table to update.
|
||||
* @param string $name The index name.
|
||||
* @param string $type The index type.
|
||||
* @param array $row The metadata record to update with.
|
||||
* @return void
|
||||
*/
|
||||
protected function _convertConstraint(TableSchema $schema, string $name, string $type, array $row): void
|
||||
{
|
||||
$constraint = $schema->getConstraint($name);
|
||||
if (!$constraint) {
|
||||
$constraint = [
|
||||
'type' => $type,
|
||||
'columns' => [],
|
||||
];
|
||||
}
|
||||
$constraint['columns'][] = $row['attname'];
|
||||
$schema->addConstraint($name, $constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeForeignKeySql(string $tableName, array $config): array
|
||||
{
|
||||
// phpcs:disable Generic.Files.LineLength
|
||||
$sql = 'SELECT
|
||||
c.conname AS name,
|
||||
c.contype AS type,
|
||||
a.attname AS column_name,
|
||||
c.confmatchtype AS match_type,
|
||||
c.confupdtype AS on_update,
|
||||
c.confdeltype AS on_delete,
|
||||
c.confrelid::regclass AS references_table,
|
||||
ab.attname AS references_field
|
||||
FROM pg_catalog.pg_namespace n
|
||||
INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace)
|
||||
INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace)
|
||||
INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey))
|
||||
INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey))
|
||||
WHERE n.nspname = ?
|
||||
AND cl.relname = ?
|
||||
ORDER BY name, a.attnum, ab.attnum DESC';
|
||||
// phpcs:enable Generic.Files.LineLength
|
||||
|
||||
$schema = empty($config['schema']) ? 'public' : $config['schema'];
|
||||
|
||||
return [$sql, [$schema, $tableName]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$data = [
|
||||
'type' => TableSchema::CONSTRAINT_FOREIGN,
|
||||
'columns' => $row['column_name'],
|
||||
'references' => [$row['references_table'], $row['references_field']],
|
||||
'update' => $this->_convertOnClause($row['on_update']),
|
||||
'delete' => $this->_convertOnClause($row['on_delete']),
|
||||
];
|
||||
$schema->addConstraint($row['name'], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _convertOnClause(string $clause): string
|
||||
{
|
||||
if ($clause === 'r') {
|
||||
return TableSchema::ACTION_RESTRICT;
|
||||
}
|
||||
if ($clause === 'a') {
|
||||
return TableSchema::ACTION_NO_ACTION;
|
||||
}
|
||||
if ($clause === 'c') {
|
||||
return TableSchema::ACTION_CASCADE;
|
||||
}
|
||||
|
||||
return TableSchema::ACTION_SET_NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getColumn($name);
|
||||
|
||||
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
|
||||
if ($sql !== null) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$out = $this->_driver->quoteIdentifier($name);
|
||||
$typeMap = [
|
||||
TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
|
||||
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
|
||||
TableSchema::TYPE_BINARY_UUID => ' UUID',
|
||||
TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
|
||||
TableSchema::TYPE_FLOAT => ' FLOAT',
|
||||
TableSchema::TYPE_DECIMAL => ' DECIMAL',
|
||||
TableSchema::TYPE_DATE => ' DATE',
|
||||
TableSchema::TYPE_TIME => ' TIME',
|
||||
TableSchema::TYPE_DATETIME => ' TIMESTAMP',
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTZ',
|
||||
TableSchema::TYPE_UUID => ' UUID',
|
||||
TableSchema::TYPE_CHAR => ' CHAR',
|
||||
TableSchema::TYPE_JSON => ' JSONB',
|
||||
];
|
||||
|
||||
if (isset($typeMap[$data['type']])) {
|
||||
$out .= $typeMap[$data['type']];
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
|
||||
$type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT';
|
||||
if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) {
|
||||
$type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL';
|
||||
unset($data['null'], $data['default']);
|
||||
}
|
||||
$out .= $type;
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
|
||||
$out .= ' TEXT';
|
||||
}
|
||||
if ($data['type'] === TableSchema::TYPE_BINARY) {
|
||||
$out .= ' BYTEA';
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_CHAR) {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
|
||||
if (
|
||||
$data['type'] === TableSchema::TYPE_STRING ||
|
||||
(
|
||||
$data['type'] === TableSchema::TYPE_TEXT &&
|
||||
$data['length'] === TableSchema::LENGTH_TINY
|
||||
)
|
||||
) {
|
||||
$out .= ' VARCHAR';
|
||||
if (isset($data['length']) && $data['length'] !== '') {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR];
|
||||
if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
|
||||
$out .= ' COLLATE "' . $data['collate'] . '"';
|
||||
}
|
||||
|
||||
$hasPrecision = [
|
||||
TableSchema::TYPE_FLOAT,
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
|
||||
];
|
||||
if (in_array($data['type'], $hasPrecision) && isset($data['precision'])) {
|
||||
$out .= '(' . $data['precision'] . ')';
|
||||
}
|
||||
|
||||
if (
|
||||
$data['type'] === TableSchema::TYPE_DECIMAL &&
|
||||
(
|
||||
isset($data['length']) ||
|
||||
isset($data['precision'])
|
||||
)
|
||||
) {
|
||||
$out .= '(' . $data['length'] . ',' . (int)$data['precision'] . ')';
|
||||
}
|
||||
|
||||
if (isset($data['null']) && $data['null'] === false) {
|
||||
$out .= ' NOT NULL';
|
||||
}
|
||||
|
||||
$datetimeTypes = [
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
|
||||
];
|
||||
if (
|
||||
isset($data['default']) &&
|
||||
in_array($data['type'], $datetimeTypes) &&
|
||||
strtolower($data['default']) === 'current_timestamp'
|
||||
) {
|
||||
$out .= ' DEFAULT CURRENT_TIMESTAMP';
|
||||
} elseif (isset($data['default'])) {
|
||||
$defaultValue = $data['default'];
|
||||
if ($data['type'] === 'boolean') {
|
||||
$defaultValue = (bool)$defaultValue;
|
||||
}
|
||||
$out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
|
||||
} elseif (isset($data['null']) && $data['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) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getIndex($name);
|
||||
$columns = array_map(
|
||||
[$this->_driver, 'quoteIdentifier'],
|
||||
$data['columns']
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'CREATE INDEX %s ON %s (%s)',
|
||||
$this->_driver->quoteIdentifier($name),
|
||||
$this->_driver->quoteIdentifier($schema->name()),
|
||||
implode(', ', $columns)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function constraintSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $schema->getConstraint($name);
|
||||
$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<string, mixed> $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 DEFERRABLE INITIALLY IMMEDIATE',
|
||||
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());
|
||||
$dbSchema = $this->_driver->schema();
|
||||
if ($dbSchema != 'public') {
|
||||
$tableName = $this->_driver->quoteIdentifier($dbSchema) . '.' . $tableName;
|
||||
}
|
||||
$temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
|
||||
$out = [];
|
||||
$out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
|
||||
foreach ($indexes as $index) {
|
||||
$out[] = $index;
|
||||
}
|
||||
foreach ($schema->columns() as $column) {
|
||||
$columnData = $schema->getColumn($column);
|
||||
if (isset($columnData['comment'])) {
|
||||
$out[] = sprintf(
|
||||
'COMMENT ON COLUMN %s.%s IS %s',
|
||||
$tableName,
|
||||
$this->_driver->quoteIdentifier($column),
|
||||
$this->_driver->schemaValue($columnData['comment'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function truncateTableSql(TableSchema $schema): array
|
||||
{
|
||||
$name = $this->_driver->quoteIdentifier($schema->name());
|
||||
|
||||
return [
|
||||
sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL to drop a table.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema Table instance
|
||||
* @return array SQL statements to drop a table.
|
||||
*/
|
||||
public function dropTableSql(TableSchema $schema): array
|
||||
{
|
||||
$sql = sprintf(
|
||||
'DROP TABLE %s CASCADE',
|
||||
$this->_driver->quoteIdentifier($schema->name())
|
||||
);
|
||||
|
||||
return [$sql];
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Schema\PostgresSchemaDialect',
|
||||
'Cake\Database\Schema\PostgresSchema'
|
||||
);
|
||||
// phpcs:enable
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
<?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\DriverInterface;
|
||||
use Cake\Database\Type\ColumnSchemaAwareInterface;
|
||||
use Cake\Database\TypeFactory;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class for schema implementations.
|
||||
*
|
||||
* This class contains methods that are common across
|
||||
* the various SQL dialects.
|
||||
*
|
||||
* @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\DriverInterface
|
||||
*/
|
||||
protected $_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\DriverInterface $driver The driver to use.
|
||||
*/
|
||||
public function __construct(DriverInterface $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($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::getMap($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|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::getMap($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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Schema\SchemaDialect',
|
||||
'Cake\Database\Schema\BaseSchema'
|
||||
);
|
||||
// phpcs:enable
|
||||
@@ -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;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\Schema\SqliteSchema is deprecated. ' .
|
||||
'Use Cake\Database\Schema\SqliteSchemaDialect instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Schema\SqliteSchemaDialect');
|
||||
@@ -0,0 +1,649 @@
|
||||
<?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\Exception\DatabaseException;
|
||||
|
||||
/**
|
||||
* Schema management/reflection features for Sqlite
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqliteSchemaDialect extends SchemaDialect
|
||||
{
|
||||
/**
|
||||
* Array containing the foreign keys constraints names
|
||||
* Necessary for composite foreign keys to be handled
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_constraintsIdMap = [];
|
||||
|
||||
/**
|
||||
* Whether there is any table in this connection to SQLite containing sequences.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_hasSequences;
|
||||
|
||||
/**
|
||||
* Convert a column definition to the abstract types.
|
||||
*
|
||||
* The returned type will be a type that
|
||||
* Cake\Database\TypeFactory can handle.
|
||||
*
|
||||
* @param string $column The column type + length
|
||||
* @throws \Cake\Database\Exception\DatabaseException when unable to parse column type
|
||||
* @return array<string, mixed> Array of column information.
|
||||
*/
|
||||
protected function _convertColumn(string $column): array
|
||||
{
|
||||
if ($column === '') {
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
preg_match('/(unsigned)?\s*([a-z]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
|
||||
if (empty($matches)) {
|
||||
throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
|
||||
}
|
||||
|
||||
$unsigned = false;
|
||||
if (strtolower($matches[1]) === 'unsigned') {
|
||||
$unsigned = true;
|
||||
}
|
||||
|
||||
$col = strtolower($matches[2]);
|
||||
$length = $precision = $scale = null;
|
||||
if (isset($matches[3])) {
|
||||
$length = $matches[3];
|
||||
if (strpos($length, ',') !== false) {
|
||||
[$length, $precision] = explode(',', $length);
|
||||
}
|
||||
$length = (int)$length;
|
||||
$precision = (int)$precision;
|
||||
}
|
||||
|
||||
$type = $this->_applyTypeSpecificColumnConversion(
|
||||
$col,
|
||||
compact('length', 'precision', 'scale')
|
||||
);
|
||||
if ($type !== null) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if ($col === 'bigint') {
|
||||
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned];
|
||||
}
|
||||
if ($col === 'smallint') {
|
||||
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned];
|
||||
}
|
||||
if ($col === 'tinyint') {
|
||||
return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned];
|
||||
}
|
||||
if (strpos($col, 'int') !== false) {
|
||||
return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned];
|
||||
}
|
||||
if (strpos($col, 'decimal') !== false) {
|
||||
return [
|
||||
'type' => TableSchema::TYPE_DECIMAL,
|
||||
'length' => $length,
|
||||
'precision' => $precision,
|
||||
'unsigned' => $unsigned,
|
||||
];
|
||||
}
|
||||
if (in_array($col, ['float', 'real', 'double'])) {
|
||||
return [
|
||||
'type' => TableSchema::TYPE_FLOAT,
|
||||
'length' => $length,
|
||||
'precision' => $precision,
|
||||
'unsigned' => $unsigned,
|
||||
];
|
||||
}
|
||||
|
||||
if (strpos($col, 'boolean') !== false) {
|
||||
return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
|
||||
}
|
||||
|
||||
if (($col === 'char' && $length === 36) || $col === 'uuid') {
|
||||
return ['type' => TableSchema::TYPE_UUID, 'length' => null];
|
||||
}
|
||||
if ($col === 'char') {
|
||||
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
if (strpos($col, 'char') !== false) {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
|
||||
}
|
||||
|
||||
if ($col === 'binary' && $length === 16) {
|
||||
return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
|
||||
}
|
||||
if (in_array($col, ['blob', 'clob', 'binary', 'varbinary'])) {
|
||||
return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
|
||||
}
|
||||
|
||||
$datetimeTypes = [
|
||||
'date',
|
||||
'time',
|
||||
'timestamp',
|
||||
'timestampfractional',
|
||||
'timestamptimezone',
|
||||
'datetime',
|
||||
'datetimefractional',
|
||||
];
|
||||
if (in_array($col, $datetimeTypes)) {
|
||||
return ['type' => $col, 'length' => null];
|
||||
}
|
||||
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return [
|
||||
'SELECT name FROM sqlite_master ' .
|
||||
'WHERE (type="table" OR type="view") ' .
|
||||
'AND name != "sqlite_sequence" ORDER BY name',
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return [
|
||||
'SELECT name FROM sqlite_master WHERE type="table" ' .
|
||||
'AND name != "sqlite_sequence" ORDER BY name',
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumnSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = sprintf(
|
||||
'PRAGMA table_info(%s)',
|
||||
$this->_driver->quoteIdentifier($tableName)
|
||||
);
|
||||
|
||||
return [$sql, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertColumnDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$field = $this->_convertColumn($row['type']);
|
||||
$field += [
|
||||
'null' => !$row['notnull'],
|
||||
'default' => $this->_defaultValue($row['dflt_value']),
|
||||
];
|
||||
$primary = $schema->getConstraint('primary');
|
||||
|
||||
if ($row['pk'] && empty($primary)) {
|
||||
$field['null'] = false;
|
||||
$field['autoIncrement'] = true;
|
||||
}
|
||||
|
||||
// SQLite does not support autoincrement on composite keys.
|
||||
if ($row['pk'] && !empty($primary)) {
|
||||
$existingColumn = $primary['columns'][0];
|
||||
/** @psalm-suppress PossiblyNullOperand */
|
||||
$schema->addColumn($existingColumn, ['autoIncrement' => null] + $schema->getColumn($existingColumn));
|
||||
}
|
||||
|
||||
$schema->addColumn($row['name'], $field);
|
||||
if ($row['pk']) {
|
||||
$constraint = (array)$schema->getConstraint('primary') + [
|
||||
'type' => TableSchema::CONSTRAINT_PRIMARY,
|
||||
'columns' => [],
|
||||
];
|
||||
$constraint['columns'] = array_merge($constraint['columns'], [$row['name']]);
|
||||
$schema->addConstraint('primary', $constraint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulate the default value.
|
||||
*
|
||||
* Sqlite includes quotes and bared NULLs in default values.
|
||||
* We need to remove those.
|
||||
*
|
||||
* @param string|int|null $default The default value.
|
||||
* @return string|int|null
|
||||
*/
|
||||
protected function _defaultValue($default)
|
||||
{
|
||||
if ($default === 'NULL' || $default === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove quotes
|
||||
if (is_string($default) && preg_match("/^'(.*)'$/", $default, $matches)) {
|
||||
return str_replace("''", "'", $matches[1]);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeIndexSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = sprintf(
|
||||
'PRAGMA index_list(%s)',
|
||||
$this->_driver->quoteIdentifier($tableName)
|
||||
);
|
||||
|
||||
return [$sql, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Since SQLite does not have a way to get metadata about all indexes at once,
|
||||
* additional queries are done here. Sqlite constraint names are not
|
||||
* stable, and the names for constraints will not match those used to create
|
||||
* the table. This is a limitation in Sqlite's metadata features.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public function convertIndexDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$sql = sprintf(
|
||||
'PRAGMA index_info(%s)',
|
||||
$this->_driver->quoteIdentifier($row['name'])
|
||||
);
|
||||
$statement = $this->_driver->prepare($sql);
|
||||
$statement->execute();
|
||||
$columns = [];
|
||||
/** @psalm-suppress PossiblyFalseIterator */
|
||||
foreach ($statement->fetchAll('assoc') as $column) {
|
||||
$columns[] = $column['name'];
|
||||
}
|
||||
$statement->closeCursor();
|
||||
if ($row['unique']) {
|
||||
$schema->addConstraint($row['name'], [
|
||||
'type' => TableSchema::CONSTRAINT_UNIQUE,
|
||||
'columns' => $columns,
|
||||
]);
|
||||
} else {
|
||||
$schema->addIndex($row['name'], [
|
||||
'type' => TableSchema::INDEX_INDEX,
|
||||
'columns' => $columns,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeForeignKeySql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = sprintf('PRAGMA foreign_key_list(%s)', $this->_driver->quoteIdentifier($tableName));
|
||||
|
||||
return [$sql, []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertForeignKeyDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$name = $row['from'] . '_fk';
|
||||
|
||||
$update = $row['on_update'] ?? '';
|
||||
$delete = $row['on_delete'] ?? '';
|
||||
$data = [
|
||||
'type' => TableSchema::CONSTRAINT_FOREIGN,
|
||||
'columns' => [$row['from']],
|
||||
'references' => [$row['table'], $row['to']],
|
||||
'update' => $this->_convertOnClause($update),
|
||||
'delete' => $this->_convertOnClause($delete),
|
||||
];
|
||||
|
||||
if (isset($this->_constraintsIdMap[$schema->name()][$row['id']])) {
|
||||
$name = $this->_constraintsIdMap[$schema->name()][$row['id']];
|
||||
} else {
|
||||
$this->_constraintsIdMap[$schema->name()][$row['id']] = $name;
|
||||
}
|
||||
|
||||
$schema->addConstraint($name, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @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.
|
||||
* @throws \Cake\Database\Exception\DatabaseException when the column type is unknown
|
||||
*/
|
||||
public function columnSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getColumn($name);
|
||||
|
||||
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
|
||||
if ($sql !== null) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$typeMap = [
|
||||
TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
|
||||
TableSchema::TYPE_UUID => ' CHAR(36)',
|
||||
TableSchema::TYPE_CHAR => ' CHAR',
|
||||
TableSchema::TYPE_TINYINTEGER => ' TINYINT',
|
||||
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
|
||||
TableSchema::TYPE_INTEGER => ' INTEGER',
|
||||
TableSchema::TYPE_BIGINTEGER => ' BIGINT',
|
||||
TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
|
||||
TableSchema::TYPE_FLOAT => ' FLOAT',
|
||||
TableSchema::TYPE_DECIMAL => ' DECIMAL',
|
||||
TableSchema::TYPE_DATE => ' DATE',
|
||||
TableSchema::TYPE_TIME => ' TIME',
|
||||
TableSchema::TYPE_DATETIME => ' DATETIME',
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIMEFRACTIONAL',
|
||||
TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMPFRACTIONAL',
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTIMEZONE',
|
||||
TableSchema::TYPE_JSON => ' TEXT',
|
||||
];
|
||||
|
||||
$out = $this->_driver->quoteIdentifier($name);
|
||||
$hasUnsigned = [
|
||||
TableSchema::TYPE_TINYINTEGER,
|
||||
TableSchema::TYPE_SMALLINTEGER,
|
||||
TableSchema::TYPE_INTEGER,
|
||||
TableSchema::TYPE_BIGINTEGER,
|
||||
TableSchema::TYPE_FLOAT,
|
||||
TableSchema::TYPE_DECIMAL,
|
||||
];
|
||||
|
||||
if (
|
||||
in_array($data['type'], $hasUnsigned, true) &&
|
||||
isset($data['unsigned']) &&
|
||||
$data['unsigned'] === true
|
||||
) {
|
||||
if ($data['type'] !== TableSchema::TYPE_INTEGER || $schema->getPrimaryKey() !== [$name]) {
|
||||
$out .= ' UNSIGNED';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($typeMap[$data['type']])) {
|
||||
$out .= $typeMap[$data['type']];
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
|
||||
$out .= ' TEXT';
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_CHAR) {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
|
||||
if (
|
||||
$data['type'] === TableSchema::TYPE_STRING ||
|
||||
(
|
||||
$data['type'] === TableSchema::TYPE_TEXT &&
|
||||
$data['length'] === TableSchema::LENGTH_TINY
|
||||
)
|
||||
) {
|
||||
$out .= ' VARCHAR';
|
||||
|
||||
if (isset($data['length'])) {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_BINARY) {
|
||||
if (isset($data['length'])) {
|
||||
$out .= ' BLOB(' . $data['length'] . ')';
|
||||
} else {
|
||||
$out .= ' BLOB';
|
||||
}
|
||||
}
|
||||
|
||||
$integerTypes = [
|
||||
TableSchema::TYPE_TINYINTEGER,
|
||||
TableSchema::TYPE_SMALLINTEGER,
|
||||
TableSchema::TYPE_INTEGER,
|
||||
];
|
||||
if (
|
||||
in_array($data['type'], $integerTypes, true) &&
|
||||
isset($data['length']) &&
|
||||
$schema->getPrimaryKey() !== [$name]
|
||||
) {
|
||||
$out .= '(' . (int)$data['length'] . ')';
|
||||
}
|
||||
|
||||
$hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
|
||||
if (
|
||||
in_array($data['type'], $hasPrecision, true) &&
|
||||
(
|
||||
isset($data['length']) ||
|
||||
isset($data['precision'])
|
||||
)
|
||||
) {
|
||||
$out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
|
||||
}
|
||||
|
||||
if (isset($data['null']) && $data['null'] === false) {
|
||||
$out .= ' NOT NULL';
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_INTEGER && $schema->getPrimaryKey() === [$name]) {
|
||||
$out .= ' PRIMARY KEY AUTOINCREMENT';
|
||||
}
|
||||
|
||||
$timestampTypes = [
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE,
|
||||
];
|
||||
if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) {
|
||||
$out .= ' DEFAULT NULL';
|
||||
}
|
||||
if (isset($data['default'])) {
|
||||
$out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Note integer primary keys will return ''. This is intentional as Sqlite requires
|
||||
* that integer primary keys be defined in the column definition.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public function constraintSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getConstraint($name);
|
||||
/** @psalm-suppress PossiblyNullArrayAccess */
|
||||
if (
|
||||
$data['type'] === TableSchema::CONSTRAINT_PRIMARY &&
|
||||
count($data['columns']) === 1 &&
|
||||
$schema->getColumn($data['columns'][0])['type'] === TableSchema::TYPE_INTEGER
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
$clause = '';
|
||||
$type = '';
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
|
||||
$type = 'PRIMARY KEY';
|
||||
}
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
|
||||
$type = 'UNIQUE';
|
||||
}
|
||||
if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
|
||||
$type = 'FOREIGN KEY';
|
||||
|
||||
$clause = sprintf(
|
||||
' REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
|
||||
$this->_driver->quoteIdentifier($data['references'][0]),
|
||||
$this->_convertConstraintColumns($data['references'][1]),
|
||||
$this->_foreignOnClause($data['update']),
|
||||
$this->_foreignOnClause($data['delete'])
|
||||
);
|
||||
}
|
||||
$columns = array_map(
|
||||
[$this->_driver, 'quoteIdentifier'],
|
||||
$data['columns']
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'CONSTRAINT %s %s (%s)%s',
|
||||
$this->_driver->quoteIdentifier($name),
|
||||
$type,
|
||||
implode(', ', $columns),
|
||||
$clause
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* SQLite can not properly handle adding a constraint to an existing table.
|
||||
* This method is no-op
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
|
||||
* @return array SQL fragment.
|
||||
*/
|
||||
public function addConstraintSql(TableSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* SQLite can not properly handle dropping a constraint to an existing table.
|
||||
* This method is no-op
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
|
||||
* @return array SQL fragment.
|
||||
*/
|
||||
public function dropConstraintSql(TableSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function indexSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getIndex($name);
|
||||
$columns = array_map(
|
||||
[$this->_driver, 'quoteIdentifier'],
|
||||
$data['columns']
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'CREATE INDEX %s ON %s (%s)',
|
||||
$this->_driver->quoteIdentifier($name),
|
||||
$this->_driver->quoteIdentifier($schema->name()),
|
||||
implode(', ', $columns)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
|
||||
{
|
||||
$lines = array_merge($columns, $constraints);
|
||||
$content = implode(",\n", array_filter($lines));
|
||||
$temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
|
||||
$table = sprintf("CREATE%sTABLE \"%s\" (\n%s\n)", $temporary, $schema->name(), $content);
|
||||
$out = [$table];
|
||||
foreach ($indexes as $index) {
|
||||
$out[] = $index;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function truncateTableSql(TableSchema $schema): array
|
||||
{
|
||||
$name = $schema->name();
|
||||
$sql = [];
|
||||
if ($this->hasSequences()) {
|
||||
$sql[] = sprintf('DELETE FROM sqlite_sequence WHERE name="%s"', $name);
|
||||
}
|
||||
|
||||
$sql[] = sprintf('DELETE FROM "%s"', $name);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether there is any table in this connection to SQLite containing
|
||||
* sequences
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSequences(): bool
|
||||
{
|
||||
$result = $this->_driver->prepare(
|
||||
'SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"'
|
||||
);
|
||||
$result->execute();
|
||||
$this->_hasSequences = (bool)$result->rowCount();
|
||||
$result->closeCursor();
|
||||
|
||||
return $this->_hasSequences;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Schema\SqliteSchemaDialect',
|
||||
'Cake\Database\Schema\SqliteSchema'
|
||||
);
|
||||
// phpcs:enable
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\Schema\SqlserverSchema is deprecated. ' .
|
||||
'Use Cake\Database\Schema\SqlServerSchemaDialect instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Schema\SqlServerSchemaDialect');
|
||||
@@ -0,0 +1,707 @@
|
||||
<?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 = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
|
||||
|
||||
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 = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
|
||||
|
||||
return [$sql, [$schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function describeColumnSql(string $tableName, array $config): array
|
||||
{
|
||||
$sql = '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]
|
||||
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]
|
||||
WHERE T.[name] = ? AND S.[name] = ?
|
||||
ORDER BY column_id';
|
||||
|
||||
$schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
|
||||
|
||||
return [$sql, [$tableName, $schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' => TableSchema::TYPE_DATETIME, 'length' => null];
|
||||
}
|
||||
if (strpos($col, 'datetime') !== false) {
|
||||
$typeName = TableSchema::TYPE_DATETIME;
|
||||
if ($scale > 0) {
|
||||
$typeName = TableSchema::TYPE_DATETIME_FRACTIONAL;
|
||||
}
|
||||
|
||||
return ['type' => $typeName, 'length' => null, 'precision' => $scale];
|
||||
}
|
||||
|
||||
if ($col === 'char') {
|
||||
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
|
||||
if ($col === 'tinyint') {
|
||||
return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $precision ?: 3];
|
||||
}
|
||||
if ($col === 'smallint') {
|
||||
return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $precision ?: 5];
|
||||
}
|
||||
if ($col === 'int' || $col === 'integer') {
|
||||
return ['type' => TableSchema::TYPE_INTEGER, 'length' => $precision ?: 10];
|
||||
}
|
||||
if ($col === 'bigint') {
|
||||
return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $precision ?: 20];
|
||||
}
|
||||
if ($col === 'bit') {
|
||||
return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
|
||||
}
|
||||
if (
|
||||
strpos($col, 'numeric') !== false ||
|
||||
strpos($col, 'money') !== false ||
|
||||
strpos($col, 'decimal') !== false
|
||||
) {
|
||||
return ['type' => TableSchema::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale];
|
||||
}
|
||||
|
||||
if ($col === 'real' || $col === 'float') {
|
||||
return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
|
||||
}
|
||||
// SqlServer schema reflection returns double length for unicode
|
||||
// columns because internally it uses UTF16/UCS2
|
||||
if ($col === 'nvarchar' || $col === 'nchar' || $col === 'ntext') {
|
||||
$length /= 2;
|
||||
}
|
||||
if (strpos($col, 'varchar') !== false && $length < 0) {
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
if (strpos($col, 'varchar') !== false) {
|
||||
return ['type' => TableSchema::TYPE_STRING, 'length' => $length ?: 255];
|
||||
}
|
||||
|
||||
if (strpos($col, 'char') !== false) {
|
||||
return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
|
||||
}
|
||||
|
||||
if (strpos($col, 'text') !== false) {
|
||||
return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
|
||||
}
|
||||
|
||||
if ($col === 'image' || strpos($col, 'binary') !== false) {
|
||||
// -1 is the value for MAX which we treat as a 'long' binary
|
||||
if ($length == -1) {
|
||||
$length = TableSchema::LENGTH_LONG;
|
||||
}
|
||||
|
||||
return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
|
||||
}
|
||||
|
||||
if ($col === 'uniqueidentifier') {
|
||||
return ['type' => TableSchema::TYPE_UUID];
|
||||
}
|
||||
|
||||
return ['type' => TableSchema::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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($type, $default)
|
||||
{
|
||||
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 === TableSchema::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 = "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]
|
||||
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]";
|
||||
|
||||
$schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
|
||||
|
||||
return [$sql, [$tableName, $schema]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function convertIndexDescription(TableSchema $schema, array $row): void
|
||||
{
|
||||
$type = TableSchema::INDEX_INDEX;
|
||||
$name = $row['index_name'];
|
||||
if ($row['is_primary_key']) {
|
||||
$name = $type = TableSchema::CONSTRAINT_PRIMARY;
|
||||
}
|
||||
if ($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 (!empty($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 describeForeignKeySql(string $tableName, array $config): array
|
||||
{
|
||||
// phpcs:disable Generic.Files.LineLength
|
||||
$sql = '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
|
||||
|
||||
$schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
|
||||
|
||||
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
|
||||
*/
|
||||
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
|
||||
{
|
||||
switch ($clause) {
|
||||
case 'NO_ACTION':
|
||||
return TableSchema::ACTION_NO_ACTION;
|
||||
case 'CASCADE':
|
||||
return TableSchema::ACTION_CASCADE;
|
||||
case 'SET_NULL':
|
||||
return TableSchema::ACTION_SET_NULL;
|
||||
case 'SET_DEFAULT':
|
||||
return TableSchema::ACTION_SET_DEFAULT;
|
||||
}
|
||||
|
||||
return TableSchema::ACTION_SET_NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getColumn($name);
|
||||
|
||||
$sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
|
||||
if ($sql !== null) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$out = $this->_driver->quoteIdentifier($name);
|
||||
$typeMap = [
|
||||
TableSchema::TYPE_TINYINTEGER => ' TINYINT',
|
||||
TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
|
||||
TableSchema::TYPE_INTEGER => ' INTEGER',
|
||||
TableSchema::TYPE_BIGINTEGER => ' BIGINT',
|
||||
TableSchema::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
|
||||
TableSchema::TYPE_BOOLEAN => ' BIT',
|
||||
TableSchema::TYPE_CHAR => ' NCHAR',
|
||||
TableSchema::TYPE_FLOAT => ' FLOAT',
|
||||
TableSchema::TYPE_DECIMAL => ' DECIMAL',
|
||||
TableSchema::TYPE_DATE => ' DATE',
|
||||
TableSchema::TYPE_TIME => ' TIME',
|
||||
TableSchema::TYPE_DATETIME => ' DATETIME2',
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME2',
|
||||
TableSchema::TYPE_TIMESTAMP => ' DATETIME2',
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2',
|
||||
TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2',
|
||||
TableSchema::TYPE_UUID => ' UNIQUEIDENTIFIER',
|
||||
TableSchema::TYPE_JSON => ' NVARCHAR(MAX)',
|
||||
];
|
||||
|
||||
if (isset($typeMap[$data['type']])) {
|
||||
$out .= $typeMap[$data['type']];
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
|
||||
if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) {
|
||||
unset($data['null'], $data['default']);
|
||||
$out .= ' IDENTITY(1, 1)';
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
|
||||
$out .= ' NVARCHAR(MAX)';
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_CHAR) {
|
||||
$out .= '(' . $data['length'] . ')';
|
||||
}
|
||||
|
||||
if ($data['type'] === TableSchema::TYPE_BINARY) {
|
||||
if (
|
||||
!isset($data['length'])
|
||||
|| in_array($data['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
|
||||
) {
|
||||
$data['length'] = 'MAX';
|
||||
}
|
||||
|
||||
if ($data['length'] === 1) {
|
||||
$out .= ' BINARY(1)';
|
||||
} else {
|
||||
$out .= ' VARBINARY';
|
||||
|
||||
$out .= sprintf('(%s)', $data['length']);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$data['type'] === TableSchema::TYPE_STRING ||
|
||||
(
|
||||
$data['type'] === TableSchema::TYPE_TEXT &&
|
||||
$data['length'] === TableSchema::LENGTH_TINY
|
||||
)
|
||||
) {
|
||||
$type = ' NVARCHAR';
|
||||
$length = $data['length'] ?? TableSchema::LENGTH_TINY;
|
||||
$out .= sprintf('%s(%d)', $type, $length);
|
||||
}
|
||||
|
||||
$hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR];
|
||||
if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
|
||||
$out .= ' COLLATE ' . $data['collate'];
|
||||
}
|
||||
|
||||
$precisionTypes = [
|
||||
TableSchema::TYPE_FLOAT,
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
];
|
||||
if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
|
||||
$out .= '(' . (int)$data['precision'] . ')';
|
||||
}
|
||||
|
||||
if (
|
||||
$data['type'] === TableSchema::TYPE_DECIMAL &&
|
||||
(
|
||||
isset($data['length']) ||
|
||||
isset($data['precision'])
|
||||
)
|
||||
) {
|
||||
$out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
|
||||
}
|
||||
|
||||
if (isset($data['null']) && $data['null'] === false) {
|
||||
$out .= ' NOT NULL';
|
||||
}
|
||||
|
||||
$dateTimeTypes = [
|
||||
TableSchema::TYPE_DATETIME,
|
||||
TableSchema::TYPE_DATETIME_FRACTIONAL,
|
||||
TableSchema::TYPE_TIMESTAMP,
|
||||
TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
|
||||
];
|
||||
$dateTimeDefaults = [
|
||||
'current_timestamp',
|
||||
'getdate()',
|
||||
'getutcdate()',
|
||||
'sysdatetime()',
|
||||
'sysutcdatetime()',
|
||||
'sysdatetimeoffset()',
|
||||
];
|
||||
if (
|
||||
isset($data['default']) &&
|
||||
in_array($data['type'], $dateTimeTypes, true) &&
|
||||
in_array(strtolower($data['default']), $dateTimeDefaults, true)
|
||||
) {
|
||||
$out .= ' DEFAULT ' . strtoupper($data['default']);
|
||||
} elseif (isset($data['default'])) {
|
||||
$default = is_bool($data['default'])
|
||||
? (int)$data['default']
|
||||
: $this->_driver->schemaValue($data['default']);
|
||||
$out .= ' DEFAULT ' . $default;
|
||||
} elseif (isset($data['null']) && $data['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) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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) {
|
||||
/** @var array $constraint */
|
||||
$constraint = $schema->getConstraint($name);
|
||||
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
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getIndex($name);
|
||||
$columns = array_map(
|
||||
[$this->_driver, 'quoteIdentifier'],
|
||||
$data['columns']
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'CREATE INDEX %s ON %s (%s)',
|
||||
$this->_driver->quoteIdentifier($name),
|
||||
$this->_driver->quoteIdentifier($schema->name()),
|
||||
implode(', ', $columns)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function constraintSql(TableSchema $schema, string $name): string
|
||||
{
|
||||
/** @var array $data */
|
||||
$data = $schema->getConstraint($name);
|
||||
$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;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
/** @var array $column */
|
||||
$column = $schema->getColumn($pk[0]);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable
|
||||
class_alias(
|
||||
'Cake\Database\Schema\SqlserverSchemaDialect',
|
||||
'Cake\Database\Schema\SqlserverSchema'
|
||||
);
|
||||
// phpcs:enable
|
||||
+793
@@ -0,0 +1,793 @@
|
||||
<?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;
|
||||
use Cake\Database\Exception\DatabaseException;
|
||||
use Cake\Database\TypeFactory;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Represents a single table in a database schema.
|
||||
*
|
||||
* Can either be populated using the reflection API's
|
||||
* or by incrementally building an instance using
|
||||
* methods.
|
||||
*
|
||||
* Once created TableSchema instances can be added to
|
||||
* Schema\Collection objects. They can also be converted into SQL using the
|
||||
* createSql(), dropSql() and truncateSql() methods.
|
||||
*/
|
||||
class TableSchema implements TableSchemaInterface, SqlGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* The name of the table
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_table;
|
||||
|
||||
/**
|
||||
* Columns in the table.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected $_columns = [];
|
||||
|
||||
/**
|
||||
* A map with columns to types
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $_typeMap = [];
|
||||
|
||||
/**
|
||||
* Indexes in the table.
|
||||
*
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected $_indexes = [];
|
||||
|
||||
/**
|
||||
* Constraints in the table.
|
||||
*
|
||||
* @var array<string, array<string, mixed>>
|
||||
*/
|
||||
protected $_constraints = [];
|
||||
|
||||
/**
|
||||
* Options for the table.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $_options = [];
|
||||
|
||||
/**
|
||||
* Whether the table is temporary
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_temporary = false;
|
||||
|
||||
/**
|
||||
* Column length when using a `tiny` column type
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LENGTH_TINY = 255;
|
||||
|
||||
/**
|
||||
* Column length when using a `medium` column type
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LENGTH_MEDIUM = 16777215;
|
||||
|
||||
/**
|
||||
* Column length when using a `long` column type
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LENGTH_LONG = 4294967295;
|
||||
|
||||
/**
|
||||
* Valid column length that can be used with text type columns
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
public static $columnLengths = [
|
||||
'tiny' => self::LENGTH_TINY,
|
||||
'medium' => self::LENGTH_MEDIUM,
|
||||
'long' => self::LENGTH_LONG,
|
||||
];
|
||||
|
||||
/**
|
||||
* The valid keys that can be used in a column
|
||||
* definition.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected static $_columnKeys = [
|
||||
'type' => null,
|
||||
'baseType' => null,
|
||||
'length' => null,
|
||||
'precision' => null,
|
||||
'null' => null,
|
||||
'default' => null,
|
||||
'comment' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Additional type specific properties.
|
||||
*
|
||||
* @var array<string, array<string, mixed>>
|
||||
*/
|
||||
protected static $_columnExtras = [
|
||||
'string' => [
|
||||
'collate' => null,
|
||||
],
|
||||
'char' => [
|
||||
'collate' => null,
|
||||
],
|
||||
'text' => [
|
||||
'collate' => null,
|
||||
],
|
||||
'tinyinteger' => [
|
||||
'unsigned' => null,
|
||||
],
|
||||
'smallinteger' => [
|
||||
'unsigned' => null,
|
||||
],
|
||||
'integer' => [
|
||||
'unsigned' => null,
|
||||
'autoIncrement' => null,
|
||||
],
|
||||
'biginteger' => [
|
||||
'unsigned' => null,
|
||||
'autoIncrement' => null,
|
||||
],
|
||||
'decimal' => [
|
||||
'unsigned' => null,
|
||||
],
|
||||
'float' => [
|
||||
'unsigned' => null,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The valid keys that can be used in an index
|
||||
* definition.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected static $_indexKeys = [
|
||||
'type' => null,
|
||||
'columns' => [],
|
||||
'length' => [],
|
||||
'references' => [],
|
||||
'update' => 'restrict',
|
||||
'delete' => 'restrict',
|
||||
];
|
||||
|
||||
/**
|
||||
* Names of the valid index types.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected static $_validIndexTypes = [
|
||||
self::INDEX_INDEX,
|
||||
self::INDEX_FULLTEXT,
|
||||
];
|
||||
|
||||
/**
|
||||
* Names of the valid constraint types.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected static $_validConstraintTypes = [
|
||||
self::CONSTRAINT_PRIMARY,
|
||||
self::CONSTRAINT_UNIQUE,
|
||||
self::CONSTRAINT_FOREIGN,
|
||||
];
|
||||
|
||||
/**
|
||||
* Names of the valid foreign key actions.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected static $_validForeignKeyActions = [
|
||||
self::ACTION_CASCADE,
|
||||
self::ACTION_SET_NULL,
|
||||
self::ACTION_SET_DEFAULT,
|
||||
self::ACTION_NO_ACTION,
|
||||
self::ACTION_RESTRICT,
|
||||
];
|
||||
|
||||
/**
|
||||
* Primary constraint type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONSTRAINT_PRIMARY = 'primary';
|
||||
|
||||
/**
|
||||
* Unique constraint type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONSTRAINT_UNIQUE = 'unique';
|
||||
|
||||
/**
|
||||
* Foreign constraint type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONSTRAINT_FOREIGN = 'foreign';
|
||||
|
||||
/**
|
||||
* Index - index type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const INDEX_INDEX = 'index';
|
||||
|
||||
/**
|
||||
* Fulltext index type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const INDEX_FULLTEXT = 'fulltext';
|
||||
|
||||
/**
|
||||
* Foreign key cascade action
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ACTION_CASCADE = 'cascade';
|
||||
|
||||
/**
|
||||
* Foreign key set null action
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ACTION_SET_NULL = 'setNull';
|
||||
|
||||
/**
|
||||
* Foreign key no action
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ACTION_NO_ACTION = 'noAction';
|
||||
|
||||
/**
|
||||
* Foreign key restrict action
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ACTION_RESTRICT = 'restrict';
|
||||
|
||||
/**
|
||||
* Foreign key restrict default
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ACTION_SET_DEFAULT = 'setDefault';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param array<string, array|string> $columns The list of columns for the schema.
|
||||
*/
|
||||
public function __construct(string $table, array $columns = [])
|
||||
{
|
||||
$this->_table = $table;
|
||||
foreach ($columns as $field => $definition) {
|
||||
$this->addColumn($field, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->_table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addColumn(string $name, $attrs)
|
||||
{
|
||||
if (is_string($attrs)) {
|
||||
$attrs = ['type' => $attrs];
|
||||
}
|
||||
$valid = static::$_columnKeys;
|
||||
if (isset(static::$_columnExtras[$attrs['type']])) {
|
||||
$valid += static::$_columnExtras[$attrs['type']];
|
||||
}
|
||||
$attrs = array_intersect_key($attrs, $valid);
|
||||
$this->_columns[$name] = $attrs + $valid;
|
||||
$this->_typeMap[$name] = $this->_columns[$name]['type'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function removeColumn(string $name)
|
||||
{
|
||||
unset($this->_columns[$name], $this->_typeMap[$name]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columns(): array
|
||||
{
|
||||
return array_keys($this->_columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getColumn(string $name): ?array
|
||||
{
|
||||
if (!isset($this->_columns[$name])) {
|
||||
return null;
|
||||
}
|
||||
$column = $this->_columns[$name];
|
||||
unset($column['baseType']);
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getColumnType(string $name): ?string
|
||||
{
|
||||
if (!isset($this->_columns[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->_columns[$name]['type'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setColumnType(string $name, string $type)
|
||||
{
|
||||
if (!isset($this->_columns[$name])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->_columns[$name]['type'] = $type;
|
||||
$this->_typeMap[$name] = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasColumn(string $name): bool
|
||||
{
|
||||
return isset($this->_columns[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function baseColumnType(string $column): ?string
|
||||
{
|
||||
if (isset($this->_columns[$column]['baseType'])) {
|
||||
return $this->_columns[$column]['baseType'];
|
||||
}
|
||||
|
||||
$type = $this->getColumnType($column);
|
||||
|
||||
if ($type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TypeFactory::getMap($type)) {
|
||||
$type = TypeFactory::build($type)->getBaseType();
|
||||
}
|
||||
|
||||
return $this->_columns[$column]['baseType'] = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function typeMap(): array
|
||||
{
|
||||
return $this->_typeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isNullable(string $name): bool
|
||||
{
|
||||
if (!isset($this->_columns[$name])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->_columns[$name]['null'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function defaultValues(): array
|
||||
{
|
||||
$defaults = [];
|
||||
foreach ($this->_columns as $name => $data) {
|
||||
if (!array_key_exists('default', $data)) {
|
||||
continue;
|
||||
}
|
||||
if ($data['default'] === null && $data['null'] !== true) {
|
||||
continue;
|
||||
}
|
||||
$defaults[$name] = $data['default'];
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addIndex(string $name, $attrs)
|
||||
{
|
||||
if (is_string($attrs)) {
|
||||
$attrs = ['type' => $attrs];
|
||||
}
|
||||
$attrs = array_intersect_key($attrs, static::$_indexKeys);
|
||||
$attrs += static::$_indexKeys;
|
||||
unset($attrs['references'], $attrs['update'], $attrs['delete']);
|
||||
|
||||
if (!in_array($attrs['type'], static::$_validIndexTypes, true)) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Invalid index type "%s" in index "%s" in table "%s".',
|
||||
$attrs['type'],
|
||||
$name,
|
||||
$this->_table
|
||||
));
|
||||
}
|
||||
$attrs['columns'] = (array)$attrs['columns'];
|
||||
foreach ($attrs['columns'] as $field) {
|
||||
if (empty($this->_columns[$field])) {
|
||||
$msg = sprintf(
|
||||
'Columns used in index "%s" in table "%s" must be added to the Table schema first. ' .
|
||||
'The column "%s" was not found.',
|
||||
$name,
|
||||
$this->_table,
|
||||
$field
|
||||
);
|
||||
throw new DatabaseException($msg);
|
||||
}
|
||||
}
|
||||
$this->_indexes[$name] = $attrs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function indexes(): array
|
||||
{
|
||||
return array_keys($this->_indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getIndex(string $name): ?array
|
||||
{
|
||||
if (!isset($this->_indexes[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->_indexes[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column(s) used for the primary key.
|
||||
*
|
||||
* @return array Column name(s) for the primary key. An
|
||||
* empty list will be returned when the table has no primary key.
|
||||
* @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}.
|
||||
*/
|
||||
public function primaryKey(): array
|
||||
{
|
||||
deprecationWarning('`TableSchema::primaryKey()` is deprecated. Use `TableSchema::getPrimaryKey()`.');
|
||||
|
||||
return $this->getPrimarykey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getPrimaryKey(): array
|
||||
{
|
||||
foreach ($this->_constraints as $data) {
|
||||
if ($data['type'] === static::CONSTRAINT_PRIMARY) {
|
||||
return $data['columns'];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addConstraint(string $name, $attrs)
|
||||
{
|
||||
if (is_string($attrs)) {
|
||||
$attrs = ['type' => $attrs];
|
||||
}
|
||||
$attrs = array_intersect_key($attrs, static::$_indexKeys);
|
||||
$attrs += static::$_indexKeys;
|
||||
if (!in_array($attrs['type'], static::$_validConstraintTypes, true)) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Invalid constraint type "%s" in table "%s".',
|
||||
$attrs['type'],
|
||||
$this->_table
|
||||
));
|
||||
}
|
||||
if (empty($attrs['columns'])) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Constraints in table "%s" must have at least one column.',
|
||||
$this->_table
|
||||
));
|
||||
}
|
||||
$attrs['columns'] = (array)$attrs['columns'];
|
||||
foreach ($attrs['columns'] as $field) {
|
||||
if (empty($this->_columns[$field])) {
|
||||
$msg = sprintf(
|
||||
'Columns used in constraints must be added to the Table schema first. ' .
|
||||
'The column "%s" was not found in table "%s".',
|
||||
$field,
|
||||
$this->_table
|
||||
);
|
||||
throw new DatabaseException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
if ($attrs['type'] === static::CONSTRAINT_FOREIGN) {
|
||||
$attrs = $this->_checkForeignKey($attrs);
|
||||
|
||||
if (isset($this->_constraints[$name])) {
|
||||
$this->_constraints[$name]['columns'] = array_unique(array_merge(
|
||||
$this->_constraints[$name]['columns'],
|
||||
$attrs['columns']
|
||||
));
|
||||
|
||||
if (isset($this->_constraints[$name]['references'])) {
|
||||
$this->_constraints[$name]['references'][1] = array_unique(array_merge(
|
||||
(array)$this->_constraints[$name]['references'][1],
|
||||
[$attrs['references'][1]]
|
||||
));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
} else {
|
||||
unset($attrs['references'], $attrs['update'], $attrs['delete']);
|
||||
}
|
||||
|
||||
$this->_constraints[$name] = $attrs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function dropConstraint(string $name)
|
||||
{
|
||||
if (isset($this->_constraints[$name])) {
|
||||
unset($this->_constraints[$name]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a table has an autoIncrement column defined.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAutoincrement(): bool
|
||||
{
|
||||
foreach ($this->_columns as $column) {
|
||||
if (isset($column['autoIncrement']) && $column['autoIncrement']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check/validate foreign keys.
|
||||
*
|
||||
* @param array<string, mixed> $attrs Attributes to set.
|
||||
* @return array<string, mixed>
|
||||
* @throws \Cake\Database\Exception\DatabaseException When foreign key definition is not valid.
|
||||
*/
|
||||
protected function _checkForeignKey(array $attrs): array
|
||||
{
|
||||
if (count($attrs['references']) < 2) {
|
||||
throw new DatabaseException('References must contain a table and column.');
|
||||
}
|
||||
if (!in_array($attrs['update'], static::$_validForeignKeyActions)) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Update action is invalid. Must be one of %s',
|
||||
implode(',', static::$_validForeignKeyActions)
|
||||
));
|
||||
}
|
||||
if (!in_array($attrs['delete'], static::$_validForeignKeyActions)) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Delete action is invalid. Must be one of %s',
|
||||
implode(',', static::$_validForeignKeyActions)
|
||||
));
|
||||
}
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function constraints(): array
|
||||
{
|
||||
return array_keys($this->_constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getConstraint(string $name): ?array
|
||||
{
|
||||
return $this->_constraints[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
{
|
||||
$this->_options = $options + $this->_options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setTemporary(bool $temporary)
|
||||
{
|
||||
$this->_temporary = $temporary;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isTemporary(): bool
|
||||
{
|
||||
return $this->_temporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createSql(Connection $connection): array
|
||||
{
|
||||
$dialect = $connection->getDriver()->schemaDialect();
|
||||
$columns = $constraints = $indexes = [];
|
||||
foreach (array_keys($this->_columns) as $name) {
|
||||
$columns[] = $dialect->columnSql($this, $name);
|
||||
}
|
||||
foreach (array_keys($this->_constraints) as $name) {
|
||||
$constraints[] = $dialect->constraintSql($this, $name);
|
||||
}
|
||||
foreach (array_keys($this->_indexes) as $name) {
|
||||
$indexes[] = $dialect->indexSql($this, $name);
|
||||
}
|
||||
|
||||
return $dialect->createTableSql($this, $columns, $constraints, $indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function dropSql(Connection $connection): array
|
||||
{
|
||||
$dialect = $connection->getDriver()->schemaDialect();
|
||||
|
||||
return $dialect->dropTableSql($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function truncateSql(Connection $connection): array
|
||||
{
|
||||
$dialect = $connection->getDriver()->schemaDialect();
|
||||
|
||||
return $dialect->truncateTableSql($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addConstraintSql(Connection $connection): array
|
||||
{
|
||||
$dialect = $connection->getDriver()->schemaDialect();
|
||||
|
||||
return $dialect->addConstraintSql($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function dropConstraintSql(Connection $connection): array
|
||||
{
|
||||
$dialect = $connection->getDriver()->schemaDialect();
|
||||
|
||||
return $dialect->dropConstraintSql($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the table schema.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return [
|
||||
'table' => $this->_table,
|
||||
'columns' => $this->_columns,
|
||||
'indexes' => $this->_indexes,
|
||||
'constraints' => $this->_constraints,
|
||||
'options' => $this->_options,
|
||||
'typeMap' => $this->_typeMap,
|
||||
'temporary' => $this->_temporary,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Defines the interface for getting the schema.
|
||||
*/
|
||||
interface TableSchemaAwareInterface
|
||||
{
|
||||
/**
|
||||
* Get and set the schema for this fixture.
|
||||
*
|
||||
* @return \Cake\Database\Schema\TableSchemaInterface&\Cake\Database\Schema\SqlGeneratorInterface
|
||||
*/
|
||||
public function getTableSchema();
|
||||
|
||||
/**
|
||||
* Get and set the schema for this fixture.
|
||||
*
|
||||
* @param \Cake\Database\Schema\TableSchemaInterface&\Cake\Database\Schema\SqlGeneratorInterface $schema The table to set.
|
||||
* @return $this
|
||||
*/
|
||||
public function setTableSchema($schema);
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?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.
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* 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, $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 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'.
|
||||
*
|
||||
* 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, $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;
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?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 $_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 $schemaCollection */
|
||||
$schemaCollection = $connection->getSchemaCollection();
|
||||
|
||||
return $schemaCollection;
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.1.0: Cake\Database\SqlDialectTrait is deprecated. ' .
|
||||
'Use Cake\Database\Driver\SqlDialectTrait instead.'
|
||||
);
|
||||
class_exists('Cake\Database\Driver\SqlDialectTrait');
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Responsible for compiling a Query object into its SQL representation
|
||||
* for SQLite
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqliteCompiler extends QueryCompiler
|
||||
{
|
||||
/**
|
||||
* SQLite does not support ORDER BY in UNION queries.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_orderedUnion = false;
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* SQLserver does not support ORDER BY in UNION queries.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_orderedUnion = false;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_templates = [
|
||||
'delete' => 'DELETE',
|
||||
'where' => ' WHERE %s',
|
||||
'group' => ' GROUP BY %s',
|
||||
'order' => ' %s',
|
||||
'offset' => ' OFFSET %s ROWS',
|
||||
'epilog' => ' %s',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_selectParts = [
|
||||
'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
|
||||
'offset', 'limit', 'union', 'epilog',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 $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($parts, $query, $binder)
|
||||
{
|
||||
$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,45 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Contains a setter for marking a Statement as buffered
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait BufferResultsTrait
|
||||
{
|
||||
/**
|
||||
* Whether to buffer results in php
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_bufferResults = true;
|
||||
|
||||
/**
|
||||
* Whether to buffer results in php
|
||||
*
|
||||
* @param bool $buffer Toggle buffering
|
||||
* @return $this
|
||||
*/
|
||||
public function bufferResults(bool $buffer)
|
||||
{
|
||||
$this->_bufferResults = $buffer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<?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 Cake\Database\DriverInterface;
|
||||
use Cake\Database\StatementInterface;
|
||||
use Cake\Database\TypeConverterTrait;
|
||||
use Iterator;
|
||||
|
||||
/**
|
||||
* A statement decorator that implements buffered results.
|
||||
*
|
||||
* This statement decorator will save fetched results in memory, allowing
|
||||
* the iterator to be rewound and reused.
|
||||
*
|
||||
* @template-implements \Iterator<mixed>
|
||||
*/
|
||||
class BufferedStatement implements Iterator, StatementInterface
|
||||
{
|
||||
use TypeConverterTrait;
|
||||
|
||||
/**
|
||||
* If true, all rows were fetched
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_allFetched = false;
|
||||
|
||||
/**
|
||||
* The decorated statement
|
||||
*
|
||||
* @var \Cake\Database\StatementInterface
|
||||
*/
|
||||
protected $statement;
|
||||
|
||||
/**
|
||||
* The driver for the statement
|
||||
*
|
||||
* @var \Cake\Database\DriverInterface
|
||||
*/
|
||||
protected $_driver;
|
||||
|
||||
/**
|
||||
* The in-memory cache containing results from previous iterators
|
||||
*
|
||||
* @var array<int, array>
|
||||
*/
|
||||
protected $buffer = [];
|
||||
|
||||
/**
|
||||
* Whether this statement has already been executed
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_hasExecuted = false;
|
||||
|
||||
/**
|
||||
* The current iterator index.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $index = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\StatementInterface $statement Statement implementation such as PDOStatement
|
||||
* @param \Cake\Database\DriverInterface $driver Driver instance
|
||||
*/
|
||||
public function __construct(StatementInterface $statement, DriverInterface $driver)
|
||||
{
|
||||
$this->statement = $statement;
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection driver.
|
||||
*
|
||||
* @return \Cake\Database\DriverInterface
|
||||
*/
|
||||
protected function getDriver(): DriverInterface
|
||||
{
|
||||
return $this->_driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter to return $queryString as read-only.
|
||||
*
|
||||
* @param string $property internal property to get
|
||||
* @return string|null
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
if ($property === 'queryString') {
|
||||
/** @psalm-suppress NoInterfaceProperties */
|
||||
return $this->statement->queryString;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function bindValue($column, $value, $type = 'string'): void
|
||||
{
|
||||
$this->statement->bindValue($column, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function closeCursor(): void
|
||||
{
|
||||
$this->statement->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function columnCount(): int
|
||||
{
|
||||
return $this->statement->columnCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function errorCode()
|
||||
{
|
||||
return $this->statement->errorCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function errorInfo(): array
|
||||
{
|
||||
return $this->statement->errorInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
$this->_reset();
|
||||
$this->_hasExecuted = true;
|
||||
|
||||
return $this->statement->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchColumn(int $position)
|
||||
{
|
||||
$result = $this->fetch(static::FETCH_TYPE_NUM);
|
||||
if ($result !== false && isset($result[$position])) {
|
||||
return $result[$position];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statements can be passed as argument for count() to return the number
|
||||
* for affected rows from last execution.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function bind(array $params, array $types): void
|
||||
{
|
||||
$this->statement->bind($params, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function lastInsertId(?string $table = null, ?string $column = null)
|
||||
{
|
||||
return $this->statement->lastInsertId($table, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param string|int $type The type to fetch.
|
||||
* @return array|false
|
||||
*/
|
||||
public function fetch($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
if ($this->_allFetched) {
|
||||
$row = false;
|
||||
if (isset($this->buffer[$this->index])) {
|
||||
$row = $this->buffer[$this->index];
|
||||
}
|
||||
$this->index += 1;
|
||||
|
||||
if ($row && $type === static::FETCH_TYPE_NUM) {
|
||||
return array_values($row);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
$record = $this->statement->fetch($type);
|
||||
if ($record === false) {
|
||||
$this->_allFetched = true;
|
||||
$this->statement->closeCursor();
|
||||
|
||||
return false;
|
||||
}
|
||||
$this->buffer[] = $record;
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc(): array
|
||||
{
|
||||
$result = $this->fetch(static::FETCH_TYPE_ASSOC);
|
||||
|
||||
return $result ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function fetchAll($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
if ($this->_allFetched) {
|
||||
return $this->buffer;
|
||||
}
|
||||
$results = $this->statement->fetchAll($type);
|
||||
if ($results !== false) {
|
||||
$this->buffer = array_merge($this->buffer, $results);
|
||||
}
|
||||
$this->_allFetched = true;
|
||||
$this->statement->closeCursor();
|
||||
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
if (!$this->_allFetched) {
|
||||
$this->fetchAll(static::FETCH_TYPE_ASSOC);
|
||||
}
|
||||
|
||||
return count($this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all properties
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _reset(): void
|
||||
{
|
||||
$this->buffer = [];
|
||||
$this->_allFetched = false;
|
||||
$this->index = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current key in the iterator
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function key()
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current record in the iterator
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current()
|
||||
{
|
||||
return $this->buffer[$this->index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewinds the collection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->index = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the iterator has more elements
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
$old = $this->index;
|
||||
$row = $this->fetch(self::FETCH_TYPE_ASSOC);
|
||||
|
||||
// Restore the index as fetch() increments during
|
||||
// the cache scenario.
|
||||
$this->index = $old;
|
||||
|
||||
return $row !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the iterator pointer to the next element
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
$this->index += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wrapped statement
|
||||
*
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function getInnerStatement(): StatementInterface
|
||||
{
|
||||
return $this->statement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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 Cake\Database\DriverInterface;
|
||||
use Cake\Database\StatementInterface;
|
||||
|
||||
/**
|
||||
* Wraps a statement in a callback that allows row results
|
||||
* to be modified when being fetched.
|
||||
*
|
||||
* This is used by CakePHP to eagerly load association data.
|
||||
*/
|
||||
class CallbackStatement extends StatementDecorator
|
||||
{
|
||||
/**
|
||||
* A callback function to be applied to results.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected $_callback;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\StatementInterface $statement The statement to decorate.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance used by the statement.
|
||||
* @param callable $callback The callback to apply to results before they are returned.
|
||||
*/
|
||||
public function __construct(StatementInterface $statement, DriverInterface $driver, callable $callback)
|
||||
{
|
||||
parent::__construct($statement, $driver);
|
||||
$this->_callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a row from the statement.
|
||||
*
|
||||
* The result will be processed by the callback when it is not `false`.
|
||||
*
|
||||
* @param string|int $type Either 'num' or 'assoc' to indicate the result format you would like.
|
||||
* @return array|false
|
||||
*/
|
||||
public function fetch($type = parent::FETCH_TYPE_NUM)
|
||||
{
|
||||
$callback = $this->_callback;
|
||||
$row = $this->_statement->fetch($type);
|
||||
|
||||
return $row === false ? $row : $callback($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Each row in the result will be processed by the callback when it is not `false.
|
||||
*/
|
||||
public function fetchAll($type = parent::FETCH_TYPE_NUM)
|
||||
{
|
||||
$results = $this->_statement->fetchAll($type);
|
||||
|
||||
return $results !== false ? array_map($this->_callback, $results) : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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 a MySQL PDO driver
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MysqlStatement extends PDOStatement
|
||||
{
|
||||
use BufferResultsTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
$connection = $this->_driver->getConnection();
|
||||
|
||||
try {
|
||||
$connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $this->_bufferResults);
|
||||
$result = $this->_statement->execute($params);
|
||||
} finally {
|
||||
$connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?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 Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\DriverInterface;
|
||||
use PDO;
|
||||
use PDOStatement as Statement;
|
||||
use function Cake\Core\getTypeName;
|
||||
|
||||
/**
|
||||
* Decorator for \PDOStatement class mainly used for converting human readable
|
||||
* fetch modes into PDO constants.
|
||||
*/
|
||||
class PDOStatement extends StatementDecorator
|
||||
{
|
||||
/**
|
||||
* PDOStatement instance
|
||||
*
|
||||
* @var \PDOStatement
|
||||
*/
|
||||
protected $_statement;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \PDOStatement $statement Original statement to be decorated.
|
||||
* @param \Cake\Database\DriverInterface $driver Driver instance.
|
||||
*/
|
||||
public function __construct(Statement $statement, DriverInterface $driver)
|
||||
{
|
||||
$this->_statement = $statement;
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter to return PDOStatement::$queryString as read-only.
|
||||
*
|
||||
* @param string $property internal property to get
|
||||
* @return string|null
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
if ($property === 'queryString' && isset($this->_statement->queryString)) {
|
||||
return $this->_statement->queryString;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* You can pass PDO compatible constants for binding values with a type or optionally
|
||||
* any type name registered in the Type class. Any value will be converted to the valid type
|
||||
* representation if needed.
|
||||
*
|
||||
* It is not allowed to combine positional and named variables in the same statement
|
||||
*
|
||||
* ### Examples:
|
||||
*
|
||||
* ```
|
||||
* $statement->bindValue(1, 'a title');
|
||||
* $statement->bindValue(2, 5, PDO::INT);
|
||||
* $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 PDO type or name of configured Type class
|
||||
* @return void
|
||||
*/
|
||||
public function bindValue($column, $value, $type = 'string'): void
|
||||
{
|
||||
if ($type === null) {
|
||||
$type = 'string';
|
||||
}
|
||||
if (!is_int($type)) {
|
||||
[$value, $type] = $this->cast($value, $type);
|
||||
}
|
||||
$this->_statement->bindValue($column, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row for the result set after executing this statement.
|
||||
* Rows can be fetched to contain columns as names or positions. If no
|
||||
* rows are left in result set, this method will return false
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type 'num' for positional columns, assoc for named columns
|
||||
* @return mixed Result array containing columns and values or false if no results
|
||||
* are left
|
||||
*/
|
||||
public function fetch($type = parent::FETCH_TYPE_NUM)
|
||||
{
|
||||
if ($type === static::FETCH_TYPE_NUM) {
|
||||
return $this->_statement->fetch(PDO::FETCH_NUM);
|
||||
}
|
||||
if ($type === static::FETCH_TYPE_ASSOC) {
|
||||
return $this->_statement->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
if ($type === static::FETCH_TYPE_OBJ) {
|
||||
return $this->_statement->fetch(PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
if (!is_int($type)) {
|
||||
throw new CakeException(sprintf(
|
||||
'Fetch type for PDOStatement must be an integer, found `%s` instead',
|
||||
getTypeName($type)
|
||||
));
|
||||
}
|
||||
|
||||
return $this->_statement->fetch($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all rows resulting from executing this statement
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
|
||||
* @return array|false list of all results from database for this statement, false on failure
|
||||
* @psalm-assert string $type
|
||||
*/
|
||||
public function fetchAll($type = parent::FETCH_TYPE_NUM)
|
||||
{
|
||||
if ($type === static::FETCH_TYPE_NUM) {
|
||||
return $this->_statement->fetchAll(PDO::FETCH_NUM);
|
||||
}
|
||||
if ($type === static::FETCH_TYPE_ASSOC) {
|
||||
return $this->_statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
if ($type === static::FETCH_TYPE_OBJ) {
|
||||
return $this->_statement->fetchAll(PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
if (!is_int($type)) {
|
||||
throw new CakeException(sprintf(
|
||||
'Fetch type for PDOStatement must be an integer, found `%s` instead',
|
||||
getTypeName($type)
|
||||
));
|
||||
}
|
||||
|
||||
return $this->_statement->fetchAll($type);
|
||||
}
|
||||
}
|
||||
@@ -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 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 StatementDecorator
|
||||
{
|
||||
use BufferResultsTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute(?array $params = null): bool
|
||||
{
|
||||
if ($this->_statement instanceof BufferedStatement) {
|
||||
$this->_statement = $this->_statement->getInnerStatement();
|
||||
}
|
||||
|
||||
if ($this->_bufferResults) {
|
||||
$this->_statement = new BufferedStatement($this->_statement, $this->_driver);
|
||||
}
|
||||
|
||||
return $this->_statement->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows returned of affected by last execution
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
/** @psalm-suppress NoInterfaceProperties */
|
||||
if (
|
||||
$this->_statement->queryString &&
|
||||
preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->_statement->queryString)
|
||||
) {
|
||||
$changes = $this->_driver->prepare('SELECT CHANGES()');
|
||||
$changes->execute();
|
||||
$row = $changes->fetch();
|
||||
$changes->closeCursor();
|
||||
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)$row[0];
|
||||
}
|
||||
|
||||
return parent::rowCount();
|
||||
}
|
||||
}
|
||||
@@ -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\Statement;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Statement class meant to be used by an Sqlserver driver
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SqlserverStatement extends PDOStatement
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* The SQL Server PDO driver requires that binary parameters be bound with the SQLSRV_ENCODING_BINARY attribute.
|
||||
* This overrides the PDOStatement::bindValue method in order to bind binary columns using the required attribute.
|
||||
*
|
||||
* @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 PDO type or name of configured Type class
|
||||
* @return void
|
||||
*/
|
||||
public function bindValue($column, $value, $type = 'string'): void
|
||||
{
|
||||
if ($type === null) {
|
||||
$type = 'string';
|
||||
}
|
||||
if (!is_int($type)) {
|
||||
[$value, $type] = $this->cast($value, $type);
|
||||
}
|
||||
if ($type === PDO::PARAM_LOB) {
|
||||
$this->_statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY);
|
||||
} else {
|
||||
$this->_statement->bindValue($column, $value, $type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?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 Cake\Database\DriverInterface;
|
||||
use Cake\Database\StatementInterface;
|
||||
use Cake\Database\TypeConverterTrait;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* Represents a database statement. Statements contains queries that can be
|
||||
* executed multiple times by binding different values on each call. This class
|
||||
* also helps convert values to their valid representation for the corresponding
|
||||
* types.
|
||||
*
|
||||
* This class is but a decorator of an actual statement implementation, such as
|
||||
* PDOStatement.
|
||||
*
|
||||
* @property-read string $queryString
|
||||
* @template-implements \IteratorAggregate<string, \Cake\Database\StatementInterface>
|
||||
*/
|
||||
class StatementDecorator implements StatementInterface, Countable, IteratorAggregate
|
||||
{
|
||||
use TypeConverterTrait;
|
||||
|
||||
/**
|
||||
* Statement instance implementation, such as PDOStatement
|
||||
* or any other custom implementation.
|
||||
*
|
||||
* @var \Cake\Database\StatementInterface
|
||||
*/
|
||||
protected $_statement;
|
||||
|
||||
/**
|
||||
* Reference to the driver object associated to this statement.
|
||||
*
|
||||
* @var \Cake\Database\DriverInterface
|
||||
*/
|
||||
protected $_driver;
|
||||
|
||||
/**
|
||||
* Whether this statement has already been executed
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_hasExecuted = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \Cake\Database\StatementInterface $statement Statement implementation
|
||||
* such as PDOStatement.
|
||||
* @param \Cake\Database\DriverInterface $driver Driver instance
|
||||
*/
|
||||
public function __construct(StatementInterface $statement, DriverInterface $driver)
|
||||
{
|
||||
$this->_statement = $statement;
|
||||
$this->_driver = $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection driver.
|
||||
*
|
||||
* @return \Cake\Database\DriverInterface
|
||||
*/
|
||||
protected function getDriver(): DriverInterface
|
||||
{
|
||||
return $this->_driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter to return $queryString as read-only.
|
||||
*
|
||||
* @param string $property internal property to get
|
||||
* @return string|null
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
if ($property === 'queryString') {
|
||||
/** @psalm-suppress NoInterfaceProperties */
|
||||
return $this->_statement->queryString;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($column, $value, $type = 'string'): void
|
||||
{
|
||||
$this->_statement->bindValue($column, $value, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a cursor in the database, freeing up any resources and memory
|
||||
* allocated to it. In most cases you don't need to call this method, as it is
|
||||
* automatically called after fetching all results from the result set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCursor(): void
|
||||
{
|
||||
$this->_statement->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns this statement's results will contain.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* echo $statement->columnCount(); // outputs 2
|
||||
* ```
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function columnCount(): int
|
||||
{
|
||||
return $this->_statement->columnCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code for the last error that occurred when executing this statement.
|
||||
*
|
||||
* @return string|int
|
||||
*/
|
||||
public function errorCode()
|
||||
{
|
||||
return $this->_statement->errorCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error information for the last error that occurred when executing
|
||||
* this statement.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function errorInfo(): array
|
||||
{
|
||||
return $this->_statement->errorInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$this->_hasExecuted = true;
|
||||
|
||||
return $this->_statement->execute($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row for the result set after executing this statement.
|
||||
* Rows can be fetched to contain columns as names or positions. If no
|
||||
* rows are left in result set, this method will return false.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type 'num' for positional columns, assoc for named columns
|
||||
* @return mixed Result array containing columns and values or false if no results
|
||||
* are left
|
||||
*/
|
||||
public function fetch($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
return $this->_statement->fetch($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next row in a result set as an associative array. Calling this function is the same as calling
|
||||
* $statement->fetch(StatementDecorator::FETCH_TYPE_ASSOC). If no results are found an empty array is returned.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc(): array
|
||||
{
|
||||
$result = $this->fetch(static::FETCH_TYPE_ASSOC);
|
||||
|
||||
return $result ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the result at position.
|
||||
*
|
||||
* @param int $position The numeric position of the column to retrieve in the result
|
||||
* @return mixed Returns the specific value of the column designated at $position
|
||||
*/
|
||||
public function fetchColumn(int $position)
|
||||
{
|
||||
$result = $this->fetch(static::FETCH_TYPE_NUM);
|
||||
if ($result && isset($result[$position])) {
|
||||
return $result[$position];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all rows resulting from executing this statement.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
|
||||
* @return array|false List of all results from database for this statement. False on failure.
|
||||
*/
|
||||
public function fetchAll($type = self::FETCH_TYPE_NUM)
|
||||
{
|
||||
return $this->_statement->fetchAll($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows affected by this SQL statement.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->rowCount()); // will show 1
|
||||
* ```
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount(): int
|
||||
{
|
||||
return $this->_statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Statements are iterable as arrays, this method will return
|
||||
* the iterator object for traversing all items in the result.
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* foreach ($statement as $row) {
|
||||
* //do stuff
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return \Cake\Database\StatementInterface
|
||||
* @psalm-suppress ImplementedReturnTypeMismatch
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
if (!$this->_hasExecuted) {
|
||||
$this->execute();
|
||||
}
|
||||
|
||||
return $this->_statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statements can be passed as argument for count() to return the number
|
||||
* for affected rows from last execution.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if (empty($params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$anonymousParams = is_int(key($params));
|
||||
$offset = 1;
|
||||
foreach ($params as $index => $value) {
|
||||
$type = $types[$index] ?? null;
|
||||
if ($anonymousParams) {
|
||||
/** @psalm-suppress InvalidOperand */
|
||||
$index += $offset;
|
||||
}
|
||||
$this->bindValue($index, $value, $type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if ($column && $this->columnCount()) {
|
||||
$row = $this->fetch(static::FETCH_TYPE_ASSOC);
|
||||
|
||||
if ($row && isset($row[$column])) {
|
||||
return $row[$column];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_driver->lastInsertId($table, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the statement object that was decorated by this class.
|
||||
*
|
||||
* @return \Cake\Database\StatementInterface
|
||||
*/
|
||||
public function getInnerStatement()
|
||||
{
|
||||
return $this->_statement;
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Represents a database statement. Concrete implementations
|
||||
* can either use PDOStatement or a native driver
|
||||
*
|
||||
* @property-read string $queryString
|
||||
*/
|
||||
interface StatementInterface
|
||||
{
|
||||
/**
|
||||
* Used to designate that numeric indexes be returned in a result when calling fetch methods
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FETCH_TYPE_NUM = 'num';
|
||||
|
||||
/**
|
||||
* Used to designate that an associated array be returned in a result when calling fetch methods
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const FETCH_TYPE_ASSOC = 'assoc';
|
||||
|
||||
/**
|
||||
* Used to designate that a stdClass object be returned in a result when calling fetch methods
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
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, or PDO type constant.
|
||||
* @return void
|
||||
*/
|
||||
public function bindValue($column, $value, $type = 'string'): void;
|
||||
|
||||
/**
|
||||
* Closes a cursor in the database, freeing up any resources and memory
|
||||
* allocated to it. In most cases you don't need to call this method, as it is
|
||||
* automatically called after fetching all results from the result set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function closeCursor(): void;
|
||||
|
||||
/**
|
||||
* Returns the number of columns this statement's results will contain
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* echo $statement->columnCount(); // outputs 2
|
||||
* ```
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function columnCount(): int;
|
||||
|
||||
/**
|
||||
* Returns the error code for the last error that occurred when executing this statement
|
||||
*
|
||||
* @return string|int
|
||||
*/
|
||||
public function errorCode();
|
||||
|
||||
/**
|
||||
* Returns the error information for the last error that occurred when executing
|
||||
* this statement
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Returns the next row for the result set after executing this statement.
|
||||
* Rows can be fetched to contain columns as names or positions. If no
|
||||
* rows are left in result set, this method will return false
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type 'num' for positional columns, assoc for named columns, or PDO fetch mode constants.
|
||||
* @return mixed Result array containing columns and values or false if no results
|
||||
* are left
|
||||
*/
|
||||
public function fetch($type = 'num');
|
||||
|
||||
/**
|
||||
* Returns an array with all rows resulting from executing this statement
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
|
||||
* ```
|
||||
*
|
||||
* @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
|
||||
* @return array|false list of all results from database for this statement or false on failure.
|
||||
*/
|
||||
public function fetchAll($type = 'num');
|
||||
|
||||
/**
|
||||
* Returns the value of the result at position.
|
||||
*
|
||||
* @param int $position The numeric position of the column to retrieve in the result
|
||||
* @return mixed Returns the specific value of the column designated at $position
|
||||
*/
|
||||
public function fetchColumn(int $position);
|
||||
|
||||
/**
|
||||
* Returns the number of rows affected by this SQL statement
|
||||
*
|
||||
* ### Example:
|
||||
*
|
||||
* ```
|
||||
* $statement = $connection->prepare('SELECT id, title from articles');
|
||||
* $statement->execute();
|
||||
* print_r($statement->rowCount()); // will show 1
|
||||
* ```
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function rowCount(): int;
|
||||
|
||||
/**
|
||||
* Statements can be passed as argument for count()
|
||||
* to return the number for affected rows from last execution
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): 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);
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
deprecationWarning(
|
||||
'Since 4.0.0: Cake\Database\Type is deprecated. Use Cake\Database\TypeFactory instead.'
|
||||
);
|
||||
class_exists('Cake\Database\TypeFactory');
|
||||
+80
@@ -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\DriverInterface;
|
||||
use Cake\Database\TypeInterface;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Base type class.
|
||||
*/
|
||||
abstract class BaseType implements TypeInterface
|
||||
{
|
||||
/**
|
||||
* Identifier name for this type
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $_name;
|
||||
|
||||
/**
|
||||
* 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($value, DriverInterface $driver)
|
||||
{
|
||||
if ($value === null) {
|
||||
return PDO::PARAM_NULL;
|
||||
}
|
||||
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function newId()
|
||||
{
|
||||
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\DriverInterface;
|
||||
|
||||
/**
|
||||
* 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\DriverInterface $driver Object from which database preferences and configuration will be extracted.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, DriverInterface $driver): array;
|
||||
}
|
||||
+92
@@ -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\Type;
|
||||
|
||||
use Cake\Core\Exception\CakeException;
|
||||
use Cake\Database\DriverInterface;
|
||||
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\DriverInterface $driver The driver instance to convert with.
|
||||
* @return resource|string
|
||||
*/
|
||||
public function toDatabase($value, DriverInterface $driver)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary into resource handles
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
|
||||
* @return resource|null
|
||||
* @throws \Cake\Core\Exception\CakeException
|
||||
*/
|
||||
public function toPHP($value, DriverInterface $driver)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return fopen('data:text/plain;base64,' . base64_encode($value), 'rb');
|
||||
}
|
||||
if (is_resource($value)) {
|
||||
return $value;
|
||||
}
|
||||
throw new CakeException(sprintf('Unable to convert %s into binary.', gettype($value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct PDO binding type for Binary data.
|
||||
*
|
||||
* @param mixed $value The value being bound.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver.
|
||||
* @return int
|
||||
*/
|
||||
public function toStatement($value, DriverInterface $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($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+145
@@ -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)
|
||||
* @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\DriverInterface;
|
||||
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\DriverInterface $driver The driver instance to convert with.
|
||||
* @return resource|string|null
|
||||
*/
|
||||
public function toDatabase($value, DriverInterface $driver)
|
||||
{
|
||||
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\DriverInterface $driver The driver instance to convert with.
|
||||
* @return resource|string|null
|
||||
* @throws \Cake\Core\Exception\CakeException
|
||||
*/
|
||||
public function toPHP($value, DriverInterface $driver)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct PDO binding type for Binary data.
|
||||
*
|
||||
* @param mixed $value The value being bound.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver.
|
||||
* @return int
|
||||
*/
|
||||
public function toStatement($value, DriverInterface $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($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a binary uuid to a string representation
|
||||
*
|
||||
* @param mixed $binary The value to convert.
|
||||
* @return string Converted value.
|
||||
*/
|
||||
protected function convertBinaryUuidToString($binary): string
|
||||
{
|
||||
$string = unpack('H*', $binary);
|
||||
|
||||
$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 = str_replace('-', '', $string);
|
||||
|
||||
return pack('H*', $string);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?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\DriverInterface;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use function Cake\Core\getTypeName;
|
||||
|
||||
/**
|
||||
* 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\DriverInterface $driver The driver instance to convert with.
|
||||
* @return bool|null
|
||||
*/
|
||||
public function toDatabase($value, DriverInterface $driver): ?bool
|
||||
{
|
||||
if ($value === true || $value === false || $value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (in_array($value, [1, 0, '1', '0'], true)) {
|
||||
return (bool)$value;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value of type `%s` to bool',
|
||||
getTypeName($value)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bool values to PHP booleans
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
|
||||
* @return bool|null
|
||||
*/
|
||||
public function toPHP($value, DriverInterface $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, DriverInterface $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct PDO binding type for bool data.
|
||||
*
|
||||
* @param mixed $value The value being bound.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver.
|
||||
* @return int
|
||||
*/
|
||||
public function toStatement($value, DriverInterface $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($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\DriverInterface;
|
||||
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\DriverInterface $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, DriverInterface $driver): ?string;
|
||||
|
||||
/**
|
||||
* Convert a SQL column definition to an abstract type definition.
|
||||
*
|
||||
* @param array $definition The column definition.
|
||||
* @param \Cake\Database\DriverInterface $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, DriverInterface $driver): ?array;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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 $_format = 'Y-m-d H:i:s.u';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_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',
|
||||
];
|
||||
}
|
||||
@@ -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 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 $_format = 'Y-m-d H:i:s.uP';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_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',
|
||||
];
|
||||
}
|
||||
+563
@@ -0,0 +1,563 @@
|
||||
<?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\DriverInterface;
|
||||
use Cake\I18n\FrozenTime;
|
||||
use Cake\I18n\I18nDateTimeInterface;
|
||||
use Cake\I18n\Time;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Datetime type converter.
|
||||
*
|
||||
* Use to convert datetime instances to strings & back.
|
||||
*/
|
||||
class DateTimeType extends BaseType implements BatchCastingInterface
|
||||
{
|
||||
/**
|
||||
* Whether we want to override the time of the converted Time objects
|
||||
* so it points to the start of the day.
|
||||
*
|
||||
* This is primarily to avoid subclasses needing to re-implement the same functionality.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $setToDateStart = false;
|
||||
|
||||
/**
|
||||
* The DateTime format used when converting to string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_format = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* The DateTime formats allowed by `marshal()`.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $_marshalFormats = [
|
||||
'Y-m-d H:i',
|
||||
'Y-m-d H:i:s',
|
||||
'Y-m-d\TH:i',
|
||||
'Y-m-d\TH:i:s',
|
||||
'Y-m-d\TH:i:sP',
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_useLocaleMarshal = false;
|
||||
|
||||
/**
|
||||
* The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
|
||||
*
|
||||
* See `Cake\I18n\Time::parseDateTime()` for accepted formats.
|
||||
*
|
||||
* @var array|string|int
|
||||
*/
|
||||
protected $_localeMarshalFormat;
|
||||
|
||||
/**
|
||||
* The classname to use when creating objects.
|
||||
*
|
||||
* @var string
|
||||
* @psalm-var class-string<\DateTime>|class-string<\DateTimeImmutable>
|
||||
*/
|
||||
protected $_className;
|
||||
|
||||
/**
|
||||
* Database time zone.
|
||||
*
|
||||
* @var \DateTimeZone|null
|
||||
*/
|
||||
protected $dbTimezone;
|
||||
|
||||
/**
|
||||
* User time zone.
|
||||
*
|
||||
* @var \DateTimeZone|null
|
||||
*/
|
||||
protected $userTimezone;
|
||||
|
||||
/**
|
||||
* Default time zone.
|
||||
*
|
||||
* @var \DateTimeZone
|
||||
*/
|
||||
protected $defaultTimezone;
|
||||
|
||||
/**
|
||||
* Whether database time zone is kept when converting
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $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->_setClassName(FrozenTime::class, DateTimeImmutable::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DateTime instance into strings.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toDatabase($value, DriverInterface $driver): ?string
|
||||
{
|
||||
if ($value === null || is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
$class = $this->_className;
|
||||
$value = new $class('@' . $value);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for `setDatabaseTimezone()`.
|
||||
*
|
||||
* @param \DateTimeZone|string|null $timezone Database timezone.
|
||||
* @return $this
|
||||
* @deprecated 4.1.0 Use {@link setDatabaseTimezone()} instead.
|
||||
*/
|
||||
public function setTimezone($timezone)
|
||||
{
|
||||
deprecationWarning('DateTimeType::setTimezone() is deprecated. Use setDatabaseTimezone() instead.');
|
||||
|
||||
return $this->setDatabaseTimezone($timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($timezone)
|
||||
{
|
||||
if (is_string($timezone)) {
|
||||
$timezone = new DateTimeZone($timezone);
|
||||
}
|
||||
$this->dbTimezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user timezone.
|
||||
*
|
||||
* This is the time zone used when marshalling strings to DateTime instances.
|
||||
*
|
||||
* @param \DateTimeZone|string|null $timezone User timezone.
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserTimezone($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\DriverInterface $driver Object from which database preferences and configuration will be extracted
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function toPHP($value, DriverInterface $driver)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = $this->_className;
|
||||
if (is_int($value)) {
|
||||
$instance = new $class('@' . $value);
|
||||
} elseif (strpos($value, '0000-00-00') === 0) {
|
||||
return null;
|
||||
} else {
|
||||
$instance = new $class($value, $this->dbTimezone);
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->keepDatabaseTimezone &&
|
||||
$instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
|
||||
) {
|
||||
$instance = $instance->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
if ($this->setToDateStart) {
|
||||
$instance = $instance->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
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, DriverInterface $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 (strpos($value, '0000-00-00') === 0) {
|
||||
$values[$field] = null;
|
||||
continue;
|
||||
} else {
|
||||
$instance = new $class($value, $this->dbTimezone);
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->keepDatabaseTimezone &&
|
||||
$instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
|
||||
) {
|
||||
$instance = $instance->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
if ($this->setToDateStart) {
|
||||
$instance = $instance->setTime(0, 0, 0);
|
||||
}
|
||||
|
||||
$values[$field] = $instance;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert request data into a datetime object.
|
||||
*
|
||||
* @param mixed $value Request data
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function marshal($value): ?DateTimeInterface
|
||||
{
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
if ($value instanceof DateTime) {
|
||||
$value = clone $value;
|
||||
}
|
||||
|
||||
if ($value instanceof ChronosDate) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** @var \Datetime|\DateTimeImmutable $value */
|
||||
return $value->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
/** @var class-string<\DateTimeInterface> $class */
|
||||
$class = $this->_className;
|
||||
try {
|
||||
if ($value === '' || $value === null || is_bool($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
|
||||
/** @var \DateTime|\DateTimeImmutable $dateTime */
|
||||
$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);
|
||||
}
|
||||
|
||||
/** @var \DateTime|\DateTimeImmutable $dateTime */
|
||||
if ($dateTime !== null) {
|
||||
$dateTime = $dateTime->setTimezone($this->defaultTimezone);
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) && implode('', $value) === '') {
|
||||
return null;
|
||||
}
|
||||
$value += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0];
|
||||
|
||||
$format = '';
|
||||
if (
|
||||
isset($value['year'], $value['month'], $value['day']) &&
|
||||
(
|
||||
is_numeric($value['year']) &&
|
||||
is_numeric($value['month']) &&
|
||||
is_numeric($value['day'])
|
||||
)
|
||||
) {
|
||||
$format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
|
||||
}
|
||||
|
||||
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(
|
||||
'%s%02d:%02d:%02d.%06d',
|
||||
empty($format) ? '' : ' ',
|
||||
$value['hour'],
|
||||
$value['minute'],
|
||||
$value['second'],
|
||||
$value['microsecond']
|
||||
);
|
||||
|
||||
/** @var \DateTime|\DateTimeImmutable $dateTime */
|
||||
$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_subclass_of($this->_className, I18nDateTimeInterface::class)) {
|
||||
$this->_useLocaleMarshal = $enable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
throw new RuntimeException(
|
||||
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($format)
|
||||
{
|
||||
$this->_localeMarshalFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the preferred class name to the FrozenTime implementation.
|
||||
*
|
||||
* @return $this
|
||||
* @deprecated 4.3.0 This method is no longer needed as using immutable datetime class is the default behavior.
|
||||
*/
|
||||
public function useImmutable()
|
||||
{
|
||||
deprecationWarning(
|
||||
'Configuring immutable or mutable classes is deprecated and immutable'
|
||||
. ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.'
|
||||
);
|
||||
|
||||
$this->_setClassName(FrozenTime::class, DateTimeImmutable::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the classname to use when building objects.
|
||||
*
|
||||
* @param string $class The classname to use.
|
||||
* @param string $fallback The classname to use when the preferred class does not exist.
|
||||
* @return void
|
||||
* @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $class
|
||||
* @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $fallback
|
||||
*/
|
||||
protected function _setClassName(string $class, string $fallback): void
|
||||
{
|
||||
if (!class_exists($class)) {
|
||||
$class = $fallback;
|
||||
}
|
||||
$this->_className = $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the classname used for building objects.
|
||||
*
|
||||
* @return string
|
||||
* @psalm-return class-string<\DateTime>|class-string<\DateTimeImmutable>
|
||||
*/
|
||||
public function getDateTimeClassName(): string
|
||||
{
|
||||
return $this->_className;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the preferred class name to the mutable Time implementation.
|
||||
*
|
||||
* @return $this
|
||||
* @deprecated 4.3.0 Using mutable datetime objects is deprecated.
|
||||
*/
|
||||
public function useMutable()
|
||||
{
|
||||
deprecationWarning(
|
||||
'Configuring immutable or mutable classes is deprecated and immutable'
|
||||
. ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.'
|
||||
);
|
||||
|
||||
$this->_setClassName(Time::class, DateTime::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\I18nDateTimeInterface|null
|
||||
*/
|
||||
protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
|
||||
{
|
||||
/** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $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 \DateTimeInterface|null
|
||||
*/
|
||||
protected function _parseValue(string $value): ?DateTimeInterface
|
||||
{
|
||||
$class = $this->_className;
|
||||
|
||||
foreach ($this->_marshalFormats as $format) {
|
||||
try {
|
||||
$dateTime = $class::createFromFormat($format, $value, $this->userTimezone);
|
||||
// Check for false in case DateTime is used directly
|
||||
if ($dateTime !== false) {
|
||||
return $dateTime;
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Chronos wraps DateTime::createFromFormat and throws
|
||||
// exception if parse fails.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts given value to Statement equivalent
|
||||
*
|
||||
* @param mixed $value value to be converted to PDO statement
|
||||
* @param \Cake\Database\DriverInterface $driver object from which database preferences and configuration will be extracted
|
||||
* @return mixed
|
||||
*/
|
||||
public function toStatement($value, DriverInterface $driver)
|
||||
{
|
||||
return PDO::PARAM_STR;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<?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\I18n\Date;
|
||||
use Cake\I18n\FrozenDate;
|
||||
use Cake\I18n\I18nDateTimeInterface;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use function Cake\Core\deprecationWarning;
|
||||
|
||||
/**
|
||||
* Class DateType
|
||||
*/
|
||||
class DateType extends DateTimeType
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_format = 'Y-m-d';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_marshalFormats = [
|
||||
'Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
* In this class we want Date objects to have their time
|
||||
* set to the beginning of the day.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $setToDateStart = true;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->_setClassName(FrozenDate::class, DateTimeImmutable::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the preferred class name to the FrozenDate implementation.
|
||||
*
|
||||
* @return $this
|
||||
* @deprecated 4.3.0 This method is no longer needed as using immutable datetime class is the default behavior.
|
||||
*/
|
||||
public function useImmutable()
|
||||
{
|
||||
deprecationWarning(
|
||||
'Configuring immutable or mutable classes is deprecated and immutable'
|
||||
. ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.'
|
||||
);
|
||||
|
||||
$this->_setClassName(FrozenDate::class, DateTimeImmutable::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the preferred class name to the mutable Date implementation.
|
||||
*
|
||||
* @return $this
|
||||
* @deprecated 4.3.0 Using mutable datetime objects is deprecated.
|
||||
*/
|
||||
public function useMutable()
|
||||
{
|
||||
deprecationWarning(
|
||||
'Configuring immutable or mutable classes is deprecated and immutable'
|
||||
. ' classes will be the permanent configuration in 5.0. Calling `useImmutable()` is unnecessary.'
|
||||
);
|
||||
|
||||
$this->_setClassName(Date::class, DateTime::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert request data into a datetime object.
|
||||
*
|
||||
* @param mixed $value Request data
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function marshal($value): ?DateTimeInterface
|
||||
{
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return new FrozenDate($value);
|
||||
}
|
||||
|
||||
/** @var class-string<\Cake\Chronos\ChronosDate> $class */
|
||||
$class = $this->_className;
|
||||
try {
|
||||
if ($value === '' || $value === null || is_bool($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_int($value) || (is_string($value) && ctype_digit($value))) {
|
||||
/** @var \Cake\I18n\FrozenDate|\DateTimeImmutable $dateTime */
|
||||
$dateTime = new $class('@' . $value);
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($this->_useLocaleMarshal) {
|
||||
$dateTime = $this->_parseLocaleValue($value);
|
||||
} else {
|
||||
$dateTime = $this->_parseValue($value);
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) && implode('', $value) === '') {
|
||||
return null;
|
||||
}
|
||||
$format = '';
|
||||
if (
|
||||
isset($value['year'], $value['month'], $value['day']) &&
|
||||
(
|
||||
is_numeric($value['year']) &&
|
||||
is_numeric($value['month']) &&
|
||||
is_numeric($value['day'])
|
||||
)
|
||||
) {
|
||||
$format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
|
||||
}
|
||||
|
||||
if (empty($format)) {
|
||||
// Invalid array format.
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var \Cake\I18n\FrozenDate|\DateTimeImmutable $dateTime */
|
||||
$dateTime = new $class($format);
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
|
||||
{
|
||||
/** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
|
||||
$class = $this->_className;
|
||||
|
||||
return $class::parseDate($value, $this->_localeMarshalFormat);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?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\DriverInterface;
|
||||
use Cake\I18n\Number;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use function Cake\Core\getTypeName;
|
||||
|
||||
/**
|
||||
* 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 string
|
||||
*/
|
||||
public static $numberClass = Number::class;
|
||||
|
||||
/**
|
||||
* Whether numbers should be parsed using a locale aware parser
|
||||
* when marshalling string inputs.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_useLocaleParser = false;
|
||||
|
||||
/**
|
||||
* Convert decimal strings into the database format.
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
|
||||
* @return string|float|int|null
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function toDatabase($value, DriverInterface $driver)
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (
|
||||
is_object($value)
|
||||
&& method_exists($value, '__toString')
|
||||
&& is_numeric(strval($value))
|
||||
) {
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot convert value of type `%s` to a decimal',
|
||||
getTypeName($value)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @param mixed $value The value to convert.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
|
||||
* @return string|null
|
||||
*/
|
||||
public function toPHP($value, DriverInterface $driver): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function manyToPHP(array $values, array $fields, DriverInterface $driver): array
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($values[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$field] = (string)$values[$field];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct PDO binding type for decimal data.
|
||||
*
|
||||
* @param mixed $value The value being bound.
|
||||
* @param \Cake\Database\DriverInterface $driver The driver.
|
||||
* @return int
|
||||
*/
|
||||
public function toStatement($value, DriverInterface $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($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 \RuntimeException
|
||||
*/
|
||||
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 RuntimeException(
|
||||
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
|
||||
{
|
||||
/** @var \Cake\I18n\Number $class */
|
||||
$class = static::$numberClass;
|
||||
|
||||
return (string)$class::parseFloat($value);
|
||||
}
|
||||
}
|
||||
@@ -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($value, ?string $type = null)
|
||||
{
|
||||
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 $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($value): ExpressionInterface;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user