diff --git a/lib/db/install.xml b/lib/db/install.xml index bf74cba12ce..a0b60ca97c8 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -4891,7 +4891,7 @@ - + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d29f71da97a..07c40d79c09 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1297,7 +1297,7 @@ function xmldb_main_upgrade($oldversion) { $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('actionname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); $table->add_field('actionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); - $table->add_field('success', XMLDB_TYPE_BINARY, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('success', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0'); $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); $table->add_field('provider', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); @@ -1365,5 +1365,13 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2024091000.01); } + if ($oldversion < 2024091700.01) { + // Convert the ai_action_register.success column to an integer, if necessary. + upgrade_change_binary_column_to_int('ai_action_register', 'success', XMLDB_NOTNULL, 'actionid'); + + // Main savepoint reached. + upgrade_main_savepoint(true, 2024091700.01); + } + return true; } diff --git a/lib/db/upgradelib.php b/lib/db/upgradelib.php index 45d52142a71..0f6fc32d576 100644 --- a/lib/db/upgradelib.php +++ b/lib/db/upgradelib.php @@ -1821,3 +1821,88 @@ function upgrade_add_foreign_key_and_indexes() { // Launch add key contextid. $dbman->add_key($table, $key); } + +/** + * Upgrade helper to change a binary column to an integer column with a length of 1 in a consistent manner across databases. + * + * This function will + * - rename the existing column to a temporary name, + * - add a new column with the integer type, + * - copy the values from the old column to the new column, + * - and finally, drop the old column. + * + * This function will do nothing if the field is already an integer. + * + * The new column with the integer type will need to have a default value of 0. + * This is to avoid breaking the not null constraint, if it's set, especially if there are existing records. + * Please make sure that the column definition in install.xml also has the `DEFAULT` attribute value set to 0. + * + * @param string $tablename The name of the table. + * @param string $fieldname The name of the field to be converted. + * @param bool|null $notnull {@see XMLDB_NOTNULL} or null. + * @param string|null $previous The name of the field that this field should come after. + * @return bool + */ +function upgrade_change_binary_column_to_int( + string $tablename, + string $fieldname, + ?bool $notnull = null, + ?string $previous = null, +): bool { + global $DB; + + // Get the information about the field to be converted. + $columns = $DB->get_columns($tablename); + $toconvert = $columns[$fieldname]; + + // Check if the field to be converted is already an integer-type column (`meta_type` property of 'I'). + if ($toconvert->meta_type === 'I') { + // Nothing to do if the field is already an integer-type. + return false; + } else if (!$toconvert->binary) { + throw new \core\exception\coding_exception( + 'This function is only used to convert XMLDB_TYPE_BINARY fields to XMLDB_TYPE_INTEGER fields. ' + . 'For other field types, please check out \database_manager::change_field_type()' + ); + } + + $dbman = $DB->get_manager(); + $table = new xmldb_table($tablename); + // Temporary rename the field. We'll drop this later. + $tmpfieldname = "tmp$fieldname"; + $field = new xmldb_field($fieldname, XMLDB_TYPE_BINARY); + $dbman->rename_field($table, $field, $tmpfieldname); + + // Add the new field wih the integer type. + $field = new xmldb_field($fieldname, XMLDB_TYPE_INTEGER, '1', null, $notnull, null, '0', $previous); + $dbman->add_field($table, $field); + + // Copy the 'true' values from the old field to the new field. + if ($DB->get_dbfamily() === 'oracle') { + // It's tricky to use the binary column in the WHERE clause in Oracle DBs. + // Let's go updating the records one by one. It's nasty, but it's only done for instances with Oracle DBs. + // The normal SQL UPDATE statement will be used for other DBs. + $columns = implode(', ', ['id', $tmpfieldname, $fieldname]); + $records = $DB->get_recordset($tablename, null, '', $columns); + if ($records->valid()) { + foreach ($records as $record) { + if (!$record->$tmpfieldname) { + continue; + } + $DB->set_field($tablename, $fieldname, 1, ['id' => $record->id]); + } + } + $records->close(); + } else { + $sql = 'UPDATE {' . $tablename . '} + SET ' . $fieldname . ' = 1 + WHERE ' . $tmpfieldname . ' = ?'; + $DB->execute($sql, [1]); + } + + // Drop the old field. + $oldfield = new xmldb_field($tmpfieldname); + $dbman->drop_field($table, $oldfield); + + return true; +} diff --git a/lib/tests/upgradelib_test.php b/lib/tests/upgradelib_test.php index 760fccbee1f..0c27364d706 100644 --- a/lib/tests/upgradelib_test.php +++ b/lib/tests/upgradelib_test.php @@ -1640,4 +1640,99 @@ calendar,core_calendar|/calendar/view.php?view=month', $upgrade = get_config('core', 'upgraderunning'); $this->assertFalse($upgrade); } + + /** + * Data provider for {@see test_upgrade_change_binary_column_to_int()}. + * + * @return array[] + */ + public static function upgrade_change_binary_column_to_int_provider(): array { + return [ + 'Binary column' => [ + XMLDB_TYPE_BINARY, + null, + true, + false, + ], + 'Integer column' => [ + XMLDB_TYPE_INTEGER, + '1', + false, + false, + ], + 'Non-binary and non-integer column' => [ + XMLDB_TYPE_TEXT, + null, + false, + true, + ], + ]; + } + + /** + * Unit test for {@see upgrade_change_binary_column_to_int()}. + * + * @dataProvider upgrade_change_binary_column_to_int_provider + * @covers ::upgrade_change_binary_column_to_int() + * @param int $type The field type. + * @param string|null $length The field length. + * @param bool $expectedresult Whether the conversion succeeded. + * @param bool $expecexception Whether to expect an exception. + * @return void + */ + public function test_upgrade_change_binary_column_to_int( + int $type, + ?string $length, + bool $expectedresult, + bool $expecexception, + ): void { + global $DB; + $this->resetAfterTest(); + + $dbman = $DB->get_manager(); + $tmptablename = 'test_convert_table'; + $fieldname = 'success'; + $table = new xmldb_table($tmptablename); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE); + $table->add_field($fieldname, $type, $length, null, XMLDB_NOTNULL); + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $dbman->create_table($table); + + // Insert sample data. + $ones = []; + $truerecord = (object)[$fieldname => 1]; + $falserecord = (object)[$fieldname => 0]; + $ones[] = $DB->insert_record($tmptablename, $truerecord); + $DB->insert_record($tmptablename, $falserecord); + $ones[] = $DB->insert_record($tmptablename, $truerecord); + $DB->insert_record($tmptablename, $falserecord); + $ones[] = $DB->insert_record($tmptablename, $truerecord); + $ones[] = $DB->insert_record($tmptablename, $truerecord); + + if ($expecexception) { + $this->expectException(coding_exception::class); + } + + $result = upgrade_change_binary_column_to_int($tmptablename, $fieldname); + $this->assertEquals($expectedresult, $result); + + // Verify converted column and data. + if ($result) { + $columns = $DB->get_columns($tmptablename); + // Verify the new field has been created and is no longer a binary field. + $this->assertArrayHasKey($fieldname, $columns); + $field = $columns[$fieldname]; + $this->assertFalse($field->binary); + + // Verify that the renamed old field has now been removed. + $this->assertArrayNotHasKey("tmp$fieldname", $columns); + + // Confirm that the values for the converted column are the same. + $records = $DB->get_fieldset($tmptablename, 'id', [$fieldname => 1]); + $this->assertEqualsCanonicalizing($ones, $records); + } + + // Cleanup. + $dbman->drop_table($table); + } } diff --git a/version.php b/version.php index 3dd985dac72..9ed749bd8e9 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2024091700.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2024091700.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.5dev+ (Build: 20240917)'; // Human-friendly version name