class Connection in Drupal driver for SQL Server and SQL Azure 8.2
Same name in this branch
- 8.2 drivers/lib/Drupal/Driver/Database/sqlsrv/Connection.php \Drupal\Driver\Database\sqlsrv\Connection
- 8.2 drivers/lib/Drupal/Driver/Database/sqlsrv/PDO/Connection.php \Drupal\Driver\Database\sqlsrv\PDO\Connection
Same name and namespace in other branches
- 8 drivers/lib/Drupal/Driver/Database/sqlsrv/Connection.php \Drupal\Driver\Database\sqlsrv\Connection
- 3.0.x drivers/lib/Drupal/Driver/Database/sqlsrv/Connection.php \Drupal\Driver\Database\sqlsrv\Connection
Temporary tables: temporary table support is done by means of global temporary tables (#) to avoid the use of DIRECT QUERIES. You can enable and disable the use of direct queries with $this->driver_settings->defaultDirectQuery = TRUE|FALSE. http://blogs.msdn.com/b/brian_swan/archive/2010/06/15/ctp2-of-microsoft-...
Hierarchy
- class \Drupal\Core\Database\Connection
- class \Drupal\Driver\Database\sqlsrv\Connection
Expanded class hierarchy of Connection
5 files declare their use of Connection
- IndexManager.php in src/
Indexes/ IndexManager.php - sqlsrv.install in ./
sqlsrv.install - Installation file for sqlsrv module.
- SqlsrvConnectionTest.php in tests/
src/ Unit/ SqlsrvConnectionTest.php - SqlsrvTest.php in tests/
src/ Kernel/ SqlsrvTest.php - Tasks.php in drivers/
lib/ Drupal/ Driver/ Database/ sqlsrv/ Install/ Tasks.php - Definition of Drupal\Driver\Database\sqlsrv\Tasks
File
- drivers/
lib/ Drupal/ Driver/ Database/ sqlsrv/ Connection.php, line 37 - Definition of Drupal\Driver\Database\sqlsrv\Connection
Namespace
Drupal\Driver\Database\sqlsrvView source
class Connection extends DatabaseConnection {
/**
* Database driver settings.
*
* @var ConnectionSettings
*/
public $driver_settings = null;
/**
* Override of DatabaseConnection::driver().
*
* @status tested
*/
public function driver() {
return 'sqlsrv';
}
/**
* Override of DatabaseConnection::databaseType().
*
* @status tested
*/
public function databaseType() {
return 'sqlsrv';
}
/**
* Get the schema object.
*
* @return Schema
*/
public function schema() {
/** @var Schema $schema */
$schema = parent::schema();
return $schema;
}
/**
* Error code for Login Failed, usually happens when
* the database does not exist.
*/
const DATABASE_NOT_FOUND = 28000;
/**
* Constructs a Connection object.
*/
public function __construct(\PDO $connection, array $connection_options) {
// Initialize settings.
$this->driver_settings = $connection_options['driver_settings'];
// Needs to happen before parent construct.
$this->statementClass = Statement::class;
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== false;
$this->transactionalDDLSupport = $this->transactionSupport;
// Store connection options for future reference.
$this->connectionOptions =& $connection_options;
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = []) {
// Get driver settings.
$driver_settings = ConnectionSettings::instanceFromData(\Drupal\Core\Site\Settings::get('mssql', []));
// Build the DSN.
$options = [];
$options['Server'] = $connection_options['host'] . (!empty($connection_options['port']) ? ',' . $connection_options['port'] : '');
// We might not have a database in the
// connection options, for example, during
// database creation in Install.
if (!empty($connection_options['database'])) {
$options['Database'] = $connection_options['database'];
}
// Set isolation level if specified.
if ($level = $driver_settings
->GetDefaultIsolationLevel()) {
$options['TransactionIsolation'] = $level;
}
// Disable MARS
$options['MultipleActiveResultSets'] = 'false';
// Build the DSN
$dsn = $driver_settings
->buildDSN($options);
// PDO Options are set at a connection level.
// and apply to all statements.
$connection_options['pdo'] = [];
// Set proper error mode for all statements
$connection_options['pdo'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
// Use native types. This makes fetches x3 faster!
// @see https://github.com/Microsoft/msphpsql/issues/189
$connection_options['pdo'][PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE] = true;
$connection_options['pdo'][PDO::ATTR_STRINGIFY_FETCHES] = false;
// Actually instantiate the PDO.
try {
$pdo = new ConnectionBase($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
} catch (\Exception $e) {
if ($e
->getCode() == static::DATABASE_NOT_FOUND) {
throw new DatabaseNotFoundException($e
->getMessage(), $e
->getCode(), $e);
}
throw $e;
}
$connection_options['driver_settings'] = $driver_settings;
return $pdo;
}
/**
* We should not be exposing the connection but...
* comes in handy some times.
*
* @return \Drupal\Driver\Database\sqlsrv\PDO\Connection
*/
public function GetConnection() {
return $this->connection;
}
/**
* Get the Scheme manager.
*
* @return \Drupal\Driver\Database\sqlsrv\Scheme
*/
public function Scheme() {
return $this->connection
->Scheme();
}
/**
* Prepared PDO statements only makes sense if we cache them...
*
* @var mixed
*/
private $statement_cache = [];
/**
* Internal prepare a query.
*/
public function prepareQuery($query, array $options = []) {
// Preprocess the query.
$bypass = isset($options['bypass_query_preprocess']) && $options['bypass_query_preprocess'] == true ? true : false;
if (!$bypass) {
$query = $this
->preprocessQuery($query);
}
// Merge default statement options. These options are
// only specific for this preparation and will only override
// the global configuration if set to different than NULL.
$options = array_merge(array(
'insecure' => false,
'statement_caching' => $this->driver_settings
->GetStatementCachingMode(),
'direct_query' => $this->driver_settings
->GetDefaultDirectQueries(),
'prefix_tables' => true,
'integrityretry' => false,
'resilientretry' => true,
), $options);
// Prefix tables. There is no global setting for this.
if ($options['prefix_tables'] !== false) {
$query = $this
->prefixTables($query);
}
// The statement caching settings only affect the storage
// in the cache, but if a statement is already available
// why not reuse it!
if (isset($this->statement_cache[$query])) {
return $this->statement_cache[$query];
}
#region PDO Options
$pdo_options = [];
// Set insecure options if requested so.
if ($options['insecure'] === true) {
// We have to log this, prepared statements are a security RISK.
// watchdog('SQL Server Driver', 'An insecure query has been executed against the database. This is not critical, but worth looking into: %query', array('%query' => $query));
// These are defined in class Connection.
// This PDO options are INSECURE, but will overcome the following issues:
// (1) Duplicate placeholders
// (2) > 2100 parameter limit
// (3) Using expressions for group by with parameters are not detected as equal.
// This options are not applied by default, they are just stored in the connection
// options and applied when needed. See {Statement} class.
// We ask PDO to perform the placeholders replacement itself because
// SQL Server is not able to detect duplicated placeholders in
// complex statements.
// E.g. This query is going to fail because SQL Server cannot
// detect that length1 and length2 are equals.
// SELECT SUBSTRING(title, 1, :length1)
// FROM node
// GROUP BY SUBSTRING(title, 1, :length2
// This is only going to work in PDO 3 but doesn't hurt in PDO 2.
// The security of parameterized queries is not in effect when you use PDO::ATTR_EMULATE_PREPARES => true.
// Your application should ensure that the data that is bound to the parameter(s) does not contain malicious
// Transact-SQL code.
// Never use this when you need special column binding.
// THIS ONLY WORKS IF SET AT THE STATEMENT LEVEL.
$pdo_options[PDO::ATTR_EMULATE_PREPARES] = true;
}
// We need this behaviour to make UPSERT and MERGE more robust.
if ($options['integrityretry'] == true) {
$pdo_options[\Drupal\Driver\Database\sqlsrv\PDO\Connection::PDO_RETRYONINTEGRITYVIOLATION] = true;
}
if ($options['resilientretry'] == true) {
$pdo_options[\Drupal\Driver\Database\sqlsrv\PDO\Connection::PDO_RESILIENTRETRY] = true;
}
// We run the statements in "direct mode" because the way PDO prepares
// statement in non-direct mode cause temporary tables to be destroyed
// at the end of the statement.
// If you are using the PDO_SQLSRV driver and you want to execute a query that
// changes a database setting (e.g. SET NOCOUNT ON), use the PDO::query method with
// the PDO::SQLSRV_ATTR_DIRECT_QUERY attribute.
// http://blogs.iis.net/bswan/archive/2010/12/09/how-to-change-database-settings-with-the-pdo-sqlsrv-driver.aspx
// If a query requires the context that was set in a previous query,
// you should execute your queries with PDO::SQLSRV_ATTR_DIRECT_QUERY set to True.
// For example, if you use temporary tables in your queries, PDO::SQLSRV_ATTR_DIRECT_QUERY must be set
// to True.
if ($this->driver_settings
->GetStatementCachingMode() != 'always' || $options['direct_query'] == true) {
$pdo_options[PDO::SQLSRV_ATTR_DIRECT_QUERY] = true;
}
// It creates a cursor for the query, which allows you to iterate over the result set
// without fetching the whole result at once. A scrollable cursor, specifically, is one that allows
// iterating backwards.
// https://msdn.microsoft.com/en-us/library/hh487158%28v=sql.105%29.aspx
$pdo_options[PDO::ATTR_CURSOR] = PDO::CURSOR_SCROLL;
// Lets you access rows in any order. Creates a client-side cursor query.
$pdo_options[PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE] = PDO::SQLSRV_CURSOR_BUFFERED;
#endregion
if ($this->driver_settings
->GetAppendCallstackComment()) {
$query = $this
->addDebugInfoToQuery($query);
}
// Call our overriden prepare.
$stmt = $this->connection
->prepare($query, $pdo_options);
// If statement caching is enabled, store current statement for reuse.
if ($options['statement_caching'] === true) {
$this->statement_cache[$query] = $stmt;
}
return $stmt;
}
/**
* Adds debugging information to a query
* in the form of comments.
*
* @param string $query
* @return string
*/
protected function addDebugInfoToQuery($query) {
// The current user service might not be available
// if this is too early bootstrap
$uid = null;
static $loading_user;
// Use loading user to prevent recursion!
// Because the user entity can be stored in
// the database itself.
if (empty($loading_user)) {
try {
$loading_user = true;
$oUser = \Drupal::currentUser();
$uid = null;
if ($oUser != null) {
$uid = $oUser
->getAccount()
->id();
}
} catch (\Exception $e) {
} finally {
$loading_user = false;
}
}
// Drupal specific aditional information for the dump.
$extra = array(
'-- uid:' . ($uid ? $uid : 'NULL'),
);
$comment = $this->connection
->GetCallstackAsComment(DRUPAL_ROOT, $extra);
$query = $comment . $query;
return $query;
}
/**
* This is the original replacement regexp from Microsoft.
*
* We could probably simplify it a lot because queries only contain
* placeholders when we modify them.
*
* NOTE: removed 'escape' from the list, because it explodes
* with LIKE xxx ESCAPE yyy syntax.
*/
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';
/**
* This method gets called between 3,000 and 10,000 times
* on cold caches. Make sure it is simple and fast.
*
* @param mixed $matches
* @return mixed
*/
protected function replaceReservedCallback($matches) {
if ($matches[1] !== '') {
// Replace reserved words. We are not calling
// quoteIdentifier() on purpose.
return '[' . $matches[1] . ']';
}
// Let other value passthru.
// by the logic of the regex above, this will always be the last match.
return end($matches);
}
/**
* {@inheritdoc}
*/
public function quoteIdentifier($identifier) {
return '[' . $identifier . ']';
}
/**
* {@inheritdoc}
*/
public function escapeField($field) {
if (!isset($this->escapedNames[$field])) {
if (empty($field)) {
$this->escapedNames[$field] = '';
}
else {
$this->escapedNames[$field] = implode('.', array_map(array(
$this,
'quoteIdentifier',
), explode('.', preg_replace('/[^A-Za-z0-9_.]+/', '', $field))));
}
}
return $this->escapedNames[$field];
}
/**
* Prefix a single table name.
*
* @param string $table
* Name of the table.
*
* @return string
*/
public function prefixTable($table) {
if (empty($table)) {
return $table;
}
$table = $this
->escapeTable($table);
return $this
->prefixTables("{{$table}}");
}
/**
* {@inheritdoc}
*/
public function quoteIdentifiers($identifiers) {
return array_map(array(
$this,
'quoteIdentifier',
), $identifiers);
}
/**
* {@inheritdoc}
*/
public function escapeLike($string) {
return preg_replace('/([\\[\\]%_])/', '[$1]', $string);
}
/**
* {@inheritdoc}
*/
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
$query = $this
->addRangeToQuery($query, $from, $count);
return $this
->query($query, $args, $options);
}
/**
* Generates a temporary table name. Because we are using
* global temporary tables, these are visible between
* connections so we need to make sure that their
* names are as unique as possible to prevent collisions.
*
* @return string
* A table name.
*/
protected function generateTemporaryTableName() {
static $temp_key;
if (!isset($temp_key)) {
$temp_key = strtoupper(md5(uniqid(rand(), true)));
}
return "db_temp_" . $this->temporaryNameIndex++ . '_' . $temp_key;
}
/** @var \Drupal\Driver\Database\sqlsrv\PDO\Connection */
protected $connection;
/**
* {@inheritdoc}
*/
public function queryTemporary($query, array $args = [], array $options = []) {
// Generate a new GLOBAL temporary table name and protect it from prefixing.
// SQL Server requires that temporary tables to be non-qualified.
$tablename = '##' . $this
->generateTemporaryTableName();
// Temporary tables cannot be introspected so using them is limited on some scenarios.
if (isset($options['real_table']) && $options['real_table'] === true) {
$tablename = trim($tablename, "#");
}
$prefixes = $this->prefixes;
$prefixes[$tablename] = '';
$this
->setPrefix($prefixes);
// Having comments in the query can be tricky and break the SELECT FROM -> SELECT INTO conversion
$comments = [];
$query = $this->connection
->Scheme()
->removeSQLComments($query, $comments);
// Replace SELECT xxx FROM table by SELECT xxx INTO #table FROM table.
$query = preg_replace('/^SELECT(.*?)FROM/is', 'SELECT$1 INTO ' . $tablename . ' FROM', $query);
$this
->query($query, $args, $options);
return $tablename;
}
/**
* {@inheritdoc}
*
* This method is overriden to manage the insecure (EMULATE_PREPARE)
* behaviour to prevent some compatibility issues with SQL Server.
*/
public function query($query, array $args = [], $options = []) {
// Use default values if not already set.
$options += $this
->defaultOptions();
$stmt = null;
try {
// We allow either a pre-bound statement object or a literal string.
// In either case, we want to end up with an executed statement object,
// which we pass to PDOStatement::execute.
if ($query instanceof StatementInterface) {
$stmt = $query;
$stmt
->execute(null, $options);
}
else {
$this
->expandArguments($query, $args);
$insecure = isset($options['insecure']) ? $options['insecure'] : false;
// Try to detect duplicate place holders, this check's performance
// is not a good addition to the driver, but does a good job preventing
// duplicate placeholder errors.
$argcount = count($args);
if ($insecure === true || $argcount >= 2100 || $argcount != substr_count($query, ':')) {
$insecure = true;
}
$stmt = $this
->prepareQuery($query, array(
'insecure' => $insecure,
));
$stmt
->execute($args, $options);
}
// Depending on the type of query we may need to return a different value.
// See DatabaseConnection::defaultOptions() for a description of each
// value.
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 (\PDOException $e) {
// Most database drivers will return NULL here, but some of them
// (e.g. the SQLite driver) may need to re-run the query, so the return
// value will be the same as for static::query().
return $this
->handleQueryException($e, $query, $args, $options);
}
}
/**
* Wraps and re-throws any PDO exception thrown by static::query().
*
* @param \PDOException $e
* The exception thrown by static::query().
* @param $query
* The query executed by static::query().
* @param array $args
* An array of arguments for the prepared statement.
* @param array $options
* An associative array of options to control how the query is run.
*
* @return \Drupal\Core\Database\StatementInterface|int|null
* Most database drivers will return NULL when a PDO exception is thrown for
* a query, but some of them may need to re-run the query, so they can also
* return a \Drupal\Core\Database\StatementInterface object or an integer.
*
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* @throws \Drupal\Core\Database\IntegrityConstraintViolationException
*/
public function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
if ($options['throw_exception']) {
// Wrap the exception in another exception, because PHP does not allow
// overriding Exception::getMessage(). Its message is the extra database
// debug information.
if ($query instanceof StatementInterface) {
/** @var Statement $statement */
$statement = $query;
$e->query_string = $statement
->getQueryString();
$e->args = $statement
->GetBoundParameters();
}
else {
$e->query_string = $query;
}
$message = $e
->getMessage();
/** @var \Drupal\Core\Database\DatabaseException $exception */
$exception = null;
// Match all SQLSTATE 23xxx errors.
if (substr($e
->getCode(), -6, -3) == '23') {
$exception = new IntegrityConstraintViolationException($message, $e
->getCode(), $e);
}
else {
$exception = new DatabaseExceptionWrapper($message, 0, $e);
}
if (empty($e->args)) {
$e->args = $args;
}
// Copy this info to the rethrown Exception for compatibility.
$exception->query_string = $e->query_string;
$exception->args = $e->args;
throw $exception;
}
return null;
}
/**
* Like query but with no insecure detection or query preprocessing.
* The caller is sure that his query is MS SQL compatible! Used internally
* from the schema class, but could be called from anywhere.
*
* @param mixed $query
* @param array $args
* @param mixed $options
* @return mixed
* @throws PDOException
*/
public function query_direct($query, array $args = [], $options = []) {
// Use default values if not already set.
$options += $this
->defaultOptions();
$stmt = null;
try {
$options['bypass_query_preprocess'] = true;
$stmt = $this
->prepareQuery($query, $options);
$stmt
->execute($args, $options);
// Depending on the type of query we may need to return a different value.
// See DatabaseConnection::defaultOptions() for a description of each
// value.
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 (\PDOException $e) {
// Most database drivers will return NULL here, but some of them
// (e.g. the SQLite driver) may need to re-run the query, so the return
// value will be the same as for static::query().
return $this
->handleQueryException($e, $query, $args, $options);
}
}
/**
* Internal function: massage a query to make it compliant with SQL Server.
*/
public function preprocessQuery($query) {
// Generate a cache signature for this query.
$query_signature = md5($query);
// Drill through everything...
if ($cache = $this->connection
->Cache('query_cache')
->Get($query_signature)) {
return $cache->data;
}
// Force quotes around some SQL Server reserved keywords.
if (preg_match('/^SELECT/i', $query)) {
$query = preg_replace_callback(self::RESERVED_REGEXP, [
$this,
'replaceReservedCallback',
], $query);
}
// Last chance to modify some SQL Server-specific syntax.
$replacements = [];
// Add prefixes to Drupal-specific functions.
$defaultSchema = $this
->schema()
->GetDefaultSchema();
foreach ($this
->schema()
->DrupalSpecificFunctions() as $function) {
$replacements['/\\b(?<![:.])(' . preg_quote($function) . ')\\(/i'] = "{$defaultSchema}.\$1(";
}
// Rename some functions.
$funcs = [
'LENGTH' => 'LEN',
'POW' => 'POWER',
];
foreach ($funcs as $function => $replacement) {
$replacements['/\\b(?<![:.])(' . preg_quote($function) . ')\\(/i'] = $replacement . '(';
}
// Replace the ANSI concatenation operator with SQL Server poor one.
$replacements['/\\|\\|/'] = '+';
// Now do all the replacements at once.
$query = preg_replace(array_keys($replacements), array_values($replacements), $query);
// Assuming that queries have placeholders, the total number of different
// queries stored in the cache is not that big.
$this->connection
->Cache('query_cache')
->Set($query_signature, $query);
return $query;
}
/**
* {@inheritdoc}
*
* Adding schema to the connection URL.
*/
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;
}
/**
* {@inheritdoc}
*
* Adding schema to the connection URL.
*/
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;
}
/**
* Internal function: add range options to a query.
*
* This cannot be set protected because it is used in other parts of the
* database engine.
*
* @status tested
*/
public function addRangeToQuery($query, $from, $count) {
if ($from == 0) {
// Easy case: just use a TOP query if we don't have to skip any rows.
$query = preg_replace('/^\\s*SELECT(\\s*DISTINCT)?/Dsi', 'SELECT$1 TOP(' . $count . ')', $query);
}
else {
if ($this->connection
->Scheme()
->EngineVersionNumber() >= 11) {
if (strripos($query, 'ORDER BY') === false) {
$query = "SELECT Q.*, 0 as TempSort FROM ({$query}) as Q ORDER BY TempSort OFFSET {$from} ROWS FETCH NEXT {$count} ROWS ONLY";
}
else {
$query = "{$query} OFFSET {$from} ROWS FETCH NEXT {$count} ROWS ONLY";
}
}
else {
// More complex case: use a TOP query to retrieve $from + $count rows, and
// filter out the first $from rows using a window function.
$query = preg_replace('/^\\s*SELECT(\\s*DISTINCT)?/Dsi', 'SELECT$1 TOP(' . ($from + $count) . ') ', $query);
$query = '
SELECT * FROM (
SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.__line2) AS __line3 FROM (
SELECT sub1.*, 1 AS __line2 FROM (' . $query . ') AS sub1
) as sub2
) AS sub3
WHERE __line3 BETWEEN ' . ($from + 1) . ' AND ' . ($from + $count);
}
}
return $query;
}
public function mapConditionOperator($operator) {
// SQL Server doesn't need special escaping for the \ character in a string
// literal, because it uses '' to escape the single quote, not \'.
static $specials = array(
'LIKE' => [],
'NOT LIKE' => [],
);
return isset($specials[$operator]) ? $specials[$operator] : null;
}
/**
* {@inhertidoc}
*/
public function nextId($existing = 0, $name = 'drupal') {
if (version_compare($this
->Scheme()
->EngineVersion()
->Version(), '11', '>')) {
// Native sequence support is only available for SLQ Server 2012 and beyound
return $this->connection
->nextId($existing, $this
->prefixTable($name));
}
else {
// If an exiting value is passed, for its insertion into the sequence table.
if ($existing > 0) {
$exists = $this
->query_direct("SELECT COUNT(*) FROM {sequences} WHERE value = :existing", [
':existing' => $existing,
])
->fetchField();
if (!$exists) {
$this
->query_direct('SET IDENTITY_INSERT {sequences} ON; INSERT INTO {sequences} (value) VALUES(:existing); SET IDENTITY_INSERT {sequences} OFF', [
':existing' => $existing,
]);
}
}
// Refactored to use OUTPUT because under high concurrency LAST_INSERTED_ID does not work properly.
return $this
->query_direct('INSERT INTO {sequences} OUTPUT (Inserted.[value]) DEFAULT VALUES')
->fetchField();
}
}
/**
* Override DatabaseConnection::escapeTable().
*
* @status needswork
*/
public function escapeTable($table) {
// A static cache is better suited for this.
static $tables = [];
if (isset($tables[$table])) {
return $tables[$table];
}
// Rescue the # prefix from the escaping.
$is_temporary = $table[0] == '#';
$is_temporary_global = $is_temporary && isset($table[1]) && $table[1] == '#';
// Any temporary table prefix will be removed.
$result = preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
// Restore the temporary prefix.
if ($is_temporary) {
if ($is_temporary_global) {
$result = '##' . $result;
}
else {
$result = '#' . $result;
}
}
$tables[$table] = $result;
return $result;
}
#region Transactions
/**
* Overriden to allow transaction settings.
*/
public function startTransaction($name = '', DatabaseTransactionSettings $settings = null) {
if ($settings == null) {
$settings = DatabaseTransactionSettings::GetDefaults();
}
return new Transaction($this, $name, $settings);
}
/**
* Overriden.
*/
public function rollback($savepoint_name = 'drupal_transaction') {
if (!$this
->supportsTransactions()) {
return;
}
if (!$this
->inTransaction()) {
throw new DatabaseTransactionNoActiveException();
}
// A previous rollback to an earlier savepoint may mean that the savepoint
// in question has already been accidentally committed.
if (!isset($this->transactionLayers[$savepoint_name])) {
throw new DatabaseTransactionNoActiveException();
}
// We need to find the point we're rolling back to, all other savepoints
// before are no longer needed. If we rolled back other active savepoints,
// we need to throw an exception.
$rolled_back_other_active_savepoints = false;
while ($savepoint = array_pop($this->transactionLayers)) {
if ($savepoint['name'] == $savepoint_name) {
// If it is the last the transaction in the stack, then it is not a
// savepoint, it is the transaction itself so we will need to roll back
// the transaction rather than a savepoint.
if (empty($this->transactionLayers)) {
break;
}
if ($savepoint['started'] == true) {
$this
->query_direct('ROLLBACK TRANSACTION ' . $savepoint['name']);
}
$this
->popCommittableTransactions();
if ($rolled_back_other_active_savepoints) {
throw new DatabaseTransactionOutOfOrderException();
}
return;
}
else {
$rolled_back_other_active_savepoints = true;
}
}
$this->connection
->rollBack();
// Restore original transaction isolation level
if ($level = $this->driver_settings
->GetDefaultTransactionIsolationLevelInStatement()) {
if ($savepoint['settings']
->Get_IsolationLevel() != DatabaseTransactionIsolationLevel::Ignore()) {
if ($level != $savepoint['settings']
->Get_IsolationLevel()) {
$this
->query_direct("SET TRANSACTION ISOLATION LEVEL {$level}");
}
}
}
if ($rolled_back_other_active_savepoints) {
throw new DatabaseTransactionOutOfOrderException();
}
}
/**
* Summary of pushTransaction
* @param string $name
* @param DatabaseTransactionSettings $settings
* @return void
* @throws DatabaseTransactionNameNonUniqueException
*/
public function pushTransaction($name, $settings = null) {
if ($settings == null) {
$settings = DatabaseTransactionSettings::GetDefaults();
}
if (!$this
->supportsTransactions()) {
return;
}
if (isset($this->transactionLayers[$name])) {
throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
}
$started = false;
// If we're already in a transaction.
// TODO: Transaction scope Options is not working properly
// for first level transactions. It assumes that - always - a first level
// transaction must be started.
if ($this
->inTransaction()) {
switch ($settings
->Get_ScopeOption()) {
case DatabaseTransactionScopeOption::RequiresNew():
$this
->query_execute('SAVE TRANSACTION ' . $name);
$started = true;
break;
case DatabaseTransactionScopeOption::Required():
// We are already in a transaction, do nothing.
break;
case DatabaseTransactionScopeOption::Supress():
// The only way to supress the ambient transaction is to use a new connection
// during the scope of this transaction, a bit messy to implement.
throw new Exception('DatabaseTransactionScopeOption::Supress not implemented.');
}
}
else {
if ($settings
->Get_IsolationLevel() != DatabaseTransactionIsolationLevel::Ignore()) {
$current_isolation_level = strtoupper($this->connection
->Scheme()
->UserOptions()
->IsolationLevel());
// Se what isolation level was requested.
$level = $settings
->Get_IsolationLevel()
->__toString();
if (strcasecmp($current_isolation_level, $level) !== 0) {
$this
->query_direct("SET TRANSACTION ISOLATION LEVEL {$level}");
}
}
// In order to start a transaction current statement cursors
// must be closed.
foreach ($this->statement_cache as $statement) {
$statement
->closeCursor();
}
$this->connection
->beginTransaction();
}
// Store the name and settings in the stack.
$this->transactionLayers[$name] = array(
'settings' => $settings,
'active' => true,
'name' => $name,
'started' => $started,
);
}
/**
* Decreases the depth of transaction nesting.
*
* If we pop off the last transaction layer, then we either commit or roll
* back the transaction as necessary. If no transaction is active, we return
* because the transaction may have manually been rolled back.
*
* @param $name
* The name of the savepoint
*
* @throws DatabaseTransactionNoActiveException
* @throws DatabaseTransactionCommitFailedException
*
* @see DatabaseTransaction
*/
public function popTransaction($name) {
if (!$this
->supportsTransactions()) {
return;
}
// The transaction has already been committed earlier. There is nothing we
// need to do. If this transaction was part of an earlier out-of-order
// rollback, an exception would already have been thrown by
// Database::rollback().
if (!isset($this->transactionLayers[$name])) {
return;
}
// Mark this layer as committable.
$this->transactionLayers[$name]['active'] = false;
$this
->popCommittableTransactions();
}
/**
* Internal function: commit all the transaction layers that can commit.
*/
protected function popCommittableTransactions() {
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $state) {
// Stop once we found an active transaction.
if ($state['active']) {
break;
}
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
try {
// PDO::commit() can either return FALSE or throw an exception itself
if (!$this->connection
->commit()) {
throw new DatabaseTransactionCommitFailedException();
}
} finally {
// Restore original transaction isolation level
if ($level = $this->driver_settings
->GetDefaultTransactionIsolationLevelInStatement()) {
if ($state['settings']
->Get_IsolationLevel() != DatabaseTransactionIsolationLevel::Ignore()) {
if ($level != $state['settings']
->Get_IsolationLevel()
->__toString()) {
$this
->query_direct("SET TRANSACTION ISOLATION LEVEL {$level}");
}
}
}
}
}
else {
// Savepoints cannot be commited, only rolled back.
}
}
}
#endregion
/**
* Overrides \Drupal\Core\Database\Connection::createDatabase().
*
* @param string $database
* The name of the database to create.
*
* @throws \Drupal\Core\Database\DatabaseNotFoundException
*/
public function createDatabase($database) {
// Escape the database name.
$database = Database::getConnection()
->escapeDatabase($database);
try {
$this->connection
->Scheme()
->DatabaseCreate($database, Schema::DEFAULT_COLLATION_CI);
} catch (\PDOException $e) {
throw new DatabaseNotFoundException($e
->getMessage());
}
}
/**
* {@inheritdoc}
*/
public function getFullQualifiedTableName($table) {
$options = $this
->getConnectionOptions();
$prefix = $this
->tablePrefix($table);
return $options['database'] . '.' . $this
->schema()
->GetDefaultSchema() . '.' . $prefix . $table;
}
/**
* Error inform from the connection.
* @return array
*/
public function errorInfo() {
return $this->connection
->errorInfo();
}
/**
* Return the name of the database in use,
* not prefixed!
*/
public function getDatabaseName() {
// Database is defaulted from active connection.
$options = $this
->getConnectionOptions();
return $options['database'];
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
Connection:: |
protected | property |
@var \Drupal\Driver\Database\sqlsrv\PDO\Connection Overrides Connection:: |
|
Connection:: |
protected | property | The connection information for this connection object. | |
Connection:: |
protected | property | Index of what driver-specific class to use for various operations. | |
Connection:: |
public | property | Database driver settings. | |
Connection:: |
protected | property | List of escaped aliases names, keyed by unescaped aliases. | |
Connection:: |
protected | property | List of escaped database, table, and field names, keyed by unescaped names. | |
Connection:: |
protected | property | The key representing this connection. | |
Connection:: |
protected | property | The current database logging object for this connection. | |
Connection:: |
protected | property | The prefixes used by this database connection. | |
Connection:: |
protected | property | List of replacement values for use in prefixTables(). | |
Connection:: |
protected | property | List of search values for use in prefixTables(). | |
Connection:: |
protected | property | Post-root (non-nested) transaction commit callbacks. | |
Connection:: |
protected | property | The schema object for this connection. | |
Connection:: |
protected | property | The name of the Statement class for this connection. | |
Connection:: |
private | property | Prepared PDO statements only makes sense if we cache them... | |
Connection:: |
protected | property | The database target this connection is for. | |
Connection:: |
protected | property | An index used to generate unique temporary table names. | |
Connection:: |
protected | property | Whether this database connection supports transactional DDL. | |
Connection:: |
protected | property | Tracks the number of "layers" of transactions currently active. | |
Connection:: |
protected | property | Whether this database connection supports transactions. | |
Connection:: |
protected | property | List of un-prefixed table names, keyed by prefixed table names. | |
Connection:: |
protected | function | Adds debugging information to a query in the form of comments. | |
Connection:: |
public | function | Internal function: add range options to a query. | |
Connection:: |
public | function | Adds a root transaction end callback. | |
Connection:: |
public | function | Returns the version of the database client. | |
Connection:: |
public | function | Throws an exception to deny direct access to transaction commits. | |
Connection:: |
public | function | Prepares and returns a CONDITION query object. | |
Connection:: |
public static | function |
Adding schema to the connection URL. Overrides Connection:: |
|
Connection:: |
public | function |
Overrides \Drupal\Core\Database\Connection::createDatabase(). Overrides Connection:: |
|
Connection:: |
public static | function |
Adding schema to the connection URL. Overrides Connection:: |
|
Connection:: |
public | function |
Override of DatabaseConnection::databaseType(). Overrides Connection:: |
|
Connection:: |
constant | Error code for Login Failed, usually happens when the database does not exist. | ||
Connection:: |
protected | function | Returns the default query options for any given query. | |
Connection:: |
public | function | Prepares and returns a DELETE query object. | |
Connection:: |
public | function | Destroys this Connection object. | |
Connection:: |
protected | function | Do the actual commit, invoke post-commit callbacks. | |
Connection:: |
public | function |
Override of DatabaseConnection::driver(). Overrides Connection:: |
|
Connection:: |
public | function | Error inform from the connection. | |
Connection:: |
public | function | Escapes an alias name string. | 2 |
Connection:: |
public | function | Escapes a database name string. | |
Connection:: |
public | function |
Escapes a field name string. Overrides Connection:: |
|
Connection:: |
public | function |
Escapes characters that work as wildcard characters in a LIKE pattern. Overrides Connection:: |
|
Connection:: |
public | function |
Override DatabaseConnection::escapeTable(). Overrides Connection:: |
|
Connection:: |
protected | function | Expands out shorthand placeholders. | |
Connection:: |
protected | function | Sanitize a query comment string. | |
Connection:: |
protected | function |
Generates a temporary table name. Because we are using
global temporary tables, these are visible between
connections so we need to make sure that their
names are as unique as possible to prevent collisions. Overrides Connection:: |
|
Connection:: |
public | function | We should not be exposing the connection but... comes in handy some times. | |
Connection:: |
public | function | Returns the connection information for this connection object. | |
Connection:: |
public | function | Return the name of the database in use, not prefixed! | |
Connection:: |
public | function | Gets the driver-specific override class if any for the specified class. | |
Connection:: |
public | function |
Get a fully qualified table name. Overrides Connection:: |
|
Connection:: |
public | function | Returns the key this connection is associated with. | |
Connection:: |
public | function | Gets the current logging object for this connection. | |
Connection:: |
protected static | function | Extracts the SQLSTATE error from the PDOException. | |
Connection:: |
public | function | Returns the target this connection is associated with. | |
Connection:: |
public | function | Gets a list of individually prefixed table names. | |
Connection:: |
public | function |
Wraps and re-throws any PDO exception thrown by static::query(). Overrides Connection:: |
|
Connection:: |
public | function | Prepares and returns an INSERT query object. | |
Connection:: |
public | function | Determines if there is an active transaction open. | |
Connection:: |
public | function | Flatten an array of query comments into a single comment string. | |
Connection:: |
public | function | Creates the appropriate sequence name for a given table and serial field. | |
Connection:: |
public | function |
Gets any special processing requirements for the condition operator. Overrides Connection:: |
|
Connection:: |
public | function | Prepares and returns a MERGE query object. | |
Connection:: |
public | function |
{@inhertidoc} Overrides Connection:: |
|
Connection:: |
public static | function |
Opens a PDO connection. Overrides Connection:: |
|
Connection:: |
protected | function |
Internal function: commit all the transaction layers that can commit. Overrides Connection:: |
|
Connection:: |
public | function |
Decreases the depth of transaction nesting. Overrides Connection:: |
|
Connection:: |
public | function | Prefix a single table name. | |
Connection:: |
public | function | Appends a database prefix to all tables in a query. | |
Connection:: |
public | function | Prepares a statement for execution and returns a statement object | 1 |
Connection:: |
public | function |
Internal prepare a query. Overrides Connection:: |
|
Connection:: |
public | function | Internal function: massage a query to make it compliant with SQL Server. | |
Connection:: |
public | function |
Summary of pushTransaction Overrides Connection:: |
|
Connection:: |
public | function |
This method is overriden to manage the insecure (EMULATE_PREPARE)
behaviour to prevent some compatibility issues with SQL Server. Overrides Connection:: |
|
Connection:: |
public | function |
Runs a limited-range query on this database object. Overrides Connection:: |
|
Connection:: |
public | function |
Runs a SELECT query and stores its results in a temporary table. Overrides Connection:: |
|
Connection:: |
public | function | Like query but with no insecure detection or query preprocessing. The caller is sure that his query is MS SQL compatible! Used internally from the schema class, but could be called from anywhere. | |
Connection:: |
public | function | Quotes a string for use in a query. | |
Connection:: |
public | function | ||
Connection:: |
public | function | ||
Connection:: |
protected | function | This method gets called between 3,000 and 10,000 times on cold caches. Make sure it is simple and fast. | |
Connection:: |
constant | This is the original replacement regexp from Microsoft. | ||
Connection:: |
public | function | Overriden. | |
Connection:: |
public | function | Rolls back the transaction entirely or to a named savepoint. | |
Connection:: |
public | function |
Get the schema object. Overrides Connection:: |
|
Connection:: |
public | function | Get the Scheme manager. | |
Connection:: |
public | function | Prepares and returns a SELECT query object. | |
Connection:: |
public | function | Tells this connection object what its key is. | |
Connection:: |
public | function | Associates a logging object with this connection. | |
Connection:: |
protected | function | Set the list of prefixes used by this database connection. | |
Connection:: |
public | function | Tells this connection object what its target value is. | |
Connection:: |
public | function |
Overriden to allow transaction settings. Overrides Connection:: |
|
Connection:: |
public | function | Determines if this driver supports transactional DDL. | |
Connection:: |
public | function | Determines if this driver supports transactions. | |
Connection:: |
public | function | Find the prefix for a table. | |
Connection:: |
public | function | Determines the current transaction depth. | |
Connection:: |
public | function | Prepares and returns a TRUNCATE query object. | |
Connection:: |
public | function | Prepares and returns an UPDATE query object. | |
Connection:: |
public | function | Prepares and returns an UPSERT query object. | 1 |
Connection:: |
public | function | Returns the version of the database server. | 1 |
Connection:: |
public | function |
Constructs a Connection object. Overrides Connection:: |
|
Connection:: |
public | function | Prevents the database connection from being serialized. |