diff --git a/lib/dml/moodle_database.php b/lib/dml/moodle_database.php index f9f7aada275..3883246cb98 100644 --- a/lib/dml/moodle_database.php +++ b/lib/dml/moodle_database.php @@ -52,6 +52,10 @@ define('SQL_QUERY_STRUCTURE', 4); /** SQL_QUERY_AUX - Auxiliary query done by driver, setting connection config, getting table info, etc. */ define('SQL_QUERY_AUX', 5); +/** SQL_QUERY_AUX_READONLY - Auxiliary query that can be done using the readonly connection: + * database parameters, table/index/column lists, if not within transaction/ddl. */ +define('SQL_QUERY_AUX_READONLY', 6); + /** * Abstract class representing moodle database interface. * @link http://docs.moodle.org/dev/DML_functions @@ -414,9 +418,11 @@ abstract class moodle_database { /** * This should be called before each db query. + * * @param string $sql The query string. * @param array $params An array of parameters. - * @param int $type The type of query. ( SQL_QUERY_SELECT | SQL_QUERY_AUX | SQL_QUERY_INSERT | SQL_QUERY_UPDATE | SQL_QUERY_STRUCTURE ) + * @param int $type The type of query ( SQL_QUERY_SELECT | SQL_QUERY_AUX_READONLY | SQL_QUERY_AUX | + * SQL_QUERY_INSERT | SQL_QUERY_UPDATE | SQL_QUERY_STRUCTURE ). * @param mixed $extrainfo This is here for any driver specific extra information. * @return void */ @@ -433,6 +439,7 @@ abstract class moodle_database { switch ($type) { case SQL_QUERY_SELECT: case SQL_QUERY_AUX: + case SQL_QUERY_AUX_READONLY: $this->reads++; break; case SQL_QUERY_INSERT: @@ -483,6 +490,7 @@ abstract class moodle_database { switch ($type) { case SQL_QUERY_SELECT: case SQL_QUERY_AUX: + case SQL_QUERY_AUX_READONLY: throw new dml_read_exception($error, $sql, $params); case SQL_QUERY_INSERT: case SQL_QUERY_UPDATE: diff --git a/lib/dml/moodle_read_slave_trait.php b/lib/dml/moodle_read_slave_trait.php index 7f06e5efb8d..2a69459d35d 100644 --- a/lib/dml/moodle_read_slave_trait.php +++ b/lib/dml/moodle_read_slave_trait.php @@ -57,8 +57,9 @@ defined('MOODLE_INTERNAL') || die(); * 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 + * are used for transaction start/end, locking etc. In that respect, query_start() and * query_end() *must not* be used during the connection phase. + * - SQL_QUERY_AUX_READONLY queries will use the master write handle if in a transaction. * - SELECT queries will use the master write handle if: * -- any of the tables involved is a temp table * -- any of the tables involved is listed in the 'exclude_tables' option @@ -91,6 +92,7 @@ trait moodle_read_slave_trait { private $wantreadslave = false; private $readsslave = 0; private $slavelatency = 1; + private $structurechange = false; private $written = []; // Track tables being written to. private $readexclude = []; // Tables to exclude from using dbhreadonly. @@ -325,6 +327,10 @@ trait moodle_read_slave_trait { // Transactions are done as AUX, we cannot play with that. switch ($type) { + case SQL_QUERY_AUX_READONLY: + // SQL_QUERY_AUX_READONLY may read the structure data. + // We don't have a way to reliably determine whether it is safe to go to readonly if the structure has changed. + return !$this->structurechange; case SQL_QUERY_SELECT: if ($this->transactions) { return false; @@ -358,6 +364,7 @@ trait moodle_read_slave_trait { } return false; case SQL_QUERY_STRUCTURE: + $this->structurechange = true; foreach ($this->table_names($sql) as $tablename) { if (!in_array($tablename, $this->readexclude)) { $this->readexclude[] = $tablename; diff --git a/lib/dml/mysqli_native_moodle_database.php b/lib/dml/mysqli_native_moodle_database.php index a02e9b9d1ae..53f948fe102 100644 --- a/lib/dml/mysqli_native_moodle_database.php +++ b/lib/dml/mysqli_native_moodle_database.php @@ -176,7 +176,7 @@ class mysqli_native_moodle_database extends moodle_database { $sql = "SELECT engine FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); if ($rec = $result->fetch_assoc()) { @@ -194,7 +194,7 @@ class mysqli_native_moodle_database extends moodle_database { // Get the default database engine. $sql = "SELECT @@default_storage_engine engine"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); if ($rec = $result->fetch_assoc()) { @@ -205,7 +205,7 @@ class mysqli_native_moodle_database extends moodle_database { if ($engine === 'MyISAM') { // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported $sql = "SHOW STORAGE ENGINES"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); $engines = array(); @@ -349,7 +349,7 @@ class mysqli_native_moodle_database extends moodle_database { $sql = "SHOW VARIABLES LIKE 'innodb_file_format'"; } - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); if ($rec = $result->fetch_assoc()) { @@ -541,8 +541,8 @@ class mysqli_native_moodle_database extends moodle_database { $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); - // dbsocket is used ONLY if host is NULL or 'localhost', - // you can not disable it because it is always tried if dbhost is 'localhost' + // The dbsocket option is used ONLY if host is null or 'localhost'. + // You can not disable it because it is always tried if dbhost is 'localhost'. if (!empty($this->dboptions['dbsocket']) and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) { $dbsocket = $this->dboptions['dbsocket']; @@ -696,7 +696,7 @@ class mysqli_native_moodle_database extends moodle_database { $this->tables = array(); $prefix = str_replace('_', '\\_', $this->prefix); $sql = "SHOW TABLES LIKE '$prefix%'"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, $usecache ? SQL_QUERY_AUX_READONLY : SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); $len = strlen($this->prefix); @@ -723,7 +723,7 @@ class mysqli_native_moodle_database extends moodle_database { $indexes = array(); $fixedtable = $this->fix_table_name($table); $sql = "SHOW INDEXES FROM $fixedtable"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = $this->mysqli->query($sql); try { $this->query_end($result); @@ -760,7 +760,7 @@ class mysqli_native_moodle_database extends moodle_database { WHERE table_name = '" . $this->prefix.$table . "' AND table_schema = '" . $this->dbname . "' ORDER BY ordinal_position"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = $this->mysqli->query($sql); $this->query_end(true); // Don't want to throw anything here ever. MDL-30147 @@ -783,7 +783,7 @@ class mysqli_native_moodle_database extends moodle_database { $result->close(); $fixedtable = $this->fix_table_name($table); $sql = "SHOW COLUMNS FROM $fixedtable"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = $this->mysqli->query($sql); $this->query_end(true); if ($result === false) { @@ -1065,7 +1065,7 @@ class mysqli_native_moodle_database extends moodle_database { $charset = reset($collationinfo); $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = '$charset'"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = $this->mysqli->query($sql); $this->query_end($result); if ($result->fetch_assoc()) { @@ -1404,28 +1404,10 @@ class mysqli_native_moodle_database extends moodle_database { } /** - * Insert multiple records into database as fast as possible. - * - * Order of inserts is maintained, but the operation is not atomic, - * use transactions if necessary. - * - * This method is intended for inserting of large number of small objects, - * do not use for huge objects with text or binary fields. - * - * @since Moodle 2.7 - * - * @param string $table The database table to be inserted into - * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach - * @return void does not return new record ids - * - * @throws coding_exception if data objects have different structure - * @throws dml_exception A DML specific exception is thrown for any errors. + * Get chunk size for multiple records insert + * @return int */ - public function insert_records($table, $dataobjects) { - if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) { - throw new coding_exception('insert_records() passed non-traversable object'); - } - + private function insert_chunk_size(): int { // MySQL has a relatively small query length limit by default, // make sure 'max_allowed_packet' in my.cnf is high enough // if you change the following default... @@ -1456,7 +1438,33 @@ class mysqli_native_moodle_database extends moodle_database { } } } + return $chunksize; + } + /** + * Insert multiple records into database as fast as possible. + * + * Order of inserts is maintained, but the operation is not atomic, + * use transactions if necessary. + * + * This method is intended for inserting of large number of small objects, + * do not use for huge objects with text or binary fields. + * + * @since Moodle 2.7 + * + * @param string $table The database table to be inserted into + * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach + * @return void does not return new record ids + * + * @throws coding_exception if data objects have different structure + * @throws dml_exception A DML specific exception is thrown for any errors. + */ + public function insert_records($table, $dataobjects) { + if (!is_array($dataobjects) && !$dataobjects instanceof Traversable) { + throw new coding_exception('insert_records() passed non-traversable object'); + } + + $chunksize = $this->insert_chunk_size(); $columns = $this->get_columns($table, true); $fields = null; $count = 0; @@ -2023,12 +2031,12 @@ class mysqli_native_moodle_database extends moodle_database { } $sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); $sql = "START TRANSACTION"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); } @@ -2044,7 +2052,7 @@ class mysqli_native_moodle_database extends moodle_database { } $sql = "COMMIT"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); } @@ -2060,7 +2068,7 @@ class mysqli_native_moodle_database extends moodle_database { } $sql = "ROLLBACK"; - $this->query_start($sql, NULL, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX); $result = $this->mysqli->query($sql); $this->query_end($result); diff --git a/lib/dml/pgsql_native_moodle_database.php b/lib/dml/pgsql_native_moodle_database.php index ba0abe1d1b1..d03c37b65ab 100644 --- a/lib/dml/pgsql_native_moodle_database.php +++ b/lib/dml/pgsql_native_moodle_database.php @@ -44,6 +44,9 @@ class pgsql_native_moodle_database extends moodle_database { query_end as read_slave_query_end; } + /** @var array $serverinfo cache */ + private $serverinfo = []; + /** @var array $dbhcursor keep track of open cursors */ private $dbhcursor = []; @@ -336,7 +339,12 @@ class pgsql_native_moodle_database extends moodle_database { error_reporting($this->last_error_reporting); try { $this->read_slave_query_end($result); - if ($this->savepointpresent and $this->last_type != SQL_QUERY_AUX and $this->last_type != SQL_QUERY_SELECT) { + if ($this->savepointpresent && + !in_array( + $this->last_type, + [SQL_QUERY_AUX, SQL_QUERY_AUX_READONLY, SQL_QUERY_SELECT], + true + )) { $res = @pg_query($this->pgsql, "RELEASE SAVEPOINT moodle_pg_savepoint; SAVEPOINT moodle_pg_savepoint"); if ($res) { pg_free_result($res); @@ -357,14 +365,16 @@ class pgsql_native_moodle_database extends moodle_database { * Returns database server info array * @return array Array containing 'description' and 'version' info */ - public function get_server_info() { - static $info; - if (!$info) { - $this->query_start("--pg_version()", null, SQL_QUERY_AUX); - $info = pg_version($this->pgsql); + public function get_server_info(): array { + if (empty($this->serverinfo)) { + $this->query_start('--pg_version()', null, SQL_QUERY_AUX); + $this->serverinfo = pg_version($this->pgsql); $this->query_end(true); } - return array('description'=>$info['server'], 'version'=>$info['server']); + return [ + 'description' => $this->serverinfo['server'], + 'version' => $this->serverinfo['server'], + ]; } /** @@ -400,7 +410,7 @@ class pgsql_native_moodle_database extends moodle_database { WHERE c.relname LIKE '$prefix%' ESCAPE '|' AND c.relkind = 'r' AND (ns.nspname = current_schema() OR ns.oid = pg_my_temp_schema())"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = pg_query($this->pgsql, $sql); $this->query_end($result); @@ -492,7 +502,7 @@ class pgsql_native_moodle_database extends moodle_database { WHERE i.tablename = '$tablename' AND (i.schemaname = current_schema() OR ns.oid = pg_my_temp_schema())"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = pg_query($this->pgsql, $sql); $this->query_end($result); @@ -545,7 +555,7 @@ class pgsql_native_moodle_database extends moodle_database { AND (ns.nspname = current_schema() OR ns.oid = pg_my_temp_schema()) ORDER BY a.attnum"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = pg_query($this->pgsql, $sql); $this->query_end($result); @@ -766,8 +776,8 @@ class pgsql_native_moodle_database extends moodle_database { */ public function setup_is_unicodedb() { // Get PostgreSQL server_encoding value - $sql = "SHOW server_encoding"; - $this->query_start($sql, null, SQL_QUERY_AUX); + $sql = 'SHOW server_encoding'; + $this->query_start($sql, null, SQL_QUERY_AUX_READONLY); $result = pg_query($this->pgsql, $sql); $this->query_end($result); diff --git a/lib/dml/tests/dml_mysqli_read_slave_test.php b/lib/dml/tests/dml_mysqli_read_slave_test.php index e415957a0d8..7c797ebff4f 100644 --- a/lib/dml/tests/dml_mysqli_read_slave_test.php +++ b/lib/dml/tests/dml_mysqli_read_slave_test.php @@ -25,6 +25,8 @@ namespace core; +use moodle_database; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php'); @@ -45,7 +47,7 @@ class dml_mysqli_read_slave_test extends \base_testcase { * * @return void */ - public function test_lock() : void { + public function test_lock(): void { $DB = new read_slave_moodle_database_mock_mysqli(); $this->assertEquals(0, $DB->perf_get_reads_slave()); @@ -62,11 +64,11 @@ class dml_mysqli_read_slave_test extends \base_testcase { } /** - * Test readonly connection failure with real mysqli connection + * Test readonly handle is used for SQL_QUERY_AUX_READONLY queries. * * @return void */ - public function test_real_readslave_connect_fail() : void { + public function test_aux_readonly(): void { global $DB; if ($DB->get_dbfamily() != 'mysql') { @@ -76,14 +78,87 @@ class dml_mysqli_read_slave_test extends \base_testcase { // Open second connection. $cfg = $DB->export_dbconfig(); if (!isset($cfg->dboptions)) { - $cfg->dboptions = array(); + $cfg->dboptions = []; + } + $cfg->dboptions['readonly'] = [ + 'instance' => [$cfg->dbhost] + ]; + $cfg->dboptions['dbengine'] = null; + $cfg->dboptions['bulkinsertsize'] = null; + + // Get a separate disposable db connection handle with guaranteed 'readonly' config. + $db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary); + $db2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions); + + $reads = $db2->perf_get_reads(); + $readsprimary = $reads - $db2->perf_get_reads_slave(); + + // Readonly handle queries. + + $db2->setup_is_unicodedb(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_tables(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_indexes('course'); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_columns('course'); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + // Readwrite handle queries. + + if (PHP_INT_SIZE !== 4) { + $rc = new \ReflectionClass(\mysqli_native_moodle_database::class); + $rcm = $rc->getMethod('insert_chunk_size'); + + $rcm->setAccessible(true); + $rcm->invoke($db2); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $readsprimary = $reads - $db2->perf_get_reads_slave()); + } + + $db2->get_dbengine(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $readsprimary = $reads - $db2->perf_get_reads_slave()); + + $db2->diagnose(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $readsprimary = $reads - $db2->perf_get_reads_slave()); + + $db2->get_row_format('course'); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $readsprimary = $reads - $db2->perf_get_reads_slave()); + } + + /** + * Test readonly connection failure with real mysqli connection + * + * @return void + */ + public function test_real_readslave_connect_fail(): void { + global $DB; + + if ($DB->get_dbfamily() != 'mysql') { + $this->markTestSkipped('Not mysql'); + } + + // Open second connection. + $cfg = $DB->export_dbconfig(); + if (!isset($cfg->dboptions)) { + $cfg->dboptions = []; } $cfg->dboptions['readonly'] = [ 'instance' => ['host.that.is.not'], '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 d98b51a83d3..2388b65a904 100644 --- a/lib/dml/tests/dml_pgsql_read_slave_test.php +++ b/lib/dml/tests/dml_pgsql_read_slave_test.php @@ -25,6 +25,9 @@ namespace core; +use moodle_database; +use xmldb_table; + defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php'); @@ -38,13 +41,13 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \pgsql_native_moodle_database */ -class dml_pgsql_read_slave_test extends \base_testcase { +class dml_pgsql_read_slave_test extends \advanced_testcase { /** * Test correct database handles are used for cursors * * @return void */ - public function test_cursors() : void { + public function test_cursors(): void { $DB = new read_slave_moodle_database_mock_pgsql(); // Declare a cursor on a table that has not been written to. @@ -73,6 +76,18 @@ class dml_pgsql_read_slave_test extends \base_testcase { $DB->query_start($sql, null, SQL_QUERY_AUX); $this->assertTrue($DB->db_handle_is_rw()); $DB->query_end(null); + + // Close the non-written to table cursor. + $sql = 'CLOSE crs1'; + $DB->query_start($sql, [], SQL_QUERY_AUX); + $this->assertTrue($DB->db_handle_is_ro()); + $DB->query_end(null); + + // Close the written to table cursor. + $sql = 'CLOSE crs2'; + $DB->query_start($sql, [], SQL_QUERY_AUX); + $this->assertTrue($DB->db_handle_is_rw()); + $DB->query_end(null); } /** @@ -80,7 +95,7 @@ class dml_pgsql_read_slave_test extends \base_testcase { * * @return void */ - public function test_read_pg_table() : void { + public function test_read_pg_table(): void { $DB = new read_slave_moodle_database_mock_pgsql(); $this->assertEquals(0, $DB->perf_get_reads_slave()); @@ -97,7 +112,7 @@ class dml_pgsql_read_slave_test extends \base_testcase { * * @return void */ - public function test_read_pg_lock_table() : void { + public function test_read_pg_lock_table(): void { $DB = new read_slave_moodle_database_mock_pgsql(); $this->assertEquals(0, $DB->perf_get_reads_slave()); @@ -111,19 +126,16 @@ class dml_pgsql_read_slave_test extends \base_testcase { } /** - * Test readonly handle is not used for reading from temptables - * and getting temptables metadata. - * This test is only possible because of no pg_query error reporting. - * It may need to be removed in the future if we decide to handle null - * results in pgsql_native_moodle_database differently. + * Test readonly handle is used for SQL_QUERY_AUX_READONLY queries. * * @return void */ - public function test_temp_table() : void { + public function test_aux_readonly(): void { global $DB; + $this->resetAfterTest(); if ($DB->get_dbfamily() != 'postgres') { - $this->markTestSkipped("Not postgres"); + $this->markTestSkipped('Not postgres'); } // Open second connection. @@ -138,12 +150,81 @@ class dml_pgsql_read_slave_test 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); + + $reads = $db2->perf_get_reads(); + $readsprimary = $reads - $db2->perf_get_reads_slave(); + + // Readonly handle queries. + + $db2->get_server_info(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $readsprimary = $reads - $db2->perf_get_reads_slave()); + + $db2->setup_is_unicodedb(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_tables(); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_indexes('course'); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + $db2->get_columns('course'); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertEquals($readsprimary, $reads - $db2->perf_get_reads_slave()); + + // Readwrite handle queries. + + $tablename = 'test_table'; + $table = new xmldb_table($tablename); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $dbman = $db2->get_manager(); + $dbman->create_table($table); + $db2->get_columns($tablename); + $this->assertGreaterThan($reads, $reads = $db2->perf_get_reads()); + $this->assertGreaterThan($readsprimary, $reads - $db2->perf_get_reads_slave()); + } + + /** + * Test readonly handle is not used for reading from temptables + * and getting temptables metadata. + * This test is only possible because of no pg_query error reporting. + * It may need to be removed in the future if we decide to handle null + * results in pgsql_native_moodle_database differently. + * + * @return void + */ + public function test_temp_table(): void { + global $DB; + + if ($DB->get_dbfamily() != 'postgres') { + $this->markTestSkipped('Not postgres'); + } + + // Open second connection. + $cfg = $DB->export_dbconfig(); + if (!isset($cfg->dboptions)) { + $cfg->dboptions = []; + } + if (!isset($cfg->dboptions['readonly'])) { + $cfg->dboptions['readonly'] = [ + 'instance' => [$cfg->dbhost] + ]; + } + + // Get a separate disposable db connection handle with guaranteed 'readonly' config. + $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']); @@ -158,7 +239,7 @@ class dml_pgsql_read_slave_test 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']); @@ -179,11 +260,11 @@ class dml_pgsql_read_slave_test extends \base_testcase { * * @return void */ - public function test_real_readslave_connect_fail() : void { + public function test_real_readslave_connect_fail(): void { global $DB; if ($DB->get_dbfamily() != 'postgres') { - $this->markTestSkipped("Not postgres"); + $this->markTestSkipped('Not postgres'); } // Open second connection. @@ -196,7 +277,7 @@ class dml_pgsql_read_slave_test 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); }