X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fdb%2FDatabaseMssql.php;h=33f81623d9a84ba344437ad7520c3da6daa5e41f;hb=f83e831b7e02ebff1ce803d5dd707867a7b328c2;hp=5acbc6ca7faf87c79dbabdee73897c6a90532757;hpb=38ba6b620be9f6333d902055ae1c0c610af4985e;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 5acbc6ca7f..33f81623d9 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -38,6 +38,7 @@ class DatabaseMssql extends Database { protected $mBinaryColumnCache = null; protected $mBitColumnCache = null; protected $mIgnoreDupKeyErrors = false; + protected $mIgnoreErrors = []; protected $mPort; @@ -106,7 +107,7 @@ class DatabaseMssql extends Database { $this->mPassword = $password; $this->mDBname = $dbName; - $connectionInfo = array(); + $connectionInfo = []; if ( $dbName ) { $connectionInfo['Database'] = $dbName; @@ -192,49 +193,45 @@ class DatabaseMssql extends Database { // 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 ); @@ -276,7 +273,15 @@ class DatabaseMssql extends Database { $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; } /** @@ -301,8 +306,7 @@ class DatabaseMssql extends Database { $res = $res->result; } - $metadata = sqlsrv_field_metadata( $res ); - return $metadata[$n]['Name']; + return sqlsrv_field_metadata( $res )[$n]['Name']; } /** @@ -385,7 +389,7 @@ class DatabaseMssql extends Database { * @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'] ) ) { @@ -437,7 +441,7 @@ class DatabaseMssql extends Database { * @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'] ); @@ -447,7 +451,7 @@ class DatabaseMssql extends Database { // 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 ) ); @@ -457,10 +461,10 @@ class DatabaseMssql extends Database { } 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 ); } } @@ -506,7 +510,7 @@ class DatabaseMssql extends Database { * @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; @@ -537,13 +541,14 @@ class DatabaseMssql extends Database { # 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" ); @@ -580,20 +585,20 @@ class DatabaseMssql extends Database { * @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 @@ -618,7 +623,7 @@ class DatabaseMssql extends Database { // 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; } @@ -697,6 +702,12 @@ class DatabaseMssql extends Database { $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; + } } } } @@ -720,7 +731,7 @@ class DatabaseMssql extends Database { * @throws Exception */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = array(), $selectOptions = array() + $insertOptions = [], $selectOptions = [] ) { $this->mScrollableCursor = false; try { @@ -768,14 +779,14 @@ class DatabaseMssql extends Database { * @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 ); } @@ -806,7 +817,7 @@ class DatabaseMssql extends Database { * @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' ); @@ -874,7 +885,7 @@ class DatabaseMssql extends Database { } } 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 = ''; @@ -1132,6 +1143,35 @@ class DatabaseMssql extends Database { 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 @@ -1155,7 +1195,7 @@ class DatabaseMssql extends Database { $tailOpts = ''; $startOpts = ''; - $noKeyOptions = array(); + $noKeyOptions = []; foreach ( $options as $key => $option ) { if ( is_numeric( $key ) ) { $noKeyOptions[$option] = true; @@ -1176,7 +1216,7 @@ class DatabaseMssql extends Database { } // we want this to be compatible with the output of parent::makeSelectOptions() - return array( $startOpts, '', $tailOpts, '' ); + return [ $startOpts, '', $tailOpts, '' ]; } /** @@ -1213,7 +1253,7 @@ class DatabaseMssql extends Database { * @since 1.23 */ public function buildGroupConcatField( $delim, $table, $field, $conds = '', - $join_conds = array() + $join_conds = [] ) { $gcsq = 'gcsq_' . $this->mSubqueryId; $this->mSubqueryId++; @@ -1221,7 +1261,7 @@ class DatabaseMssql extends Database { $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; @@ -1250,7 +1290,7 @@ class DatabaseMssql extends Database { return isset( $this->mBinaryColumnCache[$tableRaw] ) ? $this->mBinaryColumnCache[$tableRaw] - : array(); + : []; } /** @@ -1267,19 +1307,19 @@ class DatabaseMssql extends Database { 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; @@ -1323,6 +1363,24 @@ class DatabaseMssql extends Database { 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. @@ -1342,6 +1400,16 @@ class DatabaseMssql extends Database { 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 /** @@ -1429,7 +1497,7 @@ class MssqlResultWrapper extends ResultWrapper { $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 {