Clean up array() syntax in docs, part II
[lhc/web/wiklou.git] / includes / db / DatabaseMssql.php
index 5acbc6c..5e0365a 100644 (file)
@@ -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'];
        }
 
        /**
@@ -374,10 +378,10 @@ class DatabaseMssql extends Database {
         * @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
@@ -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'] ) ) {
@@ -430,14 +434,14 @@ class DatabaseMssql extends Database {
         * @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'] );
@@ -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;
+                                       }
                                }
                        }
                }
@@ -706,7 +717,7 @@ class DatabaseMssql extends Database {
 
        /**
         * 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
@@ -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 {