2013-11-16 02:06:01 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This is the MS SQL Server Native database abstraction layer.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
* @ingroup Database
|
|
|
|
|
* @author Joel Penner <a-joelpe at microsoft dot com>
|
|
|
|
|
* @author Chris Pucci <a-cpucci at microsoft dot com>
|
|
|
|
|
* @author Ryan Biesemeyer <v-ryanbi at microsoft dot com>
|
2014-01-03 03:04:26 +00:00
|
|
|
* @author Ryan Schmidt <skizzerz at gmail dot com>
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2017-02-07 04:49:57 +00:00
|
|
|
|
|
|
|
|
namespace Wikimedia\Rdbms;
|
|
|
|
|
|
2018-02-10 07:52:26 +00:00
|
|
|
use Wikimedia;
|
2017-02-07 04:49:57 +00:00
|
|
|
use Exception;
|
|
|
|
|
use stdClass;
|
2013-11-16 02:06:01 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @ingroup Database
|
|
|
|
|
*/
|
2016-12-02 19:18:27 +00:00
|
|
|
class DatabaseMssql extends Database {
|
2018-02-16 19:23:30 +00:00
|
|
|
/** @var int */
|
|
|
|
|
protected $serverPort;
|
|
|
|
|
/** @var bool */
|
|
|
|
|
protected $useWindowsAuth = false;
|
|
|
|
|
/** @var int|null */
|
|
|
|
|
protected $lastInsertId = null;
|
|
|
|
|
/** @var int|null */
|
|
|
|
|
protected $lastAffectedRowCount = null;
|
|
|
|
|
/** @var int */
|
|
|
|
|
protected $subqueryId = 0;
|
|
|
|
|
/** @var bool */
|
|
|
|
|
protected $scrollableCursor = true;
|
|
|
|
|
/** @var bool */
|
|
|
|
|
protected $prepareStatements = true;
|
|
|
|
|
/** @var stdClass[][]|null */
|
|
|
|
|
protected $binaryColumnCache = null;
|
|
|
|
|
/** @var stdClass[][]|null */
|
|
|
|
|
protected $bitColumnCache = null;
|
|
|
|
|
/** @var bool */
|
|
|
|
|
protected $ignoreDupKeyErrors = false;
|
|
|
|
|
/** @var string[] */
|
|
|
|
|
protected $ignoreErrors = [];
|
2013-12-27 00:45:19 +00:00
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function implicitGroupby() {
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function implicitOrderby() {
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function unionSupportsOrderAndLimit() {
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-09 22:30:05 +00:00
|
|
|
public function __construct( array $params ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->serverPort = $params['port'];
|
|
|
|
|
$this->useWindowsAuth = $params['UseWindowsAuth'];
|
2017-02-09 22:30:05 +00:00
|
|
|
|
|
|
|
|
parent::__construct( $params );
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
# Test for driver support, to avoid suppressed fatal error
|
|
|
|
|
if ( !function_exists( 'sqlsrv_connect' ) ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
throw new DBConnectionError(
|
|
|
|
|
$this,
|
2014-01-03 03:04:26 +00:00
|
|
|
"Microsoft SQL Server Native (sqlsrv) functions missing.
|
|
|
|
|
You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n"
|
|
|
|
|
);
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-11-20 10:13:51 +00:00
|
|
|
# e.g. the class is being loaded
|
|
|
|
|
if ( !strlen( $user ) ) {
|
2013-12-27 16:59:39 +00:00
|
|
|
return null;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->close();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->server = $server;
|
|
|
|
|
$this->user = $user;
|
|
|
|
|
$this->password = $password;
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$connectionInfo = [];
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
if ( $dbName != '' ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$connectionInfo['Database'] = $dbName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decide which auth scenerio to use
|
2017-01-19 00:54:59 +00:00
|
|
|
// if we are using Windows auth, then don't add credentials to $connectionInfo
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( !$this->useWindowsAuth ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$connectionInfo['UID'] = $user;
|
|
|
|
|
$connectionInfo['PWD'] = $password;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->conn = sqlsrv_connect( $server, $connectionInfo );
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->conn === false ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
throw new DBConnectionError( $this, $this->lastError() );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
$this->opened = true;
|
2018-08-14 23:44:41 +00:00
|
|
|
$this->currentDomain = new DatabaseDomain(
|
|
|
|
|
( $dbName != '' ) ? $dbName : null,
|
|
|
|
|
null,
|
|
|
|
|
$tablePrefix
|
|
|
|
|
);
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2018-08-22 01:34:51 +00:00
|
|
|
return (bool)$this->conn;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Closes a database connection, if it is open
|
|
|
|
|
* Returns success, true if already closed
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
protected function closeConnection() {
|
2018-02-13 06:58:57 +00:00
|
|
|
return sqlsrv_close( $this->conn );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param bool|MssqlResultWrapper|resource $result
|
|
|
|
|
* @return bool|MssqlResultWrapper
|
|
|
|
|
*/
|
2015-09-26 10:31:05 +00:00
|
|
|
protected function resultObject( $result ) {
|
|
|
|
|
if ( !$result ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
return false;
|
|
|
|
|
} elseif ( $result instanceof MssqlResultWrapper ) {
|
|
|
|
|
return $result;
|
|
|
|
|
} elseif ( $result === true ) {
|
|
|
|
|
// Successful write query
|
|
|
|
|
return $result;
|
|
|
|
|
} else {
|
|
|
|
|
return new MssqlResultWrapper( $this, $result );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $sql
|
2017-02-09 22:30:05 +00:00
|
|
|
* @return bool|MssqlResultWrapper|resource
|
2013-12-27 01:54:51 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
*/
|
2013-11-16 02:06:01 +00:00
|
|
|
protected function doQuery( $sql ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
// several extensions seem to think that all databases support limits
|
2017-01-19 00:54:59 +00:00
|
|
|
// via LIMIT N after the WHERE clause, but MSSQL uses SELECT TOP N,
|
2013-11-20 10:13:51 +00:00
|
|
|
// so to catch any of those extensions we'll do a quick check for a
|
|
|
|
|
// LIMIT clause and pass $sql through $this->LimitToTopN() which parses
|
2017-01-19 00:54:59 +00:00
|
|
|
// the LIMIT clause and passes the result to $this->limitResult();
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) {
|
|
|
|
|
// massage LIMIT -> TopN
|
|
|
|
|
$sql = $this->LimitToTopN( $sql );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MSSQL doesn't have EXTRACT(epoch FROM XXX)
|
|
|
|
|
if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
|
|
|
|
|
// This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
|
|
|
|
|
$sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// perform query
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
// SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is
|
|
|
|
|
// needed if we want to be able to seek around the result set), however CLIENT_BUFFERED
|
|
|
|
|
// has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty
|
|
|
|
|
// strings make php throw a fatal error "Severe error translating Unicode"
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->scrollableCursor ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
|
2014-01-03 03:04:26 +00:00
|
|
|
} else {
|
2016-02-17 09:09:32 +00:00
|
|
|
$scrollArr = [];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->prepareStatements ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
// we do prepare + execute so we can get its field metadata for later usage if desired
|
2018-02-13 06:58:57 +00:00
|
|
|
$stmt = sqlsrv_prepare( $this->conn, $sql, [], $scrollArr );
|
2014-01-03 03:04:26 +00:00
|
|
|
$success = sqlsrv_execute( $stmt );
|
2013-11-16 02:06:01 +00:00
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$stmt = sqlsrv_query( $this->conn, $sql, [], $scrollArr );
|
2014-01-03 03:04:26 +00:00
|
|
|
$success = (bool)$stmt;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2017-01-19 00:54:59 +00:00
|
|
|
// Make a copy to ensure what we add below does not get reflected in future queries
|
2018-02-16 19:23:30 +00:00
|
|
|
$ignoreErrors = $this->ignoreErrors;
|
2016-04-25 01:58:24 +00:00
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->ignoreDupKeyErrors ) {
|
2016-04-25 01:58:24 +00:00
|
|
|
// ignore duplicate key errors
|
2014-01-03 03:04:26 +00:00
|
|
|
// this emulates INSERT IGNORE in MySQL
|
2016-04-25 01:58:24 +00:00
|
|
|
$ignoreErrors[] = '2601'; // duplicate key error caused by unique index
|
|
|
|
|
$ignoreErrors[] = '2627'; // duplicate key error caused by primary key
|
|
|
|
|
$ignoreErrors[] = '3621'; // generic "the statement has been terminated" error
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2016-04-25 01:58:24 +00:00
|
|
|
if ( $success === false ) {
|
|
|
|
|
$errors = sqlsrv_errors();
|
|
|
|
|
$success = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2016-04-25 01:58:24 +00:00
|
|
|
foreach ( $errors as $err ) {
|
|
|
|
|
if ( !in_array( $err['code'], $ignoreErrors ) ) {
|
|
|
|
|
$success = false;
|
|
|
|
|
break;
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-04-25 01:58:24 +00:00
|
|
|
if ( $success === false ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
// remember number of rows affected
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->lastAffectedRowCount = sqlsrv_rows_affected( $stmt );
|
2014-01-03 03:04:26 +00:00
|
|
|
|
|
|
|
|
return $stmt;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function freeResult( $res ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
sqlsrv_free_stmt( $res );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
2018-08-22 03:14:26 +00:00
|
|
|
* @param IResultWrapper $res
|
2014-01-03 03:04:26 +00:00
|
|
|
* @return stdClass
|
2013-12-27 01:54:51 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function fetchObject( $res ) {
|
|
|
|
|
// $res is expected to be an instance of MssqlResultWrapper here
|
|
|
|
|
return $res->fetchObject();
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
2018-08-22 03:14:26 +00:00
|
|
|
* @param IResultWrapper $res
|
2014-01-03 03:04:26 +00:00
|
|
|
* @return array
|
2013-12-27 01:54:51 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function fetchRow( $res ) {
|
|
|
|
|
return $res->fetchRow();
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* @param mixed $res
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function numRows( $res ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2016-04-25 01:58:24 +00:00
|
|
|
$ret = sqlsrv_num_rows( $res );
|
|
|
|
|
|
|
|
|
|
if ( $ret === false ) {
|
|
|
|
|
// we cannot get an amount of rows from this cursor type
|
|
|
|
|
// has_rows returns bool true/false if the result has rows
|
|
|
|
|
$ret = (int)sqlsrv_has_rows( $res );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param mixed $res
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
public function numFields( $res ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
return sqlsrv_num_fields( $res );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param mixed $res
|
|
|
|
|
* @param int $n
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
public function fieldName( $res, $n ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2016-02-17 19:54:59 +00:00
|
|
|
return sqlsrv_field_metadata( $res )[$n]['Name'];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This must be called after nextSequenceVal
|
2013-12-27 00:45:19 +00:00
|
|
|
* @return int|null
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function insertId() {
|
2018-02-16 19:23:30 +00:00
|
|
|
return $this->lastInsertId;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
2014-01-03 03:04:26 +00:00
|
|
|
* @param MssqlResultWrapper $res
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param int $row
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function dataSeek( $res, $row ) {
|
|
|
|
|
return $res->seek( $row );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function lastError() {
|
|
|
|
|
$strRet = '';
|
|
|
|
|
$retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
|
|
|
|
|
if ( $retErrors != null ) {
|
|
|
|
|
foreach ( $retErrors as $arrError ) {
|
|
|
|
|
$strRet .= $this->formatError( $arrError ) . "\n";
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
$strRet = "No errors found";
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
|
|
|
|
|
return $strRet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-08-14 18:22:52 +00:00
|
|
|
* @param array $err
|
2014-01-03 03:04:26 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function formatError( $err ) {
|
2017-02-09 22:30:05 +00:00
|
|
|
return '[SQLSTATE ' .
|
|
|
|
|
$err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message'];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
2016-12-08 05:04:53 +00:00
|
|
|
* @return string|int
|
2014-01-03 03:04:26 +00:00
|
|
|
*/
|
|
|
|
|
public function lastErrno() {
|
2013-11-16 02:06:01 +00:00
|
|
|
$err = sqlsrv_errors( SQLSRV_ERR_ALL );
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( $err !== null && isset( $err[0] ) ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
return $err[0]['code'];
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
protected function wasKnownStatementRollbackError() {
|
|
|
|
|
$errors = sqlsrv_errors( SQLSRV_ERR_ALL );
|
|
|
|
|
if ( !$errors ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// The transaction vs statement rollback behavior depends on XACT_ABORT, so make sure
|
|
|
|
|
// that the "statement has been terminated" error (3621) is specifically present.
|
|
|
|
|
// https://docs.microsoft.com/en-us/sql/t-sql/statements/set-xact-abort-transact-sql
|
|
|
|
|
$statementOnly = false;
|
|
|
|
|
$codeWhitelist = [ '2601', '2627', '547' ];
|
|
|
|
|
foreach ( $errors as $error ) {
|
|
|
|
|
if ( $error['code'] == '3621' ) {
|
|
|
|
|
$statementOnly = true;
|
|
|
|
|
} elseif ( !in_array( $error['code'], $codeWhitelist ) ) {
|
|
|
|
|
$statementOnly = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $statementOnly;
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2018-01-28 14:10:39 +00:00
|
|
|
protected function fetchAffectedRowCount() {
|
2018-02-16 19:23:30 +00:00
|
|
|
return $this->lastAffectedRowCount;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SELECT wrapper
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param mixed $table Array or string, table name(s) (prefix auto-added)
|
|
|
|
|
* @param mixed $vars Array or string, field name(s) to be retrieved
|
|
|
|
|
* @param mixed $conds Array or string, condition(s) for WHERE
|
|
|
|
|
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
|
|
|
|
|
* @param array $options Associative array of options (e.g.
|
2016-08-07 10:27:38 +00:00
|
|
|
* [ 'GROUP BY' => 'page_title' ]), see Database::makeSelectOptions
|
2013-11-20 10:13:51 +00:00
|
|
|
* code for list of supported stuff
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param array $join_conds Associative array of table join conditions
|
2016-08-07 10:27:38 +00:00
|
|
|
* (optional) (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ]
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return mixed Database result resource (feed to Database::fetchObject
|
2013-11-20 10:13:51 +00:00
|
|
|
* or whatever), or false on failure
|
2014-12-24 13:49:20 +00:00
|
|
|
* @throws DBQueryError
|
|
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
* @throws Exception
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function select( $table, $vars, $conds = '', $fname = __METHOD__,
|
2016-02-17 09:09:32 +00:00
|
|
|
$options = [], $join_conds = []
|
2013-11-20 10:13:51 +00:00
|
|
|
) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
|
|
|
|
|
if ( isset( $options['EXPLAIN'] ) ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
|
|
|
|
$this->prepareStatements = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
$this->query( "SET SHOWPLAN_ALL ON" );
|
|
|
|
|
$ret = $this->query( $sql, $fname );
|
|
|
|
|
$this->query( "SET SHOWPLAN_ALL OFF" );
|
|
|
|
|
} catch ( DBQueryError $dqe ) {
|
|
|
|
|
if ( isset( $options['FOR COUNT'] ) ) {
|
|
|
|
|
// likely don't have privs for SHOWPLAN, so run a select count instead
|
|
|
|
|
$this->query( "SET SHOWPLAN_ALL OFF" );
|
|
|
|
|
unset( $options['EXPLAIN'] );
|
|
|
|
|
$ret = $this->select(
|
|
|
|
|
$table,
|
|
|
|
|
'COUNT(*) AS EstimateRows',
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
|
|
|
|
$options,
|
|
|
|
|
$join_conds
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// someone actually wanted the query plan instead of an est row count
|
|
|
|
|
// let them know of the error
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
|
|
|
|
$this->prepareStatements = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $dqe;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
|
|
|
|
$this->prepareStatements = true;
|
2013-11-16 02:06:01 +00:00
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
return $this->query( $sql, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SELECT wrapper
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param mixed $table Array or string, table name(s) (prefix auto-added)
|
|
|
|
|
* @param mixed $vars Array or string, field name(s) to be retrieved
|
|
|
|
|
* @param mixed $conds Array or string, condition(s) for WHERE
|
|
|
|
|
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
|
2016-08-07 10:27:38 +00:00
|
|
|
* @param array $options Associative array of options (e.g. [ 'GROUP BY' => 'page_title' ]),
|
2013-12-27 01:54:51 +00:00
|
|
|
* see Database::makeSelectOptions code for list of supported stuff
|
|
|
|
|
* @param array $join_conds Associative array of table join conditions (optional)
|
2016-08-07 10:27:38 +00:00
|
|
|
* (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ]
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return string The SQL text
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
|
2016-02-17 09:09:32 +00:00
|
|
|
$options = [], $join_conds = []
|
2013-11-20 10:13:51 +00:00
|
|
|
) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( isset( $options['EXPLAIN'] ) ) {
|
|
|
|
|
unset( $options['EXPLAIN'] );
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
$sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
|
|
|
|
|
|
|
|
|
|
// try to rewrite aggregations of bit columns (currently MAX and MIN)
|
|
|
|
|
if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$bitColumns = [];
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( is_array( $table ) ) {
|
Database: Support parenthesized JOINs
SQL supports parentheses for grouping in the FROM clause.[1] This is
useful when you want to left-join against a join of other tables.
For example, say you have tables 'a', 'b', and 'c'. You want all rows
from 'a', along with rows from 'b' + 'c' only where both of those
exist.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) JOIN c ON (b_c = c_id)
doesn't work, it'll only give you the rows where 'c' exists.
SELECT * FROM a LEFT JOIN b ON (a_b = b_id) LEFT JOIN c ON (b_c = c_id)
doesn't work either, it'll give you rows from 'b' without a
corresponding row in 'c'. What you need to do is
SELECT * FROM a LEFT JOIN (b JOIN c ON (b_c = c_id)) ON (a_b = b_id)
This patch implements this by extending the syntax for the $table
parameter to IDatabase::select(). When passing an array of tables, if a
value in the array is itself an array that is interpreted as a request
for a parenthesized join. To produce the example above, you'd do
something like
$db->select(
[ 'a', 'nest' => [ 'b', 'c' ] ],
'*',
[],
__METHOD__,
[],
[
'c' => [ 'JOIN', 'b_c = c_id ],
'nest' => [ 'LEFT JOIN', 'a_b = b_id' ],
]
);
[1]: In standards as far back as SQL-1992 (I couldn't find an earlier
version), and it seems to be supported by at least MySQL 5.6, MariaDB
10.1.28, PostgreSQL 9.3, PostgreSQL 10.0, Oracle 11g R2, SQLite 3.20.1,
and MSSQL 2014 (from local testing and sqlfiddle.com).
Change-Id: I1e0a77381e06d885650a94f53847fb82f01c2694
2017-10-13 17:51:43 +00:00
|
|
|
$tables = $table;
|
|
|
|
|
while ( $tables ) {
|
|
|
|
|
$t = array_pop( $tables );
|
|
|
|
|
if ( is_array( $t ) ) {
|
|
|
|
|
$tables = array_merge( $tables, $t );
|
|
|
|
|
} else {
|
|
|
|
|
$bitColumns += $this->getBitColumns( $this->tableName( $t ) );
|
|
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$bitColumns = $this->getBitColumns( $this->tableName( $table ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $bitColumns as $col => $info ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$replace = [
|
2014-01-03 03:04:26 +00:00
|
|
|
"MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
|
|
|
|
|
"MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2014-01-03 03:04:26 +00:00
|
|
|
$sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
|
|
|
|
|
$fname = __METHOD__
|
|
|
|
|
) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
|
|
|
|
parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname );
|
2014-03-20 18:59:20 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function delete( $table, $conds, $fname = __METHOD__ ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
|
|
|
|
parent::delete( $table, $conds, $fname );
|
2014-03-20 18:59:20 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2018-10-26 20:17:34 +00:00
|
|
|
|
|
|
|
|
return true;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Estimate rows in dataset
|
|
|
|
|
* Returns estimated count, based on SHOWPLAN_ALL output
|
|
|
|
|
* This is not necessarily an accurate estimate, so use sparingly
|
|
|
|
|
* Returns -1 if count cannot be found
|
|
|
|
|
* Takes same arguments as Database::select()
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
2018-02-15 03:46:04 +00:00
|
|
|
* @param string $var
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param array $options
|
2018-03-12 16:15:14 +00:00
|
|
|
* @param array $join_conds
|
2013-11-16 02:06:01 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
2018-02-15 03:46:04 +00:00
|
|
|
public function estimateRowCount( $table, $var = '*', $conds = '',
|
2018-03-12 16:15:14 +00:00
|
|
|
$fname = __METHOD__, $options = [], $join_conds = []
|
2013-11-20 10:13:51 +00:00
|
|
|
) {
|
2018-02-15 03:46:04 +00:00
|
|
|
$conds = $this->normalizeConditions( $conds, $fname );
|
|
|
|
|
$column = $this->extractSingleFieldFromList( $var );
|
|
|
|
|
if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
|
|
|
|
|
$conds[] = "$column IS NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-20 06:58:22 +00:00
|
|
|
// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
|
|
|
|
|
$options['EXPLAIN'] = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
$options['FOR COUNT'] = true;
|
2018-02-15 03:46:04 +00:00
|
|
|
$res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
|
2013-11-16 02:06:01 +00:00
|
|
|
|
|
|
|
|
$rows = -1;
|
|
|
|
|
if ( $res ) {
|
|
|
|
|
$row = $this->fetchRow( $res );
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( isset( $row['EstimateRows'] ) ) {
|
2015-01-22 15:36:18 +00:00
|
|
|
$rows = (int)$row['EstimateRows'];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns information about an index
|
|
|
|
|
* If errors are explicitly ignored, returns NULL on failure
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @param string $fname
|
2013-11-16 02:06:01 +00:00
|
|
|
* @return array|bool|null
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function indexInfo( $table, $index, $fname = __METHOD__ ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
# This does not return the same info as MYSQL would, but that's OK
|
|
|
|
|
# because MediaWiki never uses the returned value except to check for
|
2017-01-19 00:54:59 +00:00
|
|
|
# the existence of indexes.
|
2016-04-25 01:58:24 +00:00
|
|
|
$sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
|
2013-11-16 02:06:01 +00:00
|
|
|
$res = $this->query( $sql, $fname );
|
2016-04-25 01:58:24 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( !$res ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$result = [];
|
2013-11-16 02:06:01 +00:00
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
if ( $row->index_name == $index ) {
|
|
|
|
|
$row->Non_unique = !stristr( $row->index_description, "unique" );
|
|
|
|
|
$cols = explode( ", ", $row->index_keys );
|
|
|
|
|
foreach ( $cols as $col ) {
|
|
|
|
|
$row->Column_name = trim( $col );
|
|
|
|
|
$result[] = clone $row;
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) {
|
|
|
|
|
$row->Non_unique = 0;
|
|
|
|
|
$cols = explode( ", ", $row->index_keys );
|
|
|
|
|
foreach ( $cols as $col ) {
|
|
|
|
|
$row->Column_name = trim( $col );
|
|
|
|
|
$result[] = clone $row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2018-03-02 04:30:07 +00:00
|
|
|
return $result ?: false;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* INSERT wrapper, inserts an array into a table
|
|
|
|
|
*
|
|
|
|
|
* $arrToInsert may be a single associative array, or an array of these with numeric keys, for
|
|
|
|
|
* multi-row insert.
|
|
|
|
|
*
|
|
|
|
|
* Usually aborts on failure
|
|
|
|
|
* If errors are explicitly ignored, returns success
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @param array $arrToInsert
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param array $options
|
|
|
|
|
* @return bool
|
2014-12-24 13:49:20 +00:00
|
|
|
* @throws Exception
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2016-02-17 09:09:32 +00:00
|
|
|
public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
# No rows to insert, easy just return now
|
|
|
|
|
if ( !count( $arrToInsert ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !is_array( $options ) ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$options = [ $options ];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
|
2013-11-20 06:58:22 +00:00
|
|
|
if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
|
2016-02-17 09:09:32 +00:00
|
|
|
$arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We know the table we're inserting into, get its identity column
|
|
|
|
|
$identity = null;
|
2014-01-03 03:04:26 +00:00
|
|
|
// strip matching square brackets and the db/schema from table name
|
|
|
|
|
$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
|
|
|
|
|
$tableRaw = array_pop( $tableRawArr );
|
2013-11-20 10:13:51 +00:00
|
|
|
$res = $this->doQuery(
|
|
|
|
|
"SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " .
|
|
|
|
|
"WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'"
|
|
|
|
|
);
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( $res && sqlsrv_has_rows( $res ) ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
// There is an identity for this table.
|
2014-01-03 03:04:26 +00:00
|
|
|
$identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC );
|
|
|
|
|
$identity = array_pop( $identityArr );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
sqlsrv_free_stmt( $res );
|
|
|
|
|
|
|
|
|
|
// Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF
|
|
|
|
|
$binaryColumns = $this->getBinaryColumns( $table );
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2014-09-18 20:36:46 +00:00
|
|
|
// INSERT IGNORE is not supported by SQL Server
|
|
|
|
|
// remove IGNORE from options list and set ignore flag to true
|
|
|
|
|
if ( in_array( 'IGNORE', $options ) ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$options = array_diff( $options, [ 'IGNORE' ] );
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->ignoreDupKeyErrors = true;
|
2014-09-18 20:36:46 +00:00
|
|
|
}
|
|
|
|
|
|
2017-02-09 22:30:05 +00:00
|
|
|
$ret = null;
|
2013-11-16 02:06:01 +00:00
|
|
|
foreach ( $arrToInsert as $a ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
// start out with empty identity column, this is so we can return
|
2017-01-19 00:54:59 +00:00
|
|
|
// it as a result of the INSERT logic
|
2013-11-16 02:06:01 +00:00
|
|
|
$sqlPre = '';
|
|
|
|
|
$sqlPost = '';
|
|
|
|
|
$identityClause = '';
|
|
|
|
|
|
|
|
|
|
// if we have an identity column
|
|
|
|
|
if ( $identity ) {
|
|
|
|
|
// iterate through
|
|
|
|
|
foreach ( $a as $k => $v ) {
|
|
|
|
|
if ( $k == $identity ) {
|
|
|
|
|
if ( !is_null( $v ) ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
// there is a value being passed to us,
|
|
|
|
|
// we need to turn on and off inserted identity
|
2013-11-16 02:06:01 +00:00
|
|
|
$sqlPre = "SET IDENTITY_INSERT $table ON;";
|
|
|
|
|
$sqlPost = ";SET IDENTITY_INSERT $table OFF;";
|
|
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
// we can't insert NULL into an identity column,
|
|
|
|
|
// so remove the column from the insert.
|
2013-11-16 02:06:01 +00:00
|
|
|
unset( $a[$k] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 10:13:51 +00:00
|
|
|
|
|
|
|
|
// we want to output an identity column as result
|
|
|
|
|
$identityClause = "OUTPUT INSERTED.$identity ";
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$keys = array_keys( $a );
|
|
|
|
|
|
|
|
|
|
// Build the actual query
|
|
|
|
|
$sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) .
|
|
|
|
|
" INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES (";
|
|
|
|
|
|
|
|
|
|
$first = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
foreach ( $a as $key => $value ) {
|
|
|
|
|
if ( isset( $binaryColumns[$key] ) ) {
|
|
|
|
|
$value = new MssqlBlob( $value );
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $first ) {
|
|
|
|
|
$first = false;
|
|
|
|
|
} else {
|
|
|
|
|
$sql .= ',';
|
|
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( is_null( $value ) ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$sql .= 'null';
|
|
|
|
|
} elseif ( is_array( $value ) || is_object( $value ) ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( is_object( $value ) && $value instanceof Blob ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$sql .= $this->addQuotes( $value );
|
|
|
|
|
} else {
|
|
|
|
|
$sql .= $this->addQuotes( serialize( $value ) );
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
$sql .= $this->addQuotes( $value );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$sql .= ')' . $sqlPost;
|
|
|
|
|
|
|
|
|
|
// Run the query
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
|
|
|
|
$ret = $this->query( $sql );
|
|
|
|
|
} catch ( Exception $e ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
|
|
|
|
$this->ignoreDupKeyErrors = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2017-02-09 22:30:05 +00:00
|
|
|
if ( $ret instanceof ResultWrapper && !is_null( $identity ) ) {
|
|
|
|
|
// Then we want to get the identity column value we were assigned and save it off
|
2014-01-03 03:04:26 +00:00
|
|
|
$row = $ret->fetchObject();
|
2014-09-29 18:46:19 +00:00
|
|
|
if ( is_object( $row ) ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->lastInsertId = $row->$identity;
|
2017-02-09 22:30:05 +00:00
|
|
|
// It seems that mAffectedRows is -1 sometimes when OUTPUT INSERTED.identity is
|
|
|
|
|
// used if we got an identity back, we know for sure a row was affected, so
|
|
|
|
|
// adjust that here
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->lastAffectedRowCount == -1 ) {
|
|
|
|
|
$this->lastAffectedRowCount = 1;
|
2016-04-25 01:58:24 +00:00
|
|
|
}
|
2014-09-18 20:36:46 +00:00
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
2017-02-09 22:30:05 +00:00
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->ignoreDupKeyErrors = false;
|
2017-02-09 22:30:05 +00:00
|
|
|
|
2018-10-26 20:17:34 +00:00
|
|
|
return true;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* INSERT SELECT wrapper
|
2016-08-07 10:27:38 +00:00
|
|
|
* $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
|
2013-11-20 10:13:51 +00:00
|
|
|
* Source items may be literals rather than field names, but strings should
|
|
|
|
|
* be quoted with Database::addQuotes().
|
2013-11-16 02:06:01 +00:00
|
|
|
* @param string $destTable
|
2013-11-20 10:13:51 +00:00
|
|
|
* @param array|string $srcTable May be an array of tables.
|
2013-11-16 02:06:01 +00:00
|
|
|
* @param array $varMap
|
2013-11-20 10:13:51 +00:00
|
|
|
* @param array $conds May be "*" to copy the whole table.
|
2013-11-16 02:06:01 +00:00
|
|
|
* @param string $fname
|
|
|
|
|
* @param array $insertOptions
|
|
|
|
|
* @param array $selectOptions
|
2017-06-09 16:58:09 +00:00
|
|
|
* @param array $selectJoinConds
|
2014-12-24 13:49:20 +00:00
|
|
|
* @throws Exception
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2018-10-26 20:17:34 +00:00
|
|
|
protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
|
2017-06-09 16:58:09 +00:00
|
|
|
$insertOptions = [], $selectOptions = [], $selectJoinConds = []
|
2014-01-03 03:04:26 +00:00
|
|
|
) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
2018-10-26 20:17:34 +00:00
|
|
|
parent::nativeInsertSelect(
|
2014-01-03 03:04:26 +00:00
|
|
|
$destTable,
|
|
|
|
|
$srcTable,
|
|
|
|
|
$varMap,
|
|
|
|
|
$conds,
|
|
|
|
|
$fname,
|
|
|
|
|
$insertOptions,
|
2017-06-09 16:58:09 +00:00
|
|
|
$selectOptions,
|
|
|
|
|
$selectJoinConds
|
2014-01-03 03:04:26 +00:00
|
|
|
);
|
2014-03-20 18:59:20 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $e;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-01-03 03:04:26 +00:00
|
|
|
* UPDATE wrapper. Takes a condition array and a SET array.
|
|
|
|
|
*
|
2014-07-24 17:42:45 +00:00
|
|
|
* @param string $table Name of the table to UPDATE. This will be passed through
|
2016-09-26 22:40:07 +00:00
|
|
|
* Database::tableName().
|
2014-01-03 03:04:26 +00:00
|
|
|
*
|
|
|
|
|
* @param array $values An array of values to SET. For each array element,
|
|
|
|
|
* the key gives the field name, and the value gives the data
|
|
|
|
|
* to set that field to. The data will be quoted by
|
2016-09-26 22:40:07 +00:00
|
|
|
* Database::addQuotes().
|
2014-01-03 03:04:26 +00:00
|
|
|
*
|
|
|
|
|
* @param array $conds An array of conditions (WHERE). See
|
2016-09-26 22:40:07 +00:00
|
|
|
* Database::select() for the details of the format of
|
2014-01-03 03:04:26 +00:00
|
|
|
* condition arrays. Use '*' to update all rows.
|
|
|
|
|
*
|
|
|
|
|
* @param string $fname The function name of the caller (from __METHOD__),
|
|
|
|
|
* for logging and profiling.
|
|
|
|
|
*
|
|
|
|
|
* @param array $options An array of UPDATE options, can be:
|
|
|
|
|
* - IGNORE: Ignore unique key conflicts
|
|
|
|
|
* - LOW_PRIORITY: MySQL-specific, see MySQL manual.
|
|
|
|
|
* @return bool
|
2014-12-24 13:49:20 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
* @throws Exception
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2016-02-17 09:09:32 +00:00
|
|
|
function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$binaryColumns = $this->getBinaryColumns( $table );
|
2013-11-16 02:06:01 +00:00
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
$opts = $this->makeUpdateOptions( $options );
|
|
|
|
|
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
if ( $conds !== [] && $conds !== '*' ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = false;
|
2014-01-03 03:04:26 +00:00
|
|
|
try {
|
2016-09-26 23:38:15 +00:00
|
|
|
$this->query( $sql );
|
2014-01-03 03:04:26 +00:00
|
|
|
} catch ( Exception $e ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
throw $e;
|
|
|
|
|
}
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = true;
|
2014-01-03 03:04:26 +00:00
|
|
|
return true;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-01-03 03:04:26 +00:00
|
|
|
* Makes an encoded list of strings from an array
|
2014-07-24 17:42:45 +00:00
|
|
|
* @param array $a Containing the data
|
2014-01-03 03:04:26 +00:00
|
|
|
* @param int $mode Constant
|
|
|
|
|
* - LIST_COMMA: comma separated, no field names
|
|
|
|
|
* - LIST_AND: ANDed WHERE clause (without the WHERE). See
|
2016-09-26 22:40:07 +00:00
|
|
|
* the documentation for $conds in Database::select().
|
2014-01-03 03:04:26 +00:00
|
|
|
* - LIST_OR: ORed WHERE clause (without the WHERE)
|
|
|
|
|
* - LIST_SET: comma separated with field names, like a SET clause
|
|
|
|
|
* - LIST_NAMES: comma separated field names
|
|
|
|
|
* @param array $binaryColumns Contains a list of column names that are binary types
|
|
|
|
|
* This is a custom parameter only present for MS SQL.
|
|
|
|
|
*
|
2016-09-15 01:37:35 +00:00
|
|
|
* @throws DBUnexpectedError
|
2014-01-03 03:04:26 +00:00
|
|
|
* @return string
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2016-02-17 09:09:32 +00:00
|
|
|
public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( !is_array( $a ) ) {
|
2016-09-15 02:04:21 +00:00
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2015-01-03 10:06:35 +00:00
|
|
|
if ( $mode != LIST_NAMES ) {
|
|
|
|
|
// In MS SQL, values need to be specially encoded when they are
|
|
|
|
|
// inserted into binary fields. Perform this necessary encoding
|
|
|
|
|
// for the specified set of columns.
|
|
|
|
|
foreach ( array_keys( $a ) as $field ) {
|
|
|
|
|
if ( !isset( $binaryColumns[$field] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2015-01-03 10:06:35 +00:00
|
|
|
if ( is_array( $a[$field] ) ) {
|
|
|
|
|
foreach ( $a[$field] as &$v ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
$v = new MssqlBlob( $v );
|
|
|
|
|
}
|
2015-01-03 10:06:35 +00:00
|
|
|
unset( $v );
|
2014-01-03 03:04:26 +00:00
|
|
|
} else {
|
2015-01-03 10:06:35 +00:00
|
|
|
$a[$field] = new MssqlBlob( $a[$field] );
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2015-01-03 10:06:35 +00:00
|
|
|
return parent::makeList( $a, $mode );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @param string $field
|
|
|
|
|
* @return int Returns the size of a text field, or -1 for "unlimited"
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function textFieldSize( $table, $field ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns
|
|
|
|
|
WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'";
|
|
|
|
|
$res = $this->query( $sql );
|
|
|
|
|
$row = $this->fetchRow( $res );
|
|
|
|
|
$size = -1;
|
|
|
|
|
if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) {
|
|
|
|
|
$size = $row['CHARACTER_MAXIMUM_LENGTH'];
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Construct a LIMIT query with optional offset
|
|
|
|
|
* This is used for query pages
|
2013-12-27 01:54:51 +00:00
|
|
|
*
|
|
|
|
|
* @param string $sql SQL query we will append the limit too
|
|
|
|
|
* @param int $limit The SQL limit
|
|
|
|
|
* @param bool|int $offset The SQL offset (default false)
|
|
|
|
|
* @return array|string
|
2014-12-24 13:49:20 +00:00
|
|
|
* @throws DBUnexpectedError
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function limitResult( $sql, $limit, $offset = false ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $offset === false || $offset == 0 ) {
|
|
|
|
|
if ( strpos( $sql, "SELECT" ) === false ) {
|
|
|
|
|
return "TOP {$limit} " . $sql;
|
|
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi',
|
|
|
|
|
'SELECT$1 TOP ' . $limit, $sql, 1 );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
// This one is fun, we need to pull out the select list as well as any ORDER BY clause
|
2016-02-17 09:09:32 +00:00
|
|
|
$select = $orderby = [];
|
2014-01-03 03:04:26 +00:00
|
|
|
$s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
|
|
|
|
|
$s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
|
2017-02-09 22:30:05 +00:00
|
|
|
$postOrder = '';
|
2014-01-03 03:04:26 +00:00
|
|
|
$first = $offset + 1;
|
|
|
|
|
$last = $offset + $limit;
|
2018-02-16 19:23:30 +00:00
|
|
|
$sub1 = 'sub_' . $this->subqueryId;
|
|
|
|
|
$sub2 = 'sub_' . ( $this->subqueryId + 1 );
|
|
|
|
|
$this->subqueryId += 2;
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( !$s1 ) {
|
|
|
|
|
// wat
|
|
|
|
|
throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" );
|
|
|
|
|
}
|
|
|
|
|
if ( !$s2 ) {
|
|
|
|
|
// no ORDER BY
|
2014-09-18 20:36:46 +00:00
|
|
|
$overOrder = 'ORDER BY (SELECT 1)';
|
2014-01-03 03:04:26 +00:00
|
|
|
} else {
|
|
|
|
|
if ( !isset( $orderby[2] ) || !$orderby[2] ) {
|
|
|
|
|
// don't need to strip it out if we're using a FOR XML clause
|
|
|
|
|
$sql = str_replace( $orderby[1], '', $sql );
|
|
|
|
|
}
|
|
|
|
|
$overOrder = $orderby[1];
|
|
|
|
|
$postOrder = ' ' . $overOrder;
|
|
|
|
|
}
|
|
|
|
|
$sql = "SELECT {$select[1]}
|
|
|
|
|
FROM (
|
|
|
|
|
SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, *
|
|
|
|
|
FROM ({$sql}) {$sub1}
|
|
|
|
|
) {$sub2}
|
|
|
|
|
WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}";
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* If there is a limit clause, parse it, strip it, and pass the remaining
|
|
|
|
|
* SQL through limitResult() with the appropriate parameters. Not the
|
|
|
|
|
* prettiest solution, but better than building a whole new parser. This
|
|
|
|
|
* exists becase there are still too many extensions that don't use dynamic
|
|
|
|
|
* sql generation.
|
|
|
|
|
*
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @return array|mixed|string
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function LimitToTopN( $sql ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
// Matches: LIMIT {[offset,] row_count | row_count OFFSET offset}
|
|
|
|
|
$pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i';
|
|
|
|
|
if ( preg_match( $pattern, $sql, $matches ) ) {
|
|
|
|
|
$row_count = $matches[4];
|
2014-10-24 14:42:19 +00:00
|
|
|
$offset = $matches[3] ?: $matches[6] ?: false;
|
2013-11-16 02:06:01 +00:00
|
|
|
|
|
|
|
|
// strip the matching LIMIT clause out
|
|
|
|
|
$sql = str_replace( $matches[0], '', $sql );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $this->limitResult( $sql, $row_count, $offset );
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return string Wikitext of a link to the server software's web site
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
|
|
|
|
public function getSoftwareLink() {
|
2014-01-09 01:50:21 +00:00
|
|
|
return "[{{int:version-db-mssql-url}} MS SQL Server]";
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string Version information from the database
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function getServerVersion() {
|
2018-02-13 06:58:57 +00:00
|
|
|
$server_info = sqlsrv_server_info( $this->conn );
|
2018-10-20 21:55:44 +00:00
|
|
|
$version = $server_info['SQLServerVersion'] ?? 'Error';
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return $version;
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2014-04-22 05:46:24 +00:00
|
|
|
public function tableExists( $table, $fname = __METHOD__ ) {
|
|
|
|
|
list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-04-22 05:46:24 +00:00
|
|
|
if ( $db !== false ) {
|
|
|
|
|
// remote database
|
2017-02-09 22:30:05 +00:00
|
|
|
$this->queryLogger->error( "Attempting to call tableExists on a remote table" );
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2014-04-22 05:46:24 +00:00
|
|
|
|
2014-06-27 01:17:05 +00:00
|
|
|
if ( $schema === false ) {
|
2018-08-14 23:44:41 +00:00
|
|
|
$schema = $this->dbSchema();
|
2014-06-27 01:17:05 +00:00
|
|
|
}
|
|
|
|
|
|
2014-04-22 05:46:24 +00:00
|
|
|
$res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES
|
|
|
|
|
WHERE TABLE_TYPE = 'BASE TABLE'
|
|
|
|
|
AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" );
|
|
|
|
|
|
|
|
|
|
if ( $res->numRows() ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Query whether a given column exists in the mediawiki schema
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
|
|
|
|
* @param string $field
|
|
|
|
|
* @param string $fname
|
2013-11-16 02:06:01 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function fieldExists( $table, $field, $fname = __METHOD__ ) {
|
2014-04-22 05:46:24 +00:00
|
|
|
list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-04-22 05:46:24 +00:00
|
|
|
if ( $db !== false ) {
|
|
|
|
|
// remote database
|
2017-02-09 22:30:05 +00:00
|
|
|
$this->queryLogger->error( "Attempting to call fieldExists on a remote table" );
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2014-04-22 05:46:24 +00:00
|
|
|
|
|
|
|
|
$res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
|
|
|
|
WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
|
|
|
|
|
|
|
|
|
|
if ( $res->numRows() ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function fieldInfo( $table, $field ) {
|
2014-04-22 05:46:24 +00:00
|
|
|
list( $db, $schema, $table ) = $this->tableName( $table, 'split' );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2014-04-22 05:46:24 +00:00
|
|
|
if ( $db !== false ) {
|
|
|
|
|
// remote database
|
2017-02-09 22:30:05 +00:00
|
|
|
$this->queryLogger->error( "Attempting to call fieldInfo on a remote table" );
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2014-04-22 05:46:24 +00:00
|
|
|
|
|
|
|
|
$res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
|
|
|
|
WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
|
|
|
|
|
|
|
|
|
|
$meta = $res->fetchRow();
|
2013-11-16 02:06:01 +00:00
|
|
|
if ( $meta ) {
|
|
|
|
|
return new MssqlField( $meta );
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-17 21:59:56 +00:00
|
|
|
protected function doSavepoint( $identifier, $fname ) {
|
|
|
|
|
$this->query( 'SAVE TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doReleaseSavepoint( $identifier, $fname ) {
|
|
|
|
|
// Not supported. Also not really needed, a new doSavepoint() for the
|
|
|
|
|
// same identifier will overwrite the old.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doRollbackToSavepoint( $identifier, $fname ) {
|
|
|
|
|
$this->query( 'ROLLBACK TRANSACTION ' . $this->addIdentifierQuotes( $identifier ), $fname );
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
/**
|
|
|
|
|
* Begin a transaction, committing any previously open transaction
|
2014-08-14 18:22:52 +00:00
|
|
|
* @param string $fname
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
|
|
|
|
protected function doBegin( $fname = __METHOD__ ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
sqlsrv_begin_transaction( $this->conn );
|
|
|
|
|
$this->trxLevel = 1;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* End a transaction
|
2014-08-14 18:22:52 +00:00
|
|
|
* @param string $fname
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
|
|
|
|
protected function doCommit( $fname = __METHOD__ ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
sqlsrv_commit( $this->conn );
|
|
|
|
|
$this->trxLevel = 0;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rollback a transaction.
|
|
|
|
|
* No-op on non-transactional databases.
|
2014-08-14 18:22:52 +00:00
|
|
|
* @param string $fname
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
|
|
|
|
protected function doRollback( $fname = __METHOD__ ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
sqlsrv_rollback( $this->conn );
|
|
|
|
|
$this->trxLevel = 0;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-01-03 03:04:26 +00:00
|
|
|
* @param string $s
|
|
|
|
|
* @return string
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2015-06-17 13:28:51 +00:00
|
|
|
public function strencode( $s ) {
|
|
|
|
|
// Should not be called by us
|
2014-01-03 03:04:26 +00:00
|
|
|
return str_replace( "'", "''", $s );
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-21 18:03:29 +00:00
|
|
|
* @param string|int|null|bool|Blob $s
|
|
|
|
|
* @return string|int
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function addQuotes( $s ) {
|
|
|
|
|
if ( $s instanceof MssqlBlob ) {
|
|
|
|
|
return $s->fetch();
|
|
|
|
|
} elseif ( $s instanceof Blob ) {
|
|
|
|
|
// this shouldn't really ever be called, but it's here if needed
|
|
|
|
|
// (and will quite possibly make the SQL error out)
|
|
|
|
|
$blob = new MssqlBlob( $s->fetch() );
|
|
|
|
|
return $blob->fetch();
|
2013-11-16 02:06:01 +00:00
|
|
|
} else {
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( is_bool( $s ) ) {
|
|
|
|
|
$s = $s ? 1 : 0;
|
|
|
|
|
}
|
2013-11-16 02:06:01 +00:00
|
|
|
return parent::addQuotes( $s );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2013-11-16 02:06:01 +00:00
|
|
|
public function addIdentifierQuotes( $s ) {
|
|
|
|
|
// http://msdn.microsoft.com/en-us/library/aa223962.aspx
|
|
|
|
|
return '[' . $s . ']';
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2013-11-16 02:06:01 +00:00
|
|
|
public function isQuotedIdentifier( $name ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-09-23 23:23:49 +00:00
|
|
|
/**
|
|
|
|
|
* MS SQL supports more pattern operators than other databases (ex: [,],^)
|
|
|
|
|
*
|
|
|
|
|
* @param string $s
|
2017-09-09 20:47:04 +00:00
|
|
|
* @param string $escapeChar
|
2014-09-23 23:23:49 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
New maintenance script to clean up rows with invalid DB keys
The TitleValue constructor, used by the link cache among other things,
throws an exception for DB keys which do not satisfy a simple sanity test
(starting or ending with _, or containing a space, tab, CR or LF
character). This has broken certain special pages on a number of WMF sites;
see T99736, T146778 and T155091.
The new cleanupInvalidDbKeys.php script allows these bogus entries to be
removed from the DB, making sure these exceptions won't be thrown in the
future. It cleans the title columns of the page, archive, redirect,
logging, category, protected_titles, recentchanges, watchlist, pagelinks,
templatelinks, and categorylinks tables.
The script doesn't support batching; most wikis should have fewer than 500
broken entries in each table. If need be, the script can be run several
times.
To make the LIKE queries work properly I had to fix the broken escaping
behaviour of Database::buildLike() -- previously it had a habit of double-
escaping things. Now an ESCAPE clause is added to change the escape
character from the problematic default backslash, and tests are added to
cover the changes.
Bug: T155091
Change-Id: I908e795e884e35be91852c0eaf056d6acfda31d8
2017-03-10 13:27:27 +00:00
|
|
|
protected function escapeLikeInternal( $s, $escapeChar = '`' ) {
|
|
|
|
|
return str_replace( [ $escapeChar, '%', '_', '[', ']', '^' ],
|
|
|
|
|
[ "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_",
|
|
|
|
|
"{$escapeChar}[", "{$escapeChar}]", "{$escapeChar}^" ],
|
|
|
|
|
$s );
|
2014-09-23 23:23:49 +00:00
|
|
|
}
|
|
|
|
|
|
2018-08-14 23:44:41 +00:00
|
|
|
protected function doSelectDomain( DatabaseDomain $domain ) {
|
|
|
|
|
$encDatabase = $this->addIdentifierQuotes( $domain->getDatabase() );
|
|
|
|
|
$this->query( "USE $encDatabase" );
|
|
|
|
|
// Update that domain fields on success (no exception thrown)
|
|
|
|
|
$this->currentDomain = $domain;
|
|
|
|
|
|
|
|
|
|
return true;
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-07-24 17:42:45 +00:00
|
|
|
* @param array $options An associative array of options to be turned into
|
2013-12-27 01:54:51 +00:00
|
|
|
* an SQL query, valid keys are listed in the function.
|
|
|
|
|
* @return array
|
2013-11-16 02:06:01 +00:00
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
public function makeSelectOptions( $options ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$tailOpts = '';
|
|
|
|
|
$startOpts = '';
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$noKeyOptions = [];
|
2013-11-16 02:06:01 +00:00
|
|
|
foreach ( $options as $key => $option ) {
|
|
|
|
|
if ( is_numeric( $key ) ) {
|
|
|
|
|
$noKeyOptions[$option] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tailOpts .= $this->makeGroupByWithHaving( $options );
|
|
|
|
|
|
|
|
|
|
$tailOpts .= $this->makeOrderBy( $options );
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
$startOpts .= 'DISTINCT';
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
if ( isset( $noKeyOptions['FOR XML'] ) ) {
|
|
|
|
|
// used in group concat field emulation
|
|
|
|
|
$tailOpts .= " FOR XML PATH('')";
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
// we want this to be compatible with the output of parent::makeSelectOptions()
|
2016-07-04 10:56:40 +00:00
|
|
|
return [ $startOpts, '', $tailOpts, '', '' ];
|
2013-11-16 02:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
public function getType() {
|
2013-11-16 02:06:01 +00:00
|
|
|
return 'mssql';
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* @param array $stringList
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function buildConcat( $stringList ) {
|
2013-11-16 02:06:01 +00:00
|
|
|
return implode( ' + ', $stringList );
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Build a GROUP_CONCAT or equivalent statement for a query.
|
|
|
|
|
* MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty)
|
|
|
|
|
*
|
|
|
|
|
* This is useful for combining a field for several rows into a single string.
|
|
|
|
|
* NULL values will not appear in the output, duplicated values will appear,
|
|
|
|
|
* and the resulting delimiter-separated values have no defined sort order.
|
|
|
|
|
* Code using the results may need to use the PHP unique() or sort() methods.
|
|
|
|
|
*
|
|
|
|
|
* @param string $delim Glue to bind the results together
|
|
|
|
|
* @param string|array $table Table name
|
|
|
|
|
* @param string $field Field name
|
|
|
|
|
* @param string|array $conds Conditions
|
|
|
|
|
* @param string|array $join_conds Join conditions
|
2014-04-19 11:55:27 +00:00
|
|
|
* @return string SQL text
|
2014-01-03 03:04:26 +00:00
|
|
|
* @since 1.23
|
|
|
|
|
*/
|
|
|
|
|
public function buildGroupConcatField( $delim, $table, $field, $conds = '',
|
2016-02-17 09:09:32 +00:00
|
|
|
$join_conds = []
|
2014-01-03 03:04:26 +00:00
|
|
|
) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$gcsq = 'gcsq_' . $this->subqueryId;
|
|
|
|
|
$this->subqueryId++;
|
2014-01-03 03:04:26 +00:00
|
|
|
|
|
|
|
|
$delimLen = strlen( $delim );
|
|
|
|
|
$fld = "{$field} + {$this->addQuotes( $delim )}";
|
|
|
|
|
$sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
|
2016-02-17 09:09:32 +00:00
|
|
|
. $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
|
2014-01-03 03:04:26 +00:00
|
|
|
. ") {$gcsq} ({$field}))";
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-04 13:23:39 +00:00
|
|
|
public function buildSubstring( $input, $startPosition, $length = null ) {
|
|
|
|
|
$this->assertBuildSubstringParams( $startPosition, $length );
|
|
|
|
|
if ( $length === null ) {
|
|
|
|
|
/**
|
|
|
|
|
* MSSQL doesn't allow an empty length parameter, so when we don't want to limit the
|
|
|
|
|
* length returned use the default maximum size of text.
|
|
|
|
|
* @see https://docs.microsoft.com/en-us/sql/t-sql/statements/set-textsize-transact-sql
|
|
|
|
|
*/
|
|
|
|
|
$length = 2147483647;
|
|
|
|
|
}
|
|
|
|
|
return 'SUBSTRING(' . implode( ',', [ $input, $startPosition, $length ] ) . ')';
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-16 02:06:01 +00:00
|
|
|
/**
|
2014-01-03 03:04:26 +00:00
|
|
|
* Returns an associative array for fields that are of type varbinary, binary, or image
|
|
|
|
|
* $table can be either a raw table name or passed through tableName() first
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function getBinaryColumns( $table ) {
|
|
|
|
|
$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
|
|
|
|
|
$tableRaw = array_pop( $tableRawArr );
|
|
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->binaryColumnCache === null ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
$this->populateColumnCaches();
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-06 22:17:58 +00:00
|
|
|
return $this->binaryColumnCache[$tableRaw] ?? [];
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function getBitColumns( $table ) {
|
|
|
|
|
$tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) );
|
|
|
|
|
$tableRaw = array_pop( $tableRawArr );
|
|
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
if ( $this->bitColumnCache === null ) {
|
2014-01-03 03:04:26 +00:00
|
|
|
$this->populateColumnCaches();
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-06 22:17:58 +00:00
|
|
|
return $this->bitColumnCache[$tableRaw] ?? [];
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function populateColumnCaches() {
|
|
|
|
|
$res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
|
2016-02-17 09:09:32 +00:00
|
|
|
[
|
2018-08-14 23:44:41 +00:00
|
|
|
'TABLE_CATALOG' => $this->getDBname(),
|
|
|
|
|
'TABLE_SCHEMA' => $this->dbSchema(),
|
2016-02-17 09:09:32 +00:00
|
|
|
'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
|
|
|
|
|
] );
|
2014-01-03 03:04:26 +00:00
|
|
|
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->binaryColumnCache = [];
|
|
|
|
|
$this->bitColumnCache = [];
|
2014-01-03 03:04:26 +00:00
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
if ( $row->DATA_TYPE == 'bit' ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->bitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
|
2014-01-03 03:04:26 +00:00
|
|
|
} else {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->binaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @param string $format
|
2013-11-16 02:06:01 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
2014-01-03 03:04:26 +00:00
|
|
|
function tableName( $name, $format = 'quoted' ) {
|
|
|
|
|
# Replace reserved words with better ones
|
|
|
|
|
switch ( $name ) {
|
|
|
|
|
case 'user':
|
|
|
|
|
return $this->realTableName( 'mwuser', $format );
|
|
|
|
|
default:
|
|
|
|
|
return $this->realTableName( $name, $format );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* call this instead of tableName() in the updater when renaming tables
|
|
|
|
|
* @param string $name
|
2014-04-22 05:46:24 +00:00
|
|
|
* @param string $format One of quoted, raw, or split
|
2014-01-03 03:04:26 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
function realTableName( $name, $format = 'quoted' ) {
|
2014-04-22 05:46:24 +00:00
|
|
|
$table = parent::tableName( $name, $format );
|
|
|
|
|
if ( $format == 'split' ) {
|
|
|
|
|
// Used internally, we want the schema split off from the table name and returned
|
|
|
|
|
// as a list with 3 elements (database, schema, table)
|
|
|
|
|
$table = explode( '.', $table );
|
2014-06-27 01:17:05 +00:00
|
|
|
while ( count( $table ) < 3 ) {
|
2014-04-22 05:46:24 +00:00
|
|
|
array_unshift( $table, false );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $table;
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
2016-04-25 01:58:24 +00:00
|
|
|
/**
|
|
|
|
|
* Delete a table
|
|
|
|
|
* @param string $tableName
|
|
|
|
|
* @param string $fName
|
|
|
|
|
* @return bool|ResultWrapper
|
|
|
|
|
* @since 1.18
|
|
|
|
|
*/
|
|
|
|
|
public function dropTable( $tableName, $fName = __METHOD__ ) {
|
|
|
|
|
if ( !$this->tableExists( $tableName, $fName ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parent function incorrectly appends CASCADE, which we don't want
|
|
|
|
|
$sql = "DROP TABLE " . $this->tableName( $tableName );
|
|
|
|
|
|
|
|
|
|
return $this->query( $sql, $fName );
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-03 03:04:26 +00:00
|
|
|
/**
|
|
|
|
|
* Called in the installer and updater.
|
|
|
|
|
* Probably doesn't need to be called anywhere else in the codebase.
|
|
|
|
|
* @param bool|null $value
|
|
|
|
|
* @return bool|null
|
|
|
|
|
*/
|
|
|
|
|
public function prepareStatements( $value = null ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$old = $this->prepareStatements;
|
2017-02-09 22:30:05 +00:00
|
|
|
if ( $value !== null ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->prepareStatements = $value;
|
2017-02-09 22:30:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $old;
|
2014-01-03 03:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called in the installer and updater.
|
|
|
|
|
* Probably doesn't need to be called anywhere else in the codebase.
|
|
|
|
|
* @param bool|null $value
|
|
|
|
|
* @return bool|null
|
|
|
|
|
*/
|
|
|
|
|
public function scrollableCursor( $value = null ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$old = $this->scrollableCursor;
|
2017-02-09 22:30:05 +00:00
|
|
|
if ( $value !== null ) {
|
2018-02-16 19:23:30 +00:00
|
|
|
$this->scrollableCursor = $value;
|
2017-02-09 22:30:05 +00:00
|
|
|
}
|
2016-04-25 01:58:24 +00:00
|
|
|
|
2017-02-09 22:30:05 +00:00
|
|
|
return $old;
|
2016-04-25 01:58:24 +00:00
|
|
|
}
|
2017-02-09 22:30:05 +00:00
|
|
|
}
|
2017-02-07 04:49:57 +00:00
|
|
|
|
2018-05-29 16:21:31 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since 1.29
|
|
|
|
|
*/
|
2017-02-07 04:49:57 +00:00
|
|
|
class_alias( DatabaseMssql::class, 'DatabaseMssql' );
|