547 lines
17 KiB
PHP
547 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Part of the Joomla Framework Database Package
|
|
*
|
|
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
|
|
* @license GNU General Public License version 2 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Database\Sqlsrv;
|
|
|
|
use Joomla\Database\Exception\ExecutionFailureException;
|
|
use Joomla\Database\Exception\PrepareStatementFailureException;
|
|
use Joomla\Database\FetchMode;
|
|
use Joomla\Database\FetchOrientation;
|
|
use Joomla\Database\ParameterType;
|
|
use Joomla\Database\StatementInterface;
|
|
|
|
/**
|
|
* SQL Server Database Statement.
|
|
*
|
|
* This class is modeled on \Doctrine\DBAL\Driver\SQLSrv\SQLSrvStatement
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
class SqlsrvStatement implements StatementInterface
|
|
{
|
|
/**
|
|
* The database connection resource.
|
|
*
|
|
* @var resource
|
|
* @since 2.0.0
|
|
*/
|
|
protected $connection;
|
|
|
|
/**
|
|
* The default fetch mode for the statement.
|
|
*
|
|
* @var integer
|
|
* @since 2.0.0
|
|
*/
|
|
protected $defaultFetchStyle = FetchMode::MIXED;
|
|
|
|
/**
|
|
* The default class to use for building object result sets.
|
|
*
|
|
* @var integer
|
|
* @since 2.0.0
|
|
*/
|
|
protected $defaultObjectClass = \stdClass::class;
|
|
|
|
/**
|
|
* Mapping array converting fetch modes to the native engine type.
|
|
*
|
|
* @var array
|
|
* @since 2.0.0
|
|
*/
|
|
private $fetchMap = [
|
|
FetchMode::MIXED => SQLSRV_FETCH_BOTH,
|
|
FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
|
|
FetchMode::NUMERIC => SQLSRV_FETCH_NUMERIC,
|
|
];
|
|
|
|
/**
|
|
* The query string being prepared.
|
|
*
|
|
* @var string
|
|
* @since 2.0.0
|
|
*/
|
|
protected $query;
|
|
|
|
/**
|
|
* Internal tracking flag to set whether there is a result set available for processing
|
|
*
|
|
* @var boolean
|
|
* @since 2.0.0
|
|
*/
|
|
private $result = false;
|
|
|
|
/**
|
|
* The prepared statement.
|
|
*
|
|
* @var resource
|
|
* @since 2.0.0
|
|
*/
|
|
protected $statement;
|
|
|
|
/**
|
|
* Bound parameter types.
|
|
*
|
|
* @var array
|
|
* @since 2.0.0
|
|
*/
|
|
protected $typesKeyMapping;
|
|
|
|
/**
|
|
* References to the variables bound as statement parameters.
|
|
*
|
|
* @var array
|
|
* @since 2.0.0
|
|
*/
|
|
private $bindedValues = [];
|
|
|
|
/**
|
|
* Mapping between named parameters and position in query.
|
|
*
|
|
* @var array
|
|
* @since 2.0.0
|
|
*/
|
|
protected $parameterKeyMapping;
|
|
|
|
/**
|
|
* Mapping array for parameter types.
|
|
*
|
|
* @var array
|
|
* @since 2.0.0
|
|
*/
|
|
protected $parameterTypeMapping = [
|
|
ParameterType::BOOLEAN => ParameterType::BOOLEAN,
|
|
ParameterType::INTEGER => ParameterType::INTEGER,
|
|
ParameterType::LARGE_OBJECT => ParameterType::LARGE_OBJECT,
|
|
ParameterType::NULL => ParameterType::NULL,
|
|
ParameterType::STRING => ParameterType::STRING,
|
|
];
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param resource $connection The database connection resource
|
|
* @param string $query The query this statement will process
|
|
*
|
|
* @since 2.0.0
|
|
* @throws PrepareStatementFailureException
|
|
*/
|
|
public function __construct($connection, string $query)
|
|
{
|
|
// Initial parameter types for prepared statements
|
|
$this->parameterTypeMapping = [
|
|
ParameterType::BOOLEAN => SQLSRV_PHPTYPE_INT,
|
|
ParameterType::INTEGER => SQLSRV_PHPTYPE_INT,
|
|
ParameterType::LARGE_OBJECT => SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
|
|
ParameterType::NULL => SQLSRV_PHPTYPE_NULL,
|
|
ParameterType::STRING => SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR),
|
|
];
|
|
|
|
$this->connection = $connection;
|
|
$this->query = $this->prepareParameterKeyMapping($query);
|
|
}
|
|
|
|
/**
|
|
* Replace named parameters with numbered parameters
|
|
*
|
|
* @param string $sql The SQL statement to prepare.
|
|
*
|
|
* @return string The processed SQL statement.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function prepareParameterKeyMapping($sql)
|
|
{
|
|
$escaped = false;
|
|
$startPos = 0;
|
|
$quoteChar = '';
|
|
$literal = '';
|
|
$mapping = [];
|
|
$replace = [];
|
|
$matches = [];
|
|
$pattern = '/([:][a-zA-Z0-9_]+)/';
|
|
|
|
if (!preg_match($pattern, $sql, $matches)) {
|
|
return $sql;
|
|
}
|
|
|
|
$sql = trim($sql);
|
|
$n = \strlen($sql);
|
|
|
|
while ($startPos < $n) {
|
|
if (!preg_match($pattern, $sql, $matches, 0, $startPos)) {
|
|
break;
|
|
}
|
|
|
|
$j = strpos($sql, "'", $startPos);
|
|
$k = strpos($sql, '"', $startPos);
|
|
|
|
if (($k !== false) && (($k < $j) || ($j === false))) {
|
|
$quoteChar = '"';
|
|
$j = $k;
|
|
} else {
|
|
$quoteChar = "'";
|
|
}
|
|
|
|
if ($j === false) {
|
|
$j = $n;
|
|
}
|
|
|
|
// Search for named prepared parameters and replace it with ? and save its position
|
|
$substring = substr($sql, $startPos, $j - $startPos);
|
|
|
|
if (preg_match_all($pattern, $substring, $matches, PREG_PATTERN_ORDER + PREG_OFFSET_CAPTURE)) {
|
|
foreach ($matches[0] as $i => $match) {
|
|
if ($i === 0) {
|
|
$literal .= substr($substring, 0, $match[1]);
|
|
}
|
|
|
|
$mapping[$match[0]] = \count($mapping);
|
|
$endOfPlaceholder = $match[1] + strlen($match[0]);
|
|
$beginOfNextPlaceholder = $matches[0][$i + 1][1] ?? strlen($substring);
|
|
$beginOfNextPlaceholder -= $endOfPlaceholder;
|
|
$literal .= '?' . substr($substring, $endOfPlaceholder, $beginOfNextPlaceholder);
|
|
}
|
|
} else {
|
|
$literal .= $substring;
|
|
}
|
|
|
|
$startPos = $j;
|
|
$j++;
|
|
|
|
if ($j >= $n) {
|
|
break;
|
|
}
|
|
|
|
// Quote comes first, find end of quote
|
|
while (true) {
|
|
$k = strpos($sql, $quoteChar, $j);
|
|
$escaped = false;
|
|
|
|
if ($k === false) {
|
|
break;
|
|
}
|
|
|
|
$l = $k - 1;
|
|
|
|
while ($l >= 0 && $sql[$l] === '\\') {
|
|
$l--;
|
|
$escaped = !$escaped;
|
|
}
|
|
|
|
if ($escaped) {
|
|
$j = $k + 1;
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if ($k === false) {
|
|
// Error in the query - no end quote; ignore it
|
|
break;
|
|
}
|
|
|
|
$literal .= substr($sql, $startPos, $k - $startPos + 1);
|
|
$startPos = $k + 1;
|
|
}
|
|
|
|
if ($startPos < $n) {
|
|
$literal .= substr($sql, $startPos, $n - $startPos);
|
|
}
|
|
|
|
$this->parameterKeyMapping = $mapping;
|
|
|
|
return $literal;
|
|
}
|
|
|
|
/**
|
|
* Binds a parameter to the specified variable name.
|
|
*
|
|
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
|
|
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
|
|
* the 1-indexed position of the parameter.
|
|
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
|
|
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
|
|
* @param ?integer $length The length of the variable. Usually required for OUTPUT parameters.
|
|
* @param ?array $driverOptions Optional driver options to be used.
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null)
|
|
{
|
|
$this->bindedValues[$parameter] =& $variable;
|
|
|
|
// Validate parameter type
|
|
if (!isset($this->parameterTypeMapping[$dataType])) {
|
|
throw new \InvalidArgumentException(sprintf('Unsupported parameter type `%s`', $dataType));
|
|
}
|
|
|
|
$this->typesKeyMapping[$parameter] = $this->parameterTypeMapping[$dataType];
|
|
|
|
$this->statement = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Binds a value to the specified variable.
|
|
*
|
|
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
|
|
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
|
|
* the 1-indexed position of the parameter.
|
|
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
|
|
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
private function bindValue($parameter, $variable, $dataType = ParameterType::STRING)
|
|
{
|
|
$this->bindedValues[$parameter] = $variable;
|
|
$this->typesKeyMapping[$parameter] = $dataType;
|
|
}
|
|
|
|
/**
|
|
* Closes the cursor, enabling the statement to be executed again.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function closeCursor(): void
|
|
{
|
|
if (!$this->result || !\is_resource($this->statement)) {
|
|
return;
|
|
}
|
|
|
|
// Emulate freeing the result fetching and discarding rows, similarly to what PDO does in this case
|
|
while (sqlsrv_fetch($this->statement)) {
|
|
// Do nothing (see above)
|
|
}
|
|
|
|
$this->result = false;
|
|
}
|
|
|
|
/**
|
|
* Fetches the SQLSTATE associated with the last operation on the statement handle.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function errorCode()
|
|
{
|
|
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
|
|
|
|
if ($errors) {
|
|
return $errors[0]['code'];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Fetches extended error information associated with the last operation on the statement handle.
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function errorInfo()
|
|
{
|
|
return sqlsrv_errors(SQLSRV_ERR_ERRORS);
|
|
}
|
|
|
|
/**
|
|
* Executes a prepared statement
|
|
*
|
|
* @param array|null $parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed.
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function execute(?array $parameters = null)
|
|
{
|
|
if (empty($this->bindedValues) && $parameters !== null) {
|
|
$hasZeroIndex = array_key_exists(0, $parameters);
|
|
|
|
foreach ($parameters as $key => $val) {
|
|
$key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
|
|
$this->bindValue($key, $val);
|
|
}
|
|
}
|
|
|
|
if (!$this->statement) {
|
|
$this->statement = $this->prepare();
|
|
}
|
|
|
|
if (!sqlsrv_execute($this->statement)) {
|
|
$errors = $this->errorInfo();
|
|
|
|
throw new ExecutionFailureException($this->query, $errors[0]['message'], $errors[0]['code']);
|
|
}
|
|
|
|
$this->result = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Fetches the next row from a result set
|
|
*
|
|
* @param integer|null $fetchStyle Controls how the next row will be returned to the caller. This value must be one of the
|
|
* FetchMode constants, defaulting to value of FetchMode::MIXED.
|
|
* @param integer $cursorOrientation For a StatementInterface object representing a scrollable cursor, this value determines which row
|
|
* will be returned to the caller. This value must be one of the FetchOrientation constants,
|
|
* defaulting to FetchOrientation::NEXT.
|
|
* @param integer $cursorOffset For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
|
|
* parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
|
|
* the result set that shall be fetched. For a StatementInterface object representing a scrollable
|
|
* cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
|
|
* specifies the row to fetch relative to the cursor position before `fetch()` was called.
|
|
*
|
|
* @return mixed The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0)
|
|
{
|
|
if (!$this->result) {
|
|
return false;
|
|
}
|
|
|
|
$fetchStyle = $fetchStyle ?: $this->defaultFetchStyle;
|
|
|
|
if ($fetchStyle === FetchMode::COLUMN) {
|
|
return $this->fetchColumn();
|
|
}
|
|
|
|
if (isset($this->fetchMap[$fetchStyle])) {
|
|
return sqlsrv_fetch_array($this->statement, $this->fetchMap[$fetchStyle]) ?: false;
|
|
}
|
|
|
|
if (\in_array($fetchStyle, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
|
|
return sqlsrv_fetch_object($this->statement, $this->defaultObjectClass) ?: false;
|
|
}
|
|
|
|
throw new \InvalidArgumentException("Unknown fetch type '{$fetchStyle}'");
|
|
}
|
|
|
|
/**
|
|
* Returns a single column from the next row of a result set
|
|
*
|
|
* @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row.
|
|
* If no value is supplied, the first column is retrieved.
|
|
*
|
|
* @return mixed Returns a single column from the next row of a result set or boolean false if there are no more rows.
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function fetchColumn($columnIndex = 0)
|
|
{
|
|
$row = $this->fetch(FetchMode::NUMERIC);
|
|
|
|
if ($row === false) {
|
|
return false;
|
|
}
|
|
|
|
return $row[$columnIndex] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Prepares the SQL Server statement resource for execution
|
|
*
|
|
* @return resource
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
private function prepare()
|
|
{
|
|
$params = [];
|
|
$options = [];
|
|
|
|
foreach ($this->bindedValues as $key => &$value) {
|
|
$variable = [
|
|
&$value,
|
|
SQLSRV_PARAM_IN,
|
|
];
|
|
|
|
if ($this->typesKeyMapping[$key] === $this->parameterTypeMapping[ParameterType::LARGE_OBJECT]) {
|
|
$variable[] = $this->typesKeyMapping[$key];
|
|
$variable[] = SQLSRV_SQLTYPE_VARBINARY('max');
|
|
}
|
|
|
|
if (isset($this->parameterKeyMapping[$key])) {
|
|
$params[$this->parameterKeyMapping[$key]] = $variable;
|
|
} else {
|
|
$params[] = $variable;
|
|
}
|
|
}
|
|
|
|
// Cleanup referenced variable
|
|
unset($value);
|
|
|
|
// SQLSRV Function sqlsrv_num_rows requires a static or keyset cursor.
|
|
if (strncmp(strtoupper(ltrim($this->query)), 'SELECT', \strlen('SELECT')) === 0) {
|
|
$options = ['Scrollable' => SQLSRV_CURSOR_KEYSET];
|
|
}
|
|
|
|
$statement = sqlsrv_prepare($this->connection, $this->query, $params, $options);
|
|
|
|
if (!$statement) {
|
|
$errors = $this->errorInfo();
|
|
|
|
throw new PrepareStatementFailureException($errors[0]['message'], $errors[0]['code']);
|
|
}
|
|
|
|
return $statement;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of rows affected by the last SQL statement.
|
|
*
|
|
* @return integer
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function rowCount(): int
|
|
{
|
|
if (strncmp(strtoupper(ltrim($this->query)), 'SELECT', \strlen('SELECT')) === 0) {
|
|
return sqlsrv_num_rows($this->statement);
|
|
}
|
|
|
|
return sqlsrv_rows_affected($this->statement);
|
|
}
|
|
|
|
/**
|
|
* Sets the fetch mode to use while iterating this statement.
|
|
*
|
|
* @param integer $fetchMode The fetch mode, must be one of the FetchMode constants.
|
|
* @param mixed ...$args Optional mode-specific arguments.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
public function setFetchMode(int $fetchMode, ...$args): void
|
|
{
|
|
$this->defaultFetchStyle = $fetchMode;
|
|
|
|
if (isset($args[0])) {
|
|
$this->defaultObjectClass = $args[0];
|
|
}
|
|
}
|
|
}
|