essential.
=== New features in 1.31 ===
-* …
+* Wikimedia\Rdbms\IDatabase->select() and similar methods now support
+ joins with parentheses for grouping.
=== External library changes in 1.31 ===
// No alias? Set it equal to the table name
$alias = $table;
}
+
+ if ( is_array( $table ) ) {
+ // A parenthesized group
+ $joinedTable = '('
+ . $this->tableNamesWithIndexClauseOrJOIN( $table, $use_index, $ignore_index, $join_conds )
+ . ')';
+ } else {
+ $joinedTable = $this->tableNameWithAlias( $table, $alias );
+ }
+
// Is there a JOIN clause for this table?
if ( isset( $join_conds[$alias] ) ) {
list( $joinType, $conds ) = $join_conds[$alias];
$tableClause = $joinType;
- $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
+ $tableClause .= ' ' . $joinedTable;
if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
if ( $use != '' ) {
$retJOIN[] = $tableClause;
} elseif ( isset( $use_index[$alias] ) ) {
// Is there an INDEX clause for this table?
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$tableClause .= ' ' . $this->useIndexClause(
implode( ',', (array)$use_index[$alias] )
);
$ret[] = $tableClause;
} elseif ( isset( $ignore_index[$alias] ) ) {
// Is there an INDEX clause for this table?
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$tableClause .= ' ' . $this->ignoreIndexClause(
implode( ',', (array)$ignore_index[$alias] )
);
$ret[] = $tableClause;
} else {
- $tableClause = $this->tableNameWithAlias( $table, $alias );
+ $tableClause = $joinedTable;
$ret[] = $tableClause;
}
if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) {
$bitColumns = [];
if ( is_array( $table ) ) {
- foreach ( $table as $t ) {
- $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
+ $tables = $table;
+ while ( $tables ) {
+ $t = array_pop( $tables );
+ if ( is_array( $t ) ) {
+ $tables = array_merge( $tables, $t );
+ } else {
+ $bitColumns += $this->getBitColumns( $this->tableName( $t ) );
+ }
}
} else {
$bitColumns = $this->getBitColumns( $this->tableName( $table ) );
unset( $options[$forUpdateKey] );
$options['FOR UPDATE'] = [];
- // All tables not in $join_conds are good
- foreach ( $table as $alias => $name ) {
- if ( is_numeric( $alias ) ) {
+ $toCheck = $table;
+ reset( $toCheck );
+ while ( $toCheck ) {
+ $alias = key( $toCheck );
+ $name = $toCheck[$alias];
+ unset( $toCheck[$alias] );
+
+ $hasAlias = !is_numeric( $alias );
+ if ( !$hasAlias && is_string( $name ) ) {
$alias = $name;
}
- if ( !isset( $join_conds[$alias] ) ) {
- $options['FOR UPDATE'][] = $alias;
- }
- }
- foreach ( $join_conds as $table_cond => $join_cond ) {
- if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
- $options['FOR UPDATE'][] = $table_cond;
+ if ( !isset( $join_conds[$alias] ) ||
+ !preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_conds[$alias][0] )
+ ) {
+ if ( is_array( $name ) ) {
+ // It's a parenthesized group, process all the tables inside the group.
+ $toCheck = array_merge( $toCheck, $name );
+ } else {
+ // Quote alias names so $this->tableName() won't mangle them
+ $options['FOR UPDATE'][] = $hasAlias ? $this->addIdentifierQuotes( $alias ) : $alias;
+ }
}
}
-
- // Quote alias names so $this->tableName() won't mangle them
- $options['FOR UPDATE'] = array_map( function ( $name ) use ( $table ) {
- return isset( $table[$name] ) ? $this->addIdentifierQuotes( $name ) : $name;
- }, $options['FOR UPDATE'] );
}
if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
* This includes the user table in the query, with the alias "a" available
* for use in field names (e.g. a.user_name).
*
+ * Joins using parentheses for grouping (since MediaWiki 1.31) may be
+ * constructed using nested arrays. For example,
+ *
+ * [ 'tableA', 'nestedB' => [ 'tableB', 'b2' => 'tableB2' ] ]
+ *
+ * along with `$join_conds` like
+ *
+ * [ 'b2' => [ 'JOIN', 'b_id = b2_id' ], 'nestedB' => [ 'LEFT JOIN', 'b_a = a_id' ] ]
+ *
+ * will produce SQL something like
+ *
+ * FROM tableA LEFT JOIN (tableB JOIN tableB2 AS b2 ON (b_id = b2_id)) ON (b_a = a_id)
+ *
* All of the table names given here are automatically run through
* Database::tableName(), which causes the table prefix (if any) to be
* added, and various other table name mappings to be performed.
);
}
+ public function provideTableNamesWithIndexClauseOrJOIN() {
+ return [
+ 'one-element array' => [
+ [ 'table' ], [], 'table '
+ ],
+ 'comma join' => [
+ [ 'table1', 'table2' ], [], 'table1,table2 '
+ ],
+ 'real join' => [
+ [ 'table1', 'table2' ],
+ [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
+ 'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
+ ],
+ 'real join with multiple conditionals' => [
+ [ 'table1', 'table2' ],
+ [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
+ 'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
+ ],
+ 'join with parenthesized group' => [
+ [ 'table1', 'n' => [ 'table2', 'table3' ] ],
+ [
+ 'table3' => [ 'JOIN', 't2_id = t3_id' ],
+ 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
+ ],
+ 'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideTableNamesWithIndexClauseOrJOIN
+ * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
+ */
+ public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
+ $clause = TestingAccessWrapper::newFromObject( $this->db )
+ ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
+ $this->assertSame( $expect, $clause );
+ }
+
/**
* @covers Wikimedia\Rdbms\Database::onTransactionIdle
* @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks