1081 lines
30 KiB
PHP
1081 lines
30 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
|
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
|
*
|
|
* Licensed under The MIT License
|
|
* For full copyright and license information, please see the LICENSE.txt
|
|
* Redistributions of files 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\Exception\CakeException;
|
|
use Cake\Core\Retry\CommandRetry;
|
|
use Cake\Database\Exception\DatabaseException;
|
|
use Cake\Database\Exception\MissingConnectionException;
|
|
use Cake\Database\Exception\QueryException;
|
|
use Cake\Database\Expression\ComparisonExpression;
|
|
use Cake\Database\Expression\IdentifierExpression;
|
|
use Cake\Database\Expression\QueryExpression;
|
|
use Cake\Database\Log\LoggedQuery;
|
|
use Cake\Database\Log\QueryLogger;
|
|
use Cake\Database\Query\DeleteQuery;
|
|
use Cake\Database\Query\InsertQuery;
|
|
use Cake\Database\Query\SelectQuery;
|
|
use Cake\Database\Query\UpdateQuery;
|
|
use Cake\Database\Retry\ErrorCodeWaitStrategy;
|
|
use Cake\Database\Schema\SchemaDialect;
|
|
use Cake\Database\Schema\TableSchema;
|
|
use Cake\Database\Schema\TableSchemaInterface;
|
|
use Cake\Database\Statement\Statement;
|
|
use InvalidArgumentException;
|
|
use PDO;
|
|
use PDOException;
|
|
use Psr\Log\LoggerAwareInterface;
|
|
use Psr\Log\LoggerAwareTrait;
|
|
use Psr\Log\LoggerInterface;
|
|
use Stringable;
|
|
|
|
/**
|
|
* Represents a database driver containing all specificities for
|
|
* a database engine including its SQL dialect.
|
|
*/
|
|
abstract class Driver implements LoggerAwareInterface
|
|
{
|
|
use LoggerAwareTrait;
|
|
|
|
/**
|
|
* @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 = [];
|
|
|
|
/**
|
|
* @var class-string<\Cake\Database\Statement\Statement>
|
|
*/
|
|
protected const STATEMENT_CLASS = Statement::class;
|
|
|
|
/**
|
|
* Instance of PDO.
|
|
*
|
|
* @var \PDO|null
|
|
*/
|
|
protected ?PDO $pdo = null;
|
|
|
|
/**
|
|
* Configuration data.
|
|
*
|
|
* @var array<string, mixed>
|
|
*/
|
|
protected array $_config = [];
|
|
|
|
/**
|
|
* Base configuration that is merged into the user
|
|
* supplied configuration data.
|
|
*
|
|
* @var array<string, mixed>
|
|
*/
|
|
protected array $_baseConfig = [];
|
|
|
|
/**
|
|
* Indicates whether the driver is doing automatic identifier quoting
|
|
* for all queries
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected bool $_autoQuoting = false;
|
|
|
|
/**
|
|
* 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 = '';
|
|
|
|
/**
|
|
* Identifier quoter
|
|
*
|
|
* @var \Cake\Database\IdentifierQuoter|null
|
|
*/
|
|
protected ?IdentifierQuoter $quoter = null;
|
|
|
|
/**
|
|
* The server version
|
|
*
|
|
* @var string|null
|
|
*/
|
|
protected ?string $_version = null;
|
|
|
|
/**
|
|
* Whether to log queries generated during this connection.
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected bool $logQueries = false;
|
|
|
|
/**
|
|
* The last number of connection retry attempts.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected int $connectRetries = 0;
|
|
|
|
/**
|
|
* The schema dialect for this driver
|
|
*
|
|
* @var \Cake\Database\Schema\SchemaDialect
|
|
*/
|
|
protected SchemaDialect $_schemaDialect;
|
|
|
|
/**
|
|
* 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 + ['log' => false];
|
|
$this->_config = $config;
|
|
if (!empty($config['quoteIdentifiers'])) {
|
|
$this->enableAutoQuoting();
|
|
}
|
|
if ($config['log'] !== false) {
|
|
$this->logQueries = true;
|
|
$this->logger = $this->createLogger($config['log'] === true ? null : $config['log']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 \PDO
|
|
*/
|
|
protected function createPdo(string $dsn, array $config): PDO
|
|
{
|
|
$action = fn(): PDO => new PDO(
|
|
$dsn,
|
|
$config['username'] ?: null,
|
|
$config['password'] ?: null,
|
|
$config['flags'],
|
|
);
|
|
|
|
$retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
|
|
try {
|
|
return $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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Establishes a connection to the database server.
|
|
*
|
|
* @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
|
|
* @return void
|
|
*/
|
|
abstract public function connect(): void;
|
|
|
|
/**
|
|
* Disconnects from database server.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function disconnect(): void
|
|
{
|
|
$this->pdo = null;
|
|
$this->_version = null;
|
|
}
|
|
|
|
/**
|
|
* Returns connected server version.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function version(): string
|
|
{
|
|
return $this->_version ??= (string)$this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
|
|
}
|
|
|
|
/**
|
|
* Get the PDO connection instance.
|
|
*
|
|
* @return \PDO
|
|
*/
|
|
protected function getPdo(): PDO
|
|
{
|
|
if ($this->pdo === null) {
|
|
$this->connect();
|
|
}
|
|
assert($this->pdo !== null);
|
|
|
|
return $this->pdo;
|
|
}
|
|
|
|
/**
|
|
* Execute the SQL query using the internal PDO instance.
|
|
*
|
|
* @param string $sql SQL query.
|
|
* @return int|false Number of affected rows or false on failure
|
|
* @throws \Cake\Database\Exception\QueryException On database error
|
|
*/
|
|
public function exec(string $sql): int|false
|
|
{
|
|
try {
|
|
return $this->getPdo()->exec($sql);
|
|
} catch (PDOException $e) {
|
|
throw new QueryException($sql, $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether php is able to use this driver for connecting to database.
|
|
*
|
|
* @return bool True if it is valid to use this driver.
|
|
*/
|
|
abstract public function enabled(): bool;
|
|
|
|
/**
|
|
* Executes a query using $params for interpolating values and $types as a hint for each
|
|
* those params.
|
|
*
|
|
* @param string $sql SQL to be executed and interpolated with $params
|
|
* @param array $params List or associative array of params to be interpolated in $sql as values.
|
|
* @param array $types List or associative array of types to be used for casting values in query.
|
|
* @return \Cake\Database\StatementInterface Executed statement
|
|
*/
|
|
public function execute(string $sql, array $params = [], array $types = []): StatementInterface
|
|
{
|
|
$statement = $this->prepare($sql);
|
|
if ($params) {
|
|
$statement->bind($params, $types);
|
|
}
|
|
$this->executeStatement($statement);
|
|
|
|
return $statement;
|
|
}
|
|
|
|
/**
|
|
* Executes the provided query after compiling it for the specific driver
|
|
* dialect and returns the executed Statement object.
|
|
*
|
|
* @param \Cake\Database\Query $query The query to be executed.
|
|
* @return \Cake\Database\StatementInterface Executed statement
|
|
*/
|
|
public function run(Query $query): StatementInterface
|
|
{
|
|
$statement = $this->prepare($query);
|
|
$query->getValueBinder()->attachTo($statement);
|
|
$this->executeStatement($statement);
|
|
|
|
return $statement;
|
|
}
|
|
|
|
/**
|
|
* Execute the statement and log the query string.
|
|
*
|
|
* @param \Cake\Database\StatementInterface $statement Statement to execute.
|
|
* @param array|null $params List of values to be bound to query.
|
|
* @return void
|
|
*/
|
|
protected function executeStatement(StatementInterface $statement, ?array $params = null): void
|
|
{
|
|
if ($this->logger === null) {
|
|
try {
|
|
$statement->execute($params);
|
|
} catch (PDOException $e) {
|
|
throw $this->createQueryException($e, $statement, $params);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$exception = null;
|
|
$took = 0.0;
|
|
|
|
try {
|
|
$start = microtime(true);
|
|
$statement->execute($params);
|
|
$took = (float)number_format((microtime(true) - $start) * 1000, 1);
|
|
} catch (PDOException $e) {
|
|
$exception = $e;
|
|
}
|
|
|
|
$logContext = [
|
|
'driver' => $this,
|
|
'error' => $exception,
|
|
'params' => $params ?? $statement->getBoundParams(),
|
|
];
|
|
if (!$exception) {
|
|
$logContext['numRows'] = $statement->rowCount();
|
|
$logContext['took'] = $took;
|
|
}
|
|
$this->log($statement->queryString(), $logContext);
|
|
|
|
if ($exception) {
|
|
throw $this->createQueryException($exception, $statement, $params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a QueryException from a PDOException
|
|
*
|
|
* @param \PDOException $exception
|
|
* @param \Cake\Database\StatementInterface $statement
|
|
* @param array|null $params
|
|
* @return \Cake\Database\Exception\QueryException
|
|
*/
|
|
protected function createQueryException(
|
|
PDOException $exception,
|
|
StatementInterface $statement,
|
|
?array $params = null,
|
|
): QueryException {
|
|
$loggedQuery = new LoggedQuery();
|
|
$loggedQuery->setContext([
|
|
'query' => $statement->queryString(),
|
|
'driver' => $this,
|
|
'params' => $params ?? $statement->getBoundParams(),
|
|
]);
|
|
|
|
return new QueryException($loggedQuery, $exception);
|
|
}
|
|
|
|
/**
|
|
* 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|string $query): StatementInterface
|
|
{
|
|
try {
|
|
$statement = $this->getPdo()->prepare($query instanceof Query ? $query->sql() : $query);
|
|
} catch (PDOException $e) {
|
|
throw new QueryException(
|
|
$query instanceof Query ? $query->sql() : $query,
|
|
$e,
|
|
);
|
|
}
|
|
|
|
/** @var \Cake\Database\StatementInterface */
|
|
return new (static::STATEMENT_CLASS)($statement, $this, $this->getResultSetDecorators($query));
|
|
}
|
|
|
|
/**
|
|
* Returns the decorators to be applied to the result set incase of a SelectQuery.
|
|
*
|
|
* @param \Cake\Database\Query|string $query The query to be decorated.
|
|
* @return array<\Closure>
|
|
*/
|
|
protected function getResultSetDecorators(Query|string $query): array
|
|
{
|
|
if ($query instanceof SelectQuery) {
|
|
$decorators = $query->getResultDecorators();
|
|
if ($query->isResultsCastingEnabled()) {
|
|
$typeConverter = new FieldTypeConverter($query->getSelectTypeMap(), $this);
|
|
array_unshift($decorators, $typeConverter(...));
|
|
}
|
|
|
|
return $decorators;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Starts a transaction.
|
|
*
|
|
* @return bool True on success, false otherwise.
|
|
*/
|
|
public function beginTransaction(): bool
|
|
{
|
|
if ($this->getPdo()->inTransaction()) {
|
|
return true;
|
|
}
|
|
|
|
$this->log('BEGIN');
|
|
|
|
return $this->getPdo()->beginTransaction();
|
|
}
|
|
|
|
/**
|
|
* Commits a transaction.
|
|
*
|
|
* @return bool True on success, false otherwise.
|
|
*/
|
|
public function commitTransaction(): bool
|
|
{
|
|
if (!$this->getPdo()->inTransaction()) {
|
|
return false;
|
|
}
|
|
|
|
$this->log('COMMIT');
|
|
|
|
return $this->getPdo()->commit();
|
|
}
|
|
|
|
/**
|
|
* Rollbacks a transaction.
|
|
*
|
|
* @return bool True on success, false otherwise.
|
|
*/
|
|
public function rollbackTransaction(): bool
|
|
{
|
|
if (!$this->getPdo()->inTransaction()) {
|
|
return false;
|
|
}
|
|
|
|
$this->log('ROLLBACK');
|
|
|
|
return $this->getPdo()->rollBack();
|
|
}
|
|
|
|
/**
|
|
* Returns whether a transaction is active for connection.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function inTransaction(): bool
|
|
{
|
|
return $this->getPdo()->inTransaction();
|
|
}
|
|
|
|
/**
|
|
* Returns a SQL snippet for creating a new transaction savepoint
|
|
*
|
|
* @param string|int $name save point name
|
|
* @return string
|
|
*/
|
|
public function savePointSQL(string|int $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(string|int $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(string|int $name): string
|
|
{
|
|
return 'ROLLBACK TO SAVEPOINT LEVEL' . $name;
|
|
}
|
|
|
|
/**
|
|
* Get the SQL for disabling foreign keys.
|
|
*
|
|
* @return string
|
|
*/
|
|
abstract public function disableForeignKeySQL(): string;
|
|
|
|
/**
|
|
* Get the SQL for enabling foreign keys.
|
|
*
|
|
* @return string
|
|
*/
|
|
abstract public function enableForeignKeySQL(): string;
|
|
|
|
/**
|
|
* Transform the query to accommodate any specificities of the SQL dialect in use.
|
|
*
|
|
* It will also quote the identifiers if auto quoting is enabled.
|
|
*
|
|
* @param \Cake\Database\Query $query Query to transform.
|
|
* @return \Cake\Database\Query
|
|
*/
|
|
protected function transformQuery(Query $query): Query
|
|
{
|
|
if ($this->isAutoQuotingEnabled()) {
|
|
$query = $this->quoter()->quote($query);
|
|
}
|
|
|
|
$query = match (true) {
|
|
$query instanceof SelectQuery => $this->_selectQueryTranslator($query),
|
|
$query instanceof InsertQuery => $this->_insertQueryTranslator($query),
|
|
$query instanceof UpdateQuery => $this->_updateQueryTranslator($query),
|
|
$query instanceof DeleteQuery => $this->_deleteQueryTranslator($query),
|
|
default => throw new InvalidArgumentException(sprintf(
|
|
'Instance of SelectQuery, UpdateQuery, InsertQuery, DeleteQuery expected. Found `%s` instead.',
|
|
get_debug_type($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.
|
|
*
|
|
* @return array<class-string, string>
|
|
*/
|
|
protected function _expressionTranslators(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Apply translation steps to select queries.
|
|
*
|
|
* @param \Cake\Database\Query\SelectQuery<mixed> $query The query to translate
|
|
* @return \Cake\Database\Query\SelectQuery<mixed> The modified query
|
|
*/
|
|
protected function _selectQueryTranslator(SelectQuery $query): SelectQuery
|
|
{
|
|
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\SelectQuery<mixed> $query The query to be transformed
|
|
* @return \Cake\Database\Query\SelectQuery<mixed>
|
|
*/
|
|
protected function _transformDistinct(SelectQuery $query): SelectQuery
|
|
{
|
|
if (is_array($query->clause('distinct'))) {
|
|
$query->groupBy($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\DeleteQuery $query The query to translate
|
|
* @return \Cake\Database\Query\DeleteQuery The modified query
|
|
*/
|
|
protected function _deleteQueryTranslator(DeleteQuery $query): DeleteQuery
|
|
{
|
|
$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\UpdateQuery $query The query to translate
|
|
* @return \Cake\Database\Query\UpdateQuery The modified query
|
|
*/
|
|
protected function _updateQueryTranslator(UpdateQuery $query): UpdateQuery
|
|
{
|
|
return $this->_removeAliasesFromConditions($query);
|
|
}
|
|
|
|
/**
|
|
* Removes aliases from the `WHERE` clause of a query.
|
|
*
|
|
* @param \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery $query The query to process.
|
|
* @return \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery The modified query.
|
|
* @throws \Cake\Database\Exception\DatabaseException In case the processed query contains any joins, as removing
|
|
* aliases from the conditions can break references to the joined tables.
|
|
* @template T of \Cake\Database\Query\UpdateQuery|\Cake\Database\Query\DeleteQuery
|
|
* @phpstan-param T $query
|
|
* @phpstan-return T
|
|
*/
|
|
protected function _removeAliasesFromConditions(UpdateQuery|DeleteQuery $query): UpdateQuery|DeleteQuery
|
|
{
|
|
if ($query->clause('join')) {
|
|
throw new DatabaseException(
|
|
'Aliases are being removed from conditions for UPDATE/DELETE queries, ' .
|
|
'this can break references to joined tables.',
|
|
);
|
|
}
|
|
|
|
$conditions = $query->clause('where');
|
|
assert($conditions === null || $conditions instanceof ExpressionInterface);
|
|
if ($conditions) {
|
|
$conditions->traverse(function ($expression) {
|
|
if ($expression instanceof ComparisonExpression) {
|
|
$field = $expression->getField();
|
|
if (
|
|
is_string($field) &&
|
|
str_contains($field, '.')
|
|
) {
|
|
[, $unaliasedField] = explode('.', $field, 2);
|
|
$expression->setField($unaliasedField);
|
|
}
|
|
|
|
return $expression;
|
|
}
|
|
|
|
if ($expression instanceof IdentifierExpression) {
|
|
$identifier = $expression->getIdentifier();
|
|
if (str_contains($identifier, '.')) {
|
|
[, $unaliasedIdentifier] = explode('.', $identifier, 2);
|
|
$expression->setIdentifier($unaliasedIdentifier);
|
|
}
|
|
|
|
return $expression;
|
|
}
|
|
|
|
return $expression;
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Apply translation steps to insert queries.
|
|
*
|
|
* @param \Cake\Database\Query\InsertQuery $query The query to translate
|
|
* @return \Cake\Database\Query\InsertQuery The modified query
|
|
*/
|
|
protected function _insertQueryTranslator(InsertQuery $query): InsertQuery
|
|
{
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
abstract 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 to quote.
|
|
* @return string
|
|
*/
|
|
public function quoteIdentifier(string $identifier): string
|
|
{
|
|
return $this->quoter()->quoteIdentifier($identifier);
|
|
}
|
|
|
|
/**
|
|
* Quotes a database value.
|
|
*
|
|
* This makes values safe for concatenation in SQL queries.
|
|
*
|
|
* Using this method **is not** recommended. You should use `execute()`
|
|
* instead, as it uses prepared statements which are safer than
|
|
* string concatenation.
|
|
*
|
|
* This method should only be used for queries that do not support placeholders.
|
|
*
|
|
* @param string $value The value to quote.
|
|
* @return string
|
|
*/
|
|
public function quote(string $value): string
|
|
{
|
|
return $this->getPdo()->quote($value);
|
|
}
|
|
|
|
/**
|
|
* Get identifier quoter instance.
|
|
*
|
|
* @return \Cake\Database\IdentifierQuoter
|
|
*/
|
|
public function quoter(): IdentifierQuoter
|
|
{
|
|
return $this->quoter ??= new IdentifierQuoter($this->_startQuote, $this->_endQuote);
|
|
}
|
|
|
|
/**
|
|
* 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(mixed $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);
|
|
}
|
|
if (
|
|
(
|
|
is_int($value) ||
|
|
$value === '0'
|
|
) ||
|
|
(
|
|
is_numeric($value) &&
|
|
!str_contains($value, ',') &&
|
|
!str_starts_with($value, '0') &&
|
|
!str_contains($value, 'e')
|
|
)
|
|
) {
|
|
return (string)$value;
|
|
}
|
|
if ($value instanceof QueryExpression) {
|
|
return $value->sql(new ValueBinder());
|
|
}
|
|
|
|
return $this->getPdo()->quote((string)$value, PDO::PARAM_STR);
|
|
}
|
|
|
|
/**
|
|
* Returns the schema name that's being used.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function schema(): string
|
|
{
|
|
return $this->_config['schema'];
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @return string
|
|
*/
|
|
public function lastInsertId(?string $table = null): string
|
|
{
|
|
return (string)$this->getPdo()->lastInsertId($table);
|
|
}
|
|
|
|
/**
|
|
* Checks whether the driver is connected.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isConnected(): bool
|
|
{
|
|
if ($this->pdo === null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return (bool)$this->pdo->query('SELECT 1');
|
|
} catch (PDOException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
$this->_autoQuoting = $enable;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Disable auto quoting of identifiers in queries.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function disableAutoQuoting()
|
|
{
|
|
$this->_autoQuoting = false;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this driver should automatically quote identifiers
|
|
* in queries.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isAutoQuotingEnabled(): bool
|
|
{
|
|
return $this->_autoQuoting;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the driver supports the feature.
|
|
*
|
|
* Should return false for unknown features.
|
|
*
|
|
* @param \Cake\Database\DriverFeatureEnum $feature Driver feature
|
|
* @return bool
|
|
*/
|
|
abstract public function supports(DriverFeatureEnum $feature): 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 string The compiled SQL.
|
|
*/
|
|
public function compileQuery(Query $query, ValueBinder $binder): string
|
|
{
|
|
$processor = $this->newCompiler();
|
|
$query = $this->transformQuery($query);
|
|
|
|
return $processor->compile($query, $binder);
|
|
}
|
|
|
|
/**
|
|
* @return \Cake\Database\QueryCompiler
|
|
*/
|
|
public function newCompiler(): QueryCompiler
|
|
{
|
|
return new QueryCompiler();
|
|
}
|
|
|
|
/**
|
|
* Constructs new TableSchema.
|
|
*
|
|
* @param string $table The table name.
|
|
* @param array<string, mixed> $columns The list of columns for the schema.
|
|
* @return \Cake\Database\Schema\TableSchemaInterface
|
|
*/
|
|
public function newTableSchema(string $table, array $columns = []): TableSchemaInterface
|
|
{
|
|
/** @var class-string<\Cake\Database\Schema\TableSchemaInterface> $className */
|
|
$className = $this->_config['tableSchema'] ?? TableSchema::class;
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Get the logger instance.
|
|
*
|
|
* @return \Psr\Log\LoggerInterface|null
|
|
*/
|
|
public function getLogger(): ?LoggerInterface
|
|
{
|
|
return $this->logger;
|
|
}
|
|
|
|
/**
|
|
* Create logger instance.
|
|
*
|
|
* @param string|null $className Logger's class name
|
|
* @return \Psr\Log\LoggerInterface
|
|
*/
|
|
protected function createLogger(?string $className): LoggerInterface
|
|
{
|
|
$className ??= QueryLogger::class;
|
|
|
|
/** @var class-string<\Psr\Log\LoggerInterface>|null $className */
|
|
$className = App::className($className, 'Cake/Log', 'Log');
|
|
if ($className === null) {
|
|
throw new CakeException(
|
|
'For logging you must either set the `log` config to a FQCN which implements Psr\Log\LoggerInterface' .
|
|
' or require the cakephp/log package in your composer config.',
|
|
);
|
|
}
|
|
|
|
return new $className();
|
|
}
|
|
|
|
/**
|
|
* Logs a message or query using the configured logger object.
|
|
*
|
|
* @param \Stringable|string $message Message string or query.
|
|
* @param array $context Logging context.
|
|
* @return bool True if message was logged.
|
|
*/
|
|
public function log(Stringable|string $message, array $context = []): bool
|
|
{
|
|
if ($this->logger === null || !$this->logQueries) {
|
|
return false;
|
|
}
|
|
|
|
$context['query'] = $message;
|
|
$loggedQuery = new LoggedQuery();
|
|
$loggedQuery->setContext($context);
|
|
|
|
$this->logger->debug((string)$loggedQuery, ['query' => $loggedQuery]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the connection role this driver performs.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRole(): string
|
|
{
|
|
return $this->_config['_role'] ?? Connection::ROLE_WRITE;
|
|
}
|
|
|
|
/**
|
|
* Enable query logging.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function enableQueryLogging()
|
|
{
|
|
$this->logQueries = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Disable query logging.
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function disableQueryLogging()
|
|
{
|
|
$this->logQueries = false;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function setLogger(LoggerInterface $logger): void
|
|
{
|
|
$this->logger = $logger;
|
|
$this->enableQueryLogging();
|
|
}
|
|
|
|
/**
|
|
* Destructor
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
$this->pdo = 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->pdo !== null,
|
|
'role' => $this->getRole(),
|
|
];
|
|
}
|
|
}
|