class DatabaseConnection_sqlsrv in Drupal driver for SQL Server and SQL Azure 7.3
Same name and namespace in other branches
- 7 sqlsrv/database.inc \DatabaseConnection_sqlsrv
- 7.2 sqlsrv/database.inc \DatabaseConnection_sqlsrv
Summary of DatabaseConnection_sqlsrv
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 $conn->directQuery = TRUE|FALSE. http://blogs.msdn.com/b/brian_swan/archive/2010/06/15/ctp2-of-microsoft-...
Hierarchy
- class \DatabaseConnection
- class \DatabaseConnection_sqlsrv
Expanded class hierarchy of DatabaseConnection_sqlsrv
1 file declares its use of DatabaseConnection_sqlsrv
File
- sqlsrv/
database.inc, line 28 - Database interface code for Microsoft SQL Server.
View source
class DatabaseConnection_sqlsrv extends DatabaseConnection {
// Do not preprocess the query before execution.
public $bypassQueryPreprocess = FALSE;
// Prepare statements with SQLSRV_ATTR_DIRECT_QUERY = TRUE.
public $directQuery = FALSE;
// Wether to have or not statement caching.
public $statementCaching = FALSE;
/**
* Override of DatabaseConnection::driver().
*
* @status tested
*/
public function driver() {
return 'sqlsrv';
}
/**
* Override of DatabaseConnection::databaseType().
*
* @status tested
*/
public function databaseType() {
return 'sqlsrv';
}
public function utf8mb4IsActive() {
return TRUE;
}
public function utf8mb4IsSupported() {
return TRUE;
}
/**
* The PDO constants do not matcc the actual isolation names
* used in SQL.
*/
private static function DefaultTransactionIsolationLevelInStatement() {
return str_replace('_', ' ', DatabaseUtils::GetConfigConstant('MSSQL_DEFAULT_ISOLATION_LEVEL', FALSE));
}
/**
* Override of DatabaseConnection::databaseType().
*
* @status complete
*/
public function __construct(array $connection_options = array()) {
global $conf;
// Store connection options for future reference.
$this->connectionOptions =& $connection_options;
// Set our custom statement class.
$this->statementClass = 'DatabaseStatement_sqlsrv';
// 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;
// Build the DSN.
$options = array();
$options['Server'] = $connection_options['host'];
// If a port is specified and this is NOT a named instance
if (!empty($connection_options['port']) && stripos($connection_options['host'], '\\') === false) {
$options['Server'] .= ',' . $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 = DatabaseUtils::GetConfigConstant('MSSQL_DEFAULT_ISOLATION_LEVEL', FALSE)) {
$options['TransactionIsolation'] = $level;
}
$options['MultipleActiveResultSets'] = 'false';
// Set default direct query behaviour
$this->directQuery = DatabaseUtils::GetConfigBoolean('MSSQL_DEFAULT_DIRECTQUERY');
$this->statementCaching = DatabaseUtils::GetConfigBoolean('MSSQL_STATEMENT_CACHING');
// Build the DSN
$dsn = 'sqlsrv:';
foreach ($options as $key => $value) {
$dsn .= (empty($key) ? '' : "{$key}=") . $value . ';';
}
// PDO Options are set at a connection level.
// and apply to all statements.
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => TRUE,
// Do not convert numeric values to strings when fetching.
// This is set for legacy reasons. Core tests prefer it to be TRUE.
PDO::ATTR_STRINGIFY_FETCHES => FALSE,
// Set proper error mode for all statements
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
);
// Set a Statement class, unless the driver opted out.
if (!empty($this->statementClass)) {
$connection_options['pdo'][PDO::ATTR_STATEMENT_CLASS] = array(
$this->statementClass,
array(
$this,
),
);
}
// Initialize and prepare the connection prefix.
$this
->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
// Set the connection.
$this->connection = new PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
}
/**
* Prepared PDO statements only makes sense if we cache them...
*
* @var mixed
*/
private $statement_cache = array();
/**
* Temporary override of DatabaseConnection::prepareQuery().
*
* @todo: remove that when DatabaseConnection::prepareQuery() is fixed to call
* $this->prepare() and not parent::prepare().
* @status: tested, temporary
*
* @param mixed $query
* @param mixed $insecure
* @return PDOStatement
*/
public function prepareQuery($query, array $options = array()) {
// 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->statementCaching,
'direct_query' => $this->directQuery,
'prefix_tables' => 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 = array();
// Set insecure options if requested so. There is no global
// setting for this, only at the statement level.
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 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 we are not going to cache prepared statements, always use direct queries!
// There is no point in preparing the statement if you are not going to cache it.
if (!$this->statementCaching || $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
// Call our overriden prepare.
$stmt = $this
->PDOPrepare($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;
}
/**
* Internal function: prepare a query by calling PDO directly.
*
* This function has to be public because it is called by other parts of the
* database layer, but do not call it directly, as you risk locking down the
* PHP process.
*/
public function PDOPrepare($query, array $options = array()) {
// Preprocess the query.
if (!$this->bypassQueryPreprocess) {
$query = $this
->preprocessQuery($query);
}
// You can set the MSSQL_APPEND_CALLSTACK_COMMENT to TRUE
// to append to each query, in the form of comments, the current
// backtrace plus other details that aid in debugging deadlocks
// or long standing locks. Use in combination with MSSQL profiler.
global $conf;
if (DatabaseUtils::GetConfigBoolean('MSSQL_APPEND_CALLSTACK_COMMENT') == TRUE) {
global $user;
$trim = strlen(DRUPAL_ROOT);
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
static $request_id;
if (empty($request_id)) {
$request_id = uniqid('', TRUE);
}
// Remove las item (it's alwasy PDOPrepare)
$trace = array_splice($trace, 1);
$comment = PHP_EOL . PHP_EOL;
$comment .= '-- uid:' . (empty($user) ? 'null' : $user->uid) . PHP_EOL;
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : 'none';
$uri = preg_replace("/[^a-zA-Z0-9]/i", "_", $uri);
$comment .= '-- url:' . $uri . PHP_EOL;
//$comment .= '-- request_id:' . $request_id . PHP_EOL;
foreach ($trace as $t) {
$function = isset($t['function']) ? $t['function'] : '';
$file = '';
if (isset($t['file'])) {
$len = strlen($t['file']);
if ($len > $trim) {
$file = substr($t['file'], $trim, $len - $trim) . " [{$t['line']}]";
}
}
$comment .= '-- ' . str_pad($function, 35) . ' ' . $file . PHP_EOL;
}
$query = $comment . PHP_EOL . $query;
}
return parent::prepare($query, $options);
}
/**
* 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|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);
}
public function quoteIdentifier($identifier) {
return '[' . $identifier . ']';
}
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];
}
public function quoteIdentifiers($identifiers) {
return array_map(array(
$this,
'quoteIdentifier',
), $identifiers);
}
/**
* Override of DatabaseConnection::escapeLike().
*/
public function escapeLike($string) {
return preg_replace('/([\\[\\]%_])/', '[$1]', $string);
}
/**
* Override of DatabaseConnection::queryRange().
*/
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
$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
* 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;
}
/**
* Override of DatabaseConnection::queryTemporary().
*
* @status tested
*/
public function queryTemporary($query, array $args = array(), array $options = array()) {
// 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();
$prefixes = $this->prefixes;
$prefixes[$tablename] = '';
$this
->setPrefix($prefixes);
// Having comments in the query can be tricky and break the SELECT FROM -> SELECT INTO conversion
$query = $this
->schema()
->removeSQLComments($query);
// 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 = array(), $options = array()) {
// 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 DatabaseStatementInterface) {
$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' => TRUE,
));
$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:
return $stmt
->rowCount();
case Database::RETURN_INSERT_ID:
return $this
->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, $stmt, $args, $options);
}
}
/**
* 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
* @throws PDOException
* @return mixed
*/
public function query_direct($query, array $args = array(), $options = array()) {
// Use default values if not already set.
$options += $this
->defaultOptions();
$stmt = NULL;
try {
// Bypass query preprocessing and use direct queries.
$ctx = new \DatabaseContext($this, TRUE, TRUE);
// Prepare the statement and execute it.
$stmt = $this
->prepareQuery($query, $options);
$stmt
->execute($args, $options);
// Reset the context settings.
unset($ctx);
// 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:
return $stmt
->rowCount();
case Database::RETURN_INSERT_ID:
return $this
->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, $stmt, $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 DatabaseStatementInterface|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.
*/
public function handleQueryException(\PDOException $e, $query, array $args = array(), $options = array()) {
if ($options['throw_exception']) {
// Add additional debug information.
if ($query instanceof DatabaseStatement_sqlsrv) {
/** @var DatabaseStatement_sqlsrv $statement */
$statement = $query;
$e->query_string = $statement
->getQueryString();
$e->args = $statement
->GetBoundParameters();
}
else {
$e->query_string = $query;
}
if (empty($e->args)) {
$e->args = $args;
}
throw $e;
}
return NULL;
}
/**
* Internal function: massage a query to make it compliant with SQL Server.
*/
public function preprocessQuery($query) {
// Force quotes around some SQL Server reserved keywords.
if (preg_match('/^SELECT/i', $query)) {
$query = preg_replace_callback(self::RESERVED_REGEXP, array(
$this,
'replaceReservedCallback',
), $query);
}
// Last chance to modify some SQL Server-specific syntax.
$replacements = array();
// 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 = array(
'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);
return $query;
}
/**
* 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
->schema()
->EngineVersionNumber() >= 11) {
// As of SQL Server 2012 there is an easy (and faster!) way to page results.
$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' => array(),
'NOT LIKE' => array(),
);
return isset($specials[$operator]) ? $specials[$operator] : NULL;
}
/**
* Override of DatabaseConnection::nextId().
*
* @status tested
*/
public function nextId($existing = 0) {
// If an exiting value is passed, for its insertion into the sequence table.
if ($existing > 0) {
try {
$this
->query_direct('SET IDENTITY_INSERT {sequences} ON; INSERT INTO {sequences} (value) VALUES(:existing); SET IDENTITY_INSERT {sequences} OFF', array(
':existing' => $existing,
));
} catch (Exception $e) {
// Doesn't matter if this fails, it just means that this value is already
// present in the table.
}
}
// 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 = array();
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 DatabaseTransaction_sqlsrv($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 = static::DefaultTransactionIsolationLevelInStatement()) {
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
* @throws DatabaseTransactionNameNonUniqueException
* @return void
*/
public function pushTransaction($name, $settings = NULL) {
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_direct('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()) {
$user_options = $this
->schema()
->UserOptions();
$current_isolation_level = strtoupper($user_options['isolation level']);
// 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
$commit_check = $this->connection
->commit();
if (!$commit_check) {
throw new DatabaseTransactionCommitFailedException();
}
} finally {
// Restore original transaction isolation level
if ($level = static::DefaultTransactionIsolationLevelInStatement()) {
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.
}
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DatabaseConnection:: |
protected | property | The actual PDO connection. | |
DatabaseConnection:: |
protected | property | The connection information for this connection object. | |
DatabaseConnection:: |
protected | property | Index of what driver-specific class to use for various operations. | |
DatabaseConnection:: |
protected | property | List of escaped aliases names, keyed by unescaped aliases. | |
DatabaseConnection:: |
protected | property | List of escaped database, table, and field names, keyed by unescaped names. | |
DatabaseConnection:: |
protected | property | The key representing this connection. | |
DatabaseConnection:: |
protected | property | The current database logging object for this connection. | |
DatabaseConnection:: |
protected | property | The prefixes used by this database connection. | |
DatabaseConnection:: |
protected | property | List of replacement values for use in prefixTables(). | |
DatabaseConnection:: |
protected | property | List of search values for use in prefixTables(). | |
DatabaseConnection:: |
protected | property | The schema object for this connection. | |
DatabaseConnection:: |
protected | property | The name of the Statement class for this connection. | |
DatabaseConnection:: |
protected | property | The database target this connection is for. | |
DatabaseConnection:: |
protected | property | An index used to generate unique temporary table names. | |
DatabaseConnection:: |
protected | property | Whether this database connection supports transactional DDL. | |
DatabaseConnection:: |
protected | property | Tracks the number of "layers" of transactions currently active. | |
DatabaseConnection:: |
protected | property | Whether this database connection supports transactions. | |
DatabaseConnection:: |
protected | property | List of un-prefixed table names, keyed by prefixed table names. | |
DatabaseConnection:: |
public | function | Throws an exception to deny direct access to transaction commits. | |
DatabaseConnection:: |
protected | function | Returns the default query options for any given query. | |
DatabaseConnection:: |
public | function | Prepares and returns a DELETE query object. | |
DatabaseConnection:: |
public | function | Destroys this Connection object. | |
DatabaseConnection:: |
public | function | Escapes an alias name string. | 1 |
DatabaseConnection:: |
protected | function | Expands out shorthand placeholders. | |
DatabaseConnection:: |
protected | function | Sanitize a query comment string. | |
DatabaseConnection:: |
public | function | Returns the connection information for this connection object. | |
DatabaseConnection:: |
public | function | Gets the driver-specific override class if any for the specified class. | |
DatabaseConnection:: |
public | function | Returns the key this connection is associated with. | |
DatabaseConnection:: |
public | function | Gets the current logging object for this connection. | |
DatabaseConnection:: |
public | function | Returns the target this connection is associated with. | |
DatabaseConnection:: |
public | function | Gets a list of individually prefixed table names. | |
DatabaseConnection:: |
public | function | Prepares and returns an INSERT query object. | |
DatabaseConnection:: |
public | function | Determines if there is an active transaction open. | |
DatabaseConnection:: |
public | function | Flatten an array of query comments into a single comment string. | |
DatabaseConnection:: |
public | function | Creates the appropriate sequence name for a given table and serial field. | |
DatabaseConnection:: |
public | function | Prepares and returns a MERGE query object. | |
DatabaseConnection:: |
public | function | Appends a database prefix to all tables in a query. | |
DatabaseConnection:: |
public | function | Returns a DatabaseSchema object for manipulating the schema. | |
DatabaseConnection:: |
public | function | Prepares and returns a SELECT query object. | |
DatabaseConnection:: |
public | function | Tells this connection object what its key is. | |
DatabaseConnection:: |
public | function | Associates a logging object with this connection. | |
DatabaseConnection:: |
protected | function | Set the list of prefixes used by this database connection. | 1 |
DatabaseConnection:: |
public | function | Tells this connection object what its target value is. | |
DatabaseConnection:: |
public | function | Determines if this driver supports transactional DDL. | |
DatabaseConnection:: |
public | function | Determines if this driver supports transactions. | |
DatabaseConnection:: |
public | function | Find the prefix for a table. | |
DatabaseConnection:: |
public | function | Determines current transaction depth. | |
DatabaseConnection:: |
public | function | Prepares and returns a TRUNCATE query object. | |
DatabaseConnection:: |
public | function | Prepares and returns an UPDATE query object. | |
DatabaseConnection:: |
public | function | Checks whether utf8mb4 support is configurable in settings.php. | 1 |
DatabaseConnection:: |
public | function | Returns the version of the database server. | |
DatabaseConnection:: |
public | function | Proxy possible direct calls to the \PDO methods. | |
DatabaseConnection_sqlsrv:: |
public | property | ||
DatabaseConnection_sqlsrv:: |
public | property | ||
DatabaseConnection_sqlsrv:: |
public | property | ||
DatabaseConnection_sqlsrv:: |
private | property | Prepared PDO statements only makes sense if we cache them... | |
DatabaseConnection_sqlsrv:: |
public | function | Internal function: add range options to a query. | |
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::databaseType(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
private static | function | The PDO constants do not matcc the actual isolation names used in SQL. | |
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::driver(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Escapes a field name string. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::escapeLike(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override DatabaseConnection::escapeTable(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
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 DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function | Wraps and re-throws any PDO exception thrown by static::query(). | |
DatabaseConnection_sqlsrv:: |
public | function |
Gets any special processing requirements for the condition operator. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::nextId(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function | Internal function: prepare a query by calling PDO directly. | |
DatabaseConnection_sqlsrv:: |
protected | function |
Internal function: commit all the transaction layers that can commit. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Decreases the depth of transaction nesting. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Temporary override of DatabaseConnection::prepareQuery(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function | Internal function: massage a query to make it compliant with SQL Server. | |
DatabaseConnection_sqlsrv:: |
public | function |
Summary of pushTransaction Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
This method is overriden to manage the insecure (EMULATE_PREPARE)
behaviour to prevent some compatibility issues with SQL Server. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::queryRange(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::queryTemporary(). Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
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. | |
DatabaseConnection_sqlsrv:: |
public | function | ||
DatabaseConnection_sqlsrv:: |
public | function | ||
DatabaseConnection_sqlsrv:: |
protected | function | This method gets called between 3,000 and 10,000 times on cold caches. Make sure it is simple and fast. | |
DatabaseConnection_sqlsrv:: |
constant | This is the original replacement regexp from Microsoft. | ||
DatabaseConnection_sqlsrv:: |
public | function |
Overriden. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Overriden to allow transaction settings. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Checks whether utf8mb4 support is currently active. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Checks whether utf8mb4 support is available on the current database system. Overrides DatabaseConnection:: |
|
DatabaseConnection_sqlsrv:: |
public | function |
Override of DatabaseConnection::databaseType(). Overrides DatabaseConnection:: |