diff --git a/config-dist.php b/config-dist.php index ec68c6b2c87..3febb696075 100644 --- a/config-dist.php +++ b/config-dist.php @@ -105,8 +105,9 @@ $CFG->dboptions = array( 'latency' => 0.5, // Set read-only slave sync latency in seconds. // When 'latency' seconds have lapsed after an update to a table // it is deemed safe to use readonly slave for reading from the table. - // It is optional. If omitted once written to a table it will always - // use master handle for reading. + // It is optional, defaults to 1 second. If you want once written to a table + // to always use master handle for reading set it to something ridiculosly big, + // eg 10. // Lower values increase the performance, but setting it too low means // missing the master-slave sync. 'exclude_tables' => [ // Tables to exclude from read-only slave feature. diff --git a/lib/dml/moodle_read_slave_trait.php b/lib/dml/moodle_read_slave_trait.php index cba970ed624..2e99699d9de 100644 --- a/lib/dml/moodle_read_slave_trait.php +++ b/lib/dml/moodle_read_slave_trait.php @@ -43,8 +43,8 @@ defined('MOODLE_INTERNAL') || die(); * - It supports multiple 'instance' entries, in case one is not accessible, * but only one (first connectable) instance is used. * - 'latency' option: master -> slave sync latency in seconds (will probably - * be a fraction of a second). If specified, a table being written to is - * deemed fully synced and suitable for slave read. + * be a fraction of a second). A table being written to is deemed fully synced + * after that period and suitable for slave read. Defaults to 1 sec. * - 'exclude_tables' option: a list of tables that never go to the slave for * querying. The feature is meant to be used in emergency only, so the * readonly feature can still be used in case there is a rogue query that @@ -54,8 +54,8 @@ defined('MOODLE_INTERNAL') || die(); * * Choice of the database handle is based on following: * - SQL_QUERY_INSERT, UPDATE and STRUCTURE record table from the query - * in the $written array and microtime() the event if the 'latency' option - * is set. For those queries master write handle is used. + * in the $written array and microtime() the event. For those queries master + * write handle is used. * - SQL_QUERY_AUX queries will always use the master write handle because they * are used for transactionstart/end, locking etc. In that respect, query_start() and * query_end() *must not* be used during the connection phase. @@ -63,11 +63,9 @@ defined('MOODLE_INTERNAL') || die(); * -- any of the tables involved is a temp table * -- any of the tables involved is listed in the 'exclude_tables' option * -- any of the tables involved is in the $written array: - * * If the 'latency' option is set then the microtime() is compared to - * the write microrime, and if more then latency time has passed the slave - * handle is used. - * * Otherwise (not enough time passed or 'latency' option not set) - * we choose the master write handle + * * current microtime() is compared to the write microrime, and if more than + * latency time has passed the slave handle is used + * * otherwise (not enough time passed) we choose the master write handle * If none of the above conditions are met the slave instance is used. * * A 'latency' example: @@ -92,7 +90,7 @@ trait moodle_read_slave_trait { private $wantreadslave = false; private $readsslave = 0; - private $slavelatency = 0; + private $slavelatency = 1; private $written = []; // Track tables being written to. private $readexclude = []; // Tables to exclude from using dbhreadonly. @@ -324,23 +322,27 @@ trait moodle_read_slave_trait { } if (isset($this->written[$tablename])) { - if ($this->slavelatency) { - $now = $now ?: microtime(true); - if ($now - $this->written[$tablename] < $this->slavelatency) { - return false; - } - unset($this->written[$tablename]); - } else { + $now = $now ?: microtime(true); + // Paranoid check. + if ($this->written[$tablename] === true) { + debugging( + "$tablename last written set to true outside transaction - should not happen!", + DEBUG_DEVELOPER + ); + $this->written[$tablename] = $now; return false; } + if ($now - $this->written[$tablename] < $this->slavelatency) { + return false; + } + unset($this->written[$tablename]); } } return true; case SQL_QUERY_INSERT: case SQL_QUERY_UPDATE: - // If we are in transaction we cannot set the written time yet. - $now = $this->slavelatency && !$this->transactions ? microtime(true) : true; + $now = $this->transactions ? true : microtime(true); foreach ($this->table_names($sql) as $tablename) { $this->written[$tablename] = $now; } @@ -364,23 +366,15 @@ trait moodle_read_slave_trait { * @throws dml_transaction_exception Creates and throws transaction related exceptions. */ public function commit_delegated_transaction(moodle_transaction $transaction) { - parent::commit_delegated_transaction($transaction); - - if ($this->transactions) { - return; - } - - if (!$this->slavelatency) { - return; - } - - $now = null; - foreach ($this->written as $tablename => $when) { - if ($when === true) { - $now = $now ?: microtime(true); + if ($this->written) { + // Adjust the written time. + $now = microtime(true); + foreach ($this->written as $tablename => $when) { $this->written[$tablename] = $now; } } + + parent::commit_delegated_transaction($transaction); } /** diff --git a/lib/dml/tests/dml_mysqli_read_slave_test.php b/lib/dml/tests/dml_mysqli_read_slave_test.php index af55928f384..60924169de4 100644 --- a/lib/dml/tests/dml_mysqli_read_slave_test.php +++ b/lib/dml/tests/dml_mysqli_read_slave_test.php @@ -21,8 +21,11 @@ * @category dml * @copyright 2018 Srdjan Janković, Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mysqli_native_moodle_database */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php'); @@ -35,7 +38,7 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_dml_mysqli_read_slave_testcase extends base_testcase { +class dml_mysqli_read_slave_test extends \base_testcase { /** * Test readonly handle is not used for reading from special pg_*() call queries, * pg_try_advisory_lock and pg_advisory_unlock. @@ -80,7 +83,7 @@ class core_dml_mysqli_read_slave_testcase extends base_testcase { 'connecttimeout' => 1 ]; - $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); + $db2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); $db2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions); $this->assertTrue(count($db2->get_records('user')) > 0); } diff --git a/lib/dml/tests/dml_pgsql_read_slave_test.php b/lib/dml/tests/dml_pgsql_read_slave_test.php index b1f24ebbf9f..1ad15bf77f8 100644 --- a/lib/dml/tests/dml_pgsql_read_slave_test.php +++ b/lib/dml/tests/dml_pgsql_read_slave_test.php @@ -21,8 +21,11 @@ * @category dml * @copyright 2018 Srdjan Janković, Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \pgsql_native_moodle_database */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php'); @@ -35,7 +38,7 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_dml_pgsql_read_slave_testcase extends base_testcase { +class dml_pgsql_read_slave_test extends \base_testcase { /** * Test correct database handles are used for cursors * @@ -135,12 +138,12 @@ class core_dml_pgsql_read_slave_testcase extends base_testcase { } // Get a separate disposable db connection handle with guaranteed 'readonly' config. - $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); + $db2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); $db2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions); $dbman = $db2->get_manager(); - $table = new xmldb_table('silly_test_table'); + $table = new \xmldb_table('silly_test_table'); $table->add_field('id', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, XMLDB_SEQUENCE); $table->add_field('msg', XMLDB_TYPE_CHAR, 255); $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); @@ -155,7 +158,7 @@ class core_dml_pgsql_read_slave_testcase extends base_testcase { $db2->get_records('silly_test_table'); $this->assertEquals($reads, $db2->perf_get_reads_slave()); - $table2 = new xmldb_table('silly_test_table2'); + $table2 = new \xmldb_table('silly_test_table2'); $table2->add_field('id', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, XMLDB_SEQUENCE); $table2->add_field('msg', XMLDB_TYPE_CHAR, 255); $table2->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); @@ -193,7 +196,7 @@ class core_dml_pgsql_read_slave_testcase extends base_testcase { 'connecttimeout' => 1 ]; - $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); + $db2 = \moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); $db2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions); $this->assertTrue(count($db2->get_records('user')) > 0); } diff --git a/lib/dml/tests/dml_read_slave_test.php b/lib/dml/tests/dml_read_slave_test.php index 4ee6fc9c620..f9b5b070a94 100644 --- a/lib/dml/tests/dml_read_slave_test.php +++ b/lib/dml/tests/dml_read_slave_test.php @@ -21,12 +21,16 @@ * @category dml * @copyright 2018 Srdjan Janković, Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \moodle_temptables */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/read_slave_moodle_database_table_names.php'); require_once(__DIR__.'/fixtures/read_slave_moodle_database_special.php'); +require_once(__DIR__.'/../../tests/fixtures/event_fixtures.php'); /** * DML read/read-write database handle use tests @@ -36,10 +40,12 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_special.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class core_dml_read_slave_testcase extends base_testcase { +class dml_read_slave_test extends \base_testcase { /** @var float */ static private $dbreadonlylatency = 0.8; + /** @var float */ + static private $defaultlatency = 1; /** * Instantiates a test database interface object. @@ -223,18 +229,27 @@ class core_dml_read_slave_testcase extends base_testcase { $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); - sleep(1); $handle = $DB->get_records('table'); $this->assertEquals('test_rw::test:test', $handle); $this->assertEquals(0, $DB->perf_get_reads_slave()); - $handle = $DB->get_records('table2'); + $handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}"); + $this->assertEquals('test_rw::test:test', $handle); + $this->assertEquals(0, $DB->perf_get_reads_slave()); + + sleep(1); + + $handle = $DB->get_records('table'); $this->assert_readonly_handle($handle); $this->assertEquals(1, $DB->perf_get_reads_slave()); + $handle = $DB->get_records('table2'); + $this->assert_readonly_handle($handle); + $this->assertEquals(2, $DB->perf_get_reads_slave()); + $handle = $DB->get_records_sql("SELECT * FROM {table2} JOIN {table}"); - $this->assertEquals('test_rw::test:test', $handle); - $this->assertEquals(1, $DB->perf_get_reads_slave()); + $this->assert_readonly_handle($handle); + $this->assertEquals(3, $DB->perf_get_reads_slave()); } /** @@ -278,12 +293,15 @@ class core_dml_read_slave_testcase extends base_testcase { * so the latency parameter is applied properly. * * @return void + * @covers ::can_use_readonly + * @covers ::commit_delegated_transaction */ - public function test_transaction() : void { + public function test_transaction(): void { $DB = $this->new_db(true); $this->assertNull($DB->get_dbhwrite()); + $skip = false; $transaction = $DB->start_delegated_transaction(); $now = microtime(true); $handle = $DB->get_records_sql("SELECT * FROM {table}"); @@ -304,11 +322,92 @@ class core_dml_read_slave_testcase extends base_testcase { // Make sure enough time passes. sleep(1); + } else { + $skip = true; } // Exceeded latency time, use ro handle. $handle = $DB->get_records_sql("SELECT * FROM {table}"); $this->assert_readonly_handle($handle); + + if ($skip) { + $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); + } + } + + /** + * Test readonly handle is not used with events + * when the latency parameter is applied properly. + * + * @return void + * @covers ::can_use_readonly + * @covers ::commit_delegated_transaction + */ + public function test_transaction_with_events(): void { + $this->with_global_db(function () { + global $DB; + + $DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class); + $DB->set_tables([ + 'config_plugins' => [ + 'columns' => [ + 'plugin' => (object)['meta_type' => ''], + ] + ] + ]); + + $this->assertNull($DB->get_dbhwrite()); + + $this->_called = false; + $transaction = $DB->start_delegated_transaction(); + $now = microtime(true); + + $observers = [ + [ + 'eventname' => '\core_tests\event\unittest_executed', + 'callback' => function (\core_tests\event\unittest_executed $event) use ($DB, $now) { + $this->_called = true; + $this->assertFalse($DB->is_transaction_started()); + + // This condition should always evaluate true, however we need to + // safeguard from an unaccounted delay that can break this test. + if (microtime(true) - $now < 1 + self::$dbreadonlylatency) { + // Not enough time passed, use rw handle. + $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); + $this->assertEquals('test_rw::test:test', $handle); + + // Make sure enough time passes. + sleep(1); + } else { + $this->markTestSkipped("Delay too long to test write handle immediately after transaction"); + } + + // Exceeded latency time, use ro handle. + $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); + $this->assertEquals('test_ro::test:test', $handle); + }, + 'internal' => 0, + ], + ]; + \core\event\manager::phpunit_replace_observers($observers); + + $handle = $DB->get_records_sql_p("SELECT * FROM {table}"); + // Use rw handle during transaction. + $this->assertEquals('test_rw::test:test', $handle); + + $handle = $DB->insert_record_raw('table', array('name' => 'blah')); + // Introduce delay so we can check that table write timestamps + // are adjusted properly. + sleep(1); + $event = \core_tests\event\unittest_executed::create([ + 'context' => \context_system::instance(), + 'other' => ['sample' => 1] + ]); + $event->trigger(); + $transaction->allow_commit(); + + $this->assertTrue($this->_called); + }); } /** diff --git a/lib/dml/tests/fixtures/read_slave_moodle_database.php b/lib/dml/tests/fixtures/read_slave_moodle_database.php index 982dbad135d..db41bd977d2 100644 --- a/lib/dml/tests/fixtures/read_slave_moodle_database.php +++ b/lib/dml/tests/fixtures/read_slave_moodle_database.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/test_moodle_database.php'); @@ -37,7 +39,7 @@ require_once(__DIR__.'/../../moodle_read_slave_trait.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class read_slave_moodle_database extends test_moodle_database { - use moodle_read_slave_trait; + use \moodle_read_slave_trait; /** @var string */ protected $handle; @@ -58,7 +60,7 @@ class read_slave_moodle_database extends test_moodle_database { $this->prefix = $prefix; if ($dbhost == 'test_ro_fail') { - throw new dml_connection_exception($dbhost); + throw new \dml_connection_exception($dbhost); } return true; diff --git a/lib/dml/tests/fixtures/read_slave_moodle_database_mock_mysqli.php b/lib/dml/tests/fixtures/read_slave_moodle_database_mock_mysqli.php index 29165f6b3ba..e1b5d0a93b9 100644 --- a/lib/dml/tests/fixtures/read_slave_moodle_database_mock_mysqli.php +++ b/lib/dml/tests/fixtures/read_slave_moodle_database_mock_mysqli.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../mysqli_native_moodle_database.php'); @@ -36,7 +38,7 @@ require_once(__DIR__.'/test_moodle_read_slave_trait.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class read_slave_moodle_database_mock_mysqli extends mysqli_native_moodle_database { +class read_slave_moodle_database_mock_mysqli extends \mysqli_native_moodle_database { use test_moodle_read_slave_trait; /** diff --git a/lib/dml/tests/fixtures/read_slave_moodle_database_mock_pgsql.php b/lib/dml/tests/fixtures/read_slave_moodle_database_mock_pgsql.php index 60367fdd5f4..a59573b35c1 100644 --- a/lib/dml/tests/fixtures/read_slave_moodle_database_mock_pgsql.php +++ b/lib/dml/tests/fixtures/read_slave_moodle_database_mock_pgsql.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../pgsql_native_moodle_database.php'); @@ -36,6 +38,6 @@ require_once(__DIR__.'/test_moodle_read_slave_trait.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class read_slave_moodle_database_mock_pgsql extends pgsql_native_moodle_database { +class read_slave_moodle_database_mock_pgsql extends \pgsql_native_moodle_database { use test_moodle_read_slave_trait; } diff --git a/lib/dml/tests/fixtures/read_slave_moodle_database_special.php b/lib/dml/tests/fixtures/read_slave_moodle_database_special.php index f34491935ee..be09eba0b43 100644 --- a/lib/dml/tests/fixtures/read_slave_moodle_database_special.php +++ b/lib/dml/tests/fixtures/read_slave_moodle_database_special.php @@ -23,10 +23,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/read_slave_moodle_database.php'); -require_once(__DIR__.'/read_slave_moodle_recordset_special.php'); /** * Database driver mock test class that uses read_slave_moodle_recordset_special @@ -50,6 +51,19 @@ class read_slave_moodle_database_special extends read_slave_moodle_database { return []; } + /** + * Returns read_slave_moodle_database::get_records_sql() + * For the tests where we need both fake result and dbhandle info. + * @param string $sql the SQL select query to execute. + * @param array $params array of sql parameters + * @param int $limitfrom return a subset of records, starting at this point (optional). + * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). + * @return string $handle handle property + */ + public function get_records_sql_p($sql, array $params = null, $limitfrom = 0, $limitnum = 0) { + return parent::get_records_sql($sql, $params); + } + /** * Returns fake recordset * @param string $sql @@ -74,3 +88,46 @@ class read_slave_moodle_database_special extends read_slave_moodle_database { return 1; } } + +/** + * Database recordset mock test class + * + * @package core + * @category dml + * @copyright 2018 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class read_slave_moodle_recordset_special extends \moodle_recordset { + /** + * Iterator interface + * @return void + */ + public function close() { + } + /** + * Iterator interface + * @return stdClass + */ + public function current() { + return new stdClass(); + } + /** + * Iterator interface + * @return void + */ + public function next() { + } + /** + * Iterator interface + * @return mixed + */ + public function key() { + } + /** + * Iterator interface + * @return bool + */ + public function valid() { + return false; + } +} diff --git a/lib/dml/tests/fixtures/read_slave_moodle_database_table_names.php b/lib/dml/tests/fixtures/read_slave_moodle_database_table_names.php index a0a199c8716..b676b11c33d 100644 --- a/lib/dml/tests/fixtures/read_slave_moodle_database_table_names.php +++ b/lib/dml/tests/fixtures/read_slave_moodle_database_table_names.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/read_slave_moodle_database.php'); diff --git a/lib/dml/tests/fixtures/test_moodle_database.php b/lib/dml/tests/fixtures/test_moodle_database.php index 54f41c0b4d3..90238958ff6 100644 --- a/lib/dml/tests/fixtures/test_moodle_database.php +++ b/lib/dml/tests/fixtures/test_moodle_database.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../moodle_database.php'); @@ -38,7 +40,7 @@ require_once(__DIR__.'/test_sql_generator.php'); * @copyright 2018 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class test_moodle_database extends moodle_database { +abstract class test_moodle_database extends \moodle_database { /** @var string */ private $error; @@ -53,7 +55,7 @@ abstract class test_moodle_database extends moodle_database { public function __construct($external = false) { parent::__construct($external); - $this->temptables = new moodle_temptables($this); + $this->temptables = new \moodle_temptables($this); } /** diff --git a/lib/dml/tests/fixtures/test_moodle_read_slave_trait.php b/lib/dml/tests/fixtures/test_moodle_read_slave_trait.php index 530e71a9816..087e6e94f4c 100644 --- a/lib/dml/tests/fixtures/test_moodle_read_slave_trait.php +++ b/lib/dml/tests/fixtures/test_moodle_read_slave_trait.php @@ -23,9 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__.'/../../pgsql_native_moodle_database.php'); +namespace core; /** * Read slave helper that exposes selected moodle_read_slave_trait metods @@ -55,7 +53,7 @@ trait test_moodle_read_slave_trait { $this->dbhreadonly = $ro; $this->set_db_handle($this->dbhwrite); - $this->temptables = new moodle_temptables($this); + $this->temptables = new \moodle_temptables($this); } /** diff --git a/lib/dml/tests/fixtures/test_sql_generator.php b/lib/dml/tests/fixtures/test_sql_generator.php index a1a42aa9de9..edf06f34455 100644 --- a/lib/dml/tests/fixtures/test_sql_generator.php +++ b/lib/dml/tests/fixtures/test_sql_generator.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../../ddl/sql_generator.php'); @@ -36,7 +38,7 @@ require_once(__DIR__.'/../../../ddl/sql_generator.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * */ -class test_sql_generator extends sql_generator { +class test_sql_generator extends \sql_generator { // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod /**