diff --git a/availability/classes/info_module.php b/availability/classes/info_module.php index 952aa7b8474..4091c747a9d 100644 --- a/availability/classes/info_module.php +++ b/availability/classes/info_module.php @@ -129,7 +129,7 @@ class info_module extends info { return $sectionresult; } - return array('(' . $sectionresult[0] . ') INTERSECT (' . $moduleresult[0] . ')', + return array($DB->sql_intersect(array($sectionresult[0], $moduleresult[0]), 'id'), array_merge($sectionresult[1], $moduleresult[1])); } diff --git a/availability/classes/tree.php b/availability/classes/tree.php index 49cfc80c882..218748c1813 100644 --- a/availability/classes/tree.php +++ b/availability/classes/tree.php @@ -351,6 +351,7 @@ class tree extends tree_node { } public function get_user_list_sql($not, info $info, $onlyactive) { + global $DB; // Get logic flags from operator. list($innernot, $andoperator) = $this->get_logic_flags($not); @@ -381,17 +382,16 @@ class tree extends tree_node { // Combine results using INTERSECT or UNION. $outsql = null; - $outparams = null; + $subsql = array(); + $outparams = array(); foreach ($childresults as $childresult) { - if (!$outsql) { - $outsql = '(' . $childresult[0] . ')'; - $outparams = $childresult[1]; - } else { - $outsql .= $andoperator ? ' INTERSECT (' : ' UNION ('; - $outsql .= $childresult[0]; - $outsql .= ')'; - $outparams = array_merge($outparams, $childresult[1]); - } + $subsql[] = $childresult[0]; + $outparams = array_merge($outparams, $childresult[1]); + } + if ($andoperator) { + $outsql = $DB->sql_intersect($subsql, 'id'); + } else { + $outsql = '(' . join(') UNION (', $subsql) . ')'; } return array($outsql, $outparams); } diff --git a/lib/dml/moodle_database.php b/lib/dml/moodle_database.php index 499e406b75e..ce28102e1b8 100644 --- a/lib/dml/moodle_database.php +++ b/lib/dml/moodle_database.php @@ -2226,6 +2226,29 @@ abstract class moodle_database { return ''; } + /** + * Returns the SQL that allows to find intersection of two or more queries + * + * @since Moodle 2.8 + * + * @param array $selects array of SQL select queries, each of them only returns fields with the names from $fields + * @param string $fields comma-separated list of fields (used only by some DB engines) + * @return string SQL query that will return only values that are present in each of selects + */ + public function sql_intersect($selects, $fields) { + if (!count($selects)) { + throw new coding_exception('sql_intersect() requires at least one element in $selects'); + } else if (count($selects) == 1) { + return $selects[0]; + } + static $aliascnt = 0; + $rv = '('.$selects[0].')'; + for ($i = 1; $i < count($selects); $i++) { + $rv .= " INTERSECT (".$selects[$i].')'; + } + return $rv; + } + /** * Does this driver support tool_replace? * diff --git a/lib/dml/mysqli_native_moodle_database.php b/lib/dml/mysqli_native_moodle_database.php index 11b53bf684a..a27147a63a5 100644 --- a/lib/dml/mysqli_native_moodle_database.php +++ b/lib/dml/mysqli_native_moodle_database.php @@ -1601,6 +1601,37 @@ class mysqli_native_moodle_database extends moodle_database { return ' CAST(' . $fieldname . ' AS SIGNED) '; } + /** + * Returns the SQL that allows to find intersection of two or more queries + * + * @since Moodle 2.8 + * + * @param array $selects array of SQL select queries, each of them only returns fields with the names from $fields + * @param string $fields comma-separated list of fields + * @return string SQL query that will return only values that are present in each of selects + */ + public function sql_intersect($selects, $fields) { + if (count($selects) <= 1) { + return parent::sql_intersect($selects, $fields); + } + $fields = preg_replace('/\s/', '', $fields); + static $aliascnt = 0; + $falias = 'intsctal'.($aliascnt++); + $rv = "SELECT $falias.". + preg_replace('/,/', ','.$falias.'.', $fields). + " FROM ($selects[0]) $falias"; + for ($i = 1; $i < count($selects); $i++) { + $alias = 'intsctal'.($aliascnt++); + $rv .= " JOIN (".$selects[$i].") $alias ON ". + join(' AND ', + array_map( + create_function('$a', 'return "'.$falias.'.$a = '.$alias.'.$a";'), + preg_split('/,/', $fields)) + ); + } + return $rv; + } + /** * Does this driver support tool_replace? * diff --git a/lib/dml/tests/dml_test.php b/lib/dml/tests/dml_test.php index 3d0b6c46be8..8ca100c65f4 100644 --- a/lib/dml/tests/dml_test.php +++ b/lib/dml/tests/dml_test.php @@ -5252,6 +5252,88 @@ class core_dml_testcase extends database_driver_testcase { $totaldbqueries = $DB->perf_get_reads() + $DB->perf_get_writes(); $this->assertEquals($totaldbqueries, $DB->perf_get_queries()); } + + public function test_sql_intersect() { + $DB = $this->tdb; + $dbman = $this->tdb->get_manager(); + + $tables = array(); + for ($i = 0; $i < 3; $i++) { + $table = $this->get_test_table('i'.$i); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('ival', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, '0'); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $dbman->create_table($table); + $tables[$i] = $table; + } + $DB->insert_record($tables[0]->getName(), array('ival' => 1, 'name' => 'One'), false); + $DB->insert_record($tables[0]->getName(), array('ival' => 2, 'name' => 'Two'), false); + $DB->insert_record($tables[0]->getName(), array('ival' => 3, 'name' => 'Three'), false); + $DB->insert_record($tables[0]->getName(), array('ival' => 4, 'name' => 'Four'), false); + + $DB->insert_record($tables[1]->getName(), array('ival' => 1, 'name' => 'One'), false); + $DB->insert_record($tables[1]->getName(), array('ival' => 2, 'name' => 'Two'), false); + $DB->insert_record($tables[1]->getName(), array('ival' => 3, 'name' => 'Three'), false); + + $DB->insert_record($tables[2]->getName(), array('ival' => 1, 'name' => 'One'), false); + $DB->insert_record($tables[2]->getName(), array('ival' => 2, 'name' => 'Two'), false); + $DB->insert_record($tables[2]->getName(), array('ival' => 5, 'name' => 'Five'), false); + + // Intersection on the int column. + $params = array('excludename' => 'Two'); + $sql1 = 'SELECT ival FROM {'.$tables[0]->getName().'}'; + $sql2 = 'SELECT ival FROM {'.$tables[1]->getName().'} WHERE name <> :excludename'; + $sql3 = 'SELECT ival FROM {'.$tables[2]->getName().'}'; + + $sql = $DB->sql_intersect(array($sql1), 'ival') . ' ORDER BY ival'; + $this->assertEquals(array(1, 2, 3, 4), $DB->get_fieldset_sql($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival') . ' ORDER BY ival'; + $this->assertEquals(array(1, 3), $DB->get_fieldset_sql($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival') . ' ORDER BY ival'; + $this->assertEquals(array(1), + $DB->get_fieldset_sql($sql, $params)); + + // Intersection on the char column. + $params = array('excludeival' => 2); + $sql1 = 'SELECT name FROM {'.$tables[0]->getName().'}'; + $sql2 = 'SELECT name FROM {'.$tables[1]->getName().'} WHERE ival <> :excludeival'; + $sql3 = 'SELECT name FROM {'.$tables[2]->getName().'}'; + + $sql = $DB->sql_intersect(array($sql1), 'name') . ' ORDER BY name'; + $this->assertEquals(array('Four', 'One', 'Three', 'Two'), $DB->get_fieldset_sql($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2), 'name') . ' ORDER BY name'; + $this->assertEquals(array('One', 'Three'), $DB->get_fieldset_sql($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'name') . ' ORDER BY name'; + $this->assertEquals(array('One'), $DB->get_fieldset_sql($sql, $params)); + + // Intersection on the several columns. + $params = array('excludename' => 'Two'); + $sql1 = 'SELECT ival, name FROM {'.$tables[0]->getName().'}'; + $sql2 = 'SELECT ival, name FROM {'.$tables[1]->getName().'} WHERE name <> :excludename'; + $sql3 = 'SELECT ival, name FROM {'.$tables[2]->getName().'}'; + + $sql = $DB->sql_intersect(array($sql1), 'ival, name') . ' ORDER BY ival'; + $this->assertEquals(array(1 => 'One', 2 => 'Two', 3 => 'Three', 4 => 'Four'), + $DB->get_records_sql_menu($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2), 'ival, name') . ' ORDER BY ival'; + $this->assertEquals(array(1 => 'One', 3 => 'Three'), + $DB->get_records_sql_menu($sql, $params)); + + $sql = $DB->sql_intersect(array($sql1, $sql2, $sql3), 'ival, name') . ' ORDER BY ival'; + $this->assertEquals(array(1 => 'One'), + $DB->get_records_sql_menu($sql, $params)); + + // Drop temporary tables. + foreach ($tables as $table) { + $dbman->drop_table($table); + } + } } /**