You are here

Statement.php in Drupal driver for SQL Server and SQL Azure 8.2

File

drivers/lib/Drupal/Driver/Database/sqlsrv/PDO/Statement.php
View source
<?php

namespace Drupal\Driver\Database\sqlsrv\PDO;

use PDO;
use PDOException;
use PDOStatement;

/**
 * Turbocharged Statement class to work with MSSQL server.
 */
class Statement extends PDOStatement implements \Countable {

  /**
   * An MD5 signature for the query
   *
   * @var string
   */
  protected $query_signature;

  /**
   * Reference to the Connection
   *
   * @var Connection
   */
  protected $cnn = null;

  /**
   * Special PDO options
   *
   * @var array
   */
  protected $options = null;

  /**
   * ODBC Codes for integrity constraint violation errors.
   *
   * @see https://msdn.microsoft.com/en-us/library/ms714687(v=vs.85).aspx
   *
   * @var array
   */
  protected $INTEGRITY_VIOLATION_CONSTRAINT_CODES = [
    '23000',
    '40002',
  ];

  /**
   * ODBC Codes for failed communcation link/dropped connections
   *
   * @see https://msdn.microsoft.com/en-us/library/ms714687(v=vs.85).aspx
   *
   * @var array
   */
  protected $CONNECTION_FAILED_CODES = [
    '08S01',
    '08001',
    '08002',
    '08003',
    '08007',
  ];

  /**
   * Maximum number a failed query
   * can be retried in case of failure
   * due to integrity constraint violations
   * or dropped connections.
   */
  const RETRY_MAX = 3;

  /**
   * Delay between retries in seconds.
   *
   * This effective delay is the product
   * of this constant and the current retry
   * count.
   */
  const RETRY_DELAY = 0.4;

  /**
   * Return the number of rows.
   *
   * @return int
   */
  public function count() {
    return $this
      ->rowCount();
  }

  /**
   * Set a reference to the connection.
   *
   * @param Connection $cnn
   */
  public function SetConnection(Connection $cnn, array $options = []) {

    // Custom PDO options
    $this->options = array_merge([
      Connection::PDO_RESILIENTRETRY => false,
      Connection::PDO_RETRYONINTEGRITYVIOLATION => false,
    ], $options);
    $this->cnn = $cnn;
  }

  /**
   * Get parameters bound to this statement, useful for debugging purposes.
   *
   * @return string[]
   */
  public function &GetBoundParameters() {
    return $this->boundParams;
  }

  /**
   * @var string[] $boundParams - array of arrays containing values that have been bound to the query as parameters
   */
  protected $boundParams = array();

  /**
   * Overrides the default \PDOStatement method to add the named parameter and it's reference to the array of bound
   * parameters - then accesses and returns parent::bindParam method
   *
   * @param string $param
   * @param mixed $value
   * @param int $datatype
   * @param int $length
   * @param mixed $driverOptions
   * @return bool
   */
  public function bindParam($param, &$value, $datatype = PDO::PARAM_STR, $length = 0, $driverOptions = false) {
    $this->boundParams[$param] = array(
      "value" => &$value,
      "datatype" => $datatype,
    );
    if (empty($driverOptions)) {
      return parent::bindParam($param, $value, $datatype, $length);
    }
    else {
      return parent::bindParam($param, $value, $datatype, $length, $driverOptions);
    }
  }

  /**
   * Overrides the default \PDOStatement method to add the named parameter and it's value to the array of bound values
   * - then accesses and returns parent::bindValue method
   *
   * @param string $param
   * @param string $value
   * @param int $datatype
   * @return bool
   */
  public function bindValue($param, $value, $datatype = PDO::PARAM_STR) {
    $this->boundParams[$param] = array(
      "value" => $value,
      "datatype" => $datatype,
    );
    return parent::bindValue($param, $value, $datatype);
  }

  /**
   * Cached version of getColumnMeta().
   *
   * Who knows why this was always profiled as being
   * a memory hog, probably due some problem with
   * the PDO driver.
   *
   * @return mixed
   */
  protected function getColumnMetaCustom() {
    $meta = false;
    if ($cache = $this->cnn
      ->Cache('sqlsrv_meta')
      ->Get($this->query_signature)) {
      return $cache->data;
    }

    // Just some safety to account for some schema
    // changes.
    $meta = [];
    for ($i = 0, $count = $this
      ->columnCount(); $i < $count; $i++) {
      $meta[$i] = $this
        ->getColumnMeta($i);
      $meta[$i]['sqlsrv_type'] = explode(' ', $meta[$i]['sqlsrv:decl_type'])[0];
    }
    $this->cnn
      ->Cache('sqlsrv_meta')
      ->Set($this->query_signature, $meta);
    return $meta;
  }

  /**
   * Make sure that SQL Server types are properly binded to
   * PHP types.
   *
   * Only need when using CLIENT BASED PREFETCH.
   *
   */
  protected function fixColumnBindings() {
    $null = array();
    $meta = $this
      ->getColumnMetaCustom();
    $this->columnNames = array_column($meta, 'name');
    foreach ($meta as $i => $meta) {
      $type = $meta['sqlsrv_type'];
      switch ($type) {
        case 'varbinary':
          $null[$i] = null;
          $this
            ->bindColumn($i + 1, $null[$i], PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
          break;
        case 'int':
        case 'bit':
        case 'smallint':
        case 'tinyint':
        case 'bigint':
          $null[$i] = null;
          $this
            ->bindColumn($i + 1, $null[$i], PDO::PARAM_INT);
          break;
        case 'nvarchar':
        case 'varchar':
          $null[$i] = null;
          $this
            ->bindColumn($i + 1, $null[$i], PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8);
          break;
      }
    }
  }

  /**
   * Summary of BindArguments
   *
   * @param PDOStatement $stmt
   * @param array $values
   */
  public function BindArguments(array &$values) {
    foreach ($values as $key => &$value) {
      $this
        ->bindParam($key, $value, PDO::PARAM_STR);
    }
  }

  /**
   * Summary of BindExpressions
   *
   * @param array $values
   * @param array $remove_from
   */
  public function BindExpressions(array &$values, array &$remove_from) {
    foreach ($values as $key => $value) {
      unset($remove_from[$key]);
      if (empty($value['arguments'])) {
        continue;
      }
      if (is_array($value['arguments'])) {
        foreach ($value['arguments'] as $placeholder => $argument) {

          // We assume that an expression will never happen on a BLOB field,
          // which is a fairly safe assumption to make since in most cases
          // it would be an invalid query anyway.
          $this
            ->bindParam($placeholder, $value['arguments'][$placeholder]);
        }
      }
      else {
        $this
          ->bindParam($key, $value['arguments'], PDO::PARAM_STR);
      }
    }
  }

  /**
   * Binds a set of values to a PDO Statement,
   * taking care of properly managing binary data.
   *
   * @param PDOStatement $stmt
   *   PDOStatement to bind the values to
   *
   * @param array $values
   *   Values to bind. It's an array where the keys are column
   *   names and the values what is going to be inserted.
   *
   * @param array $blobs
   *   When sending binary data to the PDO driver, we need to keep
   *   track of the original references to data
   *
   * @param array $ref_prefix
   *   The $ref_holder might be shared between statements, use this
   *   prefix to prevent key colision.
   *
   * @param mixed $placeholder_prefix
   *   Prefix to use for generating the query placeholders.
   *
   * @param mixed $max_placeholder
   *   Placeholder count, if NULL will start with 0.
   *
   */
  public function BindValues(array &$values, array &$blobs, $placeholder_prefix, $columnInformation, &$max_placeholder = null, $blob_suffix = null) {
    if (empty($max_placeholder)) {
      $max_placeholder = 0;
    }
    foreach ($values as $field_name => &$field_value) {
      $placeholder = $placeholder_prefix . $max_placeholder++;
      $blob_key = $placeholder . $blob_suffix;
      if (isset($columnInformation['blobs'][$field_name])) {
        $blobs[$blob_key] = fopen('php://memory', 'a');
        fwrite($blobs[$blob_key], $field_value);
        rewind($blobs[$blob_key]);
        $this
          ->bindParam($placeholder, $blobs[$blob_key], PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY);
      }
      else {

        // Even though not a blob, make sure we retain a copy of these values.
        $blobs[$blob_key] = $field_value;
        $this
          ->bindParam($placeholder, $blobs[$blob_key], PDO::PARAM_STR);
      }
    }
  }

  /**
   * Execute a statement.
   *
   * @param array $args
   */
  public function execute($args = null) {
    $this->query_signature = md5($this->queryString);
    if ($this->cnn
      ->InDoomedTransaction()) {
      $this->cnn
        ->ThrowDoomedTransactionException();
    }
    $result = null;
    try {
      $count = 0;
      while (true) {
        try {
          $count++;
          $result = parent::execute($args);
          break;
        } catch (\PDOException $e) {

          // If the maximum retry limit is exceeded
          // throw the exception.
          if ($count > self::RETRY_MAX) {
            throw $e;
          }
          $safe = false;
          if ($this->options[Connection::PDO_RETRYONINTEGRITYVIOLATION] === true && in_array((string) $e
            ->getCode(), $this->INTEGRITY_VIOLATION_CONSTRAINT_CODES)) {
            $safe = true;
          }
          if ($this->options[Connection::PDO_RESILIENTRETRY] === true && in_array((string) $e
            ->getCode(), $this->CONNECTION_FAILED_CODES)) {
            $safe = true;
          }
          if (!$safe) {
            throw $e;
          }
          else {
            usleep($count * (1000000 * self::RETRY_DELAY));
          }
        }
      }
      if ($result == false) {
        $this->cnn
          ->ThrowPdoException($this, null);
      }
      return $result;
    } catch (\PDOException $e) {
      $this->cnn
        ->NotifyException($e);
      $this->cnn
        ->ThrowPdoException($this, $e);
      return null;
    }
  }

  /**
   * Optimized for common use cases.
   *
   * @param int $key_index
   * @param int $value_index
   *
   * @return array
   */
  public function fetchAllKeyed($key_index = 0, $value_index = 1) {

    // If we are asked for the default behaviour, rely
    // on the PDO as being faster. The result set needs to exactly bee 2 columns.
    if ($key_index == 0 && $value_index == 1 && $this
      ->columnCount() == 2) {
      $this
        ->setFetchMode(PDO::FETCH_KEY_PAIR);
      return $this
        ->fetchAll();
    }

    // We need to do this manually.
    $return = array();
    $this
      ->setFetchMode(PDO::FETCH_NUM);
    foreach ($this as $record) {
      $return[$record[$key_index]] = $record[$value_index];
    }
    return $return;
  }

}

Classes

Namesort descending Description
Statement Turbocharged Statement class to work with MSSQL server.