/**
* @ingroup Database
*/
-class DatabaseMssql extends Database {
+class DatabaseMssql extends DatabaseBase {
protected $mInsertId = null;
protected $mLastResult = null;
protected $mAffectedRows = null;
protected $mBinaryColumnCache = null;
protected $mBitColumnCache = null;
protected $mIgnoreDupKeyErrors = false;
+ protected $mIgnoreErrors = [];
protected $mPort;
* @param string $password
* @param string $dbName
* @throws DBConnectionError
- * @return bool|DatabaseBase|null
+ * @return bool|resource|null
*/
public function open( $server, $user, $password, $dbName ) {
# Test for driver support, to avoid suppressed fatal error
* @throws DBUnexpectedError
*/
protected function doQuery( $sql ) {
- if ( $this->debug() ) {
+ if ( $this->getFlag( DBO_DEBUG ) ) {
wfDebug( "SQL: [$sql]\n" );
}
$this->offset = 0;
$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;
}
/**
* @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
* @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__,
# 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;
}
$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
* @return null|ResultWrapper
* @throws Exception
*/
- public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
+ public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = [], $selectOptions = []
) {
$this->mScrollableCursor = false;
try {
- $ret = parent::insertSelect(
+ $ret = parent::nativeInsertSelect(
$destTable,
$srcTable,
$varMap,
* @return bool
* @throws DBUnexpectedError
* @throws Exception
- * @throws MWException
*/
function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
$table = $this->tableName( $table );
* @param array $binaryColumns Contains a list of column names that are binary types
* This is a custom parameter only present for MS SQL.
*
- * @throws MWException|DBUnexpectedError
+ * @throws DBUnexpectedError
* @return string
*/
public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = [] ) {
if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this,
- 'DatabaseBase::makeList called with incorrect parameters' );
+ throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
}
if ( $mode != LIST_NAMES ) {
* Throws an exception if it is invalid.
* Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx
* @param string $identifier
- * @throws MWException
+ * @throws InvalidArgumentException
* @return string
*/
private function escapeIdentifier( $identifier ) {
if ( strlen( $identifier ) == 0 ) {
- throw new MWException( "An identifier must not be empty" );
+ throw new InvalidArgumentException( "An identifier must not be empty" );
}
if ( strlen( $identifier ) > 128 ) {
- throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
+ throw new InvalidArgumentException( "The identifier '$identifier' is too long (max. 128)" );
}
if ( ( strpos( $identifier, '[' ) !== false )
|| ( strpos( $identifier, ']' ) !== false )
) {
// It may be allowed if you quoted with double quotation marks, but
// that would break if QUOTED_IDENTIFIER is OFF
- throw new MWException( "Square brackets are not allowed in '$identifier'" );
+ throw new InvalidArgumentException( "Square brackets are not allowed in '$identifier'" );
}
return "[$identifier]";
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
}
// we want this to be compatible with the output of parent::makeSelectOptions()
- return [ $startOpts, '', $tailOpts, '' ];
+ return [ $startOpts, '', $tailOpts, '', '' ];
}
/**
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 );
}
-} // end DatabaseMssql class
-
-/**
- * Utility class.
- *
- * @ingroup Database
- */
-class MssqlField implements Field {
- private $name, $tableName, $default, $max_length, $nullable, $type;
-
- function __construct( $info ) {
- $this->name = $info['COLUMN_NAME'];
- $this->tableName = $info['TABLE_NAME'];
- $this->default = $info['COLUMN_DEFAULT'];
- $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
- $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
- $this->type = $info['DATA_TYPE'];
- }
-
- function name() {
- return $this->name;
- }
-
- function tableName() {
- return $this->tableName;
- }
-
- function defaultValue() {
- return $this->default;
- }
-
- function maxLength() {
- return $this->max_length;
- }
-
- function isNullable() {
- return $this->nullable;
- }
-
- function type() {
- return $this->type;
- }
-}
-
-class MssqlBlob extends Blob {
- public function __construct( $data ) {
- if ( $data instanceof MssqlBlob ) {
- return $data;
- } elseif ( $data instanceof Blob ) {
- $this->mData = $data->fetch();
- } elseif ( is_array( $data ) && is_object( $data ) ) {
- $this->mData = serialize( $data );
- } else {
- $this->mData = $data;
- }
- }
-
- /**
- * Returns an unquoted hex representation of a binary string
- * for insertion into varbinary-type fields
- * @return string
- */
- public function fetch() {
- if ( $this->mData === null ) {
- return 'null';
- }
-
- $ret = '0x';
- $dataLength = strlen( $this->mData );
- for ( $i = 0; $i < $dataLength; $i++ ) {
- $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) );
- }
-
- return $ret;
- }
-}
-
-class MssqlResultWrapper extends ResultWrapper {
- private $mSeekTo = null;
-
- /**
- * @return stdClass|bool
- */
- public function fetchObject() {
- $res = $this->result;
-
- if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_object( $res, 'stdClass', [],
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
- } else {
- $result = sqlsrv_fetch_object( $res );
- }
-
- // MediaWiki expects us to return boolean false when there are no more rows instead of null
- if ( $result === null ) {
- return false;
- }
-
- return $result;
- }
-
- /**
- * @return array|bool
- */
- public function fetchRow() {
- $res = $this->result;
-
- if ( $this->mSeekTo !== null ) {
- $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH,
- SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo );
- $this->mSeekTo = null;
- } else {
- $result = sqlsrv_fetch_array( $res );
- }
-
- // MediaWiki expects us to return boolean false when there are no more rows instead of null
- if ( $result === null ) {
- return false;
- }
-
- return $result;
- }
/**
- * @param int $row
- * @return bool
+ * 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 seek( $row ) {
- $res = $this->result;
-
- // check bounds
- $numRows = $this->db->numRows( $res );
- $row = intval( $row );
-
- if ( $numRows === 0 ) {
- return false;
- } elseif ( $row < 0 || $row > $numRows - 1 ) {
- return false;
- }
-
- // Unlike MySQL, the seek actually happens on the next access
- $this->mSeekTo = $row;
- return true;
+ public function ignoreErrors( array $value = null ) {
+ return wfSetVar( $this->mIgnoreErrors, $value );
}
-}
+} // end DatabaseMssql class