View source
<?php
namespace Drupal\sqlsrv\Driver\Database\sqlsrv;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseAccessDeniedException;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Database\StatementWrapper;
use Drupal\Core\Database\TransactionNoActiveException;
use Drupal\Core\Database\TransactionOutOfOrderException;
use Drupal\Core\Database\TransactionNameNonUniqueException;
class Connection extends DatabaseConnection {
protected $statementClass = NULL;
protected $statementWrapperClass = StatementWrapper::class;
protected $identifierQuotes = [
'[',
']',
];
const DATABASE_NOT_FOUND = 42000;
const ACCESS_DENIED = 28000;
const RESERVED_REGEXP = '/\\G
# Everything that follows a boundary that is not : or _.
\\b(?<![:\\[_])(?:
# Any reserved words, followed by a boundary that is not an opening parenthesis.
(action|admin|alias|any|are|array|at|begin|boolean|class|commit|contains|current|
data|date|day|depth|domain|external|file|full|function|get|go|host|input|language|
last|less|local|map|min|module|new|no|object|old|open|operation|parameter|parameters|
path|plan|prefix|proc|public|ref|result|returns|role|row|rule|save|search|second|
section|session|size|state|statistics|temporary|than|time|timestamp|tran|translate|
translation|trim|user|value|variable|view|without)
(?!\\()
|
# Or a normal word.
([a-z]+)
)\\b
|
\\b(
[^a-z\'"\\\\]+
)\\b
|
(?=[\'"])
(
" [^\\\\"] * (?: \\\\. [^\\\\"] *) * "
|
\' [^\\\\\']* (?: \\\\. [^\\\\\']*) * \'
)
/Six';
protected $schema = NULL;
protected $tempTablePrefix = '#';
protected $tempKey;
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
if (strpos($query, " ORDER BY ") === FALSE) {
$query .= " ORDER BY (SELECT NULL)";
}
$query .= " OFFSET {$from} ROWS FETCH NEXT {$count} ROWS ONLY";
return $this
->query($query, $args, $options);
}
public function queryTemporary($query, array $args = [], array $options = []) {
$tablename = $this
->generateTemporaryTableName();
$schema = $this
->schema();
$query = $schema
->removeSQLComments($query);
$query = preg_replace('/^SELECT(.*?)FROM/is', 'SELECT$1 INTO {' . $tablename . '} FROM', $query);
$this
->query($query, $args, $options);
return $tablename;
}
public function driver() {
return 'sqlsrv';
}
public function databaseType() {
return 'sqlsrv';
}
public function createDatabase($database) {
$database = Database::getConnection()
->escapeDatabase($database);
try {
$this->connection
->exec("CREATE DATABASE {$database}");
} catch (DatabaseException $e) {
throw new DatabaseNotFoundException($e
->getMessage());
}
}
public function mapConditionOperator($operator) {
return isset(static::$sqlsrvConditionOperatorMap[$operator]) ? static::$sqlsrvConditionOperatorMap[$operator] : NULL;
}
public function nextId($existing = 0) {
if ($existing > 0) {
try {
$sql = 'SET IDENTITY_INSERT {sequences} ON;';
$sql .= ' INSERT INTO {sequences} (value) VALUES(:existing);';
$sql .= ' SET IDENTITY_INSERT {sequences} OFF';
$this
->queryDirect($sql, [
':existing' => $existing,
]);
} catch (\Exception $e) {
}
}
return $this
->queryDirect('INSERT INTO {sequences} OUTPUT (Inserted.[value]) DEFAULT VALUES')
->fetchField();
}
public function __construct(\PDO $connection, array $connection_options) {
$connection
->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, TRUE);
parent::__construct($connection, $connection_options);
$this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
$this->connectionOptions = $connection_options;
}
public static function createConnectionOptionsFromUrl($url, $root) {
$database = parent::createConnectionOptionsFromUrl($url, $root);
$url_components = parse_url($url);
if (isset($url_components['query'])) {
$query = [];
parse_str($url_components['query'], $query);
if (isset($query['schema'])) {
$database['schema'] = $query['schema'];
}
$database['cache_schema'] = isset($query['cache_schema']) && $query['cache_schema'] == 'true' ? TRUE : FALSE;
}
return $database;
}
public static function createUrlFromConnectionOptions(array $connection_options) {
if (!isset($connection_options['driver'], $connection_options['database'])) {
throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
}
$user = '';
if (isset($connection_options['username'])) {
$user = $connection_options['username'];
if (isset($connection_options['password'])) {
$user .= ':' . $connection_options['password'];
}
$user .= '@';
}
$host = empty($connection_options['host']) ? 'localhost' : $connection_options['host'];
$db_url = $connection_options['driver'] . '://' . $user . $host;
if (isset($connection_options['port'])) {
$db_url .= ':' . $connection_options['port'];
}
$db_url .= '/' . $connection_options['database'];
$query = [];
if (isset($connection_options['module'])) {
$query['module'] = $connection_options['module'];
}
if (isset($connection_options['schema'])) {
$query['schema'] = $connection_options['schema'];
}
if (isset($connection_options['cache_schema'])) {
$query['cache_schema'] = $connection_options['cache_schema'];
}
if (count($query) > 0) {
$parameters = [];
foreach ($query as $key => $values) {
$parameters[] = $key . '=' . $values;
}
$query_string = implode("&", $parameters);
$db_url .= '?' . $query_string;
}
if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
$db_url .= '#' . $connection_options['prefix']['default'];
}
return $db_url;
}
protected function generateTemporaryTableName() {
if (!isset($this->tempKey)) {
$this->tempKey = md5(rand());
}
$tablename = parent::generateTemporaryTableName() . '_' . $this->tempKey;
return $tablename;
}
public function getFullQualifiedTableName($table) {
$options = $this
->getConnectionOptions();
$prefix = $this
->tablePrefix($table);
$schema_name = $this->schema
->getDefaultSchema();
return $options['database'] . '.' . $schema_name . '.' . $prefix . $table;
}
public static function open(array &$connection_options = []) {
$options = [];
$options['Server'] = $connection_options['host'] . (!empty($connection_options['port']) ? ',' . $connection_options['port'] : '');
if (!empty($connection_options['database'])) {
$options['Database'] = $connection_options['database'];
}
$dsn = 'sqlsrv:';
foreach ($options as $key => $value) {
$dsn .= (empty($key) ? '' : "{$key}=") . $value . ';';
}
$connection_options += [
'pdo' => [],
];
$connection_options['pdo'] += [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
];
try {
$pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
} catch (\PDOException $e) {
if ($e
->getCode() == static::DATABASE_NOT_FOUND) {
throw new DatabaseNotFoundException($e
->getMessage(), $e
->getCode(), $e);
}
if ($e
->getCode() == static::ACCESS_DENIED) {
throw new DatabaseAccessDeniedException($e
->getMessage(), $e
->getCode(), $e);
}
throw $e;
}
return $pdo;
}
public function prepareStatement(string $query, array $options) : StatementInterface {
$default_options = [
'emulate_prepares' => FALSE,
'bypass_preprocess' => FALSE,
'allow_delimiter_in_query' => FALSE,
];
$options += $default_options;
if (!$options['bypass_preprocess']) {
$query = $this
->preprocessQuery($query);
}
$driver_options = [];
$driver_options['allow_delimiter_in_query'] = $options['allow_delimiter_in_query'];
if ($options['emulate_prepares'] === TRUE) {
$driver_options['pdo'][\PDO::ATTR_EMULATE_PREPARES] = TRUE;
$driver_options['pdo'][\PDO::SQLSRV_ATTR_ENCODING] = \PDO::SQLSRV_ENCODING_UTF8;
}
$driver_options['pdo'][\PDO::SQLSRV_ATTR_DIRECT_QUERY] = TRUE;
$driver_options['pdo'][\PDO::ATTR_CURSOR] = \PDO::CURSOR_SCROLL;
$driver_options['pdo'][\PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE] = \PDO::SQLSRV_CURSOR_BUFFERED;
$stmt = parent::prepareStatement($query, $driver_options);
return $stmt;
}
protected function popCommittableTransactions() {
foreach (array_reverse($this->transactionLayers) as $name => $active) {
if ($active) {
break;
}
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
$this
->doCommit();
}
else {
}
}
}
public function pushTransaction($name) {
if (isset($this->transactionLayers[$name])) {
throw new TransactionNameNonUniqueException($name . " is already in use.");
}
if ($this
->inTransaction()) {
$this
->queryDirect('SAVE TRANSACTION ' . $name);
}
else {
$this->connection
->beginTransaction();
}
$this->transactionLayers[$name] = $name;
}
public function query($query, array $args = [], $options = []) {
$options += $this
->defaultOptions();
assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033');
if (is_string($query)) {
$this
->expandArguments($query, $args);
$emulate = isset($options['emulate_prepares']) ? $options['emulate_prepares'] : FALSE;
$argcount = count($args);
if ($emulate === TRUE || $argcount >= 2100 || $argcount != substr_count($query, ':')) {
$emulate = TRUE;
}
$options['emulate_prepares'] = $emulate;
while (($pos1 = strpos($query, 'CONCAT_WS')) !== FALSE) {
$pos2 = $this
->findParenMatch($query, $pos1 + 9);
$argument_list = substr($query, $pos1 + 10, $pos2 - 10 - $pos1);
$arguments = explode(', ', $argument_list);
$closing_quote_pos = stripos($argument_list, '\'', 1);
$separator = substr($argument_list, 1, $closing_quote_pos - 1);
$strings_list = substr($argument_list, $closing_quote_pos + 3);
$arguments = explode(', ', $strings_list);
$replace = "STUFF(";
$coalesce = [];
foreach ($arguments as $argument) {
if (substr($argument, 0, 1) == ':') {
$args[$argument . '_sqlsrv_concat'] = $args[$argument];
$coalesce[] = "CASE WHEN {$argument} IS NULL THEN '' ELSE CONCAT('{$separator}', {$argument}_sqlsrv_concat) END";
}
else {
$coalesce[] = "CASE WHEN {$argument} IS NULL THEN '' ELSE CONCAT('{$separator}', {$argument}) END";
}
}
$coalesce_string = implode(' + ', $coalesce);
$sep_len = strlen($separator);
$replace = "STUFF({$coalesce_string}, 1, {$sep_len}, '')";
$query = substr($query, 0, $pos1) . $replace . substr($query, $pos2 + 1);
}
$stmt = $this
->prepareStatement($query, $options);
}
elseif ($query instanceof StatementInterface) {
@trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
$stmt = $query;
}
elseif ($query instanceof \PDOStatement) {
@trigger_error('Passing a \\PDOStatement object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
$stmt = $query;
}
try {
if (is_string($query)) {
$stmt
->execute($args, $options);
}
elseif ($query instanceof StatementInterface) {
$stmt
->execute(NULL, $options);
}
elseif ($query instanceof \PDOStatement) {
$stmt
->execute();
}
switch ($options['return']) {
case Database::RETURN_STATEMENT:
return $stmt;
case Database::RETURN_AFFECTED:
$stmt->allowRowCount = TRUE;
return $stmt
->rowCount();
case Database::RETURN_INSERT_ID:
$sequence_name = isset($options['sequence_name']) ? $options['sequence_name'] : NULL;
return $this->connection
->lastInsertId($sequence_name);
case Database::RETURN_NULL:
return NULL;
default:
throw new \PDOException('Invalid return directive: ' . $options['return']);
}
} catch (\Exception $e) {
if (is_string($query)) {
return $this
->exceptionHandler()
->handleExecutionException($e, $stmt, $args, $options);
}
else {
return $this
->handleQueryException($e, $query, $args, $options);
}
}
}
public function rollBack($savepoint_name = 'drupal_transaction') {
if (!$this
->inTransaction()) {
throw new TransactionNoActiveException();
}
if (!isset($this->transactionLayers[$savepoint_name])) {
throw new TransactionNoActiveException();
}
$rolled_back_other_active_savepoints = FALSE;
while ($savepoint = array_pop($this->transactionLayers)) {
if ($savepoint == $savepoint_name) {
if (empty($this->transactionLayers)) {
break;
}
$this
->queryDirect('ROLLBACK TRANSACTION ' . $savepoint);
$this
->popCommittableTransactions();
if ($rolled_back_other_active_savepoints) {
throw new TransactionOutOfOrderException();
}
return;
}
else {
$rolled_back_other_active_savepoints = TRUE;
}
}
$callbacks = $this->rootTransactionEndCallbacks;
$this->rootTransactionEndCallbacks = [];
foreach ($callbacks as $callback) {
call_user_func($callback, FALSE);
}
$this->connection
->rollBack();
if ($rolled_back_other_active_savepoints) {
throw new TransactionOutOfOrderException();
}
}
protected function setPrefix($prefix) {
parent::setPrefix($prefix);
array_unshift($this->prefixSearch, '{db_temporary_');
$default_parts = explode('.', $this->prefixes['default']);
$table_part = array_pop($default_parts);
$default_parts[] = '[' . $this->tempTablePrefix . $table_part;
$full_prefix = implode('.', $default_parts);
array_unshift($this->prefixReplace, $full_prefix . 'db_temporary_');
}
public function tablePrefix($table = 'default') {
if (isset($this->prefixes[$table])) {
return $this->prefixes[$table];
}
$temp_prefix = '';
if ($this
->isTemporaryTable($table)) {
$temp_prefix = $this->tempTablePrefix;
$default_parts = explode('.', $this->prefixes['default']);
$table_part = array_pop($default_parts);
$default_parts[] = $this->tempTablePrefix . $table_part;
return implode('.', $default_parts);
}
return $this->prefixes['default'];
}
private function findParenMatch($string, $start_paren) {
if ($string[$start_paren] !== '(') {
return FALSE;
}
$str_array = str_split(substr($string, $start_paren + 1));
$paren_num = 1;
foreach ($str_array as $i => $char) {
if ($char == '(') {
$paren_num++;
}
elseif ($char == ')') {
$paren_num--;
}
if ($paren_num == 0) {
return $i + $start_paren + 1;
}
}
return FALSE;
}
public function getTempTablePrefix() {
return $this->tempTablePrefix;
}
public function isTemporaryTable($table) {
return stripos($table, 'db_temporary_') !== FALSE;
}
public function queryDirect($query, array $args = [], $options = []) {
$options += $this
->defaultOptions();
$direct_query_options = [
'direct_query' => TRUE,
'bypass_preprocess' => TRUE,
'emulate_prepares' => FALSE,
];
$stmt = $this
->prepareStatement($query, $direct_query_options + $options);
try {
$stmt
->execute($args, $options);
switch ($options['return']) {
case Database::RETURN_STATEMENT:
return $stmt;
case Database::RETURN_AFFECTED:
$stmt->allowRowCount = TRUE;
return $stmt
->rowCount();
case Database::RETURN_INSERT_ID:
return $this->connection
->lastInsertId();
case Database::RETURN_NULL:
return NULL;
default:
throw new \PDOException('Invalid return directive: ' . $options['return']);
}
} catch (\Exception $e) {
return $this
->exceptionHandler()
->handleExecutionException($e, $stmt, $args, $options);
}
}
public function preprocessQuery($query) {
$replacements = [];
$schema = $this
->schema();
$defaultSchema = $schema
->GetDefaultSchema();
foreach ($schema
->DrupalSpecificFunctions() as $function) {
$replacements['/\\b(?<![:.])(' . preg_quote($function) . ')\\(/i'] = "{$defaultSchema}.\$1(";
}
$funcs = [
'LENGTH' => 'LEN',
'POW' => 'POWER',
];
foreach ($funcs as $function => $replacement) {
$replacements['/\\b(?<![:.])(' . preg_quote($function) . ')\\(/i'] = $replacement . '(';
}
$replacements['/\\|\\|/'] = '+';
$query = preg_replace(array_keys($replacements), array_values($replacements), $query);
while (($pos1 = strpos($query, 'LEAST(')) !== FALSE) {
$name_length = 5;
$pos2 = $this
->findParenMatch($query, $pos1 + $name_length);
$argument_list = substr($query, $pos1 + $name_length + 1, $pos2 - $name_length - 1 - $pos1);
$arguments = explode(', ', $argument_list);
$new_arguments = implode('), (', $arguments);
$replace = '(SELECT MIN(i) FROM (VALUES (' . $new_arguments . ')) AS T(i)) sqlsrv_least';
$query = substr($query, 0, $pos1) . $replace . substr($query, $pos2 + 1);
}
return $query;
}
protected static $sqlsrvConditionOperatorMap = [
'LIKE' => [],
'NOT LIKE' => [],
'LIKE BINARY' => [
'operator' => 'LIKE',
],
];
}