You are here

class Connection in Drupal driver for SQL Server and SQL Azure 8.2

Same name in this branch
  1. 8.2 drivers/lib/Drupal/Driver/Database/sqlsrv/Connection.php \Drupal\Driver\Database\sqlsrv\Connection
  2. 8.2 drivers/lib/Drupal/Driver/Database/sqlsrv/PDO/Connection.php \Drupal\Driver\Database\sqlsrv\PDO\Connection
Same name and namespace in other branches
  1. 8 drivers/lib/Drupal/Driver/Database/sqlsrv/Connection.php \Drupal\Driver\Database\sqlsrv\Connection
  2. 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

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\sqlsrv
View 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("&amp;", $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

Namesort descending Modifiers Type Description Overrides
Connection::$connection protected property @var \Drupal\Driver\Database\sqlsrv\PDO\Connection Overrides Connection::$connection
Connection::$connectionOptions protected property The connection information for this connection object.
Connection::$driverClasses protected property Index of what driver-specific class to use for various operations.
Connection::$driver_settings public property Database driver settings.
Connection::$escapedAliases protected property List of escaped aliases names, keyed by unescaped aliases.
Connection::$escapedNames protected property List of escaped database, table, and field names, keyed by unescaped names.
Connection::$key protected property The key representing this connection.
Connection::$logger protected property The current database logging object for this connection.
Connection::$prefixes protected property The prefixes used by this database connection.
Connection::$prefixReplace protected property List of replacement values for use in prefixTables().
Connection::$prefixSearch protected property List of search values for use in prefixTables().
Connection::$rootTransactionEndCallbacks protected property Post-root (non-nested) transaction commit callbacks.
Connection::$schema protected property The schema object for this connection.
Connection::$statementClass protected property The name of the Statement class for this connection.
Connection::$statement_cache private property Prepared PDO statements only makes sense if we cache them...
Connection::$target protected property The database target this connection is for.
Connection::$temporaryNameIndex protected property An index used to generate unique temporary table names.
Connection::$transactionalDDLSupport protected property Whether this database connection supports transactional DDL.
Connection::$transactionLayers protected property Tracks the number of "layers" of transactions currently active.
Connection::$transactionSupport protected property Whether this database connection supports transactions.
Connection::$unprefixedTablesMap protected property List of un-prefixed table names, keyed by prefixed table names.
Connection::addDebugInfoToQuery protected function Adds debugging information to a query in the form of comments.
Connection::addRangeToQuery public function Internal function: add range options to a query.
Connection::addRootTransactionEndCallback public function Adds a root transaction end callback.
Connection::clientVersion public function Returns the version of the database client.
Connection::commit public function Throws an exception to deny direct access to transaction commits.
Connection::condition public function Prepares and returns a CONDITION query object.
Connection::createConnectionOptionsFromUrl public static function Adding schema to the connection URL. Overrides Connection::createConnectionOptionsFromUrl
Connection::createDatabase public function Overrides \Drupal\Core\Database\Connection::createDatabase(). Overrides Connection::createDatabase
Connection::createUrlFromConnectionOptions public static function Adding schema to the connection URL. Overrides Connection::createUrlFromConnectionOptions
Connection::databaseType public function Override of DatabaseConnection::databaseType(). Overrides Connection::databaseType
Connection::DATABASE_NOT_FOUND constant Error code for Login Failed, usually happens when the database does not exist.
Connection::defaultOptions protected function Returns the default query options for any given query.
Connection::delete public function Prepares and returns a DELETE query object.
Connection::destroy public function Destroys this Connection object.
Connection::doCommit protected function Do the actual commit, invoke post-commit callbacks.
Connection::driver public function Override of DatabaseConnection::driver(). Overrides Connection::driver
Connection::errorInfo public function Error inform from the connection.
Connection::escapeAlias public function Escapes an alias name string. 2
Connection::escapeDatabase public function Escapes a database name string.
Connection::escapeField public function Escapes a field name string. Overrides Connection::escapeField
Connection::escapeLike public function Escapes characters that work as wildcard characters in a LIKE pattern. Overrides Connection::escapeLike
Connection::escapeTable public function Override DatabaseConnection::escapeTable(). Overrides Connection::escapeTable
Connection::expandArguments protected function Expands out shorthand placeholders.
Connection::filterComment protected function Sanitize a query comment string.
Connection::generateTemporaryTableName 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::generateTemporaryTableName
Connection::GetConnection public function We should not be exposing the connection but... comes in handy some times.
Connection::getConnectionOptions public function Returns the connection information for this connection object.
Connection::getDatabaseName public function Return the name of the database in use, not prefixed!
Connection::getDriverClass public function Gets the driver-specific override class if any for the specified class.
Connection::getFullQualifiedTableName public function Get a fully qualified table name. Overrides Connection::getFullQualifiedTableName
Connection::getKey public function Returns the key this connection is associated with.
Connection::getLogger public function Gets the current logging object for this connection.
Connection::getSQLState protected static function Extracts the SQLSTATE error from the PDOException.
Connection::getTarget public function Returns the target this connection is associated with.
Connection::getUnprefixedTablesMap public function Gets a list of individually prefixed table names.
Connection::handleQueryException public function Wraps and re-throws any PDO exception thrown by static::query(). Overrides Connection::handleQueryException
Connection::insert public function Prepares and returns an INSERT query object.
Connection::inTransaction public function Determines if there is an active transaction open.
Connection::makeComment public function Flatten an array of query comments into a single comment string.
Connection::makeSequenceName public function Creates the appropriate sequence name for a given table and serial field.
Connection::mapConditionOperator public function Gets any special processing requirements for the condition operator. Overrides Connection::mapConditionOperator
Connection::merge public function Prepares and returns a MERGE query object.
Connection::nextId public function {@inhertidoc} Overrides Connection::nextId
Connection::open public static function Opens a PDO connection. Overrides Connection::open
Connection::popCommittableTransactions protected function Internal function: commit all the transaction layers that can commit. Overrides Connection::popCommittableTransactions
Connection::popTransaction public function Decreases the depth of transaction nesting. Overrides Connection::popTransaction
Connection::prefixTable public function Prefix a single table name.
Connection::prefixTables public function Appends a database prefix to all tables in a query.
Connection::prepare public function Prepares a statement for execution and returns a statement object 1
Connection::prepareQuery public function Internal prepare a query. Overrides Connection::prepareQuery
Connection::preprocessQuery public function Internal function: massage a query to make it compliant with SQL Server.
Connection::pushTransaction public function Summary of pushTransaction Overrides Connection::pushTransaction
Connection::query public function This method is overriden to manage the insecure (EMULATE_PREPARE) behaviour to prevent some compatibility issues with SQL Server. Overrides Connection::query
Connection::queryRange public function Runs a limited-range query on this database object. Overrides Connection::queryRange
Connection::queryTemporary public function Runs a SELECT query and stores its results in a temporary table. Overrides Connection::queryTemporary
Connection::query_direct 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::quote public function Quotes a string for use in a query.
Connection::quoteIdentifier public function
Connection::quoteIdentifiers public function
Connection::replaceReservedCallback protected function This method gets called between 3,000 and 10,000 times on cold caches. Make sure it is simple and fast.
Connection::RESERVED_REGEXP constant This is the original replacement regexp from Microsoft.
Connection::rollback public function Overriden.
Connection::rollBack public function Rolls back the transaction entirely or to a named savepoint.
Connection::schema public function Get the schema object. Overrides Connection::schema
Connection::Scheme public function Get the Scheme manager.
Connection::select public function Prepares and returns a SELECT query object.
Connection::setKey public function Tells this connection object what its key is.
Connection::setLogger public function Associates a logging object with this connection.
Connection::setPrefix protected function Set the list of prefixes used by this database connection.
Connection::setTarget public function Tells this connection object what its target value is.
Connection::startTransaction public function Overriden to allow transaction settings. Overrides Connection::startTransaction
Connection::supportsTransactionalDDL public function Determines if this driver supports transactional DDL.
Connection::supportsTransactions public function Determines if this driver supports transactions.
Connection::tablePrefix public function Find the prefix for a table.
Connection::transactionDepth public function Determines the current transaction depth.
Connection::truncate public function Prepares and returns a TRUNCATE query object.
Connection::update public function Prepares and returns an UPDATE query object.
Connection::upsert public function Prepares and returns an UPSERT query object. 1
Connection::version public function Returns the version of the database server. 1
Connection::__construct public function Constructs a Connection object. Overrides Connection::__construct
Connection::__sleep public function Prevents the database connection from being serialized.