init
This commit is contained in:
+337
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\Schema\MysqlSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\StatementInterface;
|
||||
use PDO;
|
||||
use Pdo\Mysql as PdoMysql;
|
||||
|
||||
/**
|
||||
* MySQL Driver
|
||||
*/
|
||||
class Mysql extends Driver
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 256;
|
||||
|
||||
/**
|
||||
* Server type MySQL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const SERVER_TYPE_MYSQL = 'mysql';
|
||||
|
||||
/**
|
||||
* Server type MariaDB
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected const SERVER_TYPE_MARIADB = 'mariadb';
|
||||
|
||||
/**
|
||||
* Base configuration settings for MySQL driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'port' => '3306',
|
||||
'flags' => [],
|
||||
'encoding' => 'utf8mb4',
|
||||
'timezone' => null,
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '`';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '`';
|
||||
|
||||
/**
|
||||
* Server type.
|
||||
*
|
||||
* If the underlying server is MariaDB, its value will get set to `'mariadb'`
|
||||
* after `version()` method is called.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $serverType = self::SERVER_TYPE_MYSQL;
|
||||
|
||||
/**
|
||||
* Mapping of feature to db server version for feature availability checks.
|
||||
*
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
protected array $featureVersions = [
|
||||
'mysql' => [
|
||||
'json' => '5.7.0',
|
||||
'cte' => '8.0.0',
|
||||
'window' => '8.0.0',
|
||||
'intersect' => '8.0.31',
|
||||
'intersect-all' => '8.0.31',
|
||||
'check-constraints' => '8.0.16',
|
||||
],
|
||||
'mariadb' => [
|
||||
'json' => '10.2.7',
|
||||
'cte' => '10.2.1',
|
||||
'window' => '10.2.0',
|
||||
'intersect' => '10.3.0',
|
||||
'intersect-all' => '10.5.0',
|
||||
'check-constraints' => '10.2.1',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
|
||||
if ($config['timezone'] === 'UTC') {
|
||||
$config['timezone'] = '+0:00';
|
||||
}
|
||||
|
||||
if (!empty($config['timezone'])) {
|
||||
$config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
|
||||
}
|
||||
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
$this->attrUseBufferedQueryId() => true,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
|
||||
if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
|
||||
$config['flags'][$this->attrSslKeyId()] = $config['ssl_key'];
|
||||
$config['flags'][$this->attrSslCertId()] = $config['ssl_cert'];
|
||||
}
|
||||
if (!empty($config['ssl_ca'])) {
|
||||
$config['flags'][$this->attrSslCaId()] = $config['ssl_ca'];
|
||||
}
|
||||
|
||||
if (empty($config['unix_socket'])) {
|
||||
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
|
||||
} else {
|
||||
$dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
|
||||
}
|
||||
|
||||
if (!empty($config['encoding'])) {
|
||||
$dsn .= ";charset={$config['encoding']}";
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function run(Query $query): StatementInterface
|
||||
{
|
||||
$statement = $this->prepare($query);
|
||||
$query->getValueBinder()->attachTo($statement);
|
||||
|
||||
if ($query instanceof SelectQuery) {
|
||||
try {
|
||||
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), $query->isBufferedResultsEnabled());
|
||||
$this->executeStatement($statement);
|
||||
} finally {
|
||||
$this->getPdo()->setAttribute($this->attrUseBufferedQueryId(), true);
|
||||
}
|
||||
} else {
|
||||
$this->executeStatement($statement);
|
||||
}
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('mysql', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new MysqlSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schema(): string
|
||||
{
|
||||
return $this->_config['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET foreign_key_checks = 0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET foreign_key_checks = 1';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
$versionCompare = function () use ($feature) {
|
||||
return version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$this->serverType][$feature->value],
|
||||
'>=',
|
||||
);
|
||||
};
|
||||
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT => true,
|
||||
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => false,
|
||||
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::JSON,
|
||||
DriverFeatureEnum::WINDOW => $versionCompare(),
|
||||
DriverFeatureEnum::INTERSECT => $versionCompare(),
|
||||
DriverFeatureEnum::INTERSECT_ALL => $versionCompare(),
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => $versionCompare(),
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the connected server is MariaDB.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMariadb(): bool
|
||||
{
|
||||
$this->version();
|
||||
|
||||
return $this->serverType === static::SERVER_TYPE_MARIADB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns connected server version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function version(): string
|
||||
{
|
||||
if ($this->_version === null) {
|
||||
$this->_version = (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
|
||||
if (str_contains($this->_version, 'MariaDB')) {
|
||||
$this->serverType = static::SERVER_TYPE_MARIADB;
|
||||
preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
|
||||
$this->_version = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_KEY id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslKeyId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_KEY : PdoMysql::ATTR_SSL_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_CERT id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslCertId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CERT : PdoMysql::ATTR_SSL_CERT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_SSL_CA id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrSslCaId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_SSL_CA : PdoMysql::ATTR_SSL_CA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO ATTR_USE_BUFFERED_QUERY id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function attrUseBufferedQueryId(): int
|
||||
{
|
||||
return PHP_VERSION_ID < 80400 ? PDO::MYSQL_ATTR_USE_BUFFERED_QUERY : PdoMysql::ATTR_USE_BUFFERED_QUERY;
|
||||
}
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\StringExpression;
|
||||
use Cake\Database\PostgresCompiler;
|
||||
use Cake\Database\Query\InsertQuery;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\PostgresSchemaDialect;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Class Postgres
|
||||
*/
|
||||
class Postgres extends Driver
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 63;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Postgres driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => true,
|
||||
'host' => 'localhost',
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'schema' => 'public',
|
||||
'port' => 5432,
|
||||
'encoding' => 'utf8',
|
||||
'timezone' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
'ssl_key' => null,
|
||||
'ssl_cert' => null,
|
||||
'ssl_ca' => null,
|
||||
'ssl' => false,
|
||||
'ssl_mode' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
if (empty($config['unix_socket'])) {
|
||||
$dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
|
||||
} else {
|
||||
$dsn = "pgsql:dbname={$config['database']}";
|
||||
}
|
||||
|
||||
if ($this->_config['ssl']) {
|
||||
if ($this->_config['ssl_mode']) {
|
||||
$dsn .= ';sslmode=' . $this->_config['ssl_mode'];
|
||||
} else {
|
||||
$dsn .= ';sslmode=allow';
|
||||
}
|
||||
|
||||
if ($this->_config['ssl_key']) {
|
||||
$dsn .= ';sslkey=' . $this->_config['ssl_key'];
|
||||
}
|
||||
if ($this->_config['ssl_cert']) {
|
||||
$dsn .= ';sslcert=' . $this->_config['ssl_cert'];
|
||||
}
|
||||
if ($this->_config['ssl_ca']) {
|
||||
$dsn .= ';sslrootcert=' . $this->_config['ssl_ca'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if (!empty($config['encoding'])) {
|
||||
$this->setEncoding($config['encoding']);
|
||||
}
|
||||
|
||||
if (!empty($config['schema'])) {
|
||||
$this->setSchema($config['schema']);
|
||||
}
|
||||
|
||||
if (!empty($config['timezone'])) {
|
||||
$config['init'][] = sprintf('SET timezone = %s', $this->getPdo()->quote($config['timezone']));
|
||||
}
|
||||
|
||||
foreach ($config['init'] as $command) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('pgsql', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new PostgresSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets connection encoding
|
||||
*
|
||||
* @param string $encoding The encoding to use.
|
||||
* @return void
|
||||
*/
|
||||
public function setEncoding(string $encoding): void
|
||||
{
|
||||
$pdo = $this->getPdo();
|
||||
$pdo->exec('SET NAMES ' . $pdo->quote($encoding));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets connection default schema, if any relation defined in a query is not fully qualified
|
||||
* postgres will fallback to looking the relation into defined default schema
|
||||
*
|
||||
* @param string $schema The schema names to set `search_path` to.
|
||||
* @return void
|
||||
*/
|
||||
public function setSchema(string $schema): void
|
||||
{
|
||||
$pdo = $this->getPdo();
|
||||
$pdo->exec('SET search_path TO ' . $pdo->quote($schema));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL DEFERRED';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL IMMEDIATE';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::JSON,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
|
||||
DriverFeatureEnum::WINDOW => true,
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => true,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => true,
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => true,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _transformDistinct(SelectQuery $query): SelectQuery
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _insertQueryTranslator(InsertQuery $query): InsertQuery
|
||||
{
|
||||
if (!$query->clause('epilog')) {
|
||||
$query->epilog('RETURNING *');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
IdentifierExpression::class => '_transformIdentifierExpression',
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
StringExpression::class => '_transformStringExpression',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes identifer expression into postgresql format.
|
||||
*
|
||||
* @param \Cake\Database\Expression\IdentifierExpression $expression The expression to transform.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformIdentifierExpression(IdentifierExpression $expression): void
|
||||
{
|
||||
$collation = $expression->getCollation();
|
||||
if ($collation) {
|
||||
// use trim() to work around expression being transformed multiple times
|
||||
$expression->setCollation('"' . trim($collation, '"') . '"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
|
||||
* to postgres SQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 || exp2
|
||||
$expression->setName('')->setConjunction(' ||');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$expression
|
||||
->setName('')
|
||||
->setConjunction('-')
|
||||
->iterateParts(function ($p) {
|
||||
if (is_string($p)) {
|
||||
$p = ['value' => [$p => 'literal'], 'type' => null];
|
||||
} else {
|
||||
$p['value'] = [$p['value']];
|
||||
}
|
||||
|
||||
return new FunctionExpression('DATE', $p['value'], [$p['type']]);
|
||||
});
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
|
||||
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
|
||||
$expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
|
||||
break;
|
||||
case 'RAND':
|
||||
$expression->setName('RANDOM');
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$expression
|
||||
->setName('')
|
||||
->setConjunction(' + INTERVAL')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 1) {
|
||||
return sprintf("'%s'", $p);
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('EXTRACT')
|
||||
->setConjunction(' ')
|
||||
->add(['DOW FROM' => 'literal'], [], true)
|
||||
->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
|
||||
break;
|
||||
case 'JSON_VALUE':
|
||||
$expression->setName('JSONB_PATH_QUERY')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 0) {
|
||||
$p = sprintf('%s::jsonb', $p);
|
||||
} elseif ($key === 1) {
|
||||
$p = sprintf("'%s'::jsonpath", $this->quoteIdentifier($p['value']));
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes string expression into postgresql format.
|
||||
*
|
||||
* @param \Cake\Database\Expression\StringExpression $expression The string expression to transform.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformStringExpression(StringExpression $expression): void
|
||||
{
|
||||
// use trim() to work around expression being transformed multiple times
|
||||
$expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return \Cake\Database\PostgresCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new PostgresCompiler();
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\SqliteSchemaDialect;
|
||||
use Cake\Database\Statement\SqliteStatement;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Class Sqlite
|
||||
*/
|
||||
class Sqlite extends Driver
|
||||
{
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const STATEMENT_CLASS = SqliteStatement::class;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlite driver
|
||||
*
|
||||
* - `mask` The mask used for created database
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'persistent' => false,
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
'database' => ':memory:',
|
||||
'encoding' => 'utf8',
|
||||
'mask' => 0644,
|
||||
'cache' => null,
|
||||
'mode' => null,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether the connected server supports window functions.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
protected ?bool $_supportsWindowFunctions = null;
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '"';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = '"';
|
||||
|
||||
/**
|
||||
* Mapping of date parts.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $_dateParts = [
|
||||
'day' => 'd',
|
||||
'hour' => 'H',
|
||||
'month' => 'm',
|
||||
'minute' => 'M',
|
||||
'second' => 'S',
|
||||
'week' => 'W',
|
||||
'year' => 'Y',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping of feature to db server version for feature availability checks.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $featureVersions = [
|
||||
'cte' => '3.8.3',
|
||||
'window' => '3.28.0',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_PERSISTENT => $config['persistent'],
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
if (!is_string($config['database']) || $config['database'] === '') {
|
||||
$name = $config['name'] ?? 'unknown';
|
||||
throw new InvalidArgumentException(
|
||||
"The `database` key for the `{$name}` SQLite connection needs to be a non-empty string.",
|
||||
);
|
||||
}
|
||||
|
||||
$chmodFile = false;
|
||||
if ($config['database'] !== ':memory:' && $config['mode'] !== 'memory') {
|
||||
$chmodFile = !file_exists($config['database']);
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($config['cache']) {
|
||||
$params[] = 'cache=' . $config['cache'];
|
||||
}
|
||||
if ($config['mode']) {
|
||||
$params[] = 'mode=' . $config['mode'];
|
||||
}
|
||||
|
||||
if ($params) {
|
||||
$dsn = 'sqlite:file:' . $config['database'] . '?' . implode('&', $params);
|
||||
} else {
|
||||
$dsn = 'sqlite:' . $config['database'];
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if ($chmodFile) {
|
||||
// phpcs:disable
|
||||
@chmod($config['database'], $config['mask']);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether php is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('sqlite', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL for disabling foreign keys.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'PRAGMA foreign_keys = ON';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS => true,
|
||||
|
||||
DriverFeatureEnum::JSON => false,
|
||||
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::WINDOW => version_compare(
|
||||
$this->version(),
|
||||
$this->featureVersions[$feature->value],
|
||||
'>=',
|
||||
),
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => false,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ?? ($this->_schemaDialect = new SqliteSchemaDialect($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
TupleComparison::class => '_transformTupleComparison',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 || exp2
|
||||
$expression->setName('')->setConjunction(' ||');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$expression
|
||||
->setName('ROUND')
|
||||
->setConjunction('-')
|
||||
->iterateParts(function ($p) {
|
||||
return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
|
||||
});
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('DATETIME')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'RAND':
|
||||
$expression
|
||||
->setName('ABS')
|
||||
->add(['RANDOM() % 1' => 'literal'], [], true);
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$expression->setName('DATE')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$expression->setName('TIME')->add(["'now'" => 'literal']);
|
||||
break;
|
||||
case 'EXTRACT':
|
||||
$expression
|
||||
->setName('STRFTIME')
|
||||
->setConjunction(' ,')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 0) {
|
||||
$value = rtrim(strtolower($p), 's');
|
||||
if (isset($this->_dateParts[$value])) {
|
||||
$p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$expression
|
||||
->setName('DATE')
|
||||
->setConjunction(',')
|
||||
->iterateParts(function ($p, $key) {
|
||||
if ($key === 1) {
|
||||
return ['value' => $p, 'type' => null];
|
||||
}
|
||||
|
||||
return $p;
|
||||
});
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('STRFTIME')
|
||||
->setConjunction(' ')
|
||||
->add(["'%w', " => 'literal'], [], true)
|
||||
->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
|
||||
break;
|
||||
case 'JSON_VALUE':
|
||||
$expression->setName('JSON_EXTRACT');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Driver;
|
||||
|
||||
use Cake\Database\Driver;
|
||||
use Cake\Database\DriverFeatureEnum;
|
||||
use Cake\Database\Expression\FunctionExpression;
|
||||
use Cake\Database\Expression\OrderByExpression;
|
||||
use Cake\Database\Expression\OrderClauseExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Expression\UnaryExpression;
|
||||
use Cake\Database\ExpressionInterface;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use Cake\Database\QueryCompiler;
|
||||
use Cake\Database\Schema\SchemaDialect;
|
||||
use Cake\Database\Schema\SqlserverSchemaDialect;
|
||||
use Cake\Database\SqlserverCompiler;
|
||||
use Cake\Database\Statement\SqlserverStatement;
|
||||
use Cake\Database\StatementInterface;
|
||||
use InvalidArgumentException;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQLServer driver.
|
||||
*/
|
||||
class Sqlserver extends Driver
|
||||
{
|
||||
use TupleComparisonTranslatorTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const MAX_ALIAS_LENGTH = 128;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const RETRY_ERROR_CODES = [
|
||||
40613, // Azure Sql Database paused
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected const STATEMENT_CLASS = SqlserverStatement::class;
|
||||
|
||||
/**
|
||||
* Base configuration settings for Sqlserver driver
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $_baseConfig = [
|
||||
'host' => 'localhost\SQLEXPRESS',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'database' => 'cake',
|
||||
'port' => '',
|
||||
// PDO::SQLSRV_ENCODING_UTF8
|
||||
'encoding' => 65001,
|
||||
'flags' => [],
|
||||
'init' => [],
|
||||
'settings' => [],
|
||||
'attributes' => [],
|
||||
'app' => null,
|
||||
'connectionPooling' => null,
|
||||
'failoverPartner' => null,
|
||||
'loginTimeout' => null,
|
||||
'multiSubnetFailover' => null,
|
||||
'encrypt' => null,
|
||||
'trustServerCertificate' => null,
|
||||
'accessToken' => null,
|
||||
'authentication' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* String used to start a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_startQuote = '[';
|
||||
|
||||
/**
|
||||
* String used to end a database identifier quoting to make it safe
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $_endQuote = ']';
|
||||
|
||||
/**
|
||||
* Establishes a connection to the database server.
|
||||
*
|
||||
* Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
|
||||
* the SQL Server PHP PDO drivers. As a result you cannot use the
|
||||
* persistent config option when connecting to a SQL Server (for more
|
||||
* information see: https://github.com/Microsoft/msphpsql/issues/65).
|
||||
*
|
||||
* @throws \InvalidArgumentException if an unsupported setting is in the driver config
|
||||
* @return void
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
if ($this->pdo !== null) {
|
||||
return;
|
||||
}
|
||||
$config = $this->_config;
|
||||
|
||||
if (isset($config['persistent']) && $config['persistent']) {
|
||||
throw new InvalidArgumentException(
|
||||
'Config setting "persistent" cannot be set to true, '
|
||||
. 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT',
|
||||
);
|
||||
}
|
||||
|
||||
$config['flags'] += [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
];
|
||||
|
||||
if (!empty($config['encoding'])) {
|
||||
$config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
|
||||
}
|
||||
$port = '';
|
||||
if ($config['port']) {
|
||||
$port = ',' . $config['port'];
|
||||
}
|
||||
|
||||
$dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
|
||||
if ($config['app'] !== null) {
|
||||
$dsn .= ";APP={$config['app']}";
|
||||
}
|
||||
if ($config['connectionPooling'] !== null) {
|
||||
$dsn .= ";ConnectionPooling={$config['connectionPooling']}";
|
||||
}
|
||||
if ($config['failoverPartner'] !== null) {
|
||||
$dsn .= ";Failover_Partner={$config['failoverPartner']}";
|
||||
}
|
||||
if ($config['loginTimeout'] !== null) {
|
||||
$dsn .= ";LoginTimeout={$config['loginTimeout']}";
|
||||
}
|
||||
if ($config['multiSubnetFailover'] !== null) {
|
||||
$dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
|
||||
}
|
||||
if ($config['encrypt'] !== null) {
|
||||
$dsn .= ";Encrypt={$config['encrypt']}";
|
||||
}
|
||||
if ($config['trustServerCertificate'] !== null) {
|
||||
$dsn .= ";TrustServerCertificate={$config['trustServerCertificate']}";
|
||||
}
|
||||
if ($config['accessToken'] !== null) {
|
||||
$dsn .= ";AccessToken={$config['accessToken']}";
|
||||
}
|
||||
if ($config['authentication'] !== null) {
|
||||
$dsn .= ";Authentication={$config['authentication']}";
|
||||
}
|
||||
|
||||
$this->pdo = $this->createPdo($dsn, $config);
|
||||
if (!empty($config['init'])) {
|
||||
foreach ((array)$config['init'] as $command) {
|
||||
$this->pdo->exec($command);
|
||||
}
|
||||
}
|
||||
if (!empty($config['settings']) && is_array($config['settings'])) {
|
||||
foreach ($config['settings'] as $key => $value) {
|
||||
$this->pdo->exec("SET {$key} {$value}");
|
||||
}
|
||||
}
|
||||
if (!empty($config['attributes']) && is_array($config['attributes'])) {
|
||||
foreach ($config['attributes'] as $key => $value) {
|
||||
$this->pdo->setAttribute($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether PHP is able to use this driver for connecting to database
|
||||
*
|
||||
* @return bool true if it is valid to use this driver
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function prepare(Query|string $query): StatementInterface
|
||||
{
|
||||
$options = [
|
||||
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
|
||||
PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
|
||||
];
|
||||
|
||||
$sql = $query;
|
||||
if ($query instanceof Query) {
|
||||
$sql = $query->sql();
|
||||
if (count($query->getValueBinder()->bindings()) > 2100) {
|
||||
throw new InvalidArgumentException(
|
||||
'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
|
||||
'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
|
||||
'for each value in the array. ' .
|
||||
'If using an Association, try changing the `strategy` from select to subquery.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($query instanceof SelectQuery && !$query->isBufferedResultsEnabled()) {
|
||||
$options = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string $sql */
|
||||
$statement = $this->getPdo()->prepare(
|
||||
$sql,
|
||||
$options,
|
||||
);
|
||||
|
||||
/** @var \Cake\Database\StatementInterface */
|
||||
return new (static::STATEMENT_CLASS)($statement, $this, $this->getResultSetDecorators($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function savePointSQL($name): string
|
||||
{
|
||||
return 'SAVE TRANSACTION t' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function releaseSavePointSQL($name): string
|
||||
{
|
||||
// SQLServer has no release save point operation.
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function rollbackSavePointSQL($name): string
|
||||
{
|
||||
return 'ROLLBACK TRANSACTION t' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function disableForeignKeySQL(): string
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function enableForeignKeySQL(): string
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function supports(DriverFeatureEnum $feature): bool
|
||||
{
|
||||
return match ($feature) {
|
||||
DriverFeatureEnum::CTE,
|
||||
DriverFeatureEnum::DISABLE_CONSTRAINT_WITHOUT_TRANSACTION,
|
||||
DriverFeatureEnum::SAVEPOINT,
|
||||
DriverFeatureEnum::TRUNCATE_WITH_CONSTRAINTS,
|
||||
DriverFeatureEnum::WINDOW => true,
|
||||
DriverFeatureEnum::INTERSECT => true,
|
||||
DriverFeatureEnum::INTERSECT_ALL => false,
|
||||
DriverFeatureEnum::JSON => false,
|
||||
DriverFeatureEnum::SET_OPERATIONS_ORDER_BY => false,
|
||||
DriverFeatureEnum::OPTIMIZER_HINT_COMMENT => false,
|
||||
DriverFeatureEnum::CHECK_CONSTRAINTS => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function schemaDialect(): SchemaDialect
|
||||
{
|
||||
return $this->_schemaDialect ??= new SqlserverSchemaDialect($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return \Cake\Database\SqlserverCompiler
|
||||
*/
|
||||
public function newCompiler(): QueryCompiler
|
||||
{
|
||||
return new SqlserverCompiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _selectQueryTranslator(SelectQuery $query): SelectQuery
|
||||
{
|
||||
$limit = $query->clause('limit');
|
||||
$offset = $query->clause('offset');
|
||||
|
||||
if ($limit && $offset === null) {
|
||||
$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
|
||||
}
|
||||
|
||||
if ($offset !== null && !$query->clause('order')) {
|
||||
$query->orderBy($query->expr()->add('(SELECT NULL)'));
|
||||
}
|
||||
|
||||
if ($this->version() < 11 && $offset !== null) {
|
||||
return $this->_pagingSubquery($query, $limit, $offset);
|
||||
}
|
||||
|
||||
return $this->_transformDistinct($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a paging subquery for older versions of SQLserver.
|
||||
*
|
||||
* Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
|
||||
* be used.
|
||||
*
|
||||
* @param \Cake\Database\Query\SelectQuery<mixed> $original The query to wrap in a subquery.
|
||||
* @param int|null $limit The number of rows to fetch.
|
||||
* @param int|null $offset The number of rows to offset.
|
||||
* @return \Cake\Database\Query\SelectQuery<mixed> Modified query object.
|
||||
*/
|
||||
protected function _pagingSubquery(SelectQuery $original, ?int $limit, ?int $offset): SelectQuery
|
||||
{
|
||||
$field = '_cake_paging_._cake_page_rownum_';
|
||||
|
||||
/** @var \Cake\Database\Expression\OrderByExpression $originalOrder */
|
||||
$originalOrder = $original->clause('order');
|
||||
if ($originalOrder) {
|
||||
// SQL server does not support column aliases in OVER clauses. But
|
||||
// the only practical way to specify the use of calculated columns
|
||||
// is with their alias. So substitute the select SQL in place of
|
||||
// any column aliases for those entries in the order clause.
|
||||
$select = $original->clause('select');
|
||||
$order = new OrderByExpression();
|
||||
$originalOrder
|
||||
->iterateParts(function ($direction, $orderBy) use ($select, $order) {
|
||||
$key = $orderBy;
|
||||
if (
|
||||
isset($select[$orderBy]) &&
|
||||
$select[$orderBy] instanceof ExpressionInterface
|
||||
) {
|
||||
$order->add(new OrderClauseExpression($select[$orderBy], $direction));
|
||||
} else {
|
||||
$order->add([$key => $direction]);
|
||||
}
|
||||
|
||||
// Leave original order clause unchanged.
|
||||
return $orderBy;
|
||||
});
|
||||
} else {
|
||||
$order = new OrderByExpression('(SELECT NULL)');
|
||||
}
|
||||
|
||||
$query = clone $original;
|
||||
$query->select([
|
||||
'_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
|
||||
])->limit(null)
|
||||
->offset(null)
|
||||
->orderBy([], true);
|
||||
|
||||
$outer = $query->getConnection()->selectQuery();
|
||||
$outer->select('*')
|
||||
->from(['_cake_paging_' => $query]);
|
||||
|
||||
if ($offset) {
|
||||
$outer->where(["{$field} > " . $offset]);
|
||||
}
|
||||
if ($limit) {
|
||||
$value = (int)$offset + $limit;
|
||||
$outer->where(["{$field} <= {$value}"]);
|
||||
}
|
||||
|
||||
// Decorate the original query as that is what the
|
||||
// end developer will be calling execute() on originally.
|
||||
$original->decorateResults(function ($row) {
|
||||
if (isset($row['_cake_page_rownum_'])) {
|
||||
unset($row['_cake_page_rownum_']);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $outer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _transformDistinct(SelectQuery $query): SelectQuery
|
||||
{
|
||||
if (!is_array($query->clause('distinct'))) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$original = $query;
|
||||
$query = clone $original;
|
||||
|
||||
$distinct = $query->clause('distinct');
|
||||
$query->distinct(false);
|
||||
|
||||
$order = new OrderByExpression($distinct);
|
||||
$query
|
||||
->select(function (Query $q) use ($distinct, $order) {
|
||||
$over = $q->expr('ROW_NUMBER() OVER')
|
||||
->add('(PARTITION BY')
|
||||
->add($q->expr()->add($distinct)->setConjunction(','))
|
||||
->add($order)
|
||||
->add(')')
|
||||
->setConjunction(' ');
|
||||
|
||||
return [
|
||||
'_cake_distinct_pivot_' => $over,
|
||||
];
|
||||
})
|
||||
->limit(null)
|
||||
->offset(null)
|
||||
->orderBy([], true);
|
||||
|
||||
$outer = new SelectQuery($query->getConnection());
|
||||
$outer->select('*')
|
||||
->from(['_cake_distinct_' => $query])
|
||||
->where(['_cake_distinct_pivot_' => 1]);
|
||||
|
||||
// Decorate the original query as that is what the
|
||||
// end developer will be calling execute() on originally.
|
||||
$original->decorateResults(function ($row) {
|
||||
if (isset($row['_cake_distinct_pivot_'])) {
|
||||
unset($row['_cake_distinct_pivot_']);
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
return $outer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function _expressionTranslators(): array
|
||||
{
|
||||
return [
|
||||
FunctionExpression::class => '_transformFunctionExpression',
|
||||
TupleComparison::class => '_transformTupleComparison',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a FunctionExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformFunctionExpression(FunctionExpression $expression): void
|
||||
{
|
||||
switch ($expression->getName()) {
|
||||
case 'CONCAT':
|
||||
// CONCAT function is expressed as exp1 + exp2
|
||||
$expression->setName('')->setConjunction(' +');
|
||||
break;
|
||||
case 'DATEDIFF':
|
||||
$hasDay = false;
|
||||
$visitor = function ($value) use (&$hasDay) {
|
||||
if ($value === 'day') {
|
||||
$hasDay = true;
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
$expression->iterateParts($visitor);
|
||||
|
||||
if (!$hasDay) {
|
||||
$expression->add(['day' => 'literal'], [], true);
|
||||
}
|
||||
break;
|
||||
case 'CURRENT_DATE':
|
||||
$time = new FunctionExpression('GETUTCDATE');
|
||||
$expression->setName('CONVERT')->add(['date' => 'literal', $time]);
|
||||
break;
|
||||
case 'CURRENT_TIME':
|
||||
$time = new FunctionExpression('GETUTCDATE');
|
||||
$expression->setName('CONVERT')->add(['time' => 'literal', $time]);
|
||||
break;
|
||||
case 'NOW':
|
||||
$expression->setName('GETUTCDATE');
|
||||
break;
|
||||
case 'EXTRACT':
|
||||
$expression->setName('DATEPART')->setConjunction(' ,');
|
||||
break;
|
||||
case 'DATE_ADD':
|
||||
$params = [];
|
||||
$visitor = function ($p, $key) use (&$params) {
|
||||
if ($key === 0) {
|
||||
$params[2] = $p;
|
||||
} else {
|
||||
$valueUnit = explode(' ', $p);
|
||||
$params[0] = rtrim($valueUnit[1], 's');
|
||||
$params[1] = $valueUnit[0];
|
||||
}
|
||||
|
||||
return $p;
|
||||
};
|
||||
$manipulator = function ($p, $key) use (&$params) {
|
||||
return $params[$key];
|
||||
};
|
||||
|
||||
$expression
|
||||
->setName('DATEADD')
|
||||
->setConjunction(',')
|
||||
->iterateParts($visitor)
|
||||
->iterateParts($manipulator)
|
||||
->add([$params[2] => 'literal']);
|
||||
break;
|
||||
case 'DAYOFWEEK':
|
||||
$expression
|
||||
->setName('DATEPART')
|
||||
->setConjunction(' ')
|
||||
->add(['weekday, ' => 'literal'], [], true);
|
||||
break;
|
||||
case 'SUBSTR':
|
||||
$expression->setName('SUBSTRING');
|
||||
if (count($expression) < 4) {
|
||||
$params = [];
|
||||
$expression
|
||||
->iterateParts(function ($p) use (&$params) {
|
||||
return $params[] = $p;
|
||||
})
|
||||
->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @link https://cakephp.org CakePHP(tm) Project
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace Cake\Database\Driver;
|
||||
|
||||
use Cake\Database\Expression\IdentifierExpression;
|
||||
use Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Database\Expression\TupleComparison;
|
||||
use Cake\Database\Query;
|
||||
use Cake\Database\Query\SelectQuery;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Provides a translator method for tuple comparisons
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait TupleComparisonTranslatorTrait
|
||||
{
|
||||
/**
|
||||
* Receives a TupleExpression and changes it so that it conforms to this
|
||||
* SQL dialect.
|
||||
*
|
||||
* It transforms expressions looking like '(a, b) IN ((c, d), (e, f))' into an
|
||||
* equivalent expression of the form '((a = c) AND (b = d)) OR ((a = e) AND (b = f))'.
|
||||
*
|
||||
* It can also transform expressions where the right hand side is a query
|
||||
* selecting the same amount of columns as the elements in the left hand side of
|
||||
* the expression:
|
||||
*
|
||||
* (a, b) IN (SELECT c, d FROM a_table) is transformed into
|
||||
*
|
||||
* 1 = (SELECT 1 FROM a_table WHERE (a = c) AND (b = d))
|
||||
*
|
||||
* @param \Cake\Database\Expression\TupleComparison $expression The expression to transform
|
||||
* @param \Cake\Database\Query $query The query to update.
|
||||
* @return void
|
||||
*/
|
||||
protected function _transformTupleComparison(TupleComparison $expression, Query $query): void
|
||||
{
|
||||
$fields = $expression->getField();
|
||||
|
||||
if (!is_array($fields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operator = strtoupper($expression->getOperator());
|
||||
if (!in_array($operator, ['IN', '='])) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Tuple comparison transform only supports the `IN` and `=` operators, `%s` given.',
|
||||
$operator,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$value = $expression->getValue();
|
||||
$true = new QueryExpression('1');
|
||||
|
||||
if ($value instanceof SelectQuery) {
|
||||
/** @var array<string> $selected */
|
||||
$selected = array_values($value->clause('select'));
|
||||
foreach ($fields as $i => $field) {
|
||||
$value->andWhere([$field => new IdentifierExpression($selected[$i])]);
|
||||
}
|
||||
$value->select($true, true);
|
||||
$expression->setField($true);
|
||||
$expression->setOperator('=');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $expression->getType();
|
||||
if ($type) {
|
||||
/** @var array<string, string> $typeMap */
|
||||
$typeMap = array_combine($fields, $type) ?: [];
|
||||
} else {
|
||||
$typeMap = [];
|
||||
}
|
||||
|
||||
$surrogate = $query->getConnection()
|
||||
->selectQuery()
|
||||
->select($true);
|
||||
|
||||
if (!is_array(current($value))) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
$conditions = ['OR' => []];
|
||||
foreach ($value as $tuple) {
|
||||
$item = [];
|
||||
foreach (array_values($tuple) as $i => $value2) {
|
||||
$item[] = [$fields[$i] => $value2];
|
||||
}
|
||||
$conditions['OR'][] = $item;
|
||||
}
|
||||
$surrogate->where($conditions, $typeMap);
|
||||
|
||||
$expression->setField($true);
|
||||
$expression->setValue($surrogate);
|
||||
$expression->setOperator('=');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user