From 848ec32df08b06b5abfa4a8bf071feeb1e38a18f Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Wed, 23 Mar 2022 19:23:42 +0100 Subject: [PATCH] MDL-74143 database: Make get_indexes() to return original column names Sometimes, in order to provide a cross-db behaviour of unique indexes mixing null and not null columns, we create, under the hood, some function based indexes. When that happens get_indexes() is returning the name of the expression objects used to calculate that function index. But we need the original column names to be able to compare indexes and get column dependencies properly. So, this patch just looks, when the index is unique and function based, to the expressions (pretty standard CASE statements) and gets the original column name from it. Covered with tests. --- lib/dml/oci_native_moodle_database.php | 17 ++++++++++++- lib/dml/tests/dml_test.php | 35 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/dml/oci_native_moodle_database.php b/lib/dml/oci_native_moodle_database.php index b5d60188891..6db6a7bca86 100644 --- a/lib/dml/oci_native_moodle_database.php +++ b/lib/dml/oci_native_moodle_database.php @@ -438,9 +438,10 @@ class oci_native_moodle_database extends moodle_database { $indexes = array(); $tablename = strtoupper($this->prefix.$table); - $sql = "SELECT i.INDEX_NAME, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, ac.CONSTRAINT_TYPE + $sql = "SELECT i.INDEX_NAME, i.INDEX_TYPE, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, e.COLUMN_EXPRESSION, ac.CONSTRAINT_TYPE FROM ALL_INDEXES i JOIN ALL_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME + LEFT JOIN ALL_IND_EXPRESSIONS e ON (e.INDEX_NAME = c.INDEX_NAME AND e.COLUMN_POSITION = c.COLUMN_POSITION) LEFT JOIN ALL_CONSTRAINTS ac ON (ac.TABLE_NAME=i.TABLE_NAME AND ac.CONSTRAINT_NAME=i.INDEX_NAME AND ac.CONSTRAINT_TYPE='P') WHERE i.TABLE_NAME = '$tablename' ORDER BY i.INDEX_NAME, c.COLUMN_POSITION"; @@ -463,6 +464,20 @@ class oci_native_moodle_database extends moodle_database { 'unique' => ($record['UNIQUENESS'] === 'UNIQUE'), 'columns' => array()); } + + // If this is an unique, function-based, index, then we have to look to the expression + // and calculate the column name by parsing it. + if ($record['UNIQUENESS'] === 'UNIQUE' && $record['INDEX_TYPE'] === 'FUNCTION-BASED NORMAL') { + // Only if there is an expression to look. + if (!empty($record['COLUMN_EXPRESSION'])) { + // Let's parse the usual code used for these unique indexes. + $regex = '/^CASE *WHEN .* THEN "(?[^"]+)" ELSE NULL END *$/'; + if (preg_match($regex, $record['COLUMN_EXPRESSION'], $matches)) { + $record['COLUMN_NAME'] = $matches['column_name'] ?? $record['COLUMN_NAME']; + } + } + } + $indexes[$indexname]['columns'][] = strtolower($record['COLUMN_NAME']); } diff --git a/lib/dml/tests/dml_test.php b/lib/dml/tests/dml_test.php index 7337b3e050b..23f9b7a3bfa 100644 --- a/lib/dml/tests/dml_test.php +++ b/lib/dml/tests/dml_test.php @@ -719,6 +719,41 @@ EOD; $this->assertSame('id', $composed['columns'][1]); } + /** + * Let's verify get_indexes() when we mix null and not null columns in unique indexes. + * + * Some databases, for unique indexes of this type, need to create function indexes to + * provide cross-db behaviour. Here we check that those indexes don't break get_indexes(). + * + * Note that, strictly speaking, unique indexes on null columns are far from ideal. Both + * conceptually and also in practice, because they cause DBs to use full scans in a + * number of situations. But if we support them, we need to ensure get_indexes() work on them. + */ + public function test_get_indexes_unique_mixed_nullability() { + $DB = $this->tdb; + $dbman = $this->tdb->get_manager(); + $table = $this->get_test_table(); + $tablename = $table->getName(); + + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('nullable01', XMLDB_TYPE_INTEGER, 10, null, null, null, null); + $table->add_field('nullable02', XMLDB_TYPE_INTEGER, 10, null, null, null, null); + $table->add_field('nonullable01', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('nonullable02', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $indexcolumns = ['nullable01', 'nonullable01', 'nullable02', 'nonullable02']; + $table->add_index('course-id', XMLDB_INDEX_UNIQUE, $indexcolumns); + $dbman->create_table($table); + + $indexes = $DB->get_indexes($tablename); + $this->assertIsArray($indexes); + $this->assertCount(1, $indexes); + + $index = array_shift($indexes); + $this->assertTrue($index['unique']); + $this->assertSame($indexcolumns, $index['columns']); + } + public function test_get_columns() { $DB = $this->tdb; $dbman = $this->tdb->get_manager();