Composer Version Change

This commit is contained in:
2024-02-05 00:00:23 +08:00
parent 20678a6a0c
commit 6f751ead83
789 changed files with 135417 additions and 18510 deletions
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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;
}
+559
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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');
+37
View File
@@ -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;
}
}
}
+144
View File
@@ -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};
}
}
}
}
+251
View File
@@ -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;
}
}
}
+7
View File
@@ -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
+39
View File
@@ -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();
}
+51
View File
@@ -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;
}
}
}
+876
View File
@@ -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;
}
}
}
}
+87
View File
@@ -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;
}
}
+231
View File
@@ -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']);
}
}
+118
View File
@@ -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;
}
}
}
+325
View File
@@ -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;
}
}
}
+335
View File
@@ -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;
}
}
}
+163
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
);
}
}
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
+2516
View File
File diff suppressed because it is too large Load Diff
+222
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+358
View File
@@ -0,0 +1,358 @@
[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/database.svg?style=flat-square)](https://packagist.org/packages/cakephp/database)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](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 youve made your query, youll 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.
+69
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
}
+54
View File
@@ -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
View File
@@ -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');
+669
View File
@@ -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
+10
View File
@@ -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');
+704
View File
@@ -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
View File
@@ -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
View File
@@ -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');
+649
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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);
}
+283
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+361
View File
@@ -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;
}
}
+77
View File
@@ -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;
}
}
+46
View File
@@ -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
View File
@@ -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);
}
}
+70
View File
@@ -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);
}
}
}
+373
View File
@@ -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
View File
@@ -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);
}
+9
View File
@@ -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
View File
@@ -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;
}
}
+37
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+42
View File
@@ -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',
];
}
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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