/**
* @ingroup Database
*/
-class DatabaseMssql extends DatabaseBase {
+class DatabaseMssql extends Database {
protected $mInsertId = null;
protected $mLastResult = null;
protected $mAffectedRows = null;
protected $mBinaryColumnCache = null;
protected $mBitColumnCache = null;
protected $mIgnoreDupKeyErrors = false;
+ protected $mIgnoreErrors = [];
protected $mPort;
$this->mPassword = $password;
$this->mDBname = $dbName;
- $connectionInfo = array();
+ $connectionInfo = [];
if ( $dbName ) {
$connectionInfo['Database'] = $dbName;
* @param bool|MssqlResultWrapper|resource $result
* @return bool|MssqlResultWrapper
*/
- public function resultObject( $result ) {
- if ( empty( $result ) ) {
+ protected function resultObject( $result ) {
+ if ( !$result ) {
return false;
} elseif ( $result instanceof MssqlResultWrapper ) {
return $result;
// 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"
if ( $this->mScrollableCursor ) {
- $scrollArr = array( 'Scrollable' => SQLSRV_CURSOR_STATIC );
+ $scrollArr = [ 'Scrollable' => SQLSRV_CURSOR_STATIC ];
} else {
- $scrollArr = array();
+ $scrollArr = [];
}
if ( $this->mPrepareStatements ) {
// we do prepare + execute so we can get its field metadata for later usage if desired
- $stmt = sqlsrv_prepare( $this->mConn, $sql, array(), $scrollArr );
+ $stmt = sqlsrv_prepare( $this->mConn, $sql, [], $scrollArr );
$success = sqlsrv_execute( $stmt );
} else {
- $stmt = sqlsrv_query( $this->mConn, $sql, array(), $scrollArr );
+ $stmt = sqlsrv_query( $this->mConn, $sql, [], $scrollArr );
$success = (bool)$stmt;
}
+ // make a copy so that anything we add below does not get reflected in future queries
+ $ignoreErrors = $this->mIgnoreErrors;
+
if ( $this->mIgnoreDupKeyErrors ) {
- // ignore duplicate key errors, but nothing else
+ // ignore duplicate key errors
// this emulates INSERT IGNORE in MySQL
- if ( $success === false ) {
- $errors = sqlsrv_errors( SQLSRV_ERR_ERRORS );
- $success = true;
-
- foreach ( $errors as $err ) {
- if ( $err['SQLSTATE'] == '23000' && $err['code'] == '2601' ) {
- continue; // duplicate key error caused by unique index
- } elseif ( $err['SQLSTATE'] == '23000' && $err['code'] == '2627' ) {
- continue; // duplicate key error caused by primary key
- } elseif ( $err['SQLSTATE'] == '01000' && $err['code'] == '3621' ) {
- continue; // generic "the statement has been terminated" error
- }
+ $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
+ }
- $success = false; // getting here means we got an error we weren't expecting
- break;
- }
+ if ( $success === false ) {
+ $errors = sqlsrv_errors();
+ $success = true;
- if ( $success ) {
- $this->mAffectedRows = 0;
- return $stmt;
+ foreach ( $errors as $err ) {
+ if ( !in_array( $err['code'], $ignoreErrors ) ) {
+ $success = false;
+ break;
}
}
- }
- if ( $success === false ) {
- return false;
+ if ( $success === false ) {
+ return false;
+ }
}
// remember number of rows affected
$this->mAffectedRows = sqlsrv_rows_affected( $stmt );
$res = $res->result;
}
- return sqlsrv_num_rows( $res );
+ $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;
}
/**
$res = $res->result;
}
- $metadata = sqlsrv_field_metadata( $res );
- return $metadata[$n]['Name'];
+ return sqlsrv_field_metadata( $res )[$n]['Name'];
}
/**
* @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.
- * array('GROUP BY' => 'page_title')), see Database::makeSelectOptions
+ * [ 'GROUP BY' => 'page_title' ]), see Database::makeSelectOptions
* code for list of supported stuff
* @param array $join_conds Associative array of table join conditions
- * (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * (optional) (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ]
* @return mixed Database result resource (feed to Database::fetchObject
* or whatever), or false on failure
* @throws DBQueryError
* @throws Exception
*/
public function select( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = array(), $join_conds = array()
+ $options = [], $join_conds = []
) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
if ( isset( $options['EXPLAIN'] ) ) {
* @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. array('GROUP BY' => 'page_title')),
+ * @param array $options Associative array of options (e.g. [ 'GROUP BY' => 'page_title' ]),
* see Database::makeSelectOptions code for list of supported stuff
* @param array $join_conds Associative array of table join conditions (optional)
- * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * (e.g. [ 'page' => [ 'LEFT JOIN','page_latest=rev_id' ] ]
* @return string The SQL text
*/
public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
- $options = array(), $join_conds = array()
+ $options = [], $join_conds = []
) {
if ( isset( $options['EXPLAIN'] ) ) {
unset( $options['EXPLAIN'] );
// try to rewrite aggregations of bit columns (currently MAX and MIN)
if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
- $bitColumns = array();
+ $bitColumns = [];
if ( is_array( $table ) ) {
foreach ( $table as $t ) {
$bitColumns += $this->getBitColumns( $this->tableName( $t ) );
}
foreach ( $bitColumns as $col => $info ) {
- $replace = array(
+ $replace = [
"MAX({$col})" => "MAX(CAST({$col} AS tinyint))",
"MIN({$col})" => "MIN(CAST({$col} AS tinyint))",
- );
+ ];
$sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql );
}
}
* @return int
*/
public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = array()
+ $fname = __METHOD__, $options = []
) {
// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
$options['EXPLAIN'] = true;
# 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
# the existance of indexes.
- $sql = "sp_helpindex '" . $table . "'";
+ $sql = "sp_helpindex '" . $this->tableName( $table ) . "'";
$res = $this->query( $sql, $fname );
+
if ( !$res ) {
return null;
}
- $result = array();
+ $result = [];
foreach ( $res as $row ) {
if ( $row->index_name == $index ) {
$row->Non_unique = !stristr( $row->index_description, "unique" );
* @return bool
* @throws Exception
*/
- public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) {
+ public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = [] ) {
# No rows to insert, easy just return now
if ( !count( $arrToInsert ) ) {
return true;
}
if ( !is_array( $options ) ) {
- $options = array( $options );
+ $options = [ $options ];
}
$table = $this->tableName( $table );
if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row
- $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible
+ $arrToInsert = [ 0 => $arrToInsert ]; // make everything multi row compatible
}
// We know the table we're inserting into, get its identity column
// INSERT IGNORE is not supported by SQL Server
// remove IGNORE from options list and set ignore flag to true
if ( in_array( 'IGNORE', $options ) ) {
- $options = array_diff( $options, array( 'IGNORE' ) );
+ $options = array_diff( $options, [ 'IGNORE' ] );
$this->mIgnoreDupKeyErrors = true;
}
$row = $ret->fetchObject();
if ( is_object( $row ) ) {
$this->mInsertId = $row->$identity;
+
+ // 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
+ if ( $this->mAffectedRows == -1 ) {
+ $this->mAffectedRows = 1;
+ }
}
}
}
/**
* INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * $varMap must be an associative array of the form [ 'dest1' => 'source1', ... ]
* Source items may be literals rather than field names, but strings should
* be quoted with Database::addQuotes().
* @param string $destTable
* @throws Exception
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
- $insertOptions = array(), $selectOptions = array()
+ $insertOptions = [], $selectOptions = []
) {
$this->mScrollableCursor = false;
try {
* @throws Exception
* @throws MWException
*/
- function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
$table = $this->tableName( $table );
$binaryColumns = $this->getBinaryColumns( $table );
$opts = $this->makeUpdateOptions( $options );
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns );
- if ( $conds !== array() && $conds !== '*' ) {
+ if ( $conds !== [] && $conds !== '*' ) {
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns );
}
* @throws MWException|DBUnexpectedError
* @return string
*/
- public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) {
+ public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
if ( !is_array( $a ) ) {
throw new DBUnexpectedError( $this,
'DatabaseBase::makeList called with incorrect parameters' );
}
} else {
// This one is fun, we need to pull out the select list as well as any ORDER BY clause
- $select = $orderby = array();
+ $select = $orderby = [];
$s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select );
$s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby );
$overOrder = $postOrder = '';
* @param string $s
* @return string
*/
- public function strencode( $s ) { # Should not be called by us
+ public function strencode( $s ) {
+ // Should not be called by us
+
return str_replace( "'", "''", $s );
}
return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']';
}
+ /**
+ * MS SQL supports more pattern operators than other databases (ex: [,],^)
+ *
+ * @param string $s
+ * @return string
+ */
+ protected function escapeLikeInternal( $s ) {
+ return addcslashes( $s, '\%_[]^' );
+ }
+
+ /**
+ * MS SQL requires specifying the escape character used in a LIKE query
+ * or using Square brackets to surround characters that are to be escaped
+ * http://msdn.microsoft.com/en-us/library/ms179859.aspx
+ * Here we take the Specify-Escape-Character approach since it's less
+ * invasive, renders a query that is closer to other DB's and better at
+ * handling square bracket escaping
+ *
+ * @return string Fully built LIKE statement
+ */
+ public function buildLike() {
+ $params = func_get_args();
+ if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+
+ return parent::buildLike( $params ) . " ESCAPE '\' ";
+ }
+
/**
* @param string $db
* @return bool
$tailOpts = '';
$startOpts = '';
- $noKeyOptions = array();
+ $noKeyOptions = [];
foreach ( $options as $key => $option ) {
if ( is_numeric( $key ) ) {
$noKeyOptions[$option] = true;
}
// we want this to be compatible with the output of parent::makeSelectOptions()
- return array( $startOpts, '', $tailOpts, '' );
+ return [ $startOpts, '', $tailOpts, '' ];
}
/**
* @since 1.23
*/
public function buildGroupConcatField( $delim, $table, $field, $conds = '',
- $join_conds = array()
+ $join_conds = []
) {
$gcsq = 'gcsq_' . $this->mSubqueryId;
$this->mSubqueryId++;
$delimLen = strlen( $delim );
$fld = "{$field} + {$this->addQuotes( $delim )}";
$sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM ("
- . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds )
+ . $this->selectSQLText( $table, $fld, $conds, null, [ 'FOR XML' ], $join_conds )
. ") {$gcsq} ({$field}))";
return $sql;
return isset( $this->mBinaryColumnCache[$tableRaw] )
? $this->mBinaryColumnCache[$tableRaw]
- : array();
+ : [];
}
/**
return isset( $this->mBitColumnCache[$tableRaw] )
? $this->mBitColumnCache[$tableRaw]
- : array();
+ : [];
}
private function populateColumnCaches() {
$res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*',
- array(
+ [
'TABLE_CATALOG' => $this->mDBname,
'TABLE_SCHEMA' => $this->mSchema,
- 'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' )
- ) );
+ 'DATA_TYPE' => [ 'varbinary', 'binary', 'image', 'bit' ]
+ ] );
- $this->mBinaryColumnCache = array();
- $this->mBitColumnCache = array();
+ $this->mBinaryColumnCache = [];
+ $this->mBitColumnCache = [];
foreach ( $res as $row ) {
if ( $row->DATA_TYPE == 'bit' ) {
$this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row;
return $table;
}
+ /**
+ * 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 );
+ }
+
/**
* Called in the installer and updater.
* Probably doesn't need to be called anywhere else in the codebase.
public function scrollableCursor( $value = null ) {
return wfSetVar( $this->mScrollableCursor, $value );
}
+
+ /**
+ * Called in the installer and updater.
+ * Probably doesn't need to be called anywhere else in the codebase.
+ * @param array|null $value
+ * @return array|null
+ */
+ public function ignoreErrors( array $value = null ) {
+ return wfSetVar( $this->mIgnoreErrors, $value );
+ }
} // end DatabaseMssql class
/**
$res = $this->result;
if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_object( $res, 'stdClass', array(),
+ $result = sqlsrv_fetch_object( $res, 'stdClass', [],
SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
$this->mSeekTo = null;
} else {