Merge "Add Database::unionConditionPermutations()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 7 Jul 2017 18:37:08 +0000 (18:37 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 7 Jul 2017 18:37:09 +0000 (18:37 +0000)
1  2 
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/IDatabase.php

@@@ -1802,33 -1802,8 +1802,33 @@@ abstract class Database implements IDat
                }
  
                # Split database and table into proper variables.
 -              # We reverse the explode so that database.table and table both output
 -              # the correct table.
 +              list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
 +
 +              # Quote $table and apply the prefix if not quoted.
 +              # $tableName might be empty if this is called from Database::replaceVars()
 +              $tableName = "{$prefix}{$table}";
 +              if ( $format === 'quoted'
 +                      && !$this->isQuotedIdentifier( $tableName )
 +                      && $tableName !== ''
 +              ) {
 +                      $tableName = $this->addIdentifierQuotes( $tableName );
 +              }
 +
 +              # Quote $schema and $database and merge them with the table name if needed
 +              $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
 +              $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
 +
 +              return $tableName;
 +      }
 +
 +      /**
 +       * Get the table components needed for a query given the currently selected database
 +       *
 +       * @param string $name Table name in the form of db.schema.table, db.table, or table
 +       * @return array (DB name or "" for default, schema name, table prefix, table name)
 +       */
 +      protected function qualifiedTableComponents( $name ) {
 +              # We reverse the explode so that database.table and table both output the correct table.
                $dbDetails = explode( '.', $name, 3 );
                if ( count( $dbDetails ) == 3 ) {
                        list( $database, $schema, $table ) = $dbDetails;
                        }
                }
  
 -              # Quote $table and apply the prefix if not quoted.
 -              # $tableName might be empty if this is called from Database::replaceVars()
 -              $tableName = "{$prefix}{$table}";
 -              if ( $format === 'quoted'
 -                      && !$this->isQuotedIdentifier( $tableName )
 -                      && $tableName !== ''
 -              ) {
 -                      $tableName = $this->addIdentifierQuotes( $tableName );
 -              }
 -
 -              # Quote $schema and $database and merge them with the table name if needed
 -              $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
 -              $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
 -
 -              return $tableName;
 +              return [ $database, $schema, $prefix, $table ];
        }
  
        /**
                return '(' . implode( $glue, $sqls ) . ')';
        }
  
+       public function unionConditionPermutations(
+               $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               // First, build the Cartesian product of $permute_conds
+               $conds = [ [] ];
+               foreach ( $permute_conds as $field => $values ) {
+                       if ( !$values ) {
+                               // Skip empty $values
+                               continue;
+                       }
+                       $values = array_unique( $values ); // For sanity
+                       $newConds = [];
+                       foreach ( $conds as $cond ) {
+                               foreach ( $values as $value ) {
+                                       $cond[$field] = $value;
+                                       $newConds[] = $cond; // Arrays are by-value, not by-reference, so this works
+                               }
+                       }
+                       $conds = $newConds;
+               }
+               $extra_conds = $extra_conds === '' ? [] : (array)$extra_conds;
+               // If there's just one condition and no subordering, hand off to
+               // selectSQLText directly.
+               if ( count( $conds ) === 1 &&
+                       ( !isset( $options['INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
+               ) {
+                       return $this->selectSQLText(
+                               $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
+                       );
+               }
+               // Otherwise, we need to pull out the order and limit to apply after
+               // the union. Then build the SQL queries for each set of conditions in
+               // $conds. Then union them together (using UNION ALL, because the
+               // product *should* already be distinct).
+               $orderBy = $this->makeOrderBy( $options );
+               $limit = isset( $options['LIMIT'] ) ? $options['LIMIT'] : null;
+               $offset = isset( $options['OFFSET'] ) ? $options['OFFSET'] : false;
+               $all = empty( $options['NOTALL'] ) && !in_array( 'NOTALL', $options );
+               if ( !$this->unionSupportsOrderAndLimit() ) {
+                       unset( $options['ORDER BY'], $options['LIMIT'], $options['OFFSET'] );
+               } else {
+                       if ( array_key_exists( 'INNER ORDER BY', $options ) ) {
+                               $options['ORDER BY'] = $options['INNER ORDER BY'];
+                       }
+                       if ( $limit !== null && is_numeric( $offset ) && $offset != 0 ) {
+                               // We need to increase the limit by the offset rather than
+                               // using the offset directly, otherwise it'll skip incorrectly
+                               // in the subqueries.
+                               $options['LIMIT'] = $limit + $offset;
+                               unset( $options['OFFSET'] );
+                       }
+               }
+               $sqls = [];
+               foreach ( $conds as $cond ) {
+                       $sqls[] = $this->selectSQLText(
+                               $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
+                       );
+               }
+               $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
+               if ( $limit !== null ) {
+                       $sql = $this->limitResult( $sql, $limit, $offset );
+               }
+               return $sql;
+       }
        public function conditional( $cond, $trueVal, $falseVal ) {
                if ( is_array( $cond ) ) {
                        $cond = $this->makeList( $cond, self::LIST_AND );
@@@ -1252,7 -1252,7 +1252,7 @@@ interface IDatabase 
         * @param array $selectJoinConds Join conditions for the SELECT part of the query, see
         *    IDatabase::select() for details.
         *
 -       * @return IResultWrapper
 +       * @return bool
         */
        public function insertSelect( $destTable, $srcTable, $varMap, $conds,
                $fname = __METHOD__,
         */
        public function unionQueries( $sqls, $all );
  
+       /**
+        * Construct a UNION query for permutations of conditions
+        *
+        * Databases sometimes have trouble with queries that have multiple values
+        * for multiple condition parameters combined with limits and ordering.
+        * This method constructs queries for the Cartesian product of the
+        * conditions and unions them all together.
+        *
+        * @see IDatabase::select()
+        * @since 1.30
+        * @param string|array $table Table name
+        * @param string|array $vars Field names
+        * @param array $permute_conds Conditions for the Cartesian product. Keys
+        *  are field names, values are arrays of the possible values for that
+        *  field.
+        * @param string|array $extra_conds Additional conditions to include in the
+        *  query.
+        * @param string $fname Caller function name
+        * @param string|array $options Query options. In addition to the options
+        *  recognized by IDatabase::select(), the following may be used:
+        *   - NOTALL: Set to use UNION instead of UNION ALL.
+        *   - INNER ORDER BY: If specified and supported, subqueries will use this
+        *     instead of ORDER BY.
+        * @param string|array $join_conds Join conditions
+        * @return string SQL query string.
+        */
+       public function unionConditionPermutations(
+               $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       );
        /**
         * Returns an SQL expression for a simple conditional. This doesn't need
         * to be overridden unless CASE isn't supported in your DBMS.