<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace core;

use database_column_info;
use moodle_database;
use sql_generator;
use xmldb_field;
use xmldb_index;
use xmldb_key;
use xmldb_structure;
use xmldb_table;

/**
 * DDL layer tests.
 *
 * @package    core_ddl
 * @category   test
 * @copyright  2008 Nicolas Connault
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
final class ddl_test extends \database_driver_testcase {
    /** @var xmldb_table[] keys are table name. Created in setUp. */
    private $tables = array();
    /** @var array table name => array of stdClass test records loaded into that table. Created in setUp. */
    private $records = array();

    protected function setUp(): void {
        parent::setUp();
        $dbman = $this->tdb->get_manager(); // Loads DDL libs.

        $table = new xmldb_table('test_table0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('type', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'general');
        $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null);
        $table->add_field('intro', XMLDB_TYPE_TEXT, 'small', null, XMLDB_NOTNULL, null, null);
        $table->add_field('logo', XMLDB_TYPE_BINARY, 'big', null, null, null);
        $table->add_field('assessed', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('assesstimestart', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('assesstimefinish', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('scale', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('maxbytes', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('forcesubscribe', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('trackingtype', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1');
        $table->add_field('rsstype', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('rssarticles', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('grade', XMLDB_TYPE_NUMBER, '20,0', null, null, null, null);
        $table->add_field('percent', XMLDB_TYPE_NUMBER, '5,2', null, null, null, 66.6);
        $table->add_field('bignum', XMLDB_TYPE_NUMBER, '38,18', null, null, null, 1234567890.1234);
        $table->add_field('warnafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('blockafter', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('blockperiod', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_key('course', XMLDB_KEY_UNIQUE, array('course'));
        $table->add_index('type-name', XMLDB_INDEX_UNIQUE, array('type', 'name'));
        $table->add_index('rsstype', XMLDB_INDEX_NOTUNIQUE, array('rsstype'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        // Define 2 initial records for this table.
        $this->records[$table->getName()] = array(
            (object)array(
                'course' => '1',
                'type'   => 'general',
                'name'   => 'record',
                'intro'  => 'first record'),
            (object)array(
                'course' => '2',
                'type'   => 'social',
                'name'   => 'record',
                'intro'  => 'second record'));

        // Second, smaller table.
        $table = new xmldb_table ('test_table1');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, null, null, 'Moodle');
        $table->add_field('secondname', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null);
        $table->add_field('thirdname', XMLDB_TYPE_CHAR, '30', null, null, null, ''); // Nullable column with empty default.
        $table->add_field('intro', XMLDB_TYPE_TEXT, 'medium', null, XMLDB_NOTNULL, null, null);
        $table->add_field('avatar', XMLDB_TYPE_BINARY, 'medium', null, null, null, null);
        $table->add_field('grade', XMLDB_TYPE_NUMBER, '20,10', null, null, null);
        $table->add_field('gradefloat', XMLDB_TYPE_FLOAT, '20,0', null, null, null, null);
        $table->add_field('percentfloat', XMLDB_TYPE_FLOAT, '5,2', null, null, null, 99.9);
        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_key('course', XMLDB_KEY_FOREIGN_UNIQUE, array('course'), 'test_table0', array('course'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        // Define 2 initial records for this table.
        $this->records[$table->getName()] = array(
            (object)array(
                'course' => '1',
                'secondname'   => 'first record', // Less than 10 cc, please don't modify. Some tests below depend of this.
                'intro'  => 'first record'),
            (object)array(
                'course'       => '2',
                'secondname'   => 'second record', // More than 10 cc, please don't modify. Some tests below depend of this.
                'intro'  => 'second record'));
    }

    private function create_deftable($tablename) {
        $dbman = $this->tdb->get_manager();

        if (!isset($this->tables[$tablename])) {
            return null;
        }

        $table = $this->tables[$tablename];

        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);

        return $table;
    }

    /**
     * Fill the given test table with some records, as far as
     * DDL behaviour must be tested both with real data and
     * with empty tables
     * @param string $tablename
     * @return int count of records
     */
    private function fill_deftable($tablename) {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        if (!isset($this->records[$tablename])) {
            return null;
        }

        if ($dbman->table_exists($tablename)) {
            foreach ($this->records[$tablename] as $row) {
                $DB->insert_record($tablename, $row);
            }
        } else {
            return null;
        }

        return count($this->records[$tablename]);
    }

    /**
     * Test behaviour of table_exists()
     */
    public function test_table_exists(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // First make sure it returns false if table does not exist.
        $table = $this->tables['test_table0'];

        try {
            $result = $DB->get_records('test_table0');
        } catch (\dml_exception $e) {
            $result = false;
        }
        $this->resetDebugging();

        $this->assertFalse($result);

        $this->assertFalse($dbman->table_exists('test_table0')); // By name..
        $this->assertFalse($dbman->table_exists($table));        // By xmldb_table..

        // Create table and test again.
        $dbman->create_table($table);

        $this->assertSame(array(), $DB->get_records('test_table0'));
        $this->assertTrue($dbman->table_exists('test_table0')); // By name.
        $this->assertTrue($dbman->table_exists($table));        // By xmldb_table.

        // Drop table and test again.
        $dbman->drop_table($table);

        try {
            $result = $DB->get_records('test_table0');
        } catch (\dml_exception $e) {
            $result = false;
        }
        $this->resetDebugging();

        $this->assertFalse($result);

        $this->assertFalse($dbman->table_exists('test_table0')); // By name.
        $this->assertFalse($dbman->table_exists($table));        // By xmldb_table.
    }

    /**
     * Test behaviour of create_table()
     */
    public function test_create_table(): void {

        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Create table.
        $table = $this->tables['test_table1'];

        $dbman->create_table($table);
        $this->assertTrue($dbman->table_exists($table));

        // Basic get_tables() test.
        $tables = $DB->get_tables();
        $this->assertArrayHasKey('test_table1', $tables);

        // Basic get_columns() tests.
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('R', $columns['id']->meta_type);
        $this->assertSame('I', $columns['course']->meta_type);
        $this->assertSame('C', $columns['name']->meta_type);
        $this->assertSame('C', $columns['secondname']->meta_type);
        $this->assertSame('C', $columns['thirdname']->meta_type);
        $this->assertSame('X', $columns['intro']->meta_type);
        $this->assertSame('B', $columns['avatar']->meta_type);
        $this->assertSame('N', $columns['grade']->meta_type);
        $this->assertSame('N', $columns['percentfloat']->meta_type);
        $this->assertSame('I', $columns['userid']->meta_type);
        // Some defaults.
        $this->assertTrue($columns['course']->has_default);
        $this->assertEquals(0, $columns['course']->default_value);
        $this->assertTrue($columns['name']->has_default);
        $this->assertSame('Moodle', $columns['name']->default_value);
        $this->assertTrue($columns['secondname']->has_default);
        $this->assertSame('', $columns['secondname']->default_value);
        $this->assertTrue($columns['thirdname']->has_default);
        $this->assertSame('', $columns['thirdname']->default_value);
        $this->assertTrue($columns['percentfloat']->has_default);
        $this->assertEquals(99.9, $columns['percentfloat']->default_value);
        $this->assertTrue($columns['userid']->has_default);
        $this->assertEquals(0, $columns['userid']->default_value);

        // Basic get_indexes() test.
        $indexes = $DB->get_indexes('test_table1');
        $courseindex = reset($indexes);
        $this->assertEquals(1, $courseindex['unique']);
        $this->assertSame('course', $courseindex['columns'][0]);

        // Check sequence returns 1 for first insert.
        $rec = (object)array(
            'course'     => 10,
            'secondname' => 'not important',
            'intro'      => 'not important');
        $this->assertSame(1, $DB->insert_record('test_table1', $rec));

        // Check defined defaults are working ok.
        $dbrec = $DB->get_record('test_table1', array('id' => 1));
        $this->assertSame('Moodle', $dbrec->name);
        $this->assertSame('', $dbrec->thirdname);

        // Check exceptions if multiple R columns.
        $table = new xmldb_table ('test_table2');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('rid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_key('primaryx', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // Check exceptions missing primary key on R column.
        $table = new xmldb_table ('test_table2');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // Long table name names - the largest allowed by the configuration which exclude the prefix to ensure it's created.
        $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH);
        $table = new xmldb_table($tablechars);
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        $dbman->create_table($table);
        $this->assertTrue($dbman->table_exists($table));
        $dbman->drop_table($table);

        // Table name is too long, ignoring any prefix size set.
        $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH + 1);
        $table = new xmldb_table($tablechars);
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid table name.
        $table = new xmldb_table('test_tableCD');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Weird column names - the largest allowed.
        $table = new xmldb_table('test_table3');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field(str_repeat('b', xmldb_field::NAME_MAX_LENGTH), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        $dbman->create_table($table);
        $this->assertTrue($dbman->table_exists($table));
        $dbman->drop_table($table);

        // Too long field name.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field(str_repeat('a', xmldb_field::NAME_MAX_LENGTH + 1), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid field name.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('abCD', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid integer length.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '21', null, XMLDB_NOTNULL, null, '2');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid integer default.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 'x');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid decimal length - max precision is 38 digits.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_NUMBER, '39,19', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid decimal decimals - number of decimals can't be higher than total number of digits.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_NUMBER, '10,11', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid decimal whole number - the whole number part can't have more digits than integer fields.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_NUMBER, '38,17', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid decimal decimals - negative scale not supported.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_NUMBER, '30,-5', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid decimal default.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_NUMBER, '10,5', null, XMLDB_NOTNULL, null, 'x');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid float length.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_FLOAT, '21,10', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid float decimals.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_FLOAT, '10,11', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        // Invalid float default.
        $table = new xmldb_table('test_table4');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('num', XMLDB_TYPE_FLOAT, '10,5', null, XMLDB_NOTNULL, null, 'x');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");

        $this->tables[$table->getName()] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }
    }

    /**
     * Test if database supports tables with many TEXT fields,
     * InnoDB is known to failed during data insertion instead
     * of table creation when text fields contain actual data.
     */
    public function test_row_size_limits(): void {

        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $text = str_repeat('š', 1333);

        $data = new \stdClass();
        $data->name = 'test';
        $table = new xmldb_table('test_innodb');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, null, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        for ($i = 0; $i < 20; $i++) {
            $table->add_field('text'.$i, XMLDB_TYPE_TEXT, null, null, null, null, null);
            $data->{'text'.$i} = $text;
        }
        $dbman->create_table($table);

        try {
            $id = $DB->insert_record('test_innodb', $data);
            $expected = (array)$data;
            $expected['id'] = (string)$id;
            $this->assertEqualsCanonicalizing($expected, (array)$DB->get_record('test_innodb', array('id' => $id)));
        } catch (\dml_exception $e) {
            // Give some nice error message when known problematic MySQL with InnoDB detected.
            if ($DB->get_dbfamily() === 'mysql') {
                $engine = strtolower($DB->get_dbengine());
                if ($engine === 'innodb' or $engine === 'xtradb') {
                    if (!$DB->is_compressed_row_format_supported()) {
                        $this->fail("Row size limit reached in MySQL using InnoDB, configure server to use innodb_file_format=Barracuda and innodb_file_per_table=1");
                    }
                }
            }
            throw $e;
        }

        $dbman->drop_table($table);

        $data = new \stdClass();
        $data->name = 'test';
        $table = new xmldb_table('test_innodb');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, null, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_table($table);
        $DB->insert_record('test_innodb', array('name' => 'test'));

        for ($i = 0; $i < 20; $i++) {
            $field = new xmldb_field('text'.$i, XMLDB_TYPE_TEXT, null, null, null, null, null);
            $dbman->add_field($table, $field);
            $data->{'text'.$i} = $text;

            $id = $DB->insert_record('test_innodb', $data);
            $expected = (array)$data;
            $expected['id'] = (string)$id;
            $this->assertEqualsCanonicalizing($expected, (array)$DB->get_record('test_innodb', array('id' => $id)));
        }

        $dbman->drop_table($table);

        // MySQL VARCHAR fields may hit a different 65535 row size limit when creating tables.
        $data = new \stdClass();
        $data->name = 'test';
        $table = new xmldb_table('test_innodb');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, null, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        for ($i = 0; $i < 12; $i++) {
            $table->add_field('text'.$i, XMLDB_TYPE_CHAR, '1333', null, null, null, null);
            $data->{'text'.$i} = $text;
        }
        $dbman->create_table($table);

        $id = $DB->insert_record('test_innodb', $data);
        $expected = (array)$data;
        $expected['id'] = (string)$id;
        $this->assertEqualsCanonicalizing($expected, (array)$DB->get_record('test_innodb', array('id' => $id)));

        $dbman->drop_table($table);
    }

    /**
     * Test behaviour of drop_table()
     */
    public function test_drop_table(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Initially table doesn't exist.
        $this->assertFalse($dbman->table_exists('test_table0'));

        // Create table with contents.
        $table = $this->create_deftable('test_table0');
        $this->assertTrue($dbman->table_exists('test_table0'));

        // Fill the table with some records before dropping it.
        $this->fill_deftable('test_table0');

        // Drop by xmldb_table object.
        $dbman->drop_table($table);
        $this->assertFalse($dbman->table_exists('test_table0'));

        // Basic get_tables() test.
        $tables = $DB->get_tables();
        $this->assertArrayNotHasKey('test_table0', $tables);

        // Columns cache must be empty.
        $columns = $DB->get_columns('test_table0');
        $this->assertEmpty($columns);

        $indexes = $DB->get_indexes('test_table0');
        $this->assertEmpty($indexes);
    }

    /**
     * Test behaviour of rename_table()
     */
    public function test_rename_table(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');

        // Fill the table with some records before renaming it.
        $insertedrows = $this->fill_deftable('test_table1');

        $this->assertFalse($dbman->table_exists('test_table_cust1'));
        $dbman->rename_table($table, 'test_table_cust1');
        $this->assertTrue($dbman->table_exists('test_table_cust1'));

        // Check sequence returns $insertedrows + 1 for this insert (after rename).
        $rec = (object)array(
            'course'     => 20,
            'secondname' => 'not important',
            'intro'      => 'not important');
        $this->assertSame($insertedrows+1, $DB->insert_record('test_table_cust1', $rec));

        // Verify behavior when target table already exists.
        $sourcetable = $this->create_deftable('test_table0');
        $targettable = $this->create_deftable('test_table1');
        try {
            $dbman->rename_table($sourcetable, $targettable->getName());
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
            $this->assertEquals('Table "test_table1" already exists (can not rename table)', $e->getMessage());
        }
    }

    /**
     * Test behaviour of field_exists()
     */
    public function test_field_exists(): void {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table0');

        // String params.
        // Give a nonexistent table as first param (throw exception).
        try {
            $dbman->field_exists('nonexistenttable', 'id');
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('moodle_exception', $e);
        }

        // Give a nonexistent field as second param (return false).
        $this->assertFalse($dbman->field_exists('test_table0', 'nonexistentfield'));

        // Correct string params.
        $this->assertTrue($dbman->field_exists('test_table0', 'id'));

        // Object params.
        $realfield = $table->getField('id');

        // Give a nonexistent table as first param (throw exception).
        $nonexistenttable = new xmldb_table('nonexistenttable');
        try {
            $dbman->field_exists($nonexistenttable, $realfield);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('moodle_exception', $e);
        }

        // Give a nonexistent field as second param (return false).
        $nonexistentfield = new xmldb_field('nonexistentfield');
        $this->assertFalse($dbman->field_exists($table, $nonexistentfield));

        // Correct object params.
        $this->assertTrue($dbman->field_exists($table, $realfield));

        // Mix string and object params.
        // Correct ones.
        $this->assertTrue($dbman->field_exists($table, 'id'));
        $this->assertTrue($dbman->field_exists('test_table0', $realfield));
        // Non existing tables (throw exception).
        try {
            $this->assertFalse($dbman->field_exists($nonexistenttable, 'id'));
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('moodle_exception', $e);
        }
        try {
            $this->assertFalse($dbman->field_exists('nonexistenttable', $realfield));
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('moodle_exception', $e);
        }
        // Non existing fields (return false).
        $this->assertFalse($dbman->field_exists($table, 'nonexistentfield'));
        $this->assertFalse($dbman->field_exists('test_table0', $nonexistentfield));
    }

    /**
     * Test behaviour of add_field()
     */
    public function test_add_field(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');

        // Fill the table with some records before adding fields.
        $this->fill_deftable('test_table1');

        // Add one not null field without specifying default value (throws ddl_exception).
        $field = new xmldb_field('onefield');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, null);
        try {
            $dbman->add_field($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // Add one existing field (throws ddl_exception).
        $field = new xmldb_field('course');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, 2);
        try {
            $dbman->add_field($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // TODO: add one field with invalid type, must throw exception.
        // TODO: add one text field with default, must throw exception.
        // TODO: add one binary field with default, must throw exception.

        // Add one integer field and check it.
        $field = new xmldb_field('oneinteger');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, 2);
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'oneinteger'));
        $columns = $DB->get_columns('test_table1');
        $this->assertEquals('oneinteger', $columns['oneinteger']->name);
        $this->assertTrue($columns['oneinteger']->not_null);
        // Max_length and scale cannot be checked under all DBs at all for integer fields.
        $this->assertFalse($columns['oneinteger']->primary_key);
        $this->assertFalse($columns['oneinteger']->binary);
        $this->assertTrue($columns['oneinteger']->has_default);
        $this->assertEquals(2, $columns['oneinteger']->default_value);
        $this->assertSame('I', $columns['oneinteger']->meta_type);
        $this->assertEquals(2, $DB->get_field('test_table1', 'oneinteger', array(), IGNORE_MULTIPLE)); // Check default has been applied.

        // Add one numeric field and check it.
        $field = new xmldb_field('onenumber');
        $field->set_attributes(XMLDB_TYPE_NUMBER, '6,3', null, XMLDB_NOTNULL, null, 2.55);
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onenumber'));
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('onenumber', $columns['onenumber']->name);
        $this->assertEquals(6, $columns['onenumber']->max_length);
        $this->assertEquals(3, $columns['onenumber']->scale);
        $this->assertTrue($columns['onenumber']->not_null);
        $this->assertFalse($columns['onenumber']->primary_key);
        $this->assertFalse($columns['onenumber']->binary);
        $this->assertTrue($columns['onenumber']->has_default);
        $this->assertEquals(2.550, $columns['onenumber']->default_value);
        $this->assertSame('N', $columns['onenumber']->meta_type);
        $this->assertEquals(2.550, $DB->get_field('test_table1', 'onenumber', array(), IGNORE_MULTIPLE)); // Check default has been applied.

        // Add one numeric field with scale of 0 and check it.
        $field = new xmldb_field('onenumberwith0scale');
        $field->set_attributes(XMLDB_TYPE_NUMBER, '6,0', null, XMLDB_NOTNULL, null, 2);
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onenumberwith0scale'));
        $columns = $DB->get_columns('test_table1');
        $this->assertEquals(6, $columns['onenumberwith0scale']->max_length);
        // We can not use assertEquals as that accepts null/false as a valid value.
        $this->assertSame('0', strval($columns['onenumberwith0scale']->scale));

        // Add one float field and check it (not official type - must work as number).
        $field = new xmldb_field('onefloat');
        $field->set_attributes(XMLDB_TYPE_FLOAT, '6,3', null, XMLDB_NOTNULL, null, 3.550);
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onefloat'));
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('onefloat', $columns['onefloat']->name);
        $this->assertTrue($columns['onefloat']->not_null);
        // Max_length and scale cannot be checked under all DBs at all for float fields.
        $this->assertFalse($columns['onefloat']->primary_key);
        $this->assertFalse($columns['onefloat']->binary);
        $this->assertTrue($columns['onefloat']->has_default);
        $this->assertEquals(3.550, $columns['onefloat']->default_value);
        $this->assertSame('N', $columns['onefloat']->meta_type);
        // Just rounding DB information to 7 decimal digits. Fair enough to test 3.550 and avoids one nasty bug
        // in MSSQL core returning wrong floats (http://social.msdn.microsoft.com/Forums/en-US/sqldataaccess/thread/5e08de63-16bb-4f24-b645-0cf8fc669de3)
        // In any case, floats aren't officially supported by Moodle, with number/decimal type being the correct ones, so
        // this isn't a real problem at all.
        $this->assertEquals(3.550, round($DB->get_field('test_table1', 'onefloat', array(), IGNORE_MULTIPLE), 7)); // Check default has been applied.

        // Add one char field and check it.
        $field = new xmldb_field('onechar');
        $field->set_attributes(XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, 'Nice dflt!');
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onechar'));
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('onechar', $columns['onechar']->name);
        $this->assertEquals(25, $columns['onechar']->max_length);
        $this->assertNull($columns['onechar']->scale);
        $this->assertTrue($columns['onechar']->not_null);
        $this->assertFalse($columns['onechar']->primary_key);
        $this->assertFalse($columns['onechar']->binary);
        $this->assertTrue($columns['onechar']->has_default);
        $this->assertSame('Nice dflt!', $columns['onechar']->default_value);
        $this->assertSame('C', $columns['onechar']->meta_type);
        $this->assertEquals('Nice dflt!', $DB->get_field('test_table1', 'onechar', array(), IGNORE_MULTIPLE)); // Check default has been applied.

        // Add one big text field and check it.
        $field = new xmldb_field('onetext');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'big');
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onetext'));
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('onetext', $columns['onetext']->name);
        $this->assertEquals(-1, $columns['onetext']->max_length); // -1 means unknown or big.
        $this->assertNull($columns['onetext']->scale);
        $this->assertFalse($columns['onetext']->not_null);
        $this->assertFalse($columns['onetext']->primary_key);
        $this->assertFalse($columns['onetext']->binary);
        $this->assertFalse($columns['onetext']->has_default);
        $this->assertNull($columns['onetext']->default_value);
        $this->assertSame('X', $columns['onetext']->meta_type);

        // Add one medium text field and check it.
        $field = new xmldb_field('mediumtext');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'medium');
        $dbman->add_field($table, $field);
        $columns = $DB->get_columns('test_table1');
        $this->assertTrue(($columns['mediumtext']->max_length == -1) or ($columns['mediumtext']->max_length >= 16777215)); // -1 means unknown or big.

        // Add one small text field and check it.
        $field = new xmldb_field('smalltext');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'small');
        $dbman->add_field($table, $field);
        $columns = $DB->get_columns('test_table1');
        $this->assertTrue(($columns['smalltext']->max_length == -1) or ($columns['smalltext']->max_length >= 65535)); // -1 means unknown or big.

        // Add one binary field and check it.
        $field = new xmldb_field('onebinary');
        $field->set_attributes(XMLDB_TYPE_BINARY);
        $dbman->add_field($table, $field);
        $this->assertTrue($dbman->field_exists($table, 'onebinary'));
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('onebinary', $columns['onebinary']->name);
        $this->assertEquals(-1, $columns['onebinary']->max_length);
        $this->assertNull($columns['onebinary']->scale);
        $this->assertFalse($columns['onebinary']->not_null);
        $this->assertFalse($columns['onebinary']->primary_key);
        $this->assertTrue($columns['onebinary']->binary);
        $this->assertFalse($columns['onebinary']->has_default);
        $this->assertNull($columns['onebinary']->default_value);
        $this->assertSame('B', $columns['onebinary']->meta_type);

        // TODO: check datetime type. Although unused should be fully supported.
    }

    /**
     * Test behaviour of drop_field()
     */
    public function test_drop_field(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table0');

        // Fill the table with some records before dropping fields.
        $this->fill_deftable('test_table0');

        // Drop field with simple xmldb_field having indexes, must return exception.
        $field = new xmldb_field('type'); // Field has indexes and default clause.
        $this->assertTrue($dbman->field_exists($table, 'type'));
        try {
            $dbman->drop_field($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_dependency_exception', $e);
        }
        $this->assertTrue($dbman->field_exists($table, 'type')); // Continues existing, drop aborted.

        // Drop field with complete xmldb_field object and related indexes, must return exception.
        $field = $table->getField('course'); // Field has indexes and default clause.
        $this->assertTrue($dbman->field_exists($table, $field));
        try {
            $dbman->drop_field($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_dependency_exception', $e);
        }
        $this->assertTrue($dbman->field_exists($table, $field)); // Continues existing, drop aborted.

        // Drop one non-existing field, must return exception.
        $field = new xmldb_field('nonexistingfield');
        $this->assertFalse($dbman->field_exists($table, $field));
        try {
            $dbman->drop_field($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_field_missing_exception', $e);
        }

        // Drop field with simple xmldb_field, not having related indexes.
        $field = new xmldb_field('forcesubscribe'); // Field has default clause.
        $this->assertTrue($dbman->field_exists($table, 'forcesubscribe'));
        $dbman->drop_field($table, $field);
        $this->assertFalse($dbman->field_exists($table, 'forcesubscribe'));

        // Drop field with complete xmldb_field object, not having related indexes.
        $field = new xmldb_field('trackingtype'); // Field has default clause.
        $this->assertTrue($dbman->field_exists($table, $field));
        $dbman->drop_field($table, $field);
        $this->assertFalse($dbman->field_exists($table, $field));
    }

    /**
     * Test behaviour of change_field_type()
     */
    public function test_change_field_type(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Create table with indexed field and not indexed field to
        // perform tests in both fields, both having defaults.
        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('onenumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
        $table->add_field('anothernumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '4');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('onenumber', XMLDB_INDEX_NOTUNIQUE, array('onenumber'));
        $dbman->create_table($table);

        $record = new \stdClass();
        $record->onenumber = 2;
        $record->anothernumber = 4;
        $recoriginal = $DB->insert_record('test_table_cust0', $record);

        // Change column from integer to varchar. Must return exception because of dependent index.
        $field = new xmldb_field('onenumber');
        $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'test');
        try {
            $dbman->change_field_type($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_dependency_exception', $e);
        }
        // Column continues being integer 10 not null default 2.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('I', $columns['onenumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change column from integer to varchar. Must work because column has no dependencies.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'test');
        $dbman->change_field_type($table, $field);
        // Column is char 30 not null default 'test' now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('C', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change column back from char to integer.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '8', null, XMLDB_NOTNULL, null, '5');
        $dbman->change_field_type($table, $field);
        // Column is integer 8 not null default 5 now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('I', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change column once more from integer to char.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, "test'n drop");
        $dbman->change_field_type($table, $field);
        // Column is char 30 not null default "test'n drop" now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('C', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Insert one string value and try to convert to integer. Must throw exception.
        $record = new \stdClass();
        $record->onenumber = 7;
        $record->anothernumber = 'string value';
        $rectodrop = $DB->insert_record('test_table_cust0', $record);
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '5');
        try {
            $dbman->change_field_type($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_change_structure_exception', $e);
        }
        // Column continues being char 30 not null default "test'n drop" now.
        $this->assertSame('C', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.
        $DB->delete_records('test_table_cust0', array('id' => $rectodrop)); // Delete the string record.

        // Change the column from varchar to float.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_FLOAT, '20,10', null, null, null, null);
        $dbman->change_field_type($table, $field);
        // Column is float 20,10 null default null.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('N', $columns['anothernumber']->meta_type); // Floats are seen as number.
        // TODO: check the rest of attributes.

        // Change the column back from float to varchar.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'test');
        $dbman->change_field_type($table, $field);
        // Column is char 20 not null default "test" now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('C', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column from varchar to number.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_NUMBER, '20,10', null, null, null, null);
        $dbman->change_field_type($table, $field);
        // Column is number 20,10 null default null now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('N', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column from number to integer.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '2', null, null, null, null);
        $dbman->change_field_type($table, $field);
        // Column is integer 2 null default null now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('I', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column from integer to text.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
        $dbman->change_field_type($table, $field);
        // Column is char text not null default null.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('X', $columns['anothernumber']->meta_type);

        // Change the column back from text to number.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_NUMBER, '20,10', null, null, null, null);
        $dbman->change_field_type($table, $field);
        // Column is number 20,10 null default null now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('N', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column from number to text.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
        $dbman->change_field_type($table, $field);
        // Column is char text not null default "test" now.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('X', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column back from text to integer.
        $field = new xmldb_field('anothernumber');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 10);
        $dbman->change_field_type($table, $field);
        // Column is integer 10 not null default 10.
        $columns = $DB->get_columns('test_table_cust0');
        $this->assertSame('I', $columns['anothernumber']->meta_type);
        // TODO: check the rest of attributes.

        // Check original value has survived to all the type changes.
        $this->assertnotEmpty($rec = $DB->get_record('test_table_cust0', array('id' => $recoriginal)));
        $this->assertEquals(4, $rec->anothernumber);

        $dbman->drop_table($table);
        $this->assertFalse($dbman->table_exists($table));
    }

    /**
     * Test behaviour of test_change_field_precision()
     */
    public function test_change_field_precision(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');

        // Fill the table with some records before dropping fields.
        $this->fill_deftable('test_table1');

        // Change text field from medium to big.
        $field = new xmldb_field('intro');
        $field->set_attributes(XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL, null, null);
        $dbman->change_field_precision($table, $field);
        $columns = $DB->get_columns('test_table1');
        // Cannot check the text type, only the metatype.
        $this->assertSame('X', $columns['intro']->meta_type);
        // TODO: check the rest of attributes.

        // Change char field from 30 to 20.
        $field = new xmldb_field('secondname');
        $field->set_attributes(XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, null);
        $dbman->change_field_precision($table, $field);
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('C', $columns['secondname']->meta_type);
        // TODO: check the rest of attributes.

        // Change char field from 20 to 10, having contents > 10cc. Throw exception.
        $field = new xmldb_field('secondname');
        $field->set_attributes(XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, null);
        try {
            $dbman->change_field_precision($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_change_structure_exception', $e);
        }
        // No changes in field specs at all.
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('C', $columns['secondname']->meta_type);
        // TODO: check the rest of attributes.

        // Change number field from 20,10 to 10,2.
        $field = new xmldb_field('grade');
        $field->set_attributes(XMLDB_TYPE_NUMBER, '10,2', null, null, null, null);
        $dbman->change_field_precision($table, $field);
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('N', $columns['grade']->meta_type);
        // TODO: check the rest of attributes.

        // Change integer field from 10 to 2.
        $field = new xmldb_field('userid');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
        $dbman->change_field_precision($table, $field);
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('I', $columns['userid']->meta_type);
        // TODO: check the rest of attributes.

        // Change the column from integer (2) to integer (6) (forces change of type in some DBs).
        $field = new xmldb_field('userid');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', null, null, null, null);
        $dbman->change_field_precision($table, $field);
        // Column is integer 6 null default null now.
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('I', $columns['userid']->meta_type);
        // TODO: check the rest of attributes.

        // Insert one record with 6-digit field.
        $record = new \stdClass();
        $record->course = 10;
        $record->secondname  = 'third record';
        $record->intro  = 'third record';
        $record->userid = 123456;
        $DB->insert_record('test_table1', $record);
        // Change integer field from 6 to 2, contents are bigger, must throw exception.
        $field = new xmldb_field('userid');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
        try {
            $dbman->change_field_precision($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_change_structure_exception', $e);
        }
        // No changes in field specs at all.
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('I', $columns['userid']->meta_type);
        // TODO: check the rest of attributes.

        // Change integer field from 10 to 3, in field used by index. must throw exception.
        $field = new xmldb_field('course');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, '0');
        try {
            $dbman->change_field_precision($table, $field);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_dependency_exception', $e);
        }
        // No changes in field specs at all.
        $columns = $DB->get_columns('test_table1');
        $this->assertSame('I', $columns['course']->meta_type);
        // TODO: check the rest of attributes.
    }

    public function testChangeFieldNullability() {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_table($table);

        $record = new \stdClass();
        $record->name = null;

        try {
            $result = $DB->insert_record('test_table_cust0', $record, false);
        } catch (\dml_exception $e) {
            $result = false;
        }
        $this->resetDebugging();
        $this->assertFalse($result);

        $field = new xmldb_field('name');
        $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, null, null, null);
        $dbman->change_field_notnull($table, $field);

        $this->assertTrue($DB->insert_record('test_table_cust0', $record, false));

        // TODO: add some tests with existing data in table.
        $DB->delete_records('test_table_cust0');

        $field = new xmldb_field('name');
        $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null);
        $dbman->change_field_notnull($table, $field);

        try {
            $result = $DB->insert_record('test_table_cust0', $record, false);
        } catch (\dml_exception $e) {
            $result = false;
        }
        $this->resetDebugging();
        $this->assertFalse($result);

        $dbman->drop_table($table);
    }

    public function testChangeFieldDefault() {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('onenumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'Moodle');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_table($table);

        $field = new xmldb_field('name');
        $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'Moodle2');
        $dbman->change_field_default($table, $field);

        $record = new \stdClass();
        $record->onenumber = 666;
        $id = $DB->insert_record('test_table_cust0', $record);

        $record = $DB->get_record('test_table_cust0', array('id'=>$id));
        $this->assertSame('Moodle2', $record->name);

        $field = new xmldb_field('onenumber');
        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 666);
        $dbman->change_field_default($table, $field);

        $record = new \stdClass();
        $record->name = 'something';
        $id = $DB->insert_record('test_table_cust0', $record);

        $record = $DB->get_record('test_table_cust0', array('id'=>$id));
        $this->assertSame('666', $record->onenumber);

        $dbman->drop_table($table);
    }

    public function testAddUniqueIndex() {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('onenumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'Moodle');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_table($table);

        $record = new \stdClass();
        $record->onenumber = 666;
        $record->name = 'something';
        $DB->insert_record('test_table_cust0', $record, false);

        $index = new xmldb_index('onenumber-name');
        $index->set_attributes(XMLDB_INDEX_UNIQUE, array('onenumber', 'name'));
        $dbman->add_index($table, $index);

        try {
            $result = $DB->insert_record('test_table_cust0', $record, false);
        } catch (\dml_exception $e) {
            $result = false;
        }
        $this->resetDebugging();
        $this->assertFalse($result);

        $dbman->drop_table($table);
    }

    public function testAddNonUniqueIndex() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $index = new xmldb_index('secondname');
        $index->set_attributes(XMLDB_INDEX_NOTUNIQUE, array('course', 'name'));
        $dbman->add_index($table, $index);
        $this->assertTrue($dbman->index_exists($table, $index));

        try {
            $dbman->add_index($table, $index);
            $this->fail('Exception expected for duplicate indexes');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        $index = new xmldb_index('third');
        $index->set_attributes(XMLDB_INDEX_NOTUNIQUE, array('course'));
        try {
            $dbman->add_index($table, $index);
            $this->fail('Exception expected for duplicate indexes');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('onenumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'Moodle');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_key('onenumber', XMLDB_KEY_FOREIGN, array('onenumber'));

        try {
            $table->add_index('onenumber', XMLDB_INDEX_NOTUNIQUE, array('onenumber'));
            $this->fail('Coding exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

        $table = new xmldb_table('test_table_cust0');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('onenumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_field('name', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'Moodle');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('onenumber', XMLDB_INDEX_NOTUNIQUE, array('onenumber'));

        try {
            $table->add_key('onenumber', XMLDB_KEY_FOREIGN, array('onenumber'));
            $this->fail('Coding exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }

    }

    public function testFindIndexName() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $index = new xmldb_index('secondname');
        $index->set_attributes(XMLDB_INDEX_NOTUNIQUE, array('course', 'name'));
        $dbman->add_index($table, $index);

        // DBM Systems name their indices differently - do not test the actual index name.
        $result = $dbman->find_index_name($table, $index);
        $this->assertTrue(!empty($result));

        $nonexistentindex = new xmldb_index('nonexistentindex');
        $nonexistentindex->set_attributes(XMLDB_INDEX_NOTUNIQUE, array('name'));
        $this->assertFalse($dbman->find_index_name($table, $nonexistentindex));
    }

    public function testDropIndex() {
        $DB = $this->tdb; // Do not use global $DB!

        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $index = new xmldb_index('secondname');
        $index->set_attributes(XMLDB_INDEX_NOTUNIQUE, array('course', 'name'));
        $dbman->add_index($table, $index);

        $dbman->drop_index($table, $index);
        $this->assertFalse($dbman->find_index_name($table, $index));

        // Test we are able to drop indexes having hyphens MDL-22804.
        // Create index with hyphens (by hand).
        $indexname = 'test-index-with-hyphens';
        switch ($DB->get_dbfamily()) {
            case 'mysql':
                $indexname = '`' . $indexname . '`';
                break;
            default:
                $indexname = '"' . $indexname . '"';
        }
        $stmt = "CREATE INDEX {$indexname} ON {$DB->get_prefix()}test_table1 (course, name)";
        $DB->change_database_structure($stmt);
        $this->assertNotEmpty($dbman->find_index_name($table, $index));
        // Index created, let's drop it using db manager stuff.
        $index = new xmldb_index('indexname', XMLDB_INDEX_NOTUNIQUE, array('course', 'name'));
        $dbman->drop_index($table, $index);
        $this->assertFalse($dbman->find_index_name($table, $index));
    }

    public function testAddUniqueKey() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $key = new xmldb_key('id-course-grade');
        $key->set_attributes(XMLDB_KEY_UNIQUE, array('id', 'course', 'grade'));
        $dbman->add_key($table, $key);

        // No easy way to test it, this just makes sure no errors are encountered.
        $this->assertTrue(true);
    }

    public function testAddForeignUniqueKey() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $this->create_deftable('test_table0');

        $key = new xmldb_key('course');
        $key->set_attributes(XMLDB_KEY_FOREIGN_UNIQUE, array('course'), 'test_table0', array('id'));
        $dbman->add_key($table, $key);

        // No easy way to test it, this just makes sure no errors are encountered.
        $this->assertTrue(true);
    }

    public function testDropKey() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $this->create_deftable('test_table0');

        $key = new xmldb_key('course');
        $key->set_attributes(XMLDB_KEY_FOREIGN_UNIQUE, array('course'), 'test_table0', array('id'));
        $dbman->add_key($table, $key);

        $dbman->drop_key($table, $key);

        // No easy way to test it, this just makes sure no errors are encountered.
        $this->assertTrue(true);
    }

    public function testAddForeignKey() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $this->create_deftable('test_table0');

        $key = new xmldb_key('course');
        $key->set_attributes(XMLDB_KEY_FOREIGN, array('course'), 'test_table0', array('id'));
        $dbman->add_key($table, $key);

        // No easy way to test it, this just makes sure no errors are encountered.
        $this->assertTrue(true);
    }

    public function testDropForeignKey() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table1');
        $this->create_deftable('test_table0');

        $key = new xmldb_key('course');
        $key->set_attributes(XMLDB_KEY_FOREIGN, array('course'), 'test_table0', array('id'));
        $dbman->add_key($table, $key);

        $dbman->drop_key($table, $key);

        // No easy way to test it, this just makes sure no errors are encountered.
        $this->assertTrue(true);
    }

    public function testRenameField() {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table0');
        $field = new xmldb_field('type');
        $field->set_attributes(XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL, null, 'general', 'course');

        // 1. Rename the 'type' field into a generic new valid name.
        // This represents the standard use case.
        $dbman->rename_field($table, $field, 'newfieldname');

        $columns = $DB->get_columns('test_table0');

        $this->assertArrayNotHasKey('type', $columns);
        $this->assertArrayHasKey('newfieldname', $columns);
        $field->setName('newfieldname');

        // 2. Rename the 'newfieldname' field into a reserved word, for testing purposes.
        // This represents a questionable use case: we should support it but discourage the use of it on peer reviewing.
        $dbman->rename_field($table, $field, 'where');

        $columns = $DB->get_columns('test_table0');

        $this->assertArrayNotHasKey('newfieldname', $columns);
        $this->assertArrayHasKey('where', $columns);

        // 3. Create a table with a column name named w/ a reserved word and get rid of it.
        // This represents a "recovering" use case: a field name could be a reserved word in the future, at least for a DB type.
        $table = new xmldb_table('test_table_res_word');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('where', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->setComment("This is a test'n drop table. You can drop it safely");
        $dbman->create_table($table);
        $dbman->table_exists('test_table_res_word');

        $columns = $DB->get_columns('test_table_res_word');
        $this->assertArrayHasKey('where', $columns);
        $field = $table->getField('where');

        $dbman->rename_field($table, $field, 'newfieldname');

        $columns = $DB->get_columns('test_table_res_word');

        $this->assertArrayNotHasKey('where', $columns);
        $this->assertArrayHasKey('newfieldname', $columns);
    }

    public function testIndexExists() {
        // Skipping: this is just a test of find_index_name.
    }

    public function testFindKeyName() {
        $dbman = $this->tdb->get_manager();

        $table = $this->create_deftable('test_table0');
        $key = $table->getKey('primary');

        // With Mysql, the return value is actually "mdl_test_id_pk".
        $result = $dbman->find_key_name($table, $key);
        $this->assertTrue(!empty($result));
    }

    public function testDeleteTablesFromXmldbFile() {
        $dbman = $this->tdb->get_manager();

        $this->create_deftable('test_table1');

        $this->assertTrue($dbman->table_exists('test_table1'));

        // Feed nonexistent file.
        try {
            $dbman->delete_tables_from_xmldb_file('fpsoiudfposui');
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->resetDebugging();
            $this->assertInstanceOf('moodle_exception', $e);
        }

        try {
            $dbman->delete_tables_from_xmldb_file(__DIR__ . '/fixtures/invalid.xml');
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->resetDebugging();
            $this->assertInstanceOf('moodle_exception', $e);
        }

        // Check that the table has not been deleted from DB.
        $this->assertTrue($dbman->table_exists('test_table1'));

        // Real and valid xml file.
        // TODO: drop UNSINGED completely in Moodle 2.4.
        $dbman->delete_tables_from_xmldb_file(__DIR__ . '/fixtures/xmldb_table.xml');

        // Check that the table has been deleted from DB.
        $this->assertFalse($dbman->table_exists('test_table1'));
    }

    public function testInstallFromXmldbFile() {
        $dbman = $this->tdb->get_manager();

        // Feed nonexistent file.
        try {
            $dbman->install_from_xmldb_file('fpsoiudfposui');
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->resetDebugging();
            $this->assertInstanceOf('moodle_exception', $e);
        }

        try {
            $dbman->install_from_xmldb_file(__DIR__ . '/fixtures/invalid.xml');
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->resetDebugging();
            $this->assertInstanceOf('moodle_exception', $e);
        }

        // Check that the table has not yet been created in DB.
        $this->assertFalse($dbman->table_exists('test_table1'));

        // Real and valid xml file.
        $dbman->install_from_xmldb_file(__DIR__ . '/fixtures/xmldb_table.xml');
        $this->assertTrue($dbman->table_exists('test_table1'));
    }

    public function test_temp_tables(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Create temp table0.
        $table0 = $this->tables['test_table0'];
        $dbman->create_temp_table($table0);
        $this->assertTrue($dbman->table_exists('test_table0'));

        // Try to create temp table with same name, must throw exception.
        $dupetable = $this->tables['test_table0'];
        try {
            $dbman->create_temp_table($dupetable);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // Try to create table with same name, must throw exception.
        $dupetable = $this->tables['test_table0'];
        try {
            $dbman->create_table($dupetable);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_exception', $e);
        }

        // Create another temp table1.
        $table1 = $this->tables['test_table1'];
        $dbman->create_temp_table($table1);
        $this->assertTrue($dbman->table_exists('test_table1'));

        // Get columns and perform some basic tests.
        $columns = $DB->get_columns('test_table1');
        $this->assertCount(11, $columns);
        $this->assertTrue($columns['name'] instanceof database_column_info);
        $this->assertEquals(30, $columns['name']->max_length);
        $this->assertTrue($columns['name']->has_default);
        $this->assertEquals('Moodle', $columns['name']->default_value);

        // Insert some records.
        $inserted = $this->fill_deftable('test_table1');
        $records = $DB->get_records('test_table1');
        $this->assertCount($inserted, $records);
        $this->assertSame($records[1]->course, $this->records['test_table1'][0]->course);
        $this->assertSame($records[1]->secondname, $this->records['test_table1'][0]->secondname);
        $this->assertSame($records[2]->intro, $this->records['test_table1'][1]->intro);

        // Collect statistics about the data in the temp table.
        $DB->update_temp_table_stats();

        // Drop table1.
        $dbman->drop_table($table1);
        $this->assertFalse($dbman->table_exists('test_table1'));

        // Try to drop non-existing temp table, must throw exception.
        $noetable = $this->tables['test_table1'];
        try {
            $dbman->drop_table($noetable);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('ddl_table_missing_exception', $e);
        }

        // Collect statistics about the data in the temp table with less tables.
        $DB->update_temp_table_stats();

        // Fill/modify/delete a few table0 records.

        // Drop table0.
        $dbman->drop_table($table0);
        $this->assertFalse($dbman->table_exists('test_table0'));

        // Create another temp table1.
        $table1 = $this->tables['test_table1'];
        $dbman->create_temp_table($table1);
        $this->assertTrue($dbman->table_exists('test_table1'));

        // Make sure it can be dropped using deprecated drop_temp_table().
        $dbman->drop_temp_table($table1);
        $this->assertFalse($dbman->table_exists('test_table1'));
        $this->assertDebuggingCalled();

        // Try join with normal tables - MS SQL may use incompatible collation.
        $table1 = new xmldb_table('test_table');
        $table1->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table1->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table1->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_table($table1);

        $table2 = new xmldb_table('test_temp');
        $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table2->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $dbman->create_temp_table($table2);

        $record = array('name' => 'a');
        $DB->insert_record('test_table', $record);
        $DB->insert_record('test_temp', $record);

        $record = array('name' => 'b');
        $DB->insert_record('test_table', $record);

        $record = array('name' => 'c');
        $DB->insert_record('test_temp', $record);

        $sql = "SELECT *
                  FROM {test_table} n
                  JOIN {test_temp} t ON t.name = n.name";
        $records = $DB->get_records_sql($sql);
        $this->assertCount(1, $records);

        // Drop temp table.
        $dbman->drop_table($table2);
        $this->assertFalse($dbman->table_exists('test_temp'));
    }

    public function test_concurrent_temp_tables(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Define 2 records.
        $record1 = (object)array(
            'course'     =>  1,
            'secondname' => '11 important',
            'intro'      => '111 important');
        $record2 = (object)array(
            'course'     =>  2,
            'secondname' => '22 important',
            'intro'      => '222 important');

        // Create temp table1 and insert 1 record (in DB).
        $table = $this->tables['test_table1'];
        $dbman->create_temp_table($table);
        $this->assertTrue($dbman->table_exists('test_table1'));
        $inserted = $DB->insert_record('test_table1', $record1);

        // Switch to new connection.
        $cfg = $DB->export_dbconfig();
        if (!isset($cfg->dboptions)) {
            $cfg->dboptions = array();
        }
        $DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
        $DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
        $dbman2 = $DB2->get_manager();
        $this->assertFalse($dbman2->table_exists('test_table1')); // Temp table not exists in DB2.

        // Create temp table1 and insert 1 record (in DB2).
        $table = $this->tables['test_table1'];
        $dbman2->create_temp_table($table);
        $this->assertTrue($dbman2->table_exists('test_table1'));
        $inserted = $DB2->insert_record('test_table1', $record2);

        $dbman2->drop_table($table); // Drop temp table before closing DB2.
        $this->assertFalse($dbman2->table_exists('test_table1'));
        $DB2->dispose(); // Close DB2.

        $this->assertTrue($dbman->table_exists('test_table1')); // Check table continues existing for DB.
        $dbman->drop_table($table); // Drop temp table.
        $this->assertFalse($dbman->table_exists('test_table1'));
    }

    /**
     * get_columns should return an empty array for ex-temptables.
     */
    public function test_leftover_temp_tables_columns(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Create temp table0.
        $table0 = $this->tables['test_table0'];
        $dbman->create_temp_table($table0);

        $dbman->drop_table($table0);

        // Get columns and perform some basic tests.
        $columns = $DB->get_columns('test_table0');
        $this->assertEquals([], $columns);
    }

    /**
     * Deleting a temp table should not purge the whole cache
     */
    public function test_leftover_temp_tables_cache(): void {
        $DB = $this->tdb; // Do not use global $DB!
        $dbman = $this->tdb->get_manager();

        // Create 2 temp tables.
        $table0 = $this->tables['test_table0'];
        $dbman->create_temp_table($table0);
        $table1 = $this->tables['test_table1'];
        $dbman->create_temp_table($table1);

        // Create a normal table.
        $table2 = new xmldb_table ('test_table2');
        $table2->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table2->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table2->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table2->setComment("This is a test'n drop table. You can drop it safely");
        $this->tables[$table2->getName()] = $table2;
        $dbman->create_table($table2);

        // Get columns for the tables, so that relevant caches are populated with their data.
        $DB->get_columns('test_table0');
        $DB->get_columns('test_table1');
        $DB->get_columns('test_table2');

        $dbman->drop_table($table0);

        $rc = new \ReflectionClass('moodle_database');
        $rcm = $rc->getMethod('get_temp_tables_cache');
        $metacachetemp = $rcm->invokeArgs($DB, []);

        // Data of test_table0 should be removed from the cache.
        $this->assertEquals(false, $metacachetemp->has('test_table0'));

        // Data of test_table1 should be intact.
        $this->assertEquals(true, $metacachetemp->has('test_table1'));

        $rc = new \ReflectionClass('moodle_database');
        $rcm = $rc->getMethod('get_metacache');
        $metacache = $rcm->invokeArgs($DB, []);

        // Data of test_table2 should be intact.
        $this->assertEquals(true, $metacache->has('test_table2'));

        // Delete the leftover temp table.
        $dbman->drop_table($table1);
    }

    public function test_reset_sequence(): void {
        $DB = $this->tdb;
        $dbman = $DB->get_manager();

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        $record = (object)array('id'=>666, 'course'=>10);
        $DB->import_record('testtable', $record);
        $DB->delete_records('testtable'); // This delete performs one TRUNCATE.

        $dbman->reset_sequence($table); // Using xmldb object.
        $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)));

        $record = (object)array('id'=>666, 'course'=>10);
        $DB->import_record('testtable', $record);
        $DB->delete_records('testtable', array()); // This delete performs one DELETE.

        $dbman->reset_sequence($table); // Using xmldb object.
        $this->assertEquals(1, $DB->insert_record('testtable', (object)array('course'=>13)),
            'Some versions of MySQL 5.6.x are known to not support lowering of auto-increment numbers.');

        $DB->import_record('testtable', $record);
        $dbman->reset_sequence($tablename); // Using string.
        $this->assertEquals(667, $DB->insert_record('testtable', (object)array('course'=>13)));

        $dbman->drop_table($table);
    }

    public function test_reserved_words(): void {
        $reserved = sql_generator::getAllReservedWords();
        $this->assertTrue(count($reserved) > 1);
    }

    public function test_index_hints(): void {
        $DB = $this->tdb;
        $dbman = $DB->get_manager();

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table->add_field('path', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'), array('xxxx,yyyy'));
        $table->add_index('path', XMLDB_INDEX_NOTUNIQUE, array('path'), array('varchar_pattern_ops'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        $table = new xmldb_table('testtable');
        $index = new xmldb_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'), array('xxxx,yyyy'));
        $this->assertTrue($dbman->index_exists($table, $index));

        $table = new xmldb_table('testtable');
        $index = new xmldb_index('path', XMLDB_INDEX_NOTUNIQUE, array('path'), array('varchar_pattern_ops'));
        $this->assertTrue($dbman->index_exists($table, $index));

        // Try unique indexes too.
        $dbman->drop_table($this->tables[$tablename]);

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('path', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('path', XMLDB_INDEX_UNIQUE, array('path'), array('varchar_pattern_ops'));
        $dbman->create_table($table);
        $this->tables[$tablename] = $table;

        $table = new xmldb_table('testtable');
        $index = new xmldb_index('path', XMLDB_INDEX_UNIQUE, array('path'), array('varchar_pattern_ops'));
        $this->assertTrue($dbman->index_exists($table, $index));
    }

    public function test_index_max_bytes(): void {
        $DB = $this->tdb;
        $dbman = $DB->get_manager();

        $maxstr = '';
        for ($i=0; $i<255; $i++) {
            $maxstr .= '言'; // Random long string that should fix exactly the limit for one char column.
        }

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        $rec = new \stdClass();
        $rec->name = $maxstr;

        $id = $DB->insert_record($tablename, $rec);
        $this->assertTrue(!empty($id));

        $rec = $DB->get_record($tablename, array('id'=>$id));
        $this->assertSame($maxstr, $rec->name);

        $dbman->drop_table($table);

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, 255+1, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('name', XMLDB_INDEX_NOTUNIQUE, array('name'));

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }
    }

    public function test_index_composed_max_bytes(): void {
        $DB = $this->tdb;
        $dbman = $DB->get_manager();

        $maxstr = '';
        for ($i=0; $i<200; $i++) {
            $maxstr .= '言';
        }
        $reststr = '';
        for ($i=0; $i<133; $i++) {
            $reststr .= '言';
        }

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name1', XMLDB_TYPE_CHAR, 200, null, XMLDB_NOTNULL, null);
        $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1', 'name2'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        $rec = new \stdClass();
        $rec->name1 = $maxstr;
        $rec->name2 = $reststr;

        $id = $DB->insert_record($tablename, $rec);
        $this->assertTrue(!empty($id));

        $rec = $DB->get_record($tablename, array('id'=>$id));
        $this->assertSame($maxstr, $rec->name1);
        $this->assertSame($reststr, $rec->name2);

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name1', XMLDB_TYPE_CHAR, 201, null, XMLDB_NOTNULL, null);
        $table->add_field('name2', XMLDB_TYPE_CHAR, 133, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('name1-name2', XMLDB_INDEX_NOTUNIQUE, array('name1', 'name2'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }
    }

    public function test_char_size_limit(): void {
        $DB = $this->tdb;
        $dbman = $DB->get_manager();

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, xmldb_field::CHAR_MAX_LENGTH, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $dbman->create_table($table);
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        // This has to work in all DBs.
        $maxstr = '';
        for ($i=0; $i<xmldb_field::CHAR_MAX_LENGTH; $i++) {
            $maxstr .= 'a'; // Ascii only.
        }

        $rec = new \stdClass();
        $rec->name = $maxstr;

        $id = $DB->insert_record($tablename, $rec);
        $this->assertTrue(!empty($id));

        $rec = $DB->get_record($tablename, array('id'=>$id));
        $this->assertSame($maxstr, $rec->name);

        $maxstr = '';
        for ($i=0; $i<xmldb_field::CHAR_MAX_LENGTH; $i++) {
            $maxstr .= '言'; // Random long string that should fix exactly the limit for one char column.
        }

        $rec = new \stdClass();
        $rec->name = $maxstr;

        try {
            $id = $DB->insert_record($tablename, $rec);
            $this->assertTrue(!empty($id));

            $rec = $DB->get_record($tablename, array('id'=>$id));
            $this->assertSame($maxstr, $rec->name);
        } catch (dml_exception $e) {
            throw $e;
        }

        $table = new xmldb_table('testtable');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('name', XMLDB_TYPE_CHAR, xmldb_field::CHAR_MAX_LENGTH+1, null, XMLDB_NOTNULL, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));

        // Drop if exists.
        if ($dbman->table_exists($table)) {
            $dbman->drop_table($table);
        }
        $tablename = $table->getName();
        $this->tables[$tablename] = $table;

        try {
            $dbman->create_table($table);
            $this->fail('Exception expected');
        } catch (\moodle_exception $e) {
            $this->assertInstanceOf('coding_exception', $e);
        }
    }

    public function test_object_name(): void {
        $gen = $this->tdb->get_manager()->generator;

        // This will form short object name and max length should not be exceeded.
        $table = 'tablename';
        $fields = 'id';
        $suffix = 'pk';
        for ($i=0; $i<12; $i++) {
            $this->assertLessThanOrEqual($gen->names_max_length,
                    strlen($gen->getNameForObject($table, $fields, $suffix)),
                    'Generated object name is too long. $i = '.$i);
        }

        // This will form too long object name always and it must be trimmed to exactly 30 chars.
        $table = 'aaaa_bbbb_cccc_dddd_eeee_ffff_gggg';
        $fields = 'aaaaa,bbbbb,ccccc,ddddd';
        $suffix = 'idx';
        for ($i=0; $i<12; $i++) {
            $this->assertEquals($gen->names_max_length,
                    strlen($gen->getNameForObject($table, $fields, $suffix)),
                    'Generated object name is too long. $i = '.$i);
        }

        // Same test without suffix.
        $table = 'bbbb_cccc_dddd_eeee_ffff_gggg_hhhh';
        $fields = 'aaaaa,bbbbb,ccccc,ddddd';
        $suffix = '';
        for ($i=0; $i<12; $i++) {
            $this->assertEquals($gen->names_max_length,
                    strlen($gen->getNameForObject($table, $fields, $suffix)),
                    'Generated object name is too long. $i = '.$i);
        }

        // This must only trim name when counter is 10 or more.
        $table = 'cccc_dddd_eeee_ffff_gggg_hhhh_iiii';
        $fields = 'id';
        $suffix = 'idx';
        // Since we don't know how long prefix is, loop to generate tablename that gives exactly maxlengh-1 length.
        // Skip this test if prefix is too long.
        while (strlen($table) && strlen($gen->prefix.preg_replace('/_/','',$table).'_id_'.$suffix) >= $gen->names_max_length) {
            $table = rtrim(substr($table, 0, strlen($table) - 1), '_');
        }
        if (strlen($table)) {
            $this->assertEquals($gen->names_max_length - 1,
                        strlen($gen->getNameForObject($table, $fields, $suffix)));
            for ($i=0; $i<12; $i++) {
                $this->assertEquals($gen->names_max_length,
                        strlen($gen->getNameForObject($table, $fields, $suffix)),
                        'Generated object name is too long. $i = '.$i);
            }

            // Now test to confirm that a duplicate name isn't issued, even if they come from different root names.
            // Move to a new field.
            $fields = "fl";

            // Insert twice, moving is to a key with fl2.
            $this->assertEquals($gen->names_max_length - 1, strlen($gen->getNameForObject($table, $fields, $suffix)));
            $result1 = $gen->getNameForObject($table, $fields, $suffix);

            // Make sure we end up with _fl2_ in the result.
            $this->assertMatchesRegularExpression('/_fl2_/', $result1);

            // Now, use a field that would result in the same key if it wasn't already taken.
            $fields = "fl2";
            // Because we are now at the max key length, it will try:
            // - _fl2_ (the natural name)
            // - _fl2_ (removing the original 2, and adding a counter 2)
            // - then settle on _fl3_.
            $result2 = $gen->getNameForObject($table, $fields, $suffix);
            $this->assertMatchesRegularExpression('/_fl3_/', $result2);

            // Make sure they don't match.
            $this->assertNotEquals($result1, $result2);
            // But are only different in the way we expect. This confirms the test is working properly.
            $this->assertEquals(str_replace('_fl2_', '', $result1), str_replace('_fl3_', '', $result2));

            // Now go back. We would expect the next result to be fl3 again, but it is taken, so it should move to fl4.
            $fields = "fl";
            $result3 = $gen->getNameForObject($table, $fields, $suffix);

            $this->assertNotEquals($result2, $result3);
            $this->assertMatchesRegularExpression('/_fl4_/', $result3);
        }
    }

    /**
     * Data provider for test_get_enc_quoted().
     *
     * @return array The type-value pair fixture.
     */
    public static function get_enc_quoted_provider(): array {
        return array(
            // Reserved: some examples from SQL-92.
            [true, 'from'],
            [true, 'table'],
            [true, 'where'],
            // Not reserved.
            [false, 'my_awesome_column_name']
        );
    }

    /**
     * This is a test for sql_generator::getEncQuoted().
     *
     * @dataProvider get_enc_quoted_provider
     * @param bool $reserved Whether the column name is reserved or not.
     * @param string $columnname The column name to be quoted, according to the value of $reserved.
     **/
    public function test_get_enc_quoted($reserved, $columnname): void {
        $DB = $this->tdb;
        $gen = $DB->get_manager()->generator;

        if (!$reserved) {
            // No need to quote the column name.
            $this->assertSame($columnname, $gen->getEncQuoted($columnname));
        } else {
            // Column name should be quoted.
            $dbfamily = $DB->get_dbfamily();

            switch ($dbfamily) {
                case 'mysql':
                    $this->assertSame("`$columnname`", $gen->getEncQuoted($columnname));
                    break;
                case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
                case 'postgres':
                default:
                    $this->assertSame('"' . $columnname . '"', $gen->getEncQuoted($columnname));
                    break;
            }
        }
    }

    /**
     * Data provider for test_sql_generator_get_rename_field_sql().
     *
     * @return array The type-old-new tuple fixture.
     */
    public static function sql_generator_get_rename_field_sql_provider(): array {
        return array(
            // Reserved: an example from SQL-92.
            // Both names should be reserved.
            [true, 'from', 'where'],
            // Not reserved.
            [false, 'my_old_column_name', 'my_awesome_column_name']
        );
    }

    /**
     * This is a unit test for sql_generator::getRenameFieldSQL().
     *
     * @dataProvider sql_generator_get_rename_field_sql_provider
     * @param bool $reserved Whether the column name is reserved or not.
     * @param string $oldcolumnname The column name to be renamed.
     * @param string $newcolumnname The new column name.
     **/
    public function test_sql_generator_get_rename_field_sql($reserved, $oldcolumnname, $newcolumnname): void {
        $DB = $this->tdb;
        $gen = $DB->get_manager()->generator;
        $prefix = $DB->get_prefix();

        $tablename = 'test_get_rename_field_sql';
        $table = new xmldb_table($tablename);
        $field = new xmldb_field($oldcolumnname, XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null, null, '0', 'previous');

        $dbfamily = $DB->get_dbfamily();
        if (!$reserved) {
            // No need to quote the column name.
            switch ($dbfamily) {
                case 'mysql':
                    $this->assertSame(
                        [ "ALTER TABLE {$prefix}$tablename CHANGE $oldcolumnname $newcolumnname BIGINT(11) NOT NULL" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
                case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
                    $this->assertSame(
                        [ "sp_rename '{$prefix}$tablename.[$oldcolumnname]', '$newcolumnname', 'COLUMN'" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
                case 'postgres':
                default:
                    $this->assertSame(
                        [ "ALTER TABLE {$prefix}$tablename RENAME COLUMN $oldcolumnname TO $newcolumnname" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
            }
        } else {
            // Column name should be quoted.
            switch ($dbfamily) {
                case 'mysql':
                    $this->assertSame(
                        [ "ALTER TABLE {$prefix}$tablename CHANGE `$oldcolumnname` `$newcolumnname` BIGINT(11) NOT NULL" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
                case 'mssql': // The Moodle connection runs under 'QUOTED_IDENTIFIER ON'.
                    $this->assertSame(
                        [ "sp_rename '{$prefix}$tablename.[$oldcolumnname]', '$newcolumnname', 'COLUMN'" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
                case 'postgres':
                default:
                    $this->assertSame(
                        [ "ALTER TABLE {$prefix}$tablename RENAME COLUMN \"$oldcolumnname\" TO \"$newcolumnname\"" ],
                        $gen->getRenameFieldSQL($table, $field, $newcolumnname)
                    );
                    break;
            }
        }
    }

    public function test_get_nullable_fields_in_index(): void {
        $DB = $this->tdb;
        $gen = $DB->get_manager()->generator;

        $indexwithoutnulls = $this->tables['test_table0']->getIndex('type-name');
        $this->assertSame([], $gen->get_nullable_fields_in_index(
                $this->tables['test_table0'], $indexwithoutnulls));

        $indexwithnulls = new xmldb_index('course-grade', XMLDB_INDEX_UNIQUE, ['course', 'grade']);
        $this->assertSame(['grade'], $gen->get_nullable_fields_in_index(
                $this->tables['test_table0'], $indexwithnulls));

        $this->create_deftable('test_table0');

        // Now test using a minimal xmldb_table, to ensure we get the data from the DB.
        $table = new xmldb_table('test_table0');
        $this->assertSame([], $gen->get_nullable_fields_in_index(
                $table, $indexwithoutnulls));
        $this->assertSame(['grade'], $gen->get_nullable_fields_in_index(
                $table, $indexwithnulls));
    }

    // Following methods are not supported == Do not test.
    /*
        public function testRenameIndex() {
            // Unsupported!
            $dbman = $this->tdb->get_manager();

            $table = $this->create_deftable('test_table0');
            $index = new xmldb_index('course');
            $index->set_attributes(XMLDB_INDEX_UNIQUE, array('course'));

            $this->assertTrue($dbman->rename_index($table, $index, 'newindexname'));
        }

        public function testRenameKey() {
            // Unsupported!
             $dbman = $this->tdb->get_manager();

            $table = $this->create_deftable('test_table0');
            $key = new xmldb_key('course');
            $key->set_attributes(XMLDB_KEY_UNIQUE, array('course'));

            $this->assertTrue($dbman->rename_key($table, $key, 'newkeyname'));
        }
    */

    /**
     * Tests check_database_schema().
     */
    public function test_check_database_schema(): void {
        global $CFG, $DB;

        $dbmanager = $DB->get_manager();

        // Create a table in the database we will be using to compare with a schema.
        $table = new xmldb_table('test_check_db_schema');
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('extracolumn', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
        $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
        $table->add_field('binaryfield', XMLDB_TYPE_BINARY, null, null, XMLDB_NOTNULL, null, null);
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_key('extraindex', XMLDB_KEY_UNIQUE, array('extracolumn'));
        $table->setComment("This is a test table, you can drop it safely.");
        $dbmanager->create_table($table);

        // Remove the column so it is not added to the schema and gets reported as an extra column.
        $table->deleteField('extracolumn');

        // Change the 'courseid' field to a float in the schema so it gets reported as different.
        $table->deleteField('courseid');
        $table->add_field('courseid', XMLDB_TYPE_NUMBER, '10, 2', null, XMLDB_NOTNULL, null, null);

        // Add another column to the schema that won't be present in the database and gets reported as missing.
        $table->add_field('missingcolumn', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);

        // Add another key to the schema that won't be present in the database and gets reported as missing.
        $table->add_key('missingkey', XMLDB_KEY_FOREIGN, array('courseid'), 'course', array('id'));

        // Remove the key from the schema which will still be present in the database and reported as extra.
        $table->deleteKey('extraindex');

        $schema = new xmldb_structure('testschema');
        $schema->addTable($table);

        // Things we want to check for -
        // 1. Changed columns.
        // 2. Missing columns.
        // 3. Missing indexes.
        // 4. Unexpected index.
        // 5. Extra columns.
        $errors = $dbmanager->check_database_schema($schema)['test_check_db_schema'];
        // Preprocess $errors to get rid of the non compatible (SQL-dialect dependent) parts.
        array_walk($errors, function(&$error) {
            $error = trim(strtok($error, PHP_EOL));
        });
        $this->assertCount(5, $errors);
        $this->assertContains("column 'courseid' has incorrect type 'I', expected 'N'", $errors);
        $this->assertContains("column 'missingcolumn' is missing", $errors);
        $this->assertContains("Missing index 'missingkey' (not unique (courseid)).", $errors);
        $this->assertContains("Unexpected index '{$CFG->prefix}testchecdbsche_ext_uix'.", $errors);
        $this->assertContains("column 'extracolumn' is not expected (I)", $errors);
    }
}
