first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,99 @@
<?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/>.
/**
* Native Aurora MySQL class representing moodle database interface.
*
* @package core_dml
* @copyright 2020 Lafayette College ITS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_database.php');
require_once(__DIR__.'/mysqli_native_moodle_database.php');
require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
/**
* Native Aurora MySQL class representing moodle database interface.
*
* @package core_dml
* @copyright 2020 Lafayette College ITS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auroramysql_native_moodle_database extends mysqli_native_moodle_database {
/** @var bool is compressed row format supported cache */
protected $compressedrowformatsupported = false;
/**
* Returns localised database type name.
*
* Returns localised database type name. Can be used before connect().
* @return string
*/
public function get_name(): ?string {
return get_string('nativeauroramysql', 'install');
}
/**
* Returns localised database configuration help.
*
* Returns localised database configuration help. Can be used before connect().
* @return string
*/
public function get_configuration_help(): ?string {
return get_string('nativeauroramysql', 'install');
}
/**
* Returns the database vendor.
*
* Returns the database vendor. Can be used before connect().
* @return string The db vendor name, usually the same as db family name.
*/
public function get_dbvendor(): ?string {
return 'mysql';
}
/**
* Returns more specific database driver type
*
* Returns more specific database driver type. Can be used before connect().
* @return string db type mysqli, pgsql, oci, mssql, sqlsrv
*/
protected function get_dbtype(): ?string {
return 'auroramysql';
}
/**
* It is time to require transactions everywhere.
*
* MyISAM is NOT supported!
*
* @return bool
*/
protected function transactions_supported(): ?bool {
if ($this->external) {
return parent::transactions_supported();
}
return true;
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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/>.
/**
* Database column information.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Detailed database field information.
*
* It is based on the adodb library's ADOFieldObject object.
* 'column' does mean 'the field' here.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @property-read string $name Name of column - lowercase.
* @property-read string $type Driver dependent native data type. Not standardised, it's used to find meta_type.
*
* Max length:
* character type - number of characters
* blob - number of bytes
* integer - number of digits
* float - digits left from floating point
* boolean - 1
* @property-read int $max_length size of the database field, eg how much data can you put in there.
*
* @property-read int $scale Scale of field, decimal points (float), null otherwise.
* @property-read bool $not_null true if the field is set to NOT NULL.
* @property-read bool $primary_key true if the field is the primary key. (usually 'id').
* @property-read bool $auto_increment True if field is autoincrementing or sequence.
* @property-read bool $binary True if the field is binary.
* @property-read bool $has_default True if the default value is defined.
* @property-read string $default_value The default value (if defined).
* @property-read bool $unique True if the field values are unique, false if not.
*
* Standardised one character column type, uppercased and enumerated as follows:
* R - counter (integer primary key)
* I - integers
* N - numbers (floats)
* C - characters and strings
* X - texts
* B - binary blobs
* L - boolean (1 bit)
* T - timestamp - unsupported
* D - date - unsupported
* @property-read string $meta_type Standardised one character column type, uppercased and enumerated: R,I,N,C,X,B,L,T,D
*/
class database_column_info {
/**
* @var array The internal storage of column data.
*/
protected $data;
/**
* Magic set function. This is a read only object and you aren't allowed to write to any variables.
*
* @param string $name ignored.
* @param mixed $value ignored.
* @throws coding_exception You are not allowed to set data on database_column_info
*/
public function __set($name, $value) {
throw new coding_exception('database_column_info is a ready only object to allow for faster caching.');
}
/**
* Magic get function.
*
* @param string $variablename variable name to return the value of.
* @return mixed The variable contents.
*
* @throws coding_exception You cannot request a variable that is not allowed.
*/
public function __get($variablename) {
if (isset($this->data[$variablename]) || array_key_exists($variablename, $this->data)) {
return $this->data[$variablename];
}
throw new coding_exception('Asked for a variable that is not available . ('.$variablename.').');
}
/**
* Magic isset function.
*
* @param string $variablename The name of the property to test if isset().
* @return bool Whether the value is set or not.
*/
public function __isset($variablename) {
return isset($this->data[$variablename]);
}
/**
* Constructor
*
* @param mixed $data object or array with properties
*/
public function __construct($data) {
// Initialize all the allowed variables to null so the array key exists.
$validelements = array('name', 'type', 'max_length', 'scale', 'not_null', 'primary_key',
'auto_increment', 'binary', 'has_default', 'default_value',
'unique', 'meta_type');
foreach ($validelements as $element) {
if (isset($data->$element)) {
$this->data[$element] = $data->$element;
} else {
$this->data[$element] = null;
}
}
switch ($this->data['meta_type']) {
case 'R': // normalise counters (usually 'id')
$this->data['binary'] = false;
$this->data['has_default'] = false;
$this->data['default_value'] = null;
$this->data['unique'] = true;
break;
case 'C':
$this->data['auto_increment'] = false;
$this->data['binary'] = false;
break;
}
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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/>.
/**
* Native MariaDB class representing moodle database interface.
*
* @package core_dml
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_database.php');
require_once(__DIR__.'/mysqli_native_moodle_database.php');
require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
/**
* Native MariaDB class representing moodle database interface.
*
* @package core_dml
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mariadb_native_moodle_database extends mysqli_native_moodle_database {
/**
* Returns localised database type name
* Note: can be used before connect()
* @return string
*/
public function get_name() {
return get_string('nativemariadb', 'install');
}
/**
* Returns localised database configuration help.
* Note: can be used before connect()
* @return string
*/
public function get_configuration_help() {
return get_string('nativemariadbhelp', 'install');
}
/**
* Returns the database vendor.
* Note: can be used before connect()
* @return string The db vendor name, usually the same as db family name.
*/
public function get_dbvendor() {
return 'mariadb';
}
/**
* Returns more specific database driver type
* Note: can be used before connect()
* @return string db type mysqli, pgsql, oci, mssql, sqlsrv
*/
protected function get_dbtype() {
return 'mariadb';
}
protected function has_breaking_change_quoted_defaults() {
$version = $this->get_server_info()['version'];
// Breaking change since 10.2.7: MDEV-13132.
return version_compare($version, '10.2.7', '>=');
}
public function has_breaking_change_sqlmode() {
$version = $this->get_server_info()['version'];
// Breaking change since 10.2.4: https://mariadb.com/kb/en/the-mariadb-library/sql-mode/#setting-sql_mode.
return version_compare($version, '10.2.4', '>=');
}
/**
* It is time to require transactions everywhere.
*
* MyISAM is NOT supported!
*
* @return bool
*/
protected function transactions_supported() {
if ($this->external) {
return parent::transactions_supported();
}
return true;
}
/**
* Does this mariadb instance support fulltext indexes?
*
* @return bool
*/
public function is_fulltext_search_supported() {
$info = $this->get_server_info();
if (version_compare($info['version'], '10.0.5', '>=')) {
return true;
}
return false;
}
}
File diff suppressed because it is too large Load Diff
+406
View File
@@ -0,0 +1,406 @@
<?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/>.
/**
* Trait that adds read-only slave connection capability
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Trait to wrap connect() method of database driver classes that gives
* ability to use read only slave instances for SELECT queries. For the
* databases that support replication and read only connections to the slave.
* If the slave connection is configured there will be two database handles
* created, one for the master and another one for the slave. If there's no
* slave specified everything uses master handle.
*
* Classes that use this trait need to rename existing connect() method to
* raw_connect(). In addition, they need to provide get_db_handle() and
* set_db_handle() methods, due to dbhandle attributes not being named
* consistently across the database driver classes.
*
* Read only slave connection is configured in the $CFG->dboptions['readonly']
* array.
* - 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). 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
* does not go through the standard dml interface or some other unaccounted
* situation. It should not be used under normal circumstances, and its use
* indicates a problem in the system that needs addressig.
*
* 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. For those queries master
* write handle is used.
* - SQL_QUERY_AUX queries will always use the master write handle because they
* 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
* -- any of the tables involved is in the $written array:
* * 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:
* - we have set $CFG->dboptions['readonly']['latency'] to 0.2.
* - a SQL_QUERY_UPDATE to table tbl_x happens, and it is recorded in
* the $written array
* - 0.15 seconds later SQL_QUERY_SELECT with tbl_x is requested - the master
* connection is used
* - 0.10 seconds later (0.25 seconds after SQL_QUERY_UPDATE) another
* SQL_QUERY_SELECT with tbl_x is requested - this time more than 0.2 secs
* has gone and master -> slave sync is assumed, so the slave connection is
* used again
*/
trait moodle_read_slave_trait {
/** @var resource master write database handle */
protected $dbhwrite;
/** @var resource slave read only database handle */
protected $dbhreadonly;
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.
// Store original params.
private $pdbhost;
private $pdbuser;
private $pdbpass;
private $pdbname;
private $pprefix;
private $pdboptions;
/**
* Gets db handle currently used with queries
* @return resource
*/
abstract protected function get_db_handle();
/**
* Sets db handle to be used with subsequent queries
* @param resource $dbh
* @return void
*/
abstract protected function set_db_handle($dbh): void;
/**
* Connect to db
* The real connection establisment, called from connect() and set_dbhwrite()
* @param string $dbhost The database host.
* @param string $dbuser The database username.
* @param string $dbpass The database username's password.
* @param string $dbname The name of the database being connected to.
* @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
* @param array $dboptions driver specific options
* @return bool true
* @throws dml_connection_exception if error
*/
abstract protected function raw_connect(string $dbhost, string $dbuser, string $dbpass, string $dbname, $prefix, array $dboptions = null): bool;
/**
* Connect to db
* The connection parameters processor that sets up stage for master write and slave readonly handles.
* Must be called before other methods.
* @param string $dbhost The database host.
* @param string $dbuser The database username.
* @param string $dbpass The database username's password.
* @param string $dbname The name of the database being connected to.
* @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
* @param array $dboptions driver specific options
* @return bool true
* @throws dml_connection_exception if error
*/
public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions = null) {
$this->pdbhost = $dbhost;
$this->pdbuser = $dbuser;
$this->pdbpass = $dbpass;
$this->pdbname = $dbname;
$this->pprefix = $prefix;
$this->pdboptions = $dboptions;
if ($dboptions) {
if (isset($dboptions['readonly'])) {
$this->wantreadslave = true;
$dboptionsro = $dboptions['readonly'];
if (isset($dboptionsro['connecttimeout'])) {
$dboptions['connecttimeout'] = $dboptionsro['connecttimeout'];
} else if (!isset($dboptions['connecttimeout'])) {
$dboptions['connecttimeout'] = 2; // Default readonly connection timeout.
}
if (isset($dboptionsro['latency'])) {
$this->slavelatency = $dboptionsro['latency'];
}
if (isset($dboptionsro['exclude_tables'])) {
$this->readexclude = $dboptionsro['exclude_tables'];
if (!is_array($this->readexclude)) {
throw new configuration_exception('exclude_tables must be an array');
}
}
$dbport = isset($dboptions['dbport']) ? $dboptions['dbport'] : null;
$slaves = $dboptionsro['instance'];
if (!is_array($slaves) || !isset($slaves[0])) {
$slaves = [$slaves];
}
if (count($slaves) > 1) {
// Randomise things a bit.
shuffle($slaves);
}
// Find first connectable readonly slave.
$rodb = [];
foreach ($slaves as $slave) {
if (!is_array($slave)) {
$slave = ['dbhost' => $slave];
}
foreach (['dbhost', 'dbuser', 'dbpass'] as $dbparam) {
$rodb[$dbparam] = isset($slave[$dbparam]) ? $slave[$dbparam] : $$dbparam;
}
$dboptions['dbport'] = isset($slave['dbport']) ? $slave['dbport'] : $dbport;
try {
$this->raw_connect($rodb['dbhost'], $rodb['dbuser'], $rodb['dbpass'], $dbname, $prefix, $dboptions);
$this->dbhreadonly = $this->get_db_handle();
break;
} catch (dml_connection_exception $e) { // phpcs:ignore
// If readonly slave is not connectable we'll have to do without it.
}
}
// ... lock_db queries always go to master.
// Since it is a lock and as such marshalls concurrent connections,
// it is best to leave it out and avoid master/slave latency.
$this->readexclude[] = 'lock_db';
// ... and sessions.
$this->readexclude[] = 'sessions';
}
}
if (!$this->dbhreadonly) {
$this->set_dbhwrite();
}
return true;
}
/**
* Set database handle to readwrite master
* Will connect if required. Calls set_db_handle()
* @return void
*/
private function set_dbhwrite(): void {
// Lazy connect to read/write master.
if (!$this->dbhwrite) {
$temptables = $this->temptables;
$this->raw_connect($this->pdbhost, $this->pdbuser, $this->pdbpass, $this->pdbname, $this->pprefix, $this->pdboptions);
if ($temptables) {
$this->temptables = $temptables; // Restore temptables, so we don't get separate sets for rw and ro.
}
$this->dbhwrite = $this->get_db_handle();
}
$this->set_db_handle($this->dbhwrite);
}
/**
* Returns whether we want to connect to slave database for read queries.
* @return bool Want read only connection
*/
public function want_read_slave(): bool {
return $this->wantreadslave;
}
/**
* Returns the number of reads done by the read only database.
* @return int Number of reads.
*/
public function perf_get_reads_slave(): int {
return $this->readsslave;
}
/**
* On DBs that support it, switch to transaction mode and begin a transaction
* @return moodle_transaction
*/
public function start_delegated_transaction() {
$this->set_dbhwrite();
return parent::start_delegated_transaction();
}
/**
* Called before each db query.
* @param string $sql
* @param array|null $params An array of parameters.
* @param int $type type of query
* @param mixed $extrainfo driver specific extra information
* @return void
*/
protected function query_start($sql, ?array $params, $type, $extrainfo = null) {
parent::query_start($sql, $params, $type, $extrainfo);
$this->select_db_handle($type, $sql);
}
/**
* This should be called immediately after each db query. It does a clean up of resources.
*
* @param mixed $result The db specific result obtained from running a query.
* @return void
*/
protected function query_end($result) {
if ($this->written) {
// Adjust the written time.
array_walk($this->written, function (&$val) {
if ($val === true) {
$val = microtime(true);
}
});
}
parent::query_end($result);
}
/**
* Select appropriate db handle - readwrite or readonly
* @param int $type type of query
* @param string $sql
* @return void
*/
protected function select_db_handle(int $type, string $sql): void {
if ($this->dbhreadonly && $this->can_use_readonly($type, $sql)) {
$this->readsslave++;
$this->set_db_handle($this->dbhreadonly);
return;
}
$this->set_dbhwrite();
}
/**
* Check if The query qualifies for readonly connection execution
* Logging queries are exempt, those are write operations that circumvent
* standard query_start/query_end paths.
* @param int $type type of query
* @param string $sql
* @return bool
*/
protected function can_use_readonly(int $type, string $sql): bool {
if ($this->loggingquery) {
return false;
}
if (during_initial_install()) {
return false;
}
// 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;
}
$now = null;
foreach ($this->table_names($sql) as $tablename) {
if (in_array($tablename, $this->readexclude)) {
return false;
}
if ($this->temptables && $this->temptables->is_temptable($tablename)) {
return false;
}
if (isset($this->written[$tablename])) {
$now = $now ?: microtime(true);
if ($now - $this->written[$tablename] < $this->slavelatency) {
return false;
}
unset($this->written[$tablename]);
}
}
return true;
case SQL_QUERY_INSERT:
case SQL_QUERY_UPDATE:
foreach ($this->table_names($sql) as $tablename) {
$this->written[$tablename] = true;
}
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;
}
}
return false;
}
return false;
}
/**
* Indicates delegated transaction finished successfully.
* Set written times after outermost transaction finished
* @param moodle_transaction $transaction The transaction to commit
* @return void
* @throws dml_transaction_exception Creates and throws transaction related exceptions.
*/
public function commit_delegated_transaction(moodle_transaction $transaction) {
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);
}
/**
* Parse table names from query
* @param string $sql
* @return array
*/
protected function table_names(string $sql): array {
preg_match_all('/\b'.$this->prefix.'([a-z][A-Za-z0-9_]*)/', $sql, $match);
return $match[1];
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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/>.
/**
* Abstract recordset.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Abstract class for resultsets returned from database functions.
* This is a simple Iterator with needed recorset closing support.
*
* The difference from old recorset is that the records are returned
* as objects, not arrays. You should use "foreach ($recordset as $record) {}"
* followed by "$recordset->close()".
*
* Do not forget to close all recordsets when they are not needed anymore!
*/
abstract class moodle_recordset implements Iterator {
/**
* Returns current record - fields as object properties, lowercase
* @return object
*/
//public abstract function current();
/**
* Returns the key of current row
* @return int current row
*/
//public abstract function key();
/**
* Moves forward to next row
* @return void
*/
//public abstract function next();
/**
* Rewinds are not supported!
* @return void
*/
public function rewind(): void {
// no seeking, sorry - let's ignore it ;-)
return;
}
/**
* Did we reach the end?
* @return boolean
*/
//public abstract function valid();
/**
* Free resources and connections, recordset can not be used anymore.
* @return void
*/
abstract public function close();
}
+145
View File
@@ -0,0 +1,145 @@
<?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/>.
/**
* Generic temptables object store
*
* Provides support to databases lacking some "expected behaviour" related
* with some operations with temporary tables like:
*
* - databases not retrieving temp tables from information schema tables (mysql)
* - databases using a different name schema for temp tables (like mssql).
* - databases that don't collect planner stats for temp tables (like PgSQL).
*
* Basically it works as a simple store of created temporary tables, providing
* some simple getters/setters methods. Each database can extend it for its own
* purposes (for example, return correct name, see the mssql implementation)
*
* The unique instance of the object by database connection is shared by the database
* and the sql_generator, so both are able to use its facilities, with the final goal
* of doing temporary tables support 100% cross-db and transparent within the DB API.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class moodle_temptables {
/** @var circular reference, to be able to use DB facilities here if needed */
protected $mdb;
/** @var prefix to be used for all the DB objects */
protected $prefix;
/** @var simple array of moodle, not prefixed 'tablename' => DB, final (prefixed) 'tablename' */
protected $temptables;
/**
* Creates new moodle_temptables instance
* @param moodle_database $mdb An instance of moodle_database.
*/
public function __construct($mdb) {
$this->mdb = $mdb;
$this->prefix = $mdb->get_prefix();
$this->temptables = array();
}
/**
* Add one temptable to the store
*
* Given one moodle temptable name (without prefix), add it to the store, with the
* key being the original moodle name and the value being the real db temptable name
* already prefixed
*
* Override and use this *only* if the database requires modification in the table name.
*
* @param string $tablename name without prefix of the table created as temptable
*/
public function add_temptable($tablename) {
// TODO: throw exception if exists: if ($this->is_temptable...
$this->temptables[$tablename] = $tablename;
}
/**
* Delete one temptable from the store
*
* @param string $tablename name without prefix of the dropped temptable
*/
public function delete_temptable($tablename) {
// TODO: throw exception if not exists: if (!$this->is_temptable....
unset($this->temptables[$tablename]);
}
/**
* Returns all the tablenames (without prefix) existing in the store
*
* @return array containing all the tablenames in the store (tablename both key and value)
*/
public function get_temptables() {
return $this->temptables;
}
/**
* Returns if one table, based in the information present in the store, is a temp table
*
* @param string $tablename name without prefix of the table we are asking about
* @return bool true if the table is a temp table (based in the store info), false if not
*/
public function is_temptable($tablename) {
return !empty($this->temptables[$tablename]);
}
/**
* Given one tablename (no prefix), return the name of the corresponding temporary table,
* If the table isn't a "registered" temp table, returns null
*
* @param string $tablename name without prefix which corresponding temp tablename needs to know
* @return mixed DB name of the temp table or null if it isn't a temp table
*/
public function get_correct_name($tablename) {
if ($this->is_temptable($tablename)) {
return $this->temptables[$tablename];
}
return null;
}
/**
* Analyze the data in temporary tables to force statistics collection after bulk data loads.
* The database class detects all temporary tables and will automatically analyze all created tables
*
* @return void
*/
public function update_stats() {
// By default most databases do automatic on temporary tables, PgSQL does not.
// As a result, update_stats call immediately return for non-interesting database types.
}
/**
* Dispose the temptables stuff, checking for wrong situations, informing and recovering from them
*/
public function dispose() {
// We shouldn't have any temp table registered at the end of the script.
// So we error_log that and, at the same time, drop all the pending temptables
if ($temptables = $this->get_temptables()) {
error_log('Potential coding error - existing temptables found when disposing database. Must be dropped!');
foreach ($temptables as $temptable) {
$this->mdb->get_manager()->drop_table(new xmldb_table($temptable));
}
}
$this->mdb = null;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?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/>.
/**
* Delegated database transaction support.
*
* @package core_dml
* @copyright 2009 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Delegated transaction class.
*
* @package core_dml
* @copyright 2009 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodle_transaction {
/** @var array The debug_backtrace() returned array.*/
private $start_backtrace;
/**@var moodle_database The moodle_database instance.*/
private $database = null;
/**
* Delegated transaction constructor,
* can be called only from moodle_database class.
* Unfortunately PHP's protected keyword is useless.
* @param moodle_database $database
*/
public function __construct($database) {
$this->database = $database;
$this->start_backtrace = debug_backtrace();
array_shift($this->start_backtrace);
}
/**
* Returns backtrace of the code starting exception.
* @return array
*/
public function get_backtrace() {
return $this->start_backtrace;
}
/**
* Is the delegated transaction already used?
* @return bool true if commit and rollback allowed, false if already done
*/
public function is_disposed() {
return empty($this->database);
}
/**
* Mark transaction as disposed, no more
* commits and rollbacks allowed.
* To be used only from moodle_database class
* @return null
*/
public function dispose() {
return $this->database = null;
}
/**
* Commit delegated transaction.
* The real database commit SQL is executed
* only after committing all delegated transactions.
*
* Incorrect order of nested commits or rollback
* at any level is resulting in rollback of SQL transaction.
*
* @return void
*/
public function allow_commit() {
if ($this->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $this);
}
$this->database->commit_delegated_transaction($this);
}
/**
* Rollback all current delegated transactions.
*
* @param Exception|Throwable $e mandatory exception/throwable
* @return void
*/
public function rollback($e) {
if ($this->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $this);
}
$this->database->rollback_delegated_transaction($this, $e);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
<?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/>.
/**
* Mysqli specific recordset.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_recordset.php');
/**
* Mysqli specific moodle recordset class
*
* @package core
* @subpackage dml_driver
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mysqli_native_moodle_recordset extends moodle_recordset {
protected $result;
protected $current;
public function __construct($result) {
$this->result = $result;
$this->current = $this->fetch_next();
}
public function __destruct() {
$this->close();
}
private function fetch_next() {
if (!$this->result) {
return false;
}
if (!$row = $this->result->fetch_assoc()) {
$this->result->close();
$this->result = null;
return false;
}
$row = array_change_key_case($row, CASE_LOWER);
return $row;
}
public function current(): stdClass {
return (object)$this->current;
}
#[\ReturnTypeWillChange]
public function key() {
// return first column value as key
if (!$this->current) {
return false;
}
$key = reset($this->current);
return $key;
}
public function next(): void {
$this->current = $this->fetch_next();
}
public function valid(): bool {
return !empty($this->current);
}
public function close() {
if ($this->result) {
$this->result->close();
$this->result = null;
}
$this->current = null;
}
}
@@ -0,0 +1,33 @@
<?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/>.
/**
* MYSQL specific temptables store. Needed because temporary tables
* are named differently than normal tables. Also used to be able to retrieve
* temp table names included in the get_tables() method of the DB.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_temptables.php');
class mysqli_native_moodle_temptables extends moodle_temptables {
// I love these classes :-P
}
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
-- 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/>.
/**
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @version 20091010 (plz, keep this updated for easier reference)
*/
/**
* This sql script generates various PL/SQL packages needed to provide
* cross-db compatibility in the Moodle 2.x DB API with some operations
* not natively supported by Oracle, namely:
* - locking: Application locks used by Moodle DB sessions. It uses
* the DBMS_LOCK package so execution must be granted
* to the Moodle DB user by SYS to work properly.
* - bit ops: To provide cross-db bitwise operations to be used by the
* sql_bitXXX() helper functions
* - one space hacks: One space empty string substitute hacks.
*
* Moodle will not parse this file correctly if it uses Windows line endings.
*/
CREATE OR REPLACE PACKAGE MOODLELIB AS
FUNCTION BITOR (value1 IN INTEGER, value2 IN INTEGER) RETURN INTEGER;
FUNCTION BITXOR(value1 IN INTEGER, value2 IN INTEGER) RETURN INTEGER;
FUNCTION GET_HANDLE (lock_name IN VARCHAR2) RETURN VARCHAR2;
FUNCTION GET_LOCK (lock_name IN VARCHAR2, lock_timeout IN INTEGER) RETURN INTEGER;
FUNCTION RELEASE_LOCK(lock_name IN VARCHAR2) RETURN INTEGER;
FUNCTION UNDO_DIRTY_HACK(hackedstring IN VARCHAR2) RETURN VARCHAR2;
FUNCTION UNDO_MEGA_HACK(hackedstring IN VARCHAR2) RETURN VARCHAR2;
FUNCTION TRICONCAT(string1 IN VARCHAR2, string2 IN VARCHAR2, string3 IN VARCHAR2) RETURN VARCHAR2;
END MOODLELIB;
/
CREATE OR REPLACE PACKAGE BODY MOODLELIB AS
FUNCTION BITOR(value1 IN INTEGER, value2 IN INTEGER) RETURN INTEGER IS
BEGIN
RETURN value1 + value2 - BITAND(value1,value2);
END BITOR;
FUNCTION BITXOR(value1 IN INTEGER, value2 IN INTEGER) RETURN INTEGER IS
BEGIN
RETURN MOODLELIB.BITOR(value1,value2) - BITAND(value1,value2);
END BITXOR;
FUNCTION GET_HANDLE(lock_name IN VARCHAR2) RETURN VARCHAR2 IS
PRAGMA AUTONOMOUS_TRANSACTION;
lock_handle VARCHAR2(128);
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE (
lockname => lock_name,
lockhandle => lock_handle,
expiration_secs => 864000);
RETURN lock_handle;
END GET_HANDLE;
FUNCTION GET_LOCK(lock_name IN VARCHAR2, lock_timeout IN INTEGER) RETURN INTEGER IS
lock_status NUMBER;
BEGIN
lock_status := DBMS_LOCK.REQUEST(
lockhandle => GET_HANDLE(lock_name),
lockmode => DBMS_LOCK.X_MODE, -- eXclusive
timeout => lock_timeout,
release_on_commit => FALSE);
CASE lock_status
WHEN 0 THEN NULL;
WHEN 2 THEN RAISE_APPLICATION_ERROR(-20000,'deadlock detected');
WHEN 4 THEN RAISE_APPLICATION_ERROR(-20000,'lock already obtained');
ELSE RAISE_APPLICATION_ERROR(-20000,'request lock failed - ' || lock_status);
END CASE;
RETURN 1;
END GET_LOCK;
FUNCTION RELEASE_LOCK(lock_name IN VARCHAR2) RETURN INTEGER IS
lock_status NUMBER;
BEGIN
lock_status := DBMS_LOCK.RELEASE(
lockhandle => GET_HANDLE(lock_name));
IF lock_status > 0 THEN
RAISE_APPLICATION_ERROR(-20000,'release lock failed - ' || lock_status);
END IF;
RETURN 1;
END RELEASE_LOCK;
FUNCTION UNDO_DIRTY_HACK(hackedstring IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
IF hackedstring = ' ' THEN
RETURN '';
END IF;
RETURN hackedstring;
END UNDO_DIRTY_HACK;
FUNCTION UNDO_MEGA_HACK(hackedstring IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
IF hackedstring IS NULL THEN
RETURN hackedstring;
END IF;
RETURN REPLACE(hackedstring, '*OCISP*', ' ');
END UNDO_MEGA_HACK;
FUNCTION TRICONCAT(string1 IN VARCHAR2, string2 IN VARCHAR2, string3 IN VARCHAR2) RETURN VARCHAR2 IS
stringresult VARCHAR2(1333);
BEGIN
IF string1 IS NULL THEN
RETURN NULL;
END IF;
IF string2 IS NULL THEN
RETURN NULL;
END IF;
IF string3 IS NULL THEN
RETURN NULL;
END IF;
stringresult := CONCAT(CONCAT(MOODLELIB.UNDO_DIRTY_HACK(string1), MOODLELIB.UNDO_DIRTY_HACK(string2)), MOODLELIB.UNDO_DIRTY_HACK(string3));
IF stringresult IS NULL THEN
RETURN ' ';
END IF;
RETURN stringresult;
END;
END MOODLELIB;
/
SHOW ERRORS
/
+88
View File
@@ -0,0 +1,88 @@
<?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/>.
/**
* Oracle specific recordset.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_recordset.php');
class oci_native_moodle_recordset extends moodle_recordset {
protected $stmt;
protected $current;
public function __construct($stmt) {
$this->stmt = $stmt;
$this->current = $this->fetch_next();
}
public function __destruct() {
$this->close();
}
private function fetch_next() {
if (!$this->stmt) {
return false;
}
if (!$row = oci_fetch_array($this->stmt, OCI_ASSOC + OCI_RETURN_NULLS + OCI_RETURN_LOBS)) {
oci_free_statement($this->stmt);
$this->stmt = null;
return false;
}
$row = array_change_key_case($row, CASE_LOWER);
unset($row['oracle_rownum']);
array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
return $row;
}
public function current(): stdClass {
return (object)$this->current;
}
#[\ReturnTypeWillChange]
public function key() {
// return first column value as key
if (!$this->current) {
return false;
}
$key = reset($this->current);
return $key;
}
public function next(): void {
$this->current = $this->fetch_next();
}
public function valid(): bool {
return !empty($this->current);
}
public function close() {
if ($this->stmt) {
oci_free_statement($this->stmt);
$this->stmt = null;
}
$this->current = null;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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/>.
/**
* OCI specific temptables store. Needed because temporary tables
* in Oracle are global (to all sessions), so we need to rename them
* on the fly in order to get local (different for each session) table names.
* Also used to be able to retrieve temp table names included in the get_tables()
* method of the DB.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_temptables.php');
class oci_native_moodle_temptables extends moodle_temptables {
/** @var int To store unique_session_id. Needed for temp tables unique naming (upto 24cc) */
protected $unique_session_id; //
/** @var int To get incrementally different temptable names on each add_temptable() request */
protected $counter;
/**
* Creates new moodle_temptables instance
* @param object moodle_database instance
*/
public function __construct($mdb, $unique_session_id) {
$this->unique_session_id = $unique_session_id;
$this->counter = 1;
parent::__construct($mdb);
}
/**
* Add one temptable to the store.
*
* Overridden because OCI only support global temptables, so we need to change completely the name, based
* in unique session identifier, to get local-like temp tables support
* tables before the prefix.
*
* Given one moodle temptable name (without prefix), add it to the store, with the
* key being the original moodle name and the value being the real db temptable name
* already prefixed
*
* Override and use this *only* if the database requires modification in the table name.
*
* @param string $tablename name without prefix of the table created as temptable
*/
public function add_temptable($tablename) {
// TODO: throw exception if exists: if ($this->is_temptable...
$this->temptables[$tablename] = $this->prefix . $this->unique_session_id . $this->counter;
$this->counter++;
}
}
+626
View File
@@ -0,0 +1,626 @@
<?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/>.
/**
* Experimental pdo database class
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_database.php');
require_once(__DIR__.'/pdo_moodle_recordset.php');
/**
* Experimental pdo database class
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class pdo_moodle_database extends moodle_database {
protected $pdb;
protected $lastError = null;
/**
* Constructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB)
* note this has effect to decide if prefix checks must be performed or no
* @param bool true means external database used
*/
public function __construct($external=false) {
parent::__construct($external);
}
/**
* Connect to db
* Must be called before other methods.
* @param string $dbhost The database host.
* @param string $dbuser The database username.
* @param string $dbpass The database username's password.
* @param string $dbname The name of the database being connected to.
* @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
* @param array $dboptions driver specific options
* @return bool success
*/
public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
$driverstatus = $this->driver_installed();
if ($driverstatus !== true) {
throw new dml_exception('dbdriverproblem', $driverstatus);
}
$this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
try{
$this->pdb = new PDO($this->get_dsn(), $this->dbuser, $this->dbpass, $this->get_pdooptions());
// generic PDO settings to match adodb's default; subclasses can change this in configure_dbconnection
$this->pdb->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
$this->pdb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->configure_dbconnection();
return true;
} catch (PDOException $ex) {
throw new dml_connection_exception($ex->getMessage());
return false;
}
}
/**
* Returns the driver-dependent DSN for PDO based on members stored by connect.
* Must be called after connect (or after $dbname, $dbhost, etc. members have been set).
* @return string driver-dependent DSN
*/
abstract protected function get_dsn();
/**
* Returns the driver-dependent connection attributes for PDO based on members stored by connect.
* Must be called after $dbname, $dbhost, etc. members have been set.
* @return array A key=>value array of PDO driver-specific connection options
*/
protected function get_pdooptions() {
return array(PDO::ATTR_PERSISTENT => !empty($this->dboptions['dbpersist']));
}
protected function configure_dbconnection() {
//TODO: not needed preconfigure_dbconnection() stuff for PDO drivers?
}
/**
* Returns general database library name
* Note: can be used before connect()
* @return string db type pdo, native
*/
protected function get_dblibrary() {
return 'pdo';
}
/**
* Returns localised database type name
* Note: can be used before connect()
* @return string
*/
public function get_name() {
return get_string('pdo'.$this->get_dbtype(), 'install');
}
/**
* Returns localised database configuration help.
* Note: can be used before connect()
* @return string
*/
public function get_configuration_help() {
return get_string('pdo'.$this->get_dbtype().'help', 'install');
}
/**
* Returns database server info array
* @return array Array containing 'description' and 'version' info
*/
public function get_server_info() {
$result = array();
try {
$result['description'] = $this->pdb->getAttribute(PDO::ATTR_SERVER_INFO);
} catch(PDOException $ex) {}
try {
$result['version'] = $this->pdb->getAttribute(PDO::ATTR_SERVER_VERSION);
} catch(PDOException $ex) {}
return $result;
}
/**
* Returns supported query parameter types
* @return int bitmask of accepted SQL_PARAMS_*
*/
protected function allowed_param_types() {
return SQL_PARAMS_QM | SQL_PARAMS_NAMED;
}
/**
* Returns last error reported by database engine.
* @return string error message
*/
public function get_last_error() {
return $this->lastError;
}
/**
* Function to print/save/ignore debugging messages related to SQL queries.
*/
protected function debug_query($sql, $params = null) {
echo '<hr /> (', $this->get_dbtype(), '): ', htmlentities($sql, ENT_QUOTES, 'UTF-8');
if($params) {
echo ' (parameters ';
print_r($params);
echo ')';
}
echo '<hr />';
}
/**
* Do NOT use in code, to be used by database_manager only!
* @param string|array $sql query
* @param array|null $tablenames an array of xmldb table names affected by this request.
* @return bool true
* @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql, $tablenames = null) {
$this->get_manager(); // Includes DDL exceptions classes ;-)
$sqls = (array)$sql;
try {
foreach ($sqls as $sql) {
$result = true;
$this->query_start($sql, null, SQL_QUERY_STRUCTURE);
try {
$this->pdb->exec($sql);
} catch (PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
}
} catch (ddl_change_structure_exception $e) {
$this->reset_caches($tablenames);
throw $e;
}
$this->reset_caches($tablenames);
return true;
}
public function delete_records_select($table, $select, array $params=null) {
$sql = "DELETE FROM {{$table}}";
if ($select) {
$sql .= " WHERE $select";
}
return $this->execute($sql, $params);
}
/**
* Factory method that creates a recordset for return by a query. The generic pdo_moodle_recordset
* class should fit most cases, but pdo_moodle_database subclasses can override this method to return
* a subclass of pdo_moodle_recordset.
* @param object $sth instance of PDOStatement
* @return object instance of pdo_moodle_recordset
*/
protected function create_recordset($sth) {
return new pdo_moodle_recordset($sth);
}
/**
* Execute general sql query. Should be used only when no other method suitable.
* Do NOT use this to make changes in db structure, use database_manager methods instead!
* @param string $sql query
* @param array $params query parameters
* @return bool success
*/
public function execute($sql, array $params=null) {
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
$result = true;
$this->query_start($sql, $params, SQL_QUERY_UPDATE);
try {
$sth = $this->pdb->prepare($sql);
$sth->execute($params);
} catch (PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
return $result;
}
/**
* Get a number of records as an moodle_recordset. $sql must be a complete SQL query.
* Since this method is a little less readable, use of it should be restricted to
* code where it's possible there might be large datasets being returned. For known
* small datasets use get_records_sql - it leads to simpler code.
*
* The return type is like:
* @see function get_recordset.
*
* @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, required if $limitnum is set).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @return moodle_recordset instance
*/
public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
$result = true;
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
$sql = $this->get_limit_clauses($sql, $limitfrom, $limitnum);
$this->query_start($sql, $params, SQL_QUERY_SELECT);
try {
$sth = $this->pdb->prepare($sql);
$sth->execute($params);
$result = $this->create_recordset($sth);
} catch (PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
return $result;
}
/**
* Selects rows and return values of first column as array.
*
* @param string $sql The SQL query
* @param array $params array of sql parameters
* @return array of values
*/
public function get_fieldset_sql($sql, array $params=null) {
$rs = $this->get_recordset_sql($sql, $params);
if (!$rs->valid()) {
$rs->close(); // Not going to iterate (but exit), close rs
return false;
}
$result = array();
foreach($rs as $value) {
$result[] = reset($value);
}
$rs->close();
return $result;
}
/**
* Get a number of records as an array of objects.
*
* Return value is like:
* @see function get_records.
*
* @param string $sql the SQL select query to execute. The first column of this SELECT statement
* must be a unique value (usually the 'id' field), as it will be used as the key of the
* returned array.
* @param array $params array of sql parameters
* @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @return array of objects, or empty array if no records were found, or false if an error occurred.
*/
public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
global $CFG;
$rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
if (!$rs->valid()) {
$rs->close(); // Not going to iterate (but exit), close rs
return false;
}
$objects = array();
foreach($rs as $value) {
$key = reset($value);
if ($CFG->debugdeveloper && array_key_exists($key, $objects)) {
debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$key' found in column first column of '$sql'.", DEBUG_DEVELOPER);
}
$objects[$key] = (object)$value;
}
$rs->close();
return $objects;
}
/**
* Insert new record into database, as fast as possible, no safety checks, lobs not supported.
* @param string $table name
* @param mixed $params data record as object or array
* @param bool $returnit return it of inserted record
* @param bool $bulk true means repeated inserts expected
* @param bool $customsequence true if 'id' included in $params, disables $returnid
* @return bool|int true or new id
*/
public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
if (!is_array($params)) {
$params = (array)$params;
}
if ($customsequence) {
if (!isset($params['id'])) {
throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
}
$returnid = false;
} else {
unset($params['id']);
}
if (empty($params)) {
throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
}
$fields = implode(',', array_keys($params));
$qms = array_fill(0, count($params), '?');
$qms = implode(',', $qms);
$sql = "INSERT INTO {{$table}} ($fields) VALUES($qms)";
if (!$this->execute($sql, $params)) {
return false;
}
if (!$returnid) {
return true;
}
if ($id = $this->pdb->lastInsertId()) {
return (int)$id;
}
return false;
}
/**
* Insert a record into a table and return the "id" field if required,
* Some conversions and safety checks are carried out. Lobs are supported.
* If the return ID isn't required, then this just reports success as true/false.
* $data is an object containing needed data
* @param string $table The database table to be inserted into
* @param object|array $dataobject A data object with values for one or more fields in the record
* @param bool $returnid Should the id of the newly created record entry be returned? If this option is not requested then true/false is returned.
* @param bool $bulk true means repeated inserts expected
* @return bool|int true or new id
*/
public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
$dataobject = (array)$dataobject;
$columns = $this->get_columns($table);
if (empty($columns)) {
throw new dml_exception('ddltablenotexist', $table);
}
$cleaned = array();
foreach ($dataobject as $field=>$value) {
if ($field === 'id') {
continue;
}
if (!isset($columns[$field])) {
continue;
}
$column = $columns[$field];
if (is_bool($value)) {
$value = (int)$value; // prevent "false" problems
}
$cleaned[$field] = $value;
}
if (empty($cleaned)) {
return false;
}
return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
}
/**
* Update record in database, as fast as possible, no safety checks, lobs not supported.
* @param string $table name
* @param stdClass|array $params data record as object or array
* @param bool true means repeated updates expected
* @return bool success
*/
public function update_record_raw($table, $params, $bulk=false) {
$params = (array)$params;
if (!isset($params['id'])) {
throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
}
$id = $params['id'];
unset($params['id']);
if (empty($params)) {
throw new coding_exception('moodle_database::update_record_raw() no fields found.');
}
$sets = array();
foreach ($params as $field=>$value) {
$sets[] = "$field = ?";
}
$params[] = $id; // last ? in WHERE condition
$sets = implode(',', $sets);
$sql = "UPDATE {{$table}} SET $sets WHERE id=?";
return $this->execute($sql, $params);
}
/**
* Update a record in a table
*
* $dataobject is an object containing needed data
* Relies on $dataobject having a variable "id" to
* specify the record to update
*
* @param string $table The database table to be checked against.
* @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
* @param bool true means repeated updates expected
* @return bool success
*/
public function update_record($table, $dataobject, $bulk=false) {
$dataobject = (array)$dataobject;
$columns = $this->get_columns($table);
$cleaned = array();
foreach ($dataobject as $field=>$value) {
if (!isset($columns[$field])) {
continue;
}
if (is_bool($value)) {
$value = (int)$value; // prevent "false" problems
}
$cleaned[$field] = $value;
}
return $this->update_record_raw($table, $cleaned, $bulk);
}
/**
* Set a single field in every table row where the select statement evaluates to true.
*
* @param string $table The database table to be checked against.
* @param string $newfield the field to set.
* @param string $newvalue the value to set the field to.
* @param string $select A fragment of SQL to be used in a where clause in the SQL call.
* @param array $params array of sql parameters
* @return bool success
*/
public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
if ($select) {
$select = "WHERE $select";
}
if (is_null($params)) {
$params = array();
}
list($select, $params, $type) = $this->fix_sql_params($select, $params);
if (is_bool($newvalue)) {
$newvalue = (int)$newvalue; // prevent "false" problems
}
if (is_null($newvalue)) {
$newfield = "$newfield = NULL";
} else {
// make sure SET and WHERE clauses use the same type of parameters,
// because we don't support different types in the same query
switch($type) {
case SQL_PARAMS_NAMED:
$newfield = "$newfield = :newvalueforupdate";
$params['newvalueforupdate'] = $newvalue;
break;
case SQL_PARAMS_QM:
$newfield = "$newfield = ?";
array_unshift($params, $newvalue);
break;
default:
$this->lastError = __FILE__ . ' LINE: ' . __LINE__ . '.';
throw new \moodle_exception(unknowparamtype, 'error', '', $this->lastError);
}
}
$sql = "UPDATE {{$table}} SET $newfield $select";
return $this->execute($sql, $params);
}
public function sql_concat(...$arr) {
throw new \moodle_exception('TODO');
}
public function sql_concat_join($separator="' '", $elements=array()) {
throw new \moodle_exception('TODO');
}
/**
* Return SQL for performing group concatenation on given field/expression
*
* @param string $field
* @param string $separator
* @param string $sort
* @return string
*/
public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
return ''; // TODO.
}
protected function begin_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
try {
$this->pdb->beginTransaction();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
}
$this->query_end($result);
}
protected function commit_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
try {
$this->pdb->commit();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
}
$this->query_end($result);
}
protected function rollback_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
try {
$this->pdb->rollBack();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
}
$this->query_end($result);
}
/**
* Import a record into a table, id field is required.
* Basic safety checks only. Lobs are supported.
* @param string $table name of database table to be inserted into
* @param mixed $dataobject object or array with fields in the record
* @return bool success
*/
public function import_record($table, $dataobject) {
$dataobject = (object)$dataobject;
$columns = $this->get_columns($table);
$cleaned = array();
foreach ($dataobject as $field=>$value) {
if (!isset($columns[$field])) {
continue;
}
$cleaned[$field] = $value;
}
return $this->insert_record_raw($table, $cleaned, false, true, true);
}
/**
* Called before each db query.
*
* Overridden to ensure $this->lastErorr is reset each query
*
* @param string $sql
* @param array|null $params An array of parameters.
* @param int $type type of query
* @param mixed $extrainfo driver specific extra information
* @return void
*/
protected function query_start($sql, ?array $params, $type, $extrainfo=null) {
$this->lastError = null;
parent::query_start($sql, $params, $type, $extrainfo);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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/>.
/**
* Experimental pdo recordset
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_recordset.php');
/**
* Experimental pdo recordset
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class pdo_moodle_recordset extends moodle_recordset {
private $sth;
protected $current;
public function __construct($sth) {
$this->sth = $sth;
$this->sth->setFetchMode(PDO::FETCH_ASSOC);
$this->current = $this->fetch_next();
}
public function __destruct() {
$this->close();
}
private function fetch_next() {
if ($row = $this->sth->fetch()) {
$row = array_change_key_case($row, CASE_LOWER);
}
return $row;
}
public function current(): stdClass {
return (object)$this->current;
}
#[\ReturnTypeWillChange]
public function key() {
// return first column value as key
if (!$this->current) {
return false;
}
$key = reset($this->current);
return $key;
}
public function next(): void {
$this->current = $this->fetch_next();
}
public function valid(): bool {
return !empty($this->current);
}
public function close() {
if ($this->sth) {
$this->sth->closeCursor();
$this->sth = null;
}
$this->current = null;
}
}
File diff suppressed because it is too large Load Diff
+173
View File
@@ -0,0 +1,173 @@
<?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/>.
/**
* Native postgresql recordset.
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_recordset.php');
/**
* pgsql specific moodle recordset class
*
* @package core_dml
* @copyright 2008 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class pgsql_native_moodle_recordset extends moodle_recordset {
/** @var PgSql\Result|resource|null */
protected $result;
/** @var current row as array.*/
protected $current;
protected $blobs = array();
/** @var string Name of cursor or '' if none */
protected $cursorname;
/** @var pgsql_native_moodle_database Postgres database resource */
protected $db;
/** @var bool True if there are no more rows to fetch from the cursor */
protected $lastbatch;
/**
* Build a new recordset to iterate over.
*
* When using cursors, $result will be null initially.
*
* @param resource|PgSql\Result|null $result A pg_query() result object to create a recordset from.
* @param pgsql_native_moodle_database $db Database object (only required when using cursors)
* @param string $cursorname Name of cursor or '' if none
*/
public function __construct($result, pgsql_native_moodle_database $db = null, $cursorname = '') {
if ($cursorname && !$db) {
throw new coding_exception('When specifying a cursor, $db is required');
}
$this->result = $result;
$this->db = $db;
$this->cursorname = $cursorname;
// When there is a cursor, do the initial fetch.
if ($cursorname) {
$this->fetch_cursor_block();
}
// Find out if there are any blobs.
$numfields = pg_num_fields($this->result);
for ($i = 0; $i < $numfields; $i++) {
$type = $this->db->pg_field_type($this->result, $i);
if ($type == 'bytea') {
$this->blobs[] = pg_field_name($this->result, $i);
}
}
$this->current = $this->fetch_next();
}
/**
* Fetches the next block of data when using cursors.
*
* @throws coding_exception If you call this when the fetch buffer wasn't freed yet
*/
protected function fetch_cursor_block() {
if ($this->result) {
throw new coding_exception('Unexpected non-empty result when fetching from cursor');
}
list($this->result, $this->lastbatch) = $this->db->fetch_from_cursor($this->cursorname);
if (!$this->result) {
throw new coding_exception('Unexpected failure when fetching from cursor');
}
}
public function __destruct() {
$this->close();
}
private function fetch_next() {
if (!$this->result) {
return false;
}
if (!$row = pg_fetch_assoc($this->result)) {
// There are no more rows in this result.
pg_free_result($this->result);
$this->result = null;
// If using a cursor, can we fetch the next block?
if ($this->cursorname && !$this->lastbatch) {
$this->fetch_cursor_block();
if (!$row = pg_fetch_assoc($this->result)) {
pg_free_result($this->result);
$this->result = null;
return false;
}
} else {
return false;
}
}
if ($this->blobs) {
foreach ($this->blobs as $blob) {
$row[$blob] = $row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null;
}
}
return $row;
}
public function current(): stdClass {
return (object)$this->current;
}
#[\ReturnTypeWillChange]
public function key() {
// return first column value as key
if (!$this->current) {
return false;
}
$key = reset($this->current);
return $key;
}
public function next(): void {
$this->current = $this->fetch_next();
}
public function valid(): bool {
return !empty($this->current);
}
public function close() {
if ($this->result) {
pg_free_result($this->result);
$this->result = null;
}
$this->current = null;
$this->blobs = null;
// If using cursors, close the cursor.
if ($this->cursorname) {
$this->db->close_cursor($this->cursorname);
$this->cursorname = null;
}
}
}
@@ -0,0 +1,44 @@
<?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/>.
/**
* PGSQL specific temptables store. Needed because temporary tables
* are named differently than normal tables. Also used to be able to retrieve
* temp table names included in the get_tables() method of the DB.
*
* @package core_dml
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_temptables.php');
class pgsql_native_moodle_temptables extends moodle_temptables {
/**
* Analyze the data in temporary tables to force statistics collection after bulk data loads.
* PostgreSQL does not natively support automatic temporary table stats collection, so we do it.
*
* @return void
*/
public function update_stats() {
$temptables = $this->get_temptables();
foreach ($temptables as $temptablename) {
$this->mdb->execute("ANALYZE {".$temptablename."}");
}
}
}
+381
View File
@@ -0,0 +1,381 @@
<?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/>.
/**
* Experimental pdo database class.
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/pdo_moodle_database.php');
/**
* Experimental pdo database class
*
* @package core_dml
* @copyright 2008 Andrei Bautu
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sqlite3_pdo_moodle_database extends pdo_moodle_database {
protected $database_file_extension = '.sq3.php';
/**
* Detects if all needed PHP stuff installed.
* Note: can be used before connect()
* @return mixed true if ok, string if something
*/
public function driver_installed() {
if (!extension_loaded('pdo_sqlite') || !extension_loaded('pdo')){
return get_string('sqliteextensionisnotpresentinphp', 'install');
}
return true;
}
/**
* Returns database family type - describes SQL dialect
* Note: can be used before connect()
* @return string db family name (mysql, postgres, mssql, oracle, etc.)
*/
public function get_dbfamily() {
return 'sqlite';
}
/**
* Returns more specific database driver type
* Note: can be used before connect()
* @return string db type mysqli, pgsql, oci, mssql, sqlsrv
*/
protected function get_dbtype() {
return 'sqlite3';
}
protected function configure_dbconnection() {
// try to protect database file against web access;
// this is required in case that the moodledata folder is web accessible and
// .htaccess is not in place; requires that the database file extension is php
$this->pdb->exec('CREATE TABLE IF NOT EXISTS "<?php die?>" (id int)');
$this->pdb->exec('PRAGMA synchronous=OFF');
$this->pdb->exec('PRAGMA short_column_names=1');
$this->pdb->exec('PRAGMA encoding="UTF-8"');
$this->pdb->exec('PRAGMA case_sensitive_like=0');
$this->pdb->exec('PRAGMA locking_mode=NORMAL');
}
/**
* Attempt to create the database
* @param string $dbhost
* @param string $dbuser
* @param string $dbpass
* @param string $dbname
*
* @return bool success
*/
public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
global $CFG;
$this->dbhost = $dbhost;
$this->dbuser = $dbuser;
$this->dbpass = $dbpass;
$this->dbname = $dbname;
$filepath = $this->get_dbfilepath();
$dirpath = dirname($filepath);
@mkdir($dirpath, $CFG->directorypermissions, true);
return touch($filepath);
}
/**
* Returns the driver-dependent DSN for PDO based on members stored by connect.
* Must be called after connect (or after $dbname, $dbhost, etc. members have been set).
* @return string driver-dependent DSN
*/
protected function get_dsn() {
return 'sqlite:'.$this->get_dbfilepath();
}
/**
* Returns the file path for the database file, computed from dbname and/or dboptions.
* If dboptions['file'] is set, then it is used (use :memory: for in memory database);
* else if dboptions['path'] is set, then the file will be <dboptions path>/<dbname>.sq3.php;
* else if dbhost is set and not localhost, then the file will be <dbhost>/<dbname>.sq3.php;
* else the file will be <moodle data path>/<dbname>.sq3.php
* @return string file path to the SQLite database;
*/
public function get_dbfilepath() {
global $CFG;
if (!empty($this->dboptions['file'])) {
return $this->dboptions['file'];
}
if ($this->dbhost && $this->dbhost != 'localhost') {
$path = $this->dbhost;
} else {
$path = $CFG->dataroot;
}
$path = rtrim($path, '\\/').'/';
if (!empty($this->dbuser)) {
$path .= $this->dbuser.'_';
}
$path .= $this->dbname.'_'.md5($this->dbpass).$this->database_file_extension;
return $path;
}
/**
* Return tables in database WITHOUT current prefix.
* @param bool $usecache if true, returns list of cached tables.
* @return array of table names in lowercase and without prefix
*/
public function get_tables($usecache=true) {
$tables = array();
$sql = 'SELECT name FROM sqlite_master WHERE type="table" UNION ALL SELECT name FROM sqlite_temp_master WHERE type="table" ORDER BY name';
if ($this->debug) {
$this->debug_query($sql);
}
$rstables = $this->pdb->query($sql);
foreach ($rstables as $table) {
$table = $table['name'];
$table = strtolower($table);
if ($this->prefix !== false && $this->prefix !== '') {
if (strpos($table, $this->prefix) !== 0) {
continue;
}
$table = substr($table, strlen($this->prefix));
}
$tables[$table] = $table;
}
return $tables;
}
/**
* Return table indexes - everything lowercased
* @param string $table The table we want to get indexes from.
* @return array of arrays
*/
public function get_indexes($table) {
$indexes = array();
$sql = 'PRAGMA index_list('.$this->prefix.$table.')';
if ($this->debug) {
$this->debug_query($sql);
}
$rsindexes = $this->pdb->query($sql);
foreach($rsindexes as $index) {
$unique = (boolean)$index['unique'];
$index = $index['name'];
$sql = 'PRAGMA index_info("'.$index.'")';
if ($this->debug) {
$this->debug_query($sql);
}
$rscolumns = $this->pdb->query($sql);
$columns = array();
foreach($rscolumns as $row) {
$columns[] = strtolower($row['name']);
}
$index = strtolower($index);
$indexes[$index]['unique'] = $unique;
$indexes[$index]['columns'] = $columns;
}
return $indexes;
}
/**
* Returns detailed information about columns in table.
*
* @param string $table name
* @return array array of database_column_info objects indexed with column names
*/
protected function fetch_columns(string $table): array {
$structure = array();
// get table's CREATE TABLE command (we'll need it for autoincrement fields)
$sql = 'SELECT sql FROM sqlite_master WHERE type="table" AND tbl_name="'.$this->prefix.$table.'"';
if ($this->debug) {
$this->debug_query($sql);
}
$createsql = $this->pdb->query($sql)->fetch();
if (!$createsql) {
return false;
}
$createsql = $createsql['sql'];
$sql = 'PRAGMA table_info("'. $this->prefix.$table.'")';
if ($this->debug) {
$this->debug_query($sql);
}
$rscolumns = $this->pdb->query($sql);
foreach ($rscolumns as $row) {
$columninfo = array(
'name' => strtolower($row['name']), // colum names must be lowercase
'not_null' =>(boolean)$row['notnull'],
'primary_key' => (boolean)$row['pk'],
'has_default' => !is_null($row['dflt_value']),
'default_value' => $row['dflt_value'],
'auto_increment' => false,
'binary' => false,
//'unsigned' => false,
);
$type = explode('(', $row['type']);
$columninfo['type'] = strtolower($type[0]);
if (count($type) > 1) {
$size = explode(',', trim($type[1], ')'));
$columninfo['max_length'] = $size[0];
if (count($size) > 1) {
$columninfo['scale'] = $size[1];
}
}
// SQLite does not have a fixed set of datatypes (ie. it accepts any string as
// datatype in the CREATE TABLE command. We try to guess which type is used here
switch(substr($columninfo['type'], 0, 3)) {
case 'int': // int integer
if ($columninfo['primary_key'] && preg_match('/'.$columninfo['name'].'\W+integer\W+primary\W+key\W+autoincrement/im', $createsql)) {
$columninfo['meta_type'] = 'R';
$columninfo['auto_increment'] = true;
} else {
$columninfo['meta_type'] = 'I';
}
break;
case 'num': // number numeric
case 'rea': // real
case 'dou': // double
case 'flo': // float
$columninfo['meta_type'] = 'N';
break;
case 'var': // varchar
case 'cha': // char
$columninfo['meta_type'] = 'C';
break;
case 'enu': // enums
$columninfo['meta_type'] = 'C';
break;
case 'tex': // text
case 'clo': // clob
$columninfo['meta_type'] = 'X';
break;
case 'blo': // blob
case 'non': // none
$columninfo['meta_type'] = 'B';
$columninfo['binary'] = true;
break;
case 'boo': // boolean
case 'bit': // bit
case 'log': // logical
$columninfo['meta_type'] = 'L';
$columninfo['max_length'] = 1;
break;
case 'tim': // timestamp
$columninfo['meta_type'] = 'T';
break;
case 'dat': // date datetime
$columninfo['meta_type'] = 'D';
break;
}
if ($columninfo['has_default'] && ($columninfo['meta_type'] == 'X' || $columninfo['meta_type']== 'C')) {
// trim extra quotes from text default values
$columninfo['default_value'] = substr($columninfo['default_value'], 1, -1);
}
$structure[$columninfo['name']] = new database_column_info($columninfo);
}
return $structure;
}
/**
* Normalise values based in RDBMS dependencies (booleans, LOBs...)
*
* @param database_column_info $column column metadata corresponding with the value we are going to normalise
* @param mixed $value value we are going to normalise
* @return mixed the normalised value
*/
protected function normalise_value($column, $value) {
return $value;
}
/**
* Returns the sql statement with clauses to append used to limit a recordset range.
* @param string $sql the SQL statement to limit.
* @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @return string the SQL statement with limiting clauses
*/
protected function get_limit_clauses($sql, $limitfrom=0, $limitnum=0) {
if ($limitnum) {
$sql .= ' LIMIT '.$limitnum;
if ($limitfrom) {
$sql .= ' OFFSET '.$limitfrom;
}
}
return $sql;
}
/**
* Delete the records from a table where all the given conditions met.
* If conditions not specified, table is truncated.
*
* @param string $table the table to delete from.
* @param array $conditions optional array $fieldname=>requestedvalue with AND in between
* @return returns success.
*/
public function delete_records($table, array $conditions=null) {
if (is_null($conditions)) {
return $this->execute("DELETE FROM {{$table}}");
}
list($select, $params) = $this->where_clause($table, $conditions);
return $this->delete_records_select($table, $select, $params);
}
/**
* Returns the proper SQL to do CONCAT between the elements passed
* Can take many parameters
*
* @param string $elements,...
* @return string
*/
public function sql_concat(...$elements) {
return implode('||', $elements);
}
/**
* Returns the proper SQL to do CONCAT between the elements passed
* with a given separator
*
* @param string $separator
* @param array $elements
* @return string
*/
public function sql_concat_join($separator="' '", $elements=array()) {
// Intersperse $elements in the array.
// Add items to the array on the fly, walking it
// _backwards_ splicing the elements in. The loop definition
// should skip first and last positions.
for ($n=count($elements)-1; $n > 0; $n--) {
array_splice($elements, $n, 0, $separator);
}
return implode('||', $elements);
}
/**
* Returns the SQL text to be used in order to perform one bitwise XOR operation
* between 2 integers.
*
* @param integer int1 first integer in the operation
* @param integer int2 second integer in the operation
* @return string the piece of SQL code to be used in your statement.
*/
public function sql_bitxor($int1, $int2) {
return '( ~' . $this->sql_bitand($int1, $int2) . ' & ' . $this->sql_bitor($int1, $int2) . ')';
}
}
File diff suppressed because it is too large Load Diff
+149
View File
@@ -0,0 +1,149 @@
<?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 2 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/>.
/**
* sqlsrv specific recordset.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_recordset.php');
class sqlsrv_native_moodle_recordset extends moodle_recordset {
protected $rsrc;
protected $current;
/** @var array recordset buffer */
protected $buffer = null;
/** @var sqlsrv_native_moodle_database */
protected $db;
public function __construct($rsrc, sqlsrv_native_moodle_database $db) {
$this->rsrc = $rsrc;
$this->current = $this->fetch_next();
$this->db = $db;
}
/**
* Inform existing open recordsets that transaction
* is starting, this works around MARS problem described
* in MDL-37734.
*/
public function transaction_starts() {
if ($this->buffer !== null) {
$this->unregister();
return;
}
if (!$this->rsrc) {
$this->unregister();
return;
}
// This might eat memory pretty quickly...
raise_memory_limit('2G');
$this->buffer = array();
while($next = $this->fetch_next()) {
$this->buffer[] = $next;
}
}
/**
* Unregister recordset from the global list of open recordsets.
*/
private function unregister() {
if ($this->db) {
$this->db->recordset_closed($this);
$this->db = null;
}
}
public function __destruct() {
$this->close();
}
private function fetch_next() {
if (!$this->rsrc) {
return false;
}
if (!$row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) {
if (is_resource($this->rsrc)) {
// We need to make sure that the statement resource is in the correct type before freeing it.
sqlsrv_free_stmt($this->rsrc);
}
$this->rsrc = null;
$this->unregister();
return false;
}
unset($row['sqlsrvrownumber']);
$row = array_change_key_case($row, CASE_LOWER);
// Moodle expects everything from DB as strings.
foreach ($row as $k=>$v) {
if (is_null($v)) {
continue;
}
if (!is_string($v)) {
$row[$k] = (string)$v;
}
}
return $row;
}
public function current(): stdClass {
return (object)$this->current;
}
#[\ReturnTypeWillChange]
public function key() {
// return first column value as key
if (!$this->current) {
return false;
}
$key = reset($this->current);
return $key;
}
public function next(): void {
if ($this->buffer === null) {
$this->current = $this->fetch_next();
} else {
$this->current = array_shift($this->buffer);
}
}
public function valid(): bool {
return !empty($this->current);
}
public function close() {
if ($this->rsrc) {
if (is_resource($this->rsrc)) {
// We need to make sure that the statement resource is in the correct type before freeing it.
sqlsrv_free_stmt($this->rsrc);
}
$this->rsrc = null;
}
$this->current = null;
$this->buffer = null;
$this->unregister();
}
}
@@ -0,0 +1,59 @@
<?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 2 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/>.
/**
* sqlsrv specific temptables store. Needed because temporary tables
* are named differently than normal tables. Also used to be able to retrieve
* temp table names included in the get_tables() method of the DB.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/moodle_temptables.php');
/**
* This class is not specific to the SQL Server Native Driver but rather
* to the family of Microsoft SQL Servers.
*
* @package core_dml
* @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later
*/
class sqlsrv_native_moodle_temptables extends moodle_temptables {
/**
* Add one temptable to the store.
*
* Overriden because SQLSRV requires to add # for local (session) temporary
* tables before the prefix.
*
* Given one moodle temptable name (without prefix), add it to the store, with the
* key being the original moodle name and the value being the real db temptable name
* already prefixed
*
* Override and use this *only* if the database requires modification in the table name.
*
* @param string $tablename name without prefix of the table created as temptable
*/
public function add_temptable($tablename) {
// TODO: throw exception if exists: if ($this->is_temptable...
$this->temptables[$tablename] = '#' . $this->prefix . $tablename;
}
}
@@ -0,0 +1,164 @@
<?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/>.
/**
* DML read/read-write database handle tests for mysqli_native_moodle_database
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use moodle_database;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php');
/**
* DML mysqli_native_moodle_database read slave specific tests
*
* @package core
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mysqli_native_moodle_database
*/
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.
*
* @return void
*/
public function test_lock(): void {
$DB = new read_slave_moodle_database_mock_mysqli();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$DB->query_start("SELECT GET_LOCK('lock',1)", null, SQL_QUERY_SELECT);
$this->assertTrue($DB->db_handle_is_rw());
$DB->query_end(null);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$DB->query_start("SELECT RELEASE_LOCK('lock',1)", null, SQL_QUERY_SELECT);
$this->assertTrue($DB->db_handle_is_rw());
$DB->query_end(null);
$this->assertEquals(0, $DB->perf_get_reads_slave());
}
/**
* Test readonly handle is used for SQL_QUERY_AUX_READONLY queries.
*
* @return void
*/
public function test_aux_readonly(): 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' => [$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->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->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
$this->assertTrue(count($db2->get_records('user')) > 0);
}
}
+284
View File
@@ -0,0 +1,284 @@
<?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/>.
/**
* DML read/read-write database handle tests for pgsql_native_moodle_database
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use moodle_database;
use xmldb_table;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php');
/**
* DML pgsql_native_moodle_database read slave specific tests
*
* @package core
* @category dml
* @copyright 2018 Catalyst IT
* @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 \advanced_testcase {
/**
* Test correct database handles are used for cursors
*
* @return 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.
list($sql, $params, $type) = $DB->fix_sql_params("SELECT * FROM {table}");
$sql = "DECLARE crs1 NO SCROLL CURSOR WITH HOLD FOR $sql";
$DB->query_start($sql, null, SQL_QUERY_SELECT);
$DB->query_end(null);
// Declare a cursor on a table that has been written to.
list($sql, $params, $type) = $DB->fix_sql_params("INSERT INTO {table2} (name) VALUES ('blah')");
$DB->query_start($sql, null, SQL_QUERY_INSERT);
$DB->query_end(null);
list($sql, $params, $type) = $DB->fix_sql_params("SELECT * FROM {table2}");
$sql = "DECLARE crs2 NO SCROLL CURSOR WITH HOLD FOR $sql";
$DB->query_start($sql, null, SQL_QUERY_SELECT);
$DB->query_end(null);
// Read from the non-written to table cursor.
$sql = 'FETCH 1 FROM crs1';
$DB->query_start($sql, null, SQL_QUERY_AUX);
$this->assertTrue($DB->db_handle_is_ro());
$DB->query_end(null);
// Read from the written to table cursor.
$sql = 'FETCH 1 FROM crs2';
$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);
}
/**
* Test readonly handle is used for reading from random pg_*() call queries.
*
* @return void
*/
public function test_read_pg_table(): void {
$DB = new read_slave_moodle_database_mock_pgsql();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$DB->query_start('SELECT pg_whatever(1)', null, SQL_QUERY_SELECT);
$this->assertTrue($DB->db_handle_is_ro());
$DB->query_end(null);
$this->assertEquals(1, $DB->perf_get_reads_slave());
}
/**
* Test readonly handle is not used for reading from special pg_*() call queries,
* pg_try_advisory_lock and pg_advisory_unlock.
*
* @return 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());
foreach (['pg_try_advisory_lock', 'pg_advisory_unlock'] as $fn) {
$DB->query_start("SELECT $fn(1)", null, SQL_QUERY_SELECT);
$this->assertTrue($DB->db_handle_is_rw());
$DB->query_end(null);
$this->assertEquals(0, $DB->perf_get_reads_slave());
}
}
/**
* Test readonly handle is used for SQL_QUERY_AUX_READONLY queries.
*
* @return void
*/
public function test_aux_readonly(): void {
global $DB;
$this->resetAfterTest();
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);
$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->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']);
$dbman->create_temp_table($table);
// We need to go through the creation proces twice.
// create_temp_table() performs some reads before the temp table is created.
// First time around those reads should go to ro ...
$reads = $db2->perf_get_reads_slave();
$db2->get_columns('silly_test_table');
$db2->get_records('silly_test_table');
$this->assertEquals($reads, $db2->perf_get_reads_slave());
$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']);
$dbman->create_temp_table($table2);
// ... but once the first temp table is created no more ro reads should occur.
$db2->get_columns('silly_test_table2');
$db2->get_records('silly_test_table2');
$this->assertEquals($reads, $db2->perf_get_reads_slave());
// Make database driver happy.
$dbman->drop_table($table2);
$dbman->drop_table($table);
}
/**
* Test readonly connection failure with real pgsql connection
*
* @return void
*/
public function test_real_readslave_connect_fail(): 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 = array();
}
$cfg->dboptions['readonly'] = [
'instance' => ['host.that.is.not'],
'connecttimeout' => 1
];
$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);
}
}
+573
View File
@@ -0,0 +1,573 @@
<?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/>.
/**
* DML read/read-write database handle use tests
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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
*
* @package core
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \moodle_read_slave_trait
*/
class dml_read_slave_test extends \base_testcase {
/** @var float */
static private $dbreadonlylatency = 0.8;
/**
* Instantiates a test database interface object.
*
* @param bool $wantlatency
* @param mixed $readonly
* @param mixed $dbclass
* @return read_slave_moodle_database $db
*/
public function new_db(
$wantlatency = false,
$readonly = [
['dbhost' => 'test_ro1', 'dbport' => 1, 'dbuser' => 'test1', 'dbpass' => 'test1'],
['dbhost' => 'test_ro2', 'dbport' => 2, 'dbuser' => 'test2', 'dbpass' => 'test2'],
['dbhost' => 'test_ro3', 'dbport' => 3, 'dbuser' => 'test3', 'dbpass' => 'test3'],
],
$dbclass = read_slave_moodle_database::class
): read_slave_moodle_database {
$dbhost = 'test_rw';
$dbname = 'test';
$dbuser = 'test';
$dbpass = 'test';
$prefix = 'test_';
$dboptions = ['readonly' => ['instance' => $readonly, 'exclude_tables' => ['exclude']]];
if ($wantlatency) {
$dboptions['readonly']['latency'] = self::$dbreadonlylatency;
}
$db = new $dbclass();
$db->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
return $db;
}
/**
* Asert that the mock handle returned from read_slave_moodle_database methods
* is a readonly slave handle.
*
* @param string $handle
* @return void
*/
private function assert_readonly_handle($handle): void {
$this->assertMatchesRegularExpression('/^test_ro\d:\d:test\d:test\d$/', $handle);
}
/**
* moodle_read_slave_trait::table_names() test data provider
*
* @return array
* @dataProvider table_names_provider
*/
public function table_names_provider(): array {
return [
[
"SELECT *
FROM {user} u
JOIN (
SELECT DISTINCT u.id FROM {user} u
JOIN {user_enrolments} ue1 ON ue1.userid = u.id
JOIN {enrol} e ON e.id = ue1.enrolid
WHERE u.id NOT IN (
SELECT DISTINCT ue.userid FROM {user_enrolments} ue
JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = 1)
WHERE ue.status = 'active'
AND e.status = 'enabled'
AND ue.timestart < now()
AND (ue.timeend = 0 OR ue.timeend > now())
)
) je ON je.id = u.id
JOIN (
SELECT DISTINCT ra.userid
FROM {role_assignments} ra
WHERE ra.roleid IN (1, 2, 3)
AND ra.contextid = 'ctx'
) rainner ON rainner.userid = u.id
WHERE u.deleted = 0",
[
'user',
'user',
'user_enrolments',
'enrol',
'user_enrolments',
'enrol',
'role_assignments',
]
],
];
}
/**
* Test moodle_read_slave_trait::table_names() query parser.
*
* @param string $sql
* @param array $tables
* @return void
* @dataProvider table_names_provider
*/
public function test_table_names($sql, $tables): void {
$db = new read_slave_moodle_database_table_names();
$this->assertEquals($tables, $db->table_names($db->fix_sql_params($sql)[0]));
}
/**
* Test correct database handles are used in a read-read-write-read scenario.
* Test lazy creation of the write handle.
*
* @return void
*/
public function test_read_read_write_read(): void {
$DB = $this->new_db(true);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('table');
$this->assert_readonly_handle($handle);
$readsslave = $DB->perf_get_reads_slave();
$this->assertGreaterThan(0, $readsslave);
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('table2');
$this->assert_readonly_handle($handle);
$readsslave = $DB->perf_get_reads_slave();
$this->assertGreaterThan(1, $readsslave);
$this->assertNull($DB->get_dbhwrite());
$now = microtime(true);
$handle = $DB->insert_record_raw('table', array('name' => 'blah'));
$this->assertEquals('test_rw::test:test', $handle);
if (microtime(true) - $now < self::$dbreadonlylatency) {
$handle = $DB->get_records('table');
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals($readsslave, $DB->perf_get_reads_slave());
sleep(1);
}
$handle = $DB->get_records('table');
$this->assert_readonly_handle($handle);
$this->assertEquals($readsslave + 1, $DB->perf_get_reads_slave());
}
/**
* Test correct database handles are used in a read-write-write scenario.
*
* @return void
*/
public function test_read_write_write(): void {
$DB = $this->new_db();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('table');
$this->assert_readonly_handle($handle);
$readsslave = $DB->perf_get_reads_slave();
$this->assertGreaterThan(0, $readsslave);
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->insert_record_raw('table', array('name' => 'blah'));
$this->assertEquals('test_rw::test:test', $handle);
$handle = $DB->update_record_raw('table', array('id' => 1, 'name' => 'blah2'));
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals($readsslave, $DB->perf_get_reads_slave());
}
/**
* Test correct database handles are used in a write-read-read scenario.
*
* @return void
*/
public function test_write_read_read(): void {
$DB = $this->new_db();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->insert_record_raw('table', array('name' => 'blah'));
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$handle = $DB->get_records('table');
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$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->assert_readonly_handle($handle);
$this->assertEquals(3, $DB->perf_get_reads_slave());
}
/**
* Test readonly handle is not used for reading from temptables.
*
* @return void
*/
public function test_read_temptable(): void {
$DB = $this->new_db();
$DB->add_temptable('temptable1');
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('temptable1');
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$DB->delete_temptable('temptable1');
}
/**
* Test readonly handle is not used for reading from excluded tables.
*
* @return void
*/
public function test_read_excluded_tables(): void {
$DB = $this->new_db();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('exclude');
$this->assertEquals('test_rw::test:test', $handle);
$this->assertEquals(0, $DB->perf_get_reads_slave());
}
/**
* Test readonly handle is not used during transactions.
* Test last written time is adjusted post-transaction,
* so the latency parameter is applied properly.
*
* @return void
* @covers ::can_use_readonly
* @covers ::commit_delegated_transaction
*/
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}");
// 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);
$transaction->allow_commit();
// 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("SELECT * FROM {table}");
$this->assertEquals('test_rw::test:test', $handle);
// 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 immediately after update
* Test last written time is adjusted post-write,
* so the latency parameter is applied properly.
*
* @return void
* @covers ::can_use_readonly
* @covers ::query_end
*/
public function test_long_update(): void {
$DB = $this->new_db(true);
$this->assertNull($DB->get_dbhwrite());
$skip = false;
list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1");
$DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) {
sleep(1);
$now = microtime(true);
});
// This condition should always evaluate true, however we need to
// safeguard from an unaccounted delay that can break this test.
if (microtime(true) - $now < self::$dbreadonlylatency) {
// Not enough time passed, use rw handle.
$handle = $DB->get_records_sql("SELECT * FROM {table}");
$this->assertEquals('test_rw::test:test', $handle);
// 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());
$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, &$called) {
$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($called);
});
}
/**
* Test failed readonly connection falls back to write connection.
*
* @return void
*/
public function test_read_only_conn_fail(): void {
$DB = $this->new_db(false, 'test_ro_fail');
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNotNull($DB->get_dbhwrite());
$handle = $DB->get_records('table');
$this->assertEquals('test_rw::test:test', $handle);
$readsslave = $DB->perf_get_reads_slave();
$this->assertEquals(0, $readsslave);
}
/**
* In multiple slaves scenario, test failed readonly connection falls back to
* another readonly connection.
*
* @return void
*/
public function test_read_only_conn_first_fail(): void {
$DB = $this->new_db(false, ['test_ro_fail', 'test_ro_ok']);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$handle = $DB->get_records('table');
$this->assertEquals('test_ro_ok::test:test', $handle);
$readsslave = $DB->perf_get_reads_slave();
$this->assertEquals(1, $readsslave);
}
/**
* Helper to restore global $DB
*
* @param callable $test
* @return void
*/
private function with_global_db($test) {
global $DB;
$dbsave = $DB;
try {
$test();
}
finally {
$DB = $dbsave;
}
}
/**
* Test lock_db table exclusion
*
* @return void
*/
public function test_lock_db(): void {
$this->with_global_db(function () {
global $DB;
$DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class);
$DB->set_tables([
'lock_db' => [
'columns' => [
'resourcekey' => (object)['meta_type' => ''],
'owner' => (object)['meta_type' => ''],
]
]
]);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$lockfactory = new \core\lock\db_record_lock_factory('default');
if (!$lockfactory->is_available()) {
$this->markTestSkipped("db_record_lock_factory not available");
}
$lock = $lockfactory->get_lock('abc', 2);
$lock->release();
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertTrue($DB->perf_get_reads() > 0);
});
}
/**
* Test sessions table exclusion
*
* @return void
*/
public function test_sessions(): void {
$this->with_global_db(function () {
global $DB, $CFG;
$CFG->dbsessions = true;
$DB = $this->new_db(true, ['test_ro'], read_slave_moodle_database_special::class);
$DB->set_tables([
'sessions' => [
'columns' => [
'sid' => (object)['meta_type' => ''],
]
]
]);
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertNull($DB->get_dbhwrite());
$session = new \core\session\database();
$session->read('dummy');
$this->assertEquals(0, $DB->perf_get_reads_slave());
$this->assertTrue($DB->perf_get_reads() > 0);
});
\core\session\manager::restart_with_write_lock(false);
}
}
+221
View File
@@ -0,0 +1,221 @@
<?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 core\dml\table;
use xmldb_table;
/**
* DML Table tests.
*
* @package core
* @category test
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\dml\table
*/
class dml_table_test extends \database_driver_testcase {
/**
* Data provider for various \core\dml\table method tests.
*
* @return array
*/
public function get_field_select_provider(): array {
return [
'single field' => [
'tablename' => 'test_table_single',
'fieldlist' => [
'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
],
'primarykey' => 'id',
'fieldprefix' => 'ban',
'tablealias' => 'banana',
'banana.id AS banid',
],
'multiple fields' => [
'tablename' => 'test_table_multiple',
'fieldlist' => [
'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
'name' => ['name', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
],
'primarykey' => 'id',
'fieldprefix' => 'ban',
'tablealias' => 'banana',
'banana.id AS banid, banana.course AS bancourse, banana.name AS banname',
],
];
}
/**
* Ensure that \core\dml\table::get_field_select() works as expected.
*
* @dataProvider get_field_select_provider
* @covers ::get_field_select
* @param string $tablename The name of the table
* @param array $fieldlist The list of fields
* @param string $primarykey The name of the primary key
* @param string $fieldprefix The prefix to use for each field
* @param string $tablealias The table AS alias name
* @param string $expected The expected SQL
*/
public function test_get_field_select(
string $tablename,
array $fieldlist,
string $primarykey,
string $fieldprefix,
string $tablealias,
string $expected
): void {
$dbman = $this->tdb->get_manager();
$xmldbtable = new xmldb_table($tablename);
$xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
foreach ($fieldlist as $args) {
call_user_func_array([$xmldbtable, 'add_field'], $args);
}
$xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
$dbman->create_table($xmldbtable);
$table = new table($tablename, $tablealias, $fieldprefix);
$this->assertEquals($expected, $table->get_field_select());
}
/**
* Data provider for \core\dml\table::extract_from_result() tests.
*
* @return array
*/
public function extract_from_result_provider(): array {
return [
'single table' => [
'fieldlist' => [
'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
],
'primarykey' => 'id',
'prefix' => 's',
'result' => (object) [
'sid' => 1,
'scourse' => 42,
'sflag' => 'foo',
],
'expectedrecord' => (object) [
'id' => 1,
'course' => 42,
'flag' => 'foo',
],
],
'single table amongst others' => [
'fieldlist' => [
'id' => ['id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null],
'course' => ['course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'],
'flag' => ['flag', XMLDB_TYPE_CHAR, '255', null, null, null, 'lala'],
],
'primarykey' => 'id',
'prefix' => 's',
'result' => (object) [
'sid' => 1,
'scourse' => 42,
'sflag' => 'foo',
'oid' => 'id',
'ocourse' => 'course',
'oflag' => 'flag',
],
'expectedrecord' => (object) [
'id' => 1,
'course' => 42,
'flag' => 'foo',
],
],
];
}
/**
* Ensure that \core\dml\table::extract_from_result() works as expected.
*
* @dataProvider extract_from_result_provider
* @covers ::extract_from_result
* @param array $fieldlist The list of fields
* @param string $primarykey The name of the primary key
* @param string $fieldprefix The prefix to use for each field
* @param \stdClass $result The result of the get_records_sql
* @param \stdClass $expected The expected output
*/
public function test_extract_fields_from_result(
array $fieldlist,
string $primarykey,
string $fieldprefix,
\stdClass $result,
\stdClass $expected
): void {
$dbman = $this->tdb->get_manager();
$tablename = 'test_table_extraction';
$xmldbtable = new xmldb_table($tablename);
$xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
foreach ($fieldlist as $args) {
call_user_func_array([$xmldbtable, 'add_field'], $args);
}
$xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
$dbman->create_table($xmldbtable);
$table = new table($tablename, 'footable', $fieldprefix);
$this->assertEquals($expected, $table->extract_from_result($result));
}
/**
* Ensure that \core\dml\table::get_from_sql() works as expected.
*
* @dataProvider get_field_select_provider
* @covers ::get_from_sql
* @param string $tablename The name of the table
* @param array $fieldlist The list of fields
* @param string $primarykey The name of the primary key
* @param string $fieldprefix The prefix to use for each field
* @param string $tablealias The table AS alias name
* @param string $expected The expected SQL
*/
public function test_get_from_sql(
string $tablename,
array $fieldlist,
string $primarykey,
string $fieldprefix,
string $tablealias,
string $expected
): void {
$dbman = $this->tdb->get_manager();
$tablename = 'test_table_extraction';
$xmldbtable = new xmldb_table($tablename);
$xmldbtable->setComment("This is a test'n drop table. You can drop it safely");
foreach ($fieldlist as $args) {
call_user_func_array([$xmldbtable, 'add_field'], $args);
}
$xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, [$primarykey]);
$dbman->create_table($xmldbtable);
$table = new table($tablename, $tablealias, $fieldprefix);
$this->assertEquals("{{$tablename}} {$tablealias}", $table->get_from_sql());
}
}
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
제 1장 총 칙
제 1조 (개요)
이 약관은 전기통신사업법 및 동법 시행령에 의거 ㈜네오플(이하 "회사")이 제공하는 서비스 (이하 "서비스")의 이용조건, 절차, 의무, 책임사항 및 이용에 필요한 기타사항을 규정합니다.
제 2조 (목적)
이 약관은 다음과 같은 내용을 목적으로 합니다.
1. 안정적인 서비스 제공 및 서비스, 시스템의 보호를 목적으로 한다.
2. 이용자의 안정적인 서비스 이용 및 이용자의 데이타 보호를 목적으로 한다.
3. 네트워크 및 서비스 이용자 네트워크의 안정성, 프라이버시, 보안성 유지를 목적으로 한다.
4. 네트워크 및 서비스의 사용 원칙 가이드라인 제시를 목적으로 한다.
제 3조 (약관의 효력과 변경)
1. 회사는 귀하가 본 약관 내용에 동의하는 것을 조건으로 서비스를 제공 합니다.
2. 이 약관은 서비스 내에 게시하여 공시함으로써 효력을 발생합니다.
3. 당사의 서비스 제공 행위 및 귀하의 서비스 사용 행위에는 본 약관이 우선적으로 적용될 것입니다.
4. 회사는 이 약관을 개정할 경우에는 적용일자 및 개정사유를 명시하여 현행약관과 함께 초기화면에 그 적용일자 7일이전부터 적용일자 전일까지 공지합니다.
5. 이용자는 회원등록을 취소(회원탈퇴)할 수 있으며, 계속 사용의 경우는 약관 변경에 대한 동의로 간주됩니다. 변경된 약관은 공지와 동시에 그 효력이 발생됩니다.
제 4조 (약관외 준칙)
1. 서비스 이용에 관하여는 이 약관을 적용하며, 이 약관에 명시되지 아니 한 사항에 대하여는 전기통신 기본법, 전기통신사업법, 정보통신망 이용 촉진 및 정보보호 등에 관한 법률(이하통신망법), 기타 관련법령 및 회사의 공지,이용안내를 적용 합니다.
2. 이 약관의 공지 및 변경사항은 회사의 지정된 홈페이지(http://www.candybar.co.kr)에 게시하는 방법으로 공지합니다.
제 5조 (용어의 정의)
이 약관에서 사용하는 용어의 정의는 다음과 같습니다.
1. 이용자 : 서비스 이용을 신청하고 회사가 이를 승낙하여 회원ID를 발급 받은 자를 말합니다.
2. 가 입 : 회사가 제공하는 양식에 해당 정보를 기입하고, 이 약관에 동의하여 서비스 이용계약을 완료시키는 행위를 말합니다.
3. 회 원 : 회사에 개인 정보를 제공하여 회원 등록을 한 자로서, 회사가 제공하는 서비스를 이용할 수 있는 자를 말합니다.
4. 캔디바 ID : 회원의 서비스 이용을 위하여 회원이 신청하고 회사가 승인하는 문자 및 숫자의 조합을 말합니다.
5. 비밀번호 : 회원ID가 일치하는지를 확인하고 자신의 비밀보호를 위하여 이용자 자신이 선정한 문자와 숫자의 조합을 말합니다.
6. 탈퇴 및 해지 : 회원이 이용계약을 종료 시키는 행위를 말합니다.
제 2장 서비스 이용 계약
제 6조 (이용 계약의 성립)
1. 이용계약은 당사 소정의 가입신청 양식에서 요구하는 사항을 기록하여 가입을 완료 하는것으로 성립함을 원칙으로 합니다.
2. 이용신청은 이용신청자 자신의 실명으로 하여야 합니다. 실명이 아닌경우는 여러가지 불이익을 받을수 도 있습니다.
3. 이용계약은 서비스 이용희망자의 이용약관 동의 후 이용 신청에 대하여 회사가 승낙함으로써 성립합니다.
4. 서비스 이용 희망자가 14세 미만의 미성년자 또는 한정치산자인 경우 부모등 법정대리인의 동의를 받아 이용신청하며 납입책임자는 법정대리인으로 합니다. 또한 금치산자인 경우에는 법정대리인 을 이용자 및 납입책임자로 하여 신청합니다.
제 7조 (이용신청의 승낙)
1. 회사는 제 6조의 규정에 의한 서비스 이용희망자에 대하여 업무수행상 또는 기술상 지장이 없는 경우에는 원칙적으로 접수순서에 따라 이용신청을 승낙합니다.
2. 이용자의 이용신청에 대하여 회사가 이를 승낙한 경우, 회사는 회원 ID와 기타 회사가 필요하다고 인정하는 내용을 이용자에게 통지합니다.
제 8조 (이용신청에 대한 불승낙과 승낙의 보류)
1. 이용자가 신청한 이용 아이디(ID)가 이미 다른 이용자에 의해 쓰여지고 있는 경우 승낙을 하지 아니할 수 있습니다.
2. 회사는 다음 각호에 해당하는 이용신청에 대하여는 승낙을 하지 아니할 수 있습니다.
1) 타인 명의의 신청
2) 허위의 신청이거나 허위사실을 기재한 경우 예) 주민등록 생성기를 이용한 비실명 가입
3) 기타 이용신청고객의 귀책사유로 이용승낙이 곤란한 경우
3. 회사는 전항의 경우에는 이를 이용신청고객에게 가능한 빠른 시일 내에 통지하여야 합니다.
4. 회사는 이용신청고객이 미성년자, 한정치산자인 경우에 법정대리인(부모 등)의 동의없는 이용신청에 대하여 승낙을 보류할 수 있습니다.
5. 회사는 이용신청이 불승낙되거나 승낙을 제한하는 경우에는 이를 이용신청자에게 즉시 통보합니다.
제 9조 (이용 아이디 관리 및 변경)
1. 이용 아이디(ID) 및 비밀번호에 대한 모든 관리책임은 이용자에게 있습니다.
2. 이용 아이디(ID)는 다음에 해당하는 경우에는 이용자와 합의하여 변경할 수 있습니다.
(단, 이용 아이디(ID)를 변경할 경우 기존 이용ID는 소멸됩니다.)
1) 이용 아이디(ID)가 이용자의 전화번호 또는 주민등록번호 등으로 등록되어 사생활 침해가 우려 되는 경우
2) 타인에게 혐오감을 주거나 미풍양속에 어긋나는 경우
3) 기타 회사가 인정하는 합리적인 사유가 있는 경우
3. 회사는 이용 아이디(ID)에 의하여 서비스 이용요금 청구 및 게시판 관리 등 제반 이용자 관리 업무를 수행하며 이용자는 이용 아이디(ID)를 공유, 양도 또는 변경할 수 없습니다. 단, 그 사유가 명백하고 회사가 인정하는 경우에는 그러하지 아니 합니다.
4. 이용자가 신청 또는 변경하여 사용하는 이용 아이디(ID) 및 비밀번호에 의하여 발생하는 서비스 이용상의 과실 또는 제3자에 의한 부정사용등에 대한 모든 책임은 이용자에게 있습니다. 단, 회사의 고의 또는 중대한 과실이 있는 경우에는 그러하지 아니합니다.
5. 기타 아이디의 관리 및 변경 등에 관한 사항은 이 약관과 회사의 공지, 이용안내 에서 정하는 바에 의합니다.
제 3장 계약당사자의 의무 및 책임
제 10조 (회사의 의무)
1. 회사는 이용자로부터 제기되는 의견이나 불만이 정당하다고 인정할 경우에는 즉시 처리하여야 합니다. 즉시 처리가 곤란한 경우에는 그 사유와 처리일정을 서면 또는 전화 등으로 통보합니다.
2. 회사는 서비스 제공과 관련하여 취득한 이용자의 정보를 본인의 동의없이 타인에게 누설 배포할 수 없으며 서비스 관련 업무 이외의 목적으로도 사용할 수 없습니다.
3. 제2항의 경우 관계법령에 의한 규정에 의하여 수사상의 목적으로 관계기관으로부터 요구받은 경우나 정보통신윤리위원회의 요청이 있는 경우 또는 통신망법 24조 1항 '정보통신서비스의 제공에 따른 요금정산을 위하여 필요한 경우'에는 개인정보의 이용 혹은 제 3자에게 제공될수 있으며 그 범위는 회사의 개인정보보호정책을 따릅니다.
4. 회사는 계속적이고 안정적인 서비스 제공을 위하여 설비에 장애가 생기거나 멸실된 경우에는 지체없이 이를 수리 또는 복구하며, 서비스를 다시 이용할 수 있게 된 경우 이 사실을 이용자에게 통지하여야 합니다. 단 천재지변, 비상사태 또는 그밖의 부득이한 경우에는 서비스를 일시 중단하거나 중지할 수 있습니다.
5. 회사는 이용계약의 체결, 계약사항의 변경 및 해지 등 이용자와의 계약에 관련된 절차 및 내용 등에 있어서 이용자에게 편의를 제공하도록 노력합니다.
6. 회사는 이용자가 안전하게 당사서비스를 이용할 수 있도록 이용자의 개인정보(신용정보 포함)보호를 위한 보안시스템을 갖추어야 합니다.
7. 회사는 이용자가 제11조의 이용자의 의무를 위반한 경우 및 고의 또는 중대한 과실로 회사에 손해를 입힌 경우에는 사전 통보 없이 이용계약을 해지하거나 또는 기간을 정하여 서비스의 이용을 중지할 수 있습니다.
제 11 조 (이용자의 의무)
1. 이용자는 이 약관에서 규정하는 사항과 서비스 이용안내 또는 주의사항을 준수하여야 하며,기타 회사의 업무수행에 현저한 지장을 초래하는 행위를 하여서는 아니 됩니다.
2. 이용자는 서비스를 이용함에 있어 문제 발생소지 정보의 책임은 이용자에게 있습니다.
3. 이용자는 서비스를 이용하여 얻은 정보를 가공, 판매하는 행위 등 게재 된 자료를 상업적으로 이용할 수 없으며 이를 위반하여 발생하는 제반 문제에 대한 책임은 이용자에게 있습니다.
4. 이용자는 이용계약에 따라 요금을 지불하여야 하며, 서비스 이용요금의 미납으로 인하여 발생되는 모든 문제에 대한 책임은 이용자에게 있습니다. 단, 회사의 고의 또는 중과실의 경우에는 그러하지 아니 합니다.
5. 이용자는 회원가입시 정보는 정확하게 기입하여야 합니다. 주민등록생성기등을 이용하여 허위로 가입을 하거나 주민등록 생성기를 인터넷에 올리거나 이를 이용해 만든 행위에 관하여서도 주민등록법 개정안에 의거하여 엄중히 법적인 제재를 가할 수 있습니다. 또한 이미 제공된 귀하에 대한 정보가 정확한 정보가 되도록 유지, 갱신하여야 하며, 회원은 자신의 캔디바 ID 및 비밀번호를 제3자에게 이용하게 해서는 안됩니다.
6. 이용자는 당사의 사전 승낙없이 서비스를 이용하여 어떠한 영리행위도 할 수 없습니다.
7. 이용자는 당사 서비스를 이용하여 얻은 정보를 당사의 사전승낙 없이 복사, 복제, 변경, 번역, 출판·방송 기타의 방법으로 사용하거나 이를 타인에게 제공할 수 없습니다.
8. 이용자는 당사 서비스 이용과 관련하여 다음 각 호의 행위를 하여서는 안됩니다.
1) 다른 회원의 캔디바 ID를 부정 사용하는 행위
2) 범죄행위를 목적으로 하거나 기타 범죄행위와 관련된 행위
3) 선량한 풍속, 기타 사회질서를 해하는 행위
4) 타인의 명예를 훼손하거나 모욕하는 행위
5) 타인의 지적재산권 등의 권리를 침해하는 행위
6) 해킹행위 또는 컴퓨터 바이러스의 유포행위
7) 타인의 의사에 반하여 광고성 정보 등 일정한 내용을 지속적으로 전송하는 행위
8) 서비스의 안전적인 운영에 지장을 주거나 줄 우려가 있는 일체의 행위
9) 당사 사이트에 게시된 정보의 변경
10) 기타 전기통신법 제53조와 전기통신사업법 시행령 16조(불온통신), 통신사업법 제53조3항에 위배되는 행위
9. 다른 이용자들에게 피해를 주는 다음 각 호에 해당하는 행위를 하여서는 안됩니다.
1) 회사가 인정하는 서비스의 공식 운영자인 아이디를 사칭하는 내용
2) 선정적이고 음란한 내용
3) 반사회적이고 관계법령에 저촉되는 내용
4) 기타 제3자의 상표권, 저작권에 위배될 가능성이 있는 내용
5) 비어, 속어라고 판단되는 내용
6) 욕설이나 노골적인 성 묘사를 하는 내용
7) 다른 이용자를 희롱하거나, 위협하거나, 특정 이용자에게 지속적으로 고통을 주거나,불편을 주는 행위
8) 서비스 내 또는 웹사이트 상에서 다른 회사 제품의 광고나 판촉활동을 하는 행위
9) 저작권자의 허가 없이 저작권 문제가 발생할 소지가 있는 내용을 서비스 내 또는 웹사이트 상에 배포하는 행위
제 4장 서비스 이용·제한·정지등
제 12조 (서비스의 이용)
1. 캔디바 서비스의 이용은 기본적으로 무료입니다. 단 회사에서 정한 별도의 유효정보 및 유료서비스에 대해서는 그러지 아니하며, 유료서비스 이용에 관한 사항은 회사가 별도로 정한 약관 및 정책에 따릅니다.
2. 유료 서비스 이용의 결제에 관한 사항(충전방법, 사용, 환불 등)은 바(Bar)정책 이용약관에 따릅니다.
3. 아바타 및 게임 아이템 등 유료서비스의 소유권은 회사에 있으며, 웹상의 사용권은 해당 컨텐츠를 구매한 이용자에게 있습니다.
제 13조 (서비스 이용시간 및 중지)
1. 서비스 이용은 회사의 업무상 또는 기술상 특별한 지장이 없는 한 연중 무휴, 1일 24시간을 원칙으로 합니다. 다만, 회사는 서비스의 데이터 베이스별로 이용가능시간을 정할 수 있으며, 이 경우 그 내용은 회사가 정하여 서비스에 게시하거나 별도로 공시하는 바에 따릅니다.
1) 제1항의 규정에도 불구하고 정기점검 등의 필요로 회사가 정한 날 또는 시간은 예외적으로 서비스 이용을 제한할 수 있습니다.
2. 전시,사변,천재지변 또는 이에 준하는 국가비상사태가 발생하거나 발생할 우려가 있는 경우와 전기통신사업법에 의한 기간통신사업자가 전기통신서비스를 중지하는등 기타 부득이한 사유가 있는 경우에는 서비스의 전부 또는 일부를 제한하거나 정지할 수 있습니다.
1) 회사는 제1항의 규정에 의하여 서비스의 전부 또는 일부를 제한하거나 정지한 때에는 지체없이 이용자에게 알려야 합니다.
3. 이용자가 국익 또는 사회적 공익을 저해할 목적이나 범죄적 목적으로 서비스를 이용하고 있다고 판단되는 경우에 회사는 이용자에게 사전 통보 없이 서비스를 중단할 수 있으며 그에 따른 데이터도 복구를 전제로 하지않고 삭제할 수 있습니다.
4. 회사는 이용정지 기간 중에 사유가 해소된 것이 확인된 경우에는 지체없이 서비스를 재개통 또는 이용정지를 해제합니다.
제 14조 (각종 자료의 저장기간)
회사는 필요한 경우 서비스의 일정 부분별로 이용자가 게시한 자료나 이용자의 필요에 의해 저장하고 있는 자료에 대해 일정한 게재기간 또는 저장기간을 정할 수 있으며, 필요에 따라 기간을 변경할 수 있습니다.
제 15조 (게시물의 저작권)
1. 이용자가 서비스 홈페이지에 게시하거나 등록한 자료의 지적재산권은 이용자에게 귀속됩니다. 단, 회사는 서비스 홈페이지의 게재권을 가지며 비상업적 목적으로는 이용자의 게시물을 활용할 수 있습니다.
2. 이용자는 서비스를 이용하여 얻은 정보를 가공, 판매하는 행위 등 게재 된 자료를 상업적으로 이용할 수 없으며 이를 위반하여 발생하는 제반 문제에 대한 책임은 이용자에게 있습니다.
3. 회사는 이용자 게시물의 내용 검열, 검색 및 관리에 따른 일체의 손해배상 책임을 지지아니 합니다.
제 5장 개인정보 보호
제 16조 개인정보보호
회사는 관계법령이 정하는 바에 따라 회원등록정보를 포함한 회원의 개인정보를 보호하기 위하여 노력을 합니다. 회원의 개인정보보호에 관하여 관계법령 및 회사가 정하는 개인정보보호정책에 정한 바에 따릅니다.
제 6장 계약해지 및 이용제한
제 17조 (계약의 해지)
회원이 이용계약을 해지하고자 하는 경우에는 회원 본인이 온라인을 통하여 등록해지신청을 하여야 합니다.
제 18조 (이용제한)
1. 회사는 회원이 다음 각 호의 사유에 해당하는 경우 사전통지 없이 회원의 서비스 이용제한 및 적법한 조치를 취할 수 있으며 이용계약을 해지하거나 또는 기간을 정하여 서비스를 중지할 수 있습니다.
1) 회원 가입신청 또는 변경시 허위 내용을 등록한 경우
2) 타인의 서비스 이용을 방해하거나 그 정보를 도용한 경우
3) 회사의 운영진, 직원 또는 관계자를 사칭하는 경우
4) 회사의 사전승락없이 서비스를 이용하여 영업활동을 하는 경우
5) 회원ID를 타인과 거래하거나 회원ID의 게임상 사이버 자산을 타인과 매매하는 행위를 하는 경우
6) 회사 프로그램상의 버그를 악용하여 정상적이지 아니한 방법으로 게임상 사이버자산을 취득하는 행위를 하는 경우
7) 서비스를 통하여 얻은 정보를 회사의 사전승낙없이 서비스 이용 외이 목적으로 복제하거나 이를 출판 및 방송 등에 사용하거나, 제3자에게 제공하는 경우
8) 회사 또는 제3자의 저작권 등 기타 지적재산권을 침해하는 내용을 전송, 게시, 전자우편 또는 기타의 방법으로 타인에게 유포하는 경우
9) 공공질서 및 미풍약속에 위반되는 음란한 내용의 정보, 문장, 도형, 음향, 동영상을 전송, 게시, 전자우편 또는 기타의 방법으로 타인에게 유포하는 경우
10) 심히 모욕적이거나 개인신상에 대한 내용이어서 타인의 명예나 프라이버시를 침해할 수 있는 내용을 전송, 게시, 전자우편 또는 기타의 방법으로 타인에게 유포하는 경우
11) 서비스에 위해를 가하거나 고의로 방해한 경우
12) 다른 회원을 희롱, 위협하거나 특정 이용자에게 지속적으로 고통, 불편을 주는 행위를 하는 경우
13) 범죄와 관련이 있다고 객관적으로 판단되는 행위를 하는 경우
14) 본 서비스를 이용함에 있어 본 약관 및 기타 회사가 정한 정책 또한 운영 규칙을 위반하는 경우
15) 기타 관련 법령에 위배하는 행위를 하는 경우
제 7장 분쟁조정 및 기타사항
제 19조 (손해배상)
회사는 무료로 제공되는 서비스 이용과 관련하여 회원에게 발생한 어떠한 손해에 관하여도 책임을 지지 않습니다.
제 20조 (면책조항)
1. 회사는 천재지변 또는 이에 준하는 불가항력으로 인하여 서비스를 제공 할 수 없는 경우에는 서비스 제공에 관한 책임이 면제됩니다.
2. 회사는 회원의 귀책사유로 인한 서비스 이용의 장애에 대하여 책임을 지지 않습니다.
3. 회사는 회원이 서비스를 이용하여 기대하는 수익을 상실한 것이나 서비스를 통하여 얻은 자료로 인한 손해에 관하여 책임을 지지 않습니다.
4. 회사는 회원이 서비스에 게재한 정보, 자료, 사실의 신뢰도, 정확성 등 내용에 관하여는 책임을 지지 않습니다.
5. 회사는 서비스 이용과 관련하여 가입자에게 발생한 손해 가운데 가입자의 고의, 과실에 의한 손해에 대하여 책임을 지지 않습니다.
6. 회사는 회원간 또는 회원과 제3자간에 서비스를 매개로 하여 물품거래 혹은 금전적 거래등과 관련하여 어떠한 책임도 부담하지 않습니다.
제 21조(분쟁조정)
회사와 이용고객은 개인정보에 관한 분쟁이 있는 경우 신속하고 효과적인 분쟁해결을 위하여 한국정보보호진흥원내의 개인정보분쟁조정위원회에 그 처리를 의뢰할 수 있습니다.
제 22조(회사의 소유권)
1. 회사의 서비스, 소프트웨어, 이미지, 마크, 로고, 디자인, 서비스명칭, 정보 및 상표 등과 관련된 지적재산권 및 기타 권리는 회사에게 소유권이 있습니다.
2. 이용자는 회사가 명시적으로 승인한 경우를 제외하고는 전항의 소정의 각 재산에 대한 전부 또는 일부의 수정, 대여, 대출, 판매, 배포, 제 작, 양도, 재라이센스, 담보권 설정 행위, 상업적 이용 행위를 할 수 없으며, 제3자로 하여금 이와 같은 행위를 하도록 허락할 수 없습니다.
Binary file not shown.
+230
View File
@@ -0,0 +1,230 @@
<?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/>.
/**
* Database driver test class for testing moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
require_once(__DIR__.'/../../moodle_read_slave_trait.php');
/**
* Database driver test class with moodle_read_slave_trait
*
* @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_database extends test_moodle_database {
use \moodle_read_slave_trait;
/** @var string */
protected $handle;
/**
* Does not connect to the database. Sets handle property to $dbhost
* @param string $dbhost
* @param string $dbuser
* @param string $dbpass
* @param string $dbname
* @param mixed $prefix
* @param array $dboptions
* @return bool true
*/
public function raw_connect(string $dbhost, string $dbuser, string $dbpass, string $dbname, $prefix, array $dboptions = null): bool {
$dbport = isset($dboptions['dbport']) ? $dboptions['dbport'] : "";
$this->handle = implode(':', [$dbhost, $dbport, $dbuser, $dbpass]);
$this->prefix = $prefix;
if ($dbhost == 'test_ro_fail') {
throw new \dml_connection_exception($dbhost);
}
return true;
}
/**
* Begin database transaction
* @return void
*/
protected function begin_transaction() {
}
/**
* Commit database transaction
* @return void
*/
protected function commit_transaction() {
}
/**
* Query wrapper that calls query_start() and query_end()
* @param string $sql
* @param array|null $params
* @param int $querytype
* @param ?callable $callback
* @return string $handle handle property
*/
public function with_query_start_end($sql, ?array $params, $querytype, $callback = null) {
$this->query_start($sql, $params, $querytype);
$ret = $this->handle;
if ($callback) {
call_user_func($callback, $ret);
}
$this->query_end(null);
return $ret;
}
/**
* get_dbhwrite()
* @return string $dbhwrite handle property
*/
public function get_dbhwrite() {
return $this->dbhwrite;
}
/**
* Calls with_query_start_end()
* @param string $sql
* @param array $params
* @return bool true
* @throws \Exception
*/
public function execute($sql, array $params = null) {
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
return $this->with_query_start_end($sql, $params, SQL_QUERY_UPDATE);
}
/**
* get_records_sql() override, calls with_query_start_end()
* @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($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
return $this->with_query_start_end($sql, $params, SQL_QUERY_SELECT);
}
/**
* Calls with_query_start_end()
* @param string $sql
* @param array $params
* @param int $limitfrom
* @param int $limitnum
* @return bool true
*/
public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
return $this->with_query_start_end($sql, $params, SQL_QUERY_SELECT);
}
/**
* Calls with_query_start_end()
* @param string $table
* @param array $params
* @param bool $returnid
* @param bool $bulk
* @param bool $customsequence
* @return string $handle handle property
*/
public function insert_record_raw($table, $params, $returnid = true, $bulk = false, $customsequence = false) {
$fields = implode(',', array_keys($params));
$i = 1;
foreach ($params as $value) {
$values[] = "\$".$i++;
}
$values = implode(',', $values);
$sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($values)";
return $this->with_query_start_end($sql, $params, SQL_QUERY_INSERT);
}
/**
* Calls with_query_start_end()
* @param string $table
* @param array $params
* @param bool $bulk
* @return string $handle handle property
*/
public function update_record_raw($table, $params, $bulk = false) {
$id = $params['id'];
unset($params['id']);
$i = 1;
$sets = array();
foreach ($params as $field => $value) {
$sets[] = "$field = \$".$i++;
}
$params[] = $id;
$sets = implode(',', $sets);
$sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=\$".$i;
return $this->with_query_start_end($sql, $params, SQL_QUERY_UPDATE);
}
/**
* Gets handle property
* @return string $handle handle property
*/
protected function get_db_handle() {
return $this->handle;
}
/**
* Sets handle property
* @param string $dbh
* @return void
*/
protected function set_db_handle($dbh): void {
$this->handle = $dbh;
}
/**
* Add temptable
* @param string $temptable
* @return void
*/
public function add_temptable($temptable) {
$this->temptables->add_temptable($temptable);
}
/**
* Remove temptable
* @param string $temptable
* @return void
*/
public function delete_temptable($temptable) {
$this->temptables->delete_temptable($temptable);
}
/**
* Is session lock supported in this driver?
* @return bool
*/
public function session_lock_supported() {
return true;
}
}
@@ -0,0 +1,66 @@
<?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/>.
/**
* Database driver test class for testing mysqli_native_moodle_database with moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
require_once(__DIR__.'/test_moodle_read_slave_trait.php');
/**
* Database driver mock test class that exposes some methods
*
* @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_database_mock_mysqli extends \mysqli_native_moodle_database {
use test_moodle_read_slave_trait;
/**
* Return tables in database WITHOUT current prefix
* @param bool $usecache if true, returns list of cached tables.
* @return array of table names in lowercase and without prefix
*/
public function get_tables($usecache = true) {
if ($this->tables === null) {
$this->tables = [];
}
return $this->tables;
}
/**
* To be used by database_manager
* @param string|array $sql query
* @param array|null $tablenames an array of xmldb table names affected by this request.
* @return bool true
* @throws \ddl_change_structure_exception A DDL specific exception is thrown for any errors.
*/
public function change_database_structure($sql, $tablenames = null) {
return true;
}
}
@@ -0,0 +1,43 @@
<?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/>.
/**
* Database driver test class for testing pgsql_native_moodle_database with moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
require_once(__DIR__.'/test_moodle_read_slave_trait.php');
/**
* Database driver mock test class that exposes some methods
*
* @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_database_mock_pgsql extends \pgsql_native_moodle_database {
use test_moodle_read_slave_trait;
}
@@ -0,0 +1,134 @@
<?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/>.
/**
* Database driver test class for testing moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
/**
* Database driver mock test class that uses read_slave_moodle_recordset_special
*
* @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_database_special extends read_slave_moodle_database {
/**
* Returns empty array
* @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($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
$dbhandle = parent::get_records_sql($sql, $params);
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
* @param array $params
* @param int $limitfrom
* @param int $limitnum
* @return bool true
*/
public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
$dbhandle = parent::get_recordset_sql($sql, $params);
return new read_slave_moodle_recordset_special();
}
/**
* Count the records in a table where all the given conditions met.
*
* @param string $table The table to query.
* @param array $conditions optional array $fieldname=>requestedvalue with AND in between
* @return int The count of records returned from the specified criteria.
*/
public function count_records($table, array $conditions = null) {
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(): \stdClass {
return new \stdClass();
}
/**
* Iterator interface
* @return void
*/
public function next(): void {
}
/**
* Iterator interface
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key() {
}
/**
* Iterator interface
* @return bool
*/
public function valid(): bool {
return false;
}
}
@@ -0,0 +1,54 @@
<?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/>.
/**
* Database driver test class for testing moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
/**
* Database driver test class that exposes table_names()
*
* @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_database_table_names extends read_slave_moodle_database {
/**
* @var string
*/
protected $prefix = 't_';
/**
* Upgrade to public
* @param string $sql
* @return array
*/
public function table_names(string $sql): array {
return parent::table_names($sql);
}
}
@@ -0,0 +1,70 @@
<?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/>.
/**
* Database driver test class for testing moodle_read_slave_trait
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* 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(): stdClass {
return new stdClass();
}
/**
* Iterator interface
* @return void
*/
public function next(): void {
}
/**
* Iterator interface
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key() {
}
/**
* Iterator interface
* @return bool
*/
public function valid(): bool {
return false;
}
}
@@ -0,0 +1,94 @@
<?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/>.
/**
* Test SQL debugging fixture
*
* @package core
* @category dml
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test SQL debugging fixture
*
* @package core
* @category dml
* @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_dml_sql_debugging_fixture {
/** @var db handle */
private $db;
/**
* constructor
* @param testcase $testcase test object
*/
public function __construct($testcase) {
$this->db = $testcase->getMockBuilder(\moodle_database::class)
->getMockForAbstractClass();
}
/**
* Get db handle
* @return a db handle
*/
public function get_mock() {
return $this->db;
}
/**
* Test caller in stacktrace
* @param string $sql original sql
* @return string sql with comments
*/
public function one(string $sql) {
$method = new \ReflectionMethod($this->db, 'add_sql_debugging');
return $method->invoke($this->db, $sql);
}
/**
* Test caller in stacktrace
* @param string $sql original sql
* @return string sql with comments
*/
public function two(string $sql) {
return $this->one($sql);
}
/**
* Test caller in stacktrace
* @param string $sql original sql
* @return string sql with comments
*/
public function three(string $sql) {
return $this->two($sql);
}
/**
* Test caller in stacktrace
* @param string $sql original sql
* @return string sql with comments
*/
public function four(string $sql) {
return $this->three($sql);
}
}
+401
View File
@@ -0,0 +1,401 @@
<?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/>.
/**
* Abstract database driver test class providing some moodle database interface
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use Exception;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/../../moodle_database.php');
require_once(__DIR__.'/../../moodle_temptables.php');
require_once(__DIR__.'/../../../ddl/database_manager.php');
require_once(__DIR__.'/test_sql_generator.php');
/**
* Abstract database driver test class
*
* @package core
* @category dml
* @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 {
/** @var string */
private $error;
/** @var array */
private $_tables = [];
/**
* Constructor - Instantiates the database
* @param bool $external True means that an external database is used.
*/
public function __construct($external = false) {
parent::__construct($external);
$this->temptables = new \moodle_temptables($this);
}
/**
* Default implementation
* @return boolean true
*/
public function driver_installed() {
return true;
}
/**
* Default implementation
* @return string 'test'
*/
public function get_dbfamily() {
return 'test';
}
/**
* Default implementation
* @return string 'test'
*/
protected function get_dbtype() {
return 'test';
}
/**
* Default implementation
* @return string 'test'
*/
protected function get_dblibrary() {
return 'test';
}
/**
* Default implementation
* @return string 'test'
*/
public function get_name() {
return 'test';
}
/**
* Default implementation
* @return string
*/
public function get_configuration_help() {
return 'test database driver';
}
/**
* Default implementation
* @return array
*/
public function get_server_info() {
return ['description' => $this->name(), 'version' => '0'];
}
/**
* Default implementation
* @return int 0
*/
protected function allowed_param_types() {
return 0;
}
/**
* Returns error property
* @return string $error
*/
public function get_last_error() {
return $this->error;
}
/**
* Sets tables property
* @param array $tables
* @return void
*/
public function set_tables($tables) {
$this->_tables = $tables;
}
/**
* Returns keys of tables property
* @param bool $usecache
* @return array $tablenames
*/
public function get_tables($usecache = true) {
return array_keys($this->_tables);
}
/**
* Return table indexes
* @param string $table
* @return array $indexes
*/
public function get_indexes($table) {
return isset($this->_tables[$table]['indexes']) ? $this->_tables[$table]['indexes'] : [];
}
/**
* Return table columns
* @param string $table
* @return array database_column_info[] of database_column_info objects indexed with column names
*/
public function fetch_columns($table): array {
return $this->_tables[$table]['columns'];
}
/**
* Default implementation
* @param \stdClass $column metadata
* @param mixed $value
* @return mixed $value
*/
protected function normalise_value($column, $value) {
return $value;
}
/**
* Default implementation
* @param string|array $sql
* @param array|null $tablenames
* @return bool true
*/
public function change_database_structure($sql, $tablenames = null) {
return true;
}
/**
* Default implementation, throws Exception
* @param string $sql
* @param array $params
* @return bool true
* @throws Exception
*/
public function execute($sql, array $params = null) {
throw new Exception("execute() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $sql
* @param array $params
* @param int $limitfrom
* @param int $limitnum
* @return bool true
* @throws Exception
*/
public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
throw new Exception("get_recordset_sql() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $sql
* @param array $params
* @param int $limitfrom
* @param int $limitnum
* @return bool true
* @throws Exception
*/
public function get_records_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) {
throw new Exception("get_records_sql() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $sql
* @param array $params
* @return bool true
* @throws Exception
*/
public function get_fieldset_sql($sql, array $params = null) {
throw new Exception("get_fieldset_sql() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param array $params
* @param bool $returnid
* @param bool $bulk
* @param bool $customsequence
* @return bool|int true or new id
* @throws Exception
*/
public function insert_record_raw($table, $params, $returnid = true, $bulk = false, $customsequence = false) {
throw new Exception("insert_record_raw() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param object|array $dataobject
* @param bool $returnid
* @param bool $bulk
* @return bool|int true or new id
* @throws Exception
*/
public function insert_record($table, $dataobject, $returnid = true, $bulk = false) {
return $this->insert_record_raw($table, (array)$dataobject, $returnid, $bulk);
}
/**
* Default implementation, throws Exception
* @param string $table
* @param StdObject $dataobject
* @return bool true
* @throws Exception
*/
public function import_record($table, $dataobject) {
throw new Exception("import_record() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param array $params
* @param bool $bulk
* @return bool true
* @throws Exception
*/
public function update_record_raw($table, $params, $bulk = false) {
throw new Exception("update_record_raw() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param StdObject $dataobject
* @param bool $bulk
* @return bool true
* @throws Exception
*/
public function update_record($table, $dataobject, $bulk = false) {
throw new Exception("update_record() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param string $newfield
* @param string $newvalue
* @param string $select
* @param array $params
* @return bool true
* @throws Exception
*/
public function set_field_select($table, $newfield, $newvalue, $select, array $params = null) {
throw new Exception("set_field_select() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $table
* @param string $select
* @param array $params
* @return bool true
* @throws Exception
*/
public function delete_records_select($table, $select, array $params = null) {
throw new Exception("delete_records_select() not implemented");
}
/**
* Default implementation, throws Exception
* @return string $arr,...
* @throws Exception
*/
public function sql_concat(...$arr) {
throw new Exception("sql_concat() not implemented");
}
/**
* Default implementation, throws Exception
* @param string $separator
* @param array $elements
* @return string $sql
* @throws Exception
*/
public function sql_concat_join($separator = "' '", $elements = []) {
throw new Exception("sql_concat_join() not implemented");
}
/**
* Default implementation, throws Exception
*
* @param string $field
* @param string $separator
* @param string $sort
* @return string
* @throws Exception
*/
public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
throw new Exception('sql_group_concat() not implemented');
}
/**
* Default implementation, throws Exception
* @return void
* @throws Exception
*/
protected function begin_transaction() {
throw new Exception("begin_transaction() not implemented");
}
/**
* Default implementation, throws Exception
* @return void
* @throws Exception
*/
protected function commit_transaction() {
throw new Exception("commit_transaction() not implemented");
}
/**
* Default implementation, throws Exception
* @return void
* @throws Exception
*/
protected function rollback_transaction() {
throw new Exception("rollback_transaction() not implemented");
}
/**
* Returns the database manager used for db manipulation.
* Used mostly in upgrade.php scripts.
* @return database_manager The instance used to perform ddl operations.
* @see lib/ddl/database_manager.php
*/
public function get_manager() {
if (!$this->database_manager) {
$generator = new test_sql_generator($this, $this->temptables);
$this->database_manager = new database_manager($this, $generator);
}
return $this->database_manager;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?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/>.
/**
* Read slave helper that exposes selected moodle_read_slave_trait metods
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use ReflectionProperty;
/**
* Read slave helper that exposes selected moodle_read_slave_trait metods
*
* @package core
* @category dml
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait test_moodle_read_slave_trait {
/**
* Constructs a mock db driver
*
* @param bool $external
*/
public function __construct($external = false) {
parent::__construct($external);
$rw = fopen("php://memory", 'r+');
fputs($rw, 'rw');
$ro = fopen("php://memory", 'r+');
fputs($ro, 'ro');
$this->prefix = 'test_'; // Default, not to leave empty.
$rcp = new ReflectionProperty(parent::class, 'wantreadslave');
$rcp->setValue($this, true);
$this->dbhwrite = $rw;
$this->dbhreadonly = $ro;
$this->set_db_handle($this->dbhwrite);
$this->temptables = new \moodle_temptables($this);
}
/**
* Check db handle
* @param string $id
* @return bool
*/
public function db_handle_is($id) {
$dbh = $this->get_db_handle();
rewind($dbh);
return stream_get_contents($dbh) == $id;
}
/**
* Check db handle is rw
* @return bool
*/
public function db_handle_is_rw() {
return $this->db_handle_is('rw');
}
/**
* Check db handle is ro
* @return bool
*/
public function db_handle_is_ro() {
return $this->db_handle_is('ro');
}
/**
* Upgrade to public
* @return resource
*/
public function get_db_handle() {
return parent::get_db_handle();
}
/**
* Upgrade to public
* @param string $sql
* @param array|null $params
* @param int $type
* @param array $extrainfo
*/
public function query_start($sql, ?array $params, $type, $extrainfo = null) {
return parent::query_start($sql, $params, $type);
}
/**
* Upgrade to public
* @param mixed $result
*/
public function query_end($result) {
parent::query_end($result);
$this->set_db_handle($this->dbhwrite);
}
/**
* Upgrade to public
*/
public function dispose() {
}
}
+122
View File
@@ -0,0 +1,122 @@
<?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/>.
/**
* Test SQL code generator class
*
* @package core
* @category dml
* @copyright 2018 Srdjan Janković, Catalyst IT
* @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');
use xmldb_table;
use xmldb_field;
/**
* Test SQL code generator class
*
* @package core
* @category ddl
* @copyright 2018 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
*/
class test_sql_generator extends \sql_generator {
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Reset a sequence to the id field of a table.
*
* @param xmldb_table|string $table name of table or the table object.
* @return array of sql statements
*/
public function getResetSequenceSQL($table) {
return [];
}
/**
* Given one correct xmldb_table, returns the SQL statements
* to create temporary table (inside one array).
*
* @param xmldb_table $xmldbtable The xmldb_table object instance.
* @return array of sql statements
*/
public function getCreateTempTableSQL($xmldbtable) {
return [];
}
/**
* Given one XMLDB Type, length and decimals, returns the DB proper SQL type.
*
* @param int $xmldbtype The xmldb_type defined constant. XMLDB_TYPE_INTEGER and other XMLDB_TYPE_* constants.
* @param int $xmldblength The length of that data type.
* @param int $xmldbdecimals The decimal places of precision of the data type.
* @return string The DB defined data type.
*/
public function getTypeSQL($xmldbtype, $xmldblength = null, $xmldbdecimals = null) {
return '';
}
/**
* Returns the code (array of statements) needed to add one comment to the table.
*
* @param xmldb_table $xmldbtable The xmldb_table object instance.
* @return array Array of SQL statements to add one comment to the table.
*/
function getCommentSQL($xmldbtable) {
return [];
}
/**
* Given one xmldb_table and one xmldb_field, return the SQL statements needed to add its default
* (usually invoked from getModifyDefaultSQL()
*
* @param xmldb_table $xmldbtable The xmldb_table object instance.
* @param xmldb_field $xmldbfield The xmldb_field object instance.
* @return array Array of SQL statements to create a field's default.
*/
public function getCreateDefaultSQL($xmldbtable, $xmldbfield) {
return [];
}
/**
* Given one xmldb_table and one xmldb_field, return the SQL statements needed to drop its default
* (usually invoked from getModifyDefaultSQL()
*
* @param xmldb_table $xmldbtable The xmldb_table object instance.
* @param xmldb_field $xmldbfield The xmldb_field object instance.
* @return array Array of SQL statements to create a field's default.
*/
public function getDropDefaultSQL($xmldbtable, $xmldbfield) {
return [];
}
/**
* Returns an array of reserved words (lowercase) for this DB
* @return array An array of database specific reserved words
*/
public static function getReservedWords() {
return [];
}
}
@@ -0,0 +1,141 @@
<?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 ReflectionClass;
use mysqli;
use moodle_database, mysqli_native_moodle_database;
use moodle_exception;
/**
* Test specific features of the MySql dml.
*
* @package core
* @category test
* @copyright 2023 Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mysqli_native_moodle_database
*/
class mysqli_native_moodle_database_test extends \advanced_testcase {
/**
* Set up.
*/
public function setUp(): void {
global $DB;
parent::setUp();
// Skip tests if not using Postgres.
if (!($DB instanceof mysqli_native_moodle_database)) {
$this->markTestSkipped('MySql-only test');
}
}
/**
* SSL connection helper.
*
* @param bool|null $compress
* @param string|null $ssl
* @return mysqli
* @throws moodle_exception
*/
public function new_connection(?bool $compress = false, ?string $ssl = null): mysqli {
global $DB;
// Open new connection.
$cfg = $DB->export_dbconfig();
if (!isset($cfg->dboptions)) {
$cfg->dboptions = [];
}
$cfg->dboptions['clientcompress'] = $compress;
$cfg->dboptions['ssl'] = $ssl;
// Get a separate disposable db connection handle with guaranteed 'readonly' config.
$db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
$db2->raw_connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
$reflector = new ReflectionClass($db2);
$rp = $reflector->getProperty('mysqli');
return $rp->getValue($db2);
}
/**
* Test client compression helper.
*
* @param mysqli $mysqli
* @return array
*/
public function connection_status($mysqli): array {
$mysqli->query("SELECT * FROM INFORMATION_SCHEMA.TABLES");
$stats = [];
foreach ($mysqli->query('SHOW SESSION STATUS')->fetch_all(MYSQLI_ASSOC) as $r) {
$stats[$r['Variable_name']] = $r['Value'];
}
return $stats;
}
/**
* Test client compression.
*
* @return void
*/
public function test_client_compression(): void {
$mysqli = $this->new_connection();
$stats = $this->connection_status($mysqli);
$this->assertEquals('OFF', $stats['Compression']);
$sent = $stats['Bytes_sent'];
$mysqlic = $this->new_connection(true);
$stats = $this->connection_status($mysqlic);
$this->assertEquals('ON', $stats['Compression']);
$sentc = $stats['Bytes_sent'];
$this->assertLessThan($sent, $sentc);
}
/**
* Test SSL connection.
*
* Well as much as we can, mysqli does not reliably report connect errors.
* @return void
*/
public function test_ssl_connection(): void {
try {
$mysqli = $this->new_connection(false, 'require');
// Either connect ...
$this->assertNotNull($mysqli);
} catch (moodle_exception $e) {
// ... or fail.
// Unfortunately we cannot be sure with the error string.
$this->markTestSkipped('MySQL server does not support SSL. Unable to complete the test.');
return;
}
try {
$mysqli = $this->new_connection(false, 'verify-full');
// Either connect ...
$this->assertNotNull($mysqli);
} catch (moodle_exception $e) {
// ... or fail with invalid cert.
// Same as above, but we cannot really expect properly signed cert, so ignore.
}
$this->expectException(moodle_exception::class);
$this->new_connection(false, 'invalid-mode');
}
}
@@ -0,0 +1,429 @@
<?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/>.
/**
* Test specific features of the Postgres dml.
*
* @package core
* @category test
* @copyright 2020 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use stdClass, ReflectionClass;
use moodle_database, pgsql_native_moodle_database;
use xmldb_table;
use moodle_exception;
/**
* Test specific features of the Postgres dml.
*
* @package core
* @category test
* @copyright 2020 Ruslan Kabalin
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \pgsql_native_moodle_database
*/
class pgsql_native_moodle_database_test extends \advanced_testcase {
/**
* Setup before class.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->libdir.'/dml/pgsql_native_moodle_database.php');
}
/**
* Set up.
*/
public function setUp(): void {
global $DB;
parent::setUp();
// Skip tests if not using Postgres.
if (!($DB instanceof pgsql_native_moodle_database)) {
$this->markTestSkipped('Postgres-only test');
}
}
/**
* Get a xmldb_table object for testing, deleting any existing table
* of the same name, for example if one was left over from a previous test
* run that crashed.
*
* @param string $suffix table name suffix, use if you need more test tables
* @return xmldb_table the table object.
*/
private function get_test_table($suffix = ''): xmldb_table {
$tablename = "test_table";
if ($suffix !== '') {
$tablename .= $suffix;
}
$table = new xmldb_table($tablename);
$table->setComment("This is a test'n drop table. You can drop it safely");
return $table;
}
/**
* Find out the current index used for unique SQL_PARAMS_NAMED.
*
* @return int
*/
private function get_current_index(): int {
global $DB;
$reflector = new ReflectionClass($DB);
$property = $reflector->getProperty('inorequaluniqueindex');
return (int) $property->getValue($DB);
}
public function test_get_in_or_equal_below_limit(): void {
global $DB;
// Just less than 65535 values, expect fallback to parent method.
$invalues = range(1, 65533);
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertSame('IN ('.implode(',', array_fill(0, count($invalues), '?')).')', $usql);
$this->assertEquals(count($invalues), count($params));
foreach ($params as $key => $value) {
$this->assertSame($invalues[$key], $value);
}
}
public function test_get_in_or_equal_single_array_value(): void {
global $DB;
// Single value (in an array), expect fallback to parent method.
$invalues = array('value1');
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertEquals("= ?", $usql);
$this->assertCount(1, $params);
$this->assertEquals($invalues[0], $params[0]);
}
public function test_get_in_or_equal_single_scalar_value(): void {
global $DB;
// Single value (scalar), expect fallback to parent method.
$invalue = 'value1';
list($usql, $params) = $DB->get_in_or_equal($invalue);
$this->assertEquals("= ?", $usql);
$this->assertCount(1, $params);
$this->assertEquals($invalue, $params[0]);
}
public function test_get_in_or_equal_multiple_int_value(): void {
global $DB;
// 65535 values, int.
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertSame('IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
$this->assertEquals($params, $invalues);
}
public function test_get_in_or_equal_multiple_int_value_not_equal(): void {
global $DB;
// 65535 values, not equal, int.
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_QM, 'param', false);
$this->assertSame('NOT IN (VALUES ('.implode('),(', array_fill(0, count($invalues), '?::bigint')).'))', $usql);
$this->assertEquals($params, $invalues);
}
public function test_get_in_or_equal_named_int_value_default_name(): void {
global $DB;
// 65535 values, int, SQL_PARAMS_NAMED.
$index = $this->get_current_index();
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED);
$regex = '/^'.
preg_quote('IN (VALUES (:param'.$index.'::bigint),(:param'.++$index.'::bigint),(:param'.++$index.'::bigint)').'/';
$this->assertMatchesRegularExpression($regex, $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_named_int_value_specified_name(): void {
global $DB;
// 65535 values, int, SQL_PARAMS_NAMED, define param name.
$index = $this->get_current_index();
$invalues = range(1, 65535);
list($usql, $params) = $DB->get_in_or_equal($invalues, SQL_PARAMS_NAMED, 'ppp');
// We are in same DBI instance, expect uniqie param indexes.
$regex = '/^'.
preg_quote('IN (VALUES (:ppp'.$index.'::bigint),(:ppp'.++$index.'::bigint),(:ppp'.++$index.'::bigint)').'/';
$this->assertMatchesRegularExpression($regex, $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_named_scalar_value_specified_name(): void {
global $DB;
// 65535 values, string.
$invalues = array_fill(1, 65535, 'abc');
list($usql, $params) = $DB->get_in_or_equal($invalues);
$this->assertMatchesRegularExpression('/^' . preg_quote('IN (VALUES (?::text),(?::text),(?::text)') . '/', $usql);
foreach ($params as $value) {
$this->assertEquals(current($invalues), $value);
next($invalues);
}
}
public function test_get_in_or_equal_query_use(): void {
global $DB;
$this->resetAfterTest();
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null, null);
$table->add_field('content', XMLDB_TYPE_TEXT, 'big', null, XMLDB_NOTNULL);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$rec1 = ['course' => 3, 'content' => 'hello', 'name' => 'xyz'];
$DB->insert_record($tablename, $rec1);
$rec2 = ['course' => 3, 'content' => 'world', 'name' => 'abc'];
$DB->insert_record($tablename, $rec2);
$rec3 = ['course' => 5, 'content' => 'hello', 'name' => 'xyz'];
$DB->insert_record($tablename, $rec3);
$rec4 = ['course' => 6, 'content' => 'universe'];
$DB->insert_record($tablename, $rec4);
$currentcount = $DB->count_records($tablename);
// Getting all 4.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE id $insql
ORDER BY id ASC";
$this->assertCount($currentcount, $DB->get_records_sql($sql, $inparams));
// Getting 'hello' records (text).
$values = array_fill(1, 65535, 'hello');
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE content $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 3], array_keys($result));
// Getting NOT 'hello' records (text).
$values = array_fill(1, 65535, 'hello');
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE content $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([2, 4], array_keys($result));
// Getting 'xyz' records (char and NULL mix).
$values = array_fill(1, 65535, 'xyz');
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE name $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 3], array_keys($result));
// Getting NOT 'xyz' records (char and NULL mix).
$values = array_fill(1, 65535, 'xyz');
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE name $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
// NULL will not be in result.
$this->assertCount(1, $result);
$this->assertEquals([2], array_keys($result));
// Getting numbeic records.
$values = array_fill(1, 65535, 3);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE course $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([1, 2], array_keys($result));
// Getting numbeic records with NOT condition.
$values = array_fill(1, 65535, 3);
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE course $insql
ORDER BY id ASC";
$result = $DB->get_records_sql($sql, $inparams);
$this->assertCount(2, $result);
$this->assertEquals([3, 4], array_keys($result));
}
public function test_get_in_or_equal_big_table_query(): void {
global $DB;
$this->resetAfterTest();
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
$table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
$table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
$table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$dbman->create_table($table);
$record = new stdClass();
$record->course = 1;
$record->oneint = null;
$record->onenum = 1.0;
$record->onechar = 'a';
$record->onetext = 'aaa';
$records = [];
for ($i = 1; $i <= 65535; $i++) {
$rec = clone($record);
$rec->oneint = $i;
$records[$i] = $rec;
}
// Populate table with 65535 records.
$DB->insert_records($tablename, $records);
// And one more record.
$record->oneint = -1;
$DB->insert_record($tablename, $record);
// Check we can fetch all.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(65535, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals($values, $oneint);
// Check we can fetch all, SQL_PARAMS_NAMED.
$values = range(1, 65535);
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(65535, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals($values, $oneint);
// Check we can fetch one using NOT IN.
list($insql, $inparams) = $DB->get_in_or_equal($values, SQL_PARAMS_QM, 'param', false);
$sql = "SELECT *
FROM {{$tablename}}
WHERE oneint $insql
ORDER BY id ASC";
$stored = $DB->get_records_sql($sql, $inparams);
// Check we got correct set of records.
$this->assertCount(1, $stored);
$oneint = array_column($stored, 'oneint');
$this->assertEquals([-1], $oneint);
}
/**
* SSL connection helper.
*
* @param mixed $ssl
* @return resource|PgSql\Connection
* @throws moodle_exception
*/
public function new_connection($ssl) {
global $DB;
// Open new connection.
$cfg = $DB->export_dbconfig();
if (!isset($cfg->dboptions)) {
$cfg->dboptions = [];
}
$cfg->dboptions['ssl'] = $ssl;
// Get a separate disposable db connection handle with guaranteed 'readonly' config.
$db2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
$db2->raw_connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
$reflector = new ReflectionClass($db2);
$rp = $reflector->getProperty('pgsql');
return $rp->getValue($db2);
}
/**
* Test SSL connection.
*
* @return void
* @covers ::raw_connect
*/
public function test_ssl_connection(): void {
$pgconnerr = 'pg_connect(): Unable to connect to PostgreSQL server:';
try {
$pgsql = $this->new_connection('require');
// Either connect ...
$this->assertNotNull($pgsql);
} catch (moodle_exception $e) {
// ... or fail with SSL not supported.
$this->assertStringContainsString($pgconnerr, $e->debuginfo);
$this->assertStringContainsString('server does not support SSL', $e->debuginfo);
$this->markTestSkipped('Postgres server does not support SSL. Unable to complete the test.');
return;
}
try {
$pgsql = $this->new_connection('verify-full');
// Either connect ...
$this->assertNotNull($pgsql);
} catch (moodle_exception $e) {
// ... or fail with invalid cert.
$this->assertStringContainsString($pgconnerr, $e->debuginfo);
$this->assertStringContainsString('change sslmode to disable server certificate verification', $e->debuginfo);
}
$this->expectException(moodle_exception::class);
$this->new_connection('invalid-mode');
}
}
@@ -0,0 +1,433 @@
<?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/>.
/**
* Test specific features of the Postgres dml support relating to recordsets.
*
* @package core
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/lib/dml/pgsql_native_moodle_database.php');
/**
* Test specific features of the Postgres dml support relating to recordsets.
*
* @package core
* @category test
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class pgsql_native_recordset_test extends basic_testcase {
/** @var pgsql_native_moodle_database Special database connection */
protected $specialdb;
/**
* Creates a second db connection and a temp table with values in for testing.
*/
protected function setUp(): void {
global $DB;
parent::setUp();
// Skip tests if not using Postgres.
if (!($DB instanceof pgsql_native_moodle_database)) {
$this->markTestSkipped('Postgres-only test');
}
}
/**
* Initialises database connection with given fetch buffer size
* @param int $fetchbuffersize Size of fetch buffer
*/
protected function init_db($fetchbuffersize) {
global $CFG, $DB;
// To make testing easier, create a database with the same dboptions as the real one,
// but a low number for the cursor size.
$this->specialdb = \moodle_database::get_driver_instance('pgsql', 'native', true);
$dboptions = $CFG->dboptions;
$dboptions['fetchbuffersize'] = $fetchbuffersize;
$this->specialdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname,
$DB->get_prefix(), $dboptions);
// Create a temp table.
$dbman = $this->specialdb->get_manager();
$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']);
$dbman->create_temp_table($table);
// Add some records to the table.
for ($index = 1; $index <= 7; $index++) {
$this->specialdb->insert_record('silly_test_table', ['msg' => 'record' . $index]);
}
}
/**
* Gets rid of the second db connection.
*/
protected function tearDown(): void {
if ($this->specialdb) {
$table = new xmldb_table('silly_test_table');
$this->specialdb->get_manager()->drop_table($table);
$this->specialdb->dispose();
$this->specialdb = null;
}
parent::tearDown();
}
/**
* Tests that get_recordset_sql works when using cursors, which it does when no limit is
* specified.
*/
public function test_recordset_cursors(): void {
$this->init_db(3);
// Query the table and check the actual queries using debug mode, also check the count.
$this->specialdb->set_debug(true);
$before = $this->specialdb->perf_get_queries();
ob_start();
$rs = $this->specialdb->get_recordset_sql('SELECT * FROM {silly_test_table} ORDER BY id');
$index = 0;
foreach ($rs as $rec) {
$index++;
$this->assertEquals('record' . $index, $rec->msg);
}
$this->assertEquals(7, $index);
$rs->close();
$debugging = ob_get_contents();
ob_end_clean();
// Expect 4 fetches - first three, next three, last one (with 2).
$this->assert_query_regexps([
'~SELECT \* FROM~',
'~FETCH 3 FROM crs1~',
'~FETCH 3 FROM crs1~',
'~FETCH 3 FROM crs1~',
'~CLOSE crs1~'], $debugging);
// There should have been 7 queries tracked for perf log.
$this->assertEquals(5, $this->specialdb->perf_get_queries() - $before);
// Try a second time - this time we'll request exactly 3 items so that it has to query
// twice (as it can't tell if the first batch is the last).
$before = $this->specialdb->perf_get_queries();
ob_start();
$rs = $this->specialdb->get_recordset_sql(
'SELECT * FROM {silly_test_table} WHERE id <= ? ORDER BY id', [3]);
$index = 0;
foreach ($rs as $rec) {
$index++;
$this->assertEquals('record' . $index, $rec->msg);
}
$this->assertEquals(3, $index);
$rs->close();
$debugging = ob_get_contents();
ob_end_clean();
$this->specialdb->set_debug(false);
// Expect 2 fetches - first three, then next one (empty).
$this->assert_query_regexps([
'~SELECT \* FROM~',
'~FETCH 3 FROM crs2~',
'~FETCH 3 FROM crs2~',
'~CLOSE crs2~'], $debugging);
// There should have been 4 queries tracked for perf log.
$this->assertEquals(4, $this->specialdb->perf_get_queries() - $before);
}
/**
* Tests that get_recordset_sql works when using cursors and when there are two overlapping
* recordsets being used.
*/
public function test_recordset_cursors_overlapping(): void {
$this->init_db(3);
$rs1 = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$rs2 = $this->specialdb->get_recordset('silly_test_table', null, 'id DESC');
// Read first 3 from first recordset.
$read = [];
$read[] = $rs1->current()->id;
$rs1->next();
$read[] = $rs1->current()->id;
$rs1->next();
$read[] = $rs1->current()->id;
$rs1->next();
$this->assertEquals([1, 2, 3], $read);
// Read 5 from second recordset.
$read = [];
$read[] = $rs2->current()->id;
$rs2->next();
$read[] = $rs2->current()->id;
$rs2->next();
$read[] = $rs2->current()->id;
$rs2->next();
$read[] = $rs2->current()->id;
$rs2->next();
$read[] = $rs2->current()->id;
$rs2->next();
$this->assertEquals([7, 6, 5, 4, 3], $read);
// Now read remainder of first recordset and close it.
$read = [];
$read[] = $rs1->current()->id;
$rs1->next();
$read[] = $rs1->current()->id;
$rs1->next();
$read[] = $rs1->current()->id;
$rs1->next();
$read[] = $rs1->current()->id;
$rs1->next();
$this->assertFalse($rs1->valid());
$this->assertEquals([4, 5, 6, 7], $read);
$rs1->close();
// And remainder of second.
$read = [];
$read[] = $rs2->current()->id;
$rs2->next();
$read[] = $rs2->current()->id;
$rs2->next();
$this->assertFalse($rs2->valid());
$this->assertEquals([2, 1], $read);
$rs2->close();
}
/**
* Tests that get_recordset_sql works when using cursors and transactions inside.
*/
public function test_recordset_cursors_transaction_inside(): void {
$this->init_db(3);
// Transaction inside the recordset processing.
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
$transaction = $this->specialdb->start_delegated_transaction();
$transaction->allow_commit();
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
$rs->close();
}
/**
* Tests that get_recordset_sql works when using cursors and a transaction outside.
*/
public function test_recordset_cursors_transaction_outside(): void {
$this->init_db(3);
// Transaction outside the recordset processing.
$transaction = $this->specialdb->start_delegated_transaction();
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
$rs->close();
$transaction->allow_commit();
}
/**
* Tests that get_recordset_sql works when using cursors and a transaction overlapping.
*/
public function test_recordset_cursors_transaction_overlapping_before(): void {
$this->init_db(3);
// Transaction outside the recordset processing.
$transaction = $this->specialdb->start_delegated_transaction();
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$transaction->allow_commit();
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
$rs->close();
}
/**
* Tests that get_recordset_sql works when using cursors and a transaction overlapping.
*/
public function test_recordset_cursors_transaction_overlapping_after(): void {
$this->init_db(3);
// Transaction outside the recordset processing.
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$transaction = $this->specialdb->start_delegated_transaction();
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
$rs->close();
$transaction->allow_commit();
}
/**
* Tests that get_recordset_sql works when using cursors and a transaction that 'fails' and gets
* rolled back.
*/
public function test_recordset_cursors_transaction_rollback(): void {
$this->init_db(3);
try {
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$transaction = $this->specialdb->start_delegated_transaction();
$this->specialdb->delete_records('silly_test_table', ['id' => 5]);
$transaction->rollback(new dml_transaction_exception('rollback please'));
$this->fail('should not get here');
} catch (dml_transaction_exception $e) {
$this->assertStringContainsString('rollback please', $e->getMessage());
} finally {
// Rollback should not kill our recordset.
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
// This would happen in real code (that isn't within the same function) anyway because
// it would go out of scope.
$rs->close();
}
// OK, transaction aborted, now get the recordset again and check nothing was deleted.
$rs = $this->specialdb->get_recordset('silly_test_table', null, 'id');
$read = [];
foreach ($rs as $rec) {
$read[] = $rec->id;
}
$this->assertEquals([1, 2, 3, 4, 5, 6, 7], $read);
$rs->close();
}
/**
* Tests that get_recordset_sql works when not using cursors, because a limit is specified.
*/
public function test_recordset_no_cursors_limit(): void {
$this->init_db(3);
$this->specialdb->set_debug(true);
$before = $this->specialdb->perf_get_queries();
ob_start();
$rs = $this->specialdb->get_recordset_sql(
'SELECT * FROM {silly_test_table} ORDER BY id', [], 0, 100);
$index = 0;
foreach ($rs as $rec) {
$index++;
$this->assertEquals('record' . $index, $rec->msg);
}
$this->assertEquals(7, $index);
$rs->close();
$this->specialdb->set_debug(false);
$debugging = ob_get_contents();
ob_end_clean();
// Expect direct request without using cursors.
$this->assert_query_regexps(['~SELECT \* FROM~'], $debugging);
// There should have been 1 query tracked for perf log.
$this->assertEquals(1, $this->specialdb->perf_get_queries() - $before);
}
/**
* Tests that get_recordset_sql works when not using cursors, because the config setting turns
* them off.
*/
public function test_recordset_no_cursors_config(): void {
$this->init_db(0);
$this->specialdb->set_debug(true);
$before = $this->specialdb->perf_get_queries();
ob_start();
$rs = $this->specialdb->get_recordset_sql('SELECT * FROM {silly_test_table} ORDER BY id');
$index = 0;
foreach ($rs as $rec) {
$index++;
$this->assertEquals('record' . $index, $rec->msg);
}
$this->assertEquals(7, $index);
$rs->close();
$this->specialdb->set_debug(false);
$debugging = ob_get_contents();
ob_end_clean();
// Expect direct request without using cursors.
$this->assert_query_regexps(['~SELECT \* FROM~'], $debugging);
// There should have been 1 query tracked for perf log.
$this->assertEquals(1, $this->specialdb->perf_get_queries() - $before);
}
/**
* Asserts that database debugging output matches the expected list of SQL queries, specified
* as an array of regular expressions.
*
* @param string[] $expected Expected regular expressions
* @param string $debugging Debugging text from the database
*/
protected function assert_query_regexps(array $expected, $debugging) {
$lines = explode("\n", $debugging);
$index = 0;
$params = false;
foreach ($lines as $line) {
if ($params) {
if ($line === ')]') {
$params = false;
}
continue;
}
// Skip irrelevant lines.
if (preg_match('~^---~', $line)) {
continue;
}
if (preg_match('~^Query took~', $line)) {
continue;
}
if (trim($line) === '') {
continue;
}
// Skip param lines.
if ($line === '[array (') {
$params = true;
continue;
}
if (!array_key_exists($index, $expected)) {
$this->fail('More queries than expected');
}
$this->assertMatchesRegularExpression($expected[$index++], $line);
}
if (array_key_exists($index, $expected)) {
$this->fail('Fewer queries than expected');
}
}
}
+130
View File
@@ -0,0 +1,130 @@
<?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;
/**
* Test case for recordset_walk.
*
* @package core
* @category test
* @copyright 2015 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class recordset_walk_test extends \advanced_testcase {
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
}
public function test_no_data(): void {
global $DB;
$recordset = $DB->get_recordset('assign');
$walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
$this->assertFalse($walker->valid());
$count = 0;
foreach ($walker as $data) {
// No error here.
$count++;
}
$this->assertEquals(0, $count);
$walker->close();
}
public function test_simple_callback(): void {
global $DB;
/** @var \mod_assign_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$courses = array();
for ($i = 0; $i < 10; $i++) {
$courses[$i] = $generator->create_instance(array('course' => SITEID));
}
// Simple iteration.
$recordset = $DB->get_recordset('assign');
$walker = new \core\dml\recordset_walk($recordset, array($this, 'simple_callback'));
$count = 0;
foreach ($walker as $data) {
// Checking that the callback is being executed on each iteration.
$this->assertEquals($data->id . ' potatoes', $data->newfield);
$count++;
}
$this->assertEquals(10, $count);
// No exception if we double-close.
$walker->close();
}
public function test_extra_params_callback(): void {
global $DB;
/** @var \mod_assign_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$courses = array();
for ($i = 0; $i < 10; $i++) {
$courses[$i] = $generator->create_instance(array('course' => SITEID));
}
// Iteration with extra callback arguments.
$recordset = $DB->get_recordset('assign');
$walker = new \core\dml\recordset_walk(
$recordset,
array($this, 'extra_callback'),
array('brown' => 'onions')
);
$count = 0;
foreach ($walker as $data) {
// Checking that the callback is being executed on each
// iteration and the param is being passed.
$this->assertEquals('onions', $data->brown);
$count++;
}
$this->assertEquals(10, $count);
$walker->close();
}
/**
* Simple callback requiring 1 row fields.
*
* @param \stdClass $data
* @return \Traversable
*/
public function simple_callback($data, $nothing = 'notpassed') {
// Confirm nothing was passed.
$this->assertEquals('notpassed', $nothing);
$data->newfield = $data->id . ' potatoes';
return $data;
}
/**
* Callback requiring 1 row fields + other params.
*
* @param \stdClass $data
* @param mixed $extra
* @return \Traversable
*/
public function extra_callback($data, $extra) {
$data->brown = $extra['brown'];
return $data;
}
}
@@ -0,0 +1,287 @@
<?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/>.
/**
* Test case for sqlsrv dml support.
*
* @package core
* @category test
* @copyright 2017 John Okely
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
use sqlsrv_native_moodle_database;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/lib/dml/sqlsrv_native_moodle_database.php');
/**
* Test case for sqlsrv dml support.
*
* @package core
* @category test
* @copyright 2017 John Okely
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sqlsrv_native_moodle_database_test extends \advanced_testcase {
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
}
/**
* Dataprovider for test_add_no_lock_to_temp_tables
* @return array Data for test_add_no_lock_to_temp_tables
*/
public function add_no_lock_to_temp_tables_provider() {
return [
"Basic temp table, nothing following" => [
'input' => 'SELECT * FROM {table_temp}',
'expected' => 'SELECT * FROM {table_temp} WITH (NOLOCK)'
],
"Basic temp table, with capitalised alias" => [
'input' => 'SELECT * FROM {table_temp} MYTABLE',
'expected' => 'SELECT * FROM {table_temp} MYTABLE WITH (NOLOCK)'
],
"Temp table with alias, and another non-temp table" => [
'input' => 'SELECT * FROM {table_temp} x WHERE y in (SELECT y from {table2})',
'expected' => 'SELECT * FROM {table_temp} x WITH (NOLOCK) WHERE y in (SELECT y from {table2})'
],
"Temp table with reserve word following, no alias" => [
'input' => 'SELECT DISTINCT * FROM {table_temp} WHERE y in (SELECT y from {table2} nottemp)',
'expected' => 'SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK) WHERE y in (SELECT y from {table2} nottemp)'
],
"Temp table with reserve word, lower case" => [
'input' => 'SELECT DISTINCT * FROM {table_temp} where y in (SELECT y from {table2} nottemp)',
'expected' => 'SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK) where y in (SELECT y from {table2} nottemp)'
],
"Another reserve word test" => [
'input' => 'SELECT DISTINCT * FROM {table_temp} PIVOT y in (SELECT y from {table2} nottemp)',
'expected' => 'SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK) PIVOT y in (SELECT y from {table2} nottemp)'
],
"Another reserve word test should fail" => [
'input' => 'SELECT DISTINCT * FROM {table_temp} PIVOT y in (SELECT y from {table2} nottemp)',
'expected' => 'SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK) PIVOT y in (SELECT y from {table2} nottemp)'
],
"Temp table with an alias starting with a keyword" => [
'input' => 'SELECT * FROM {table_temp} asx',
'expected' => 'SELECT * FROM {table_temp} asx WITH (NOLOCK)'
],
"Keep alias with underscore" => [
'input' => 'SELECT * FROM {table_temp} alias_for_table',
'expected' => 'SELECT * FROM {table_temp} alias_for_table WITH (NOLOCK)'
],
"Alias with number" => [
'input' => 'SELECT * FROM {table_temp} a5 WHERE y',
'expected' => 'SELECT * FROM {table_temp} a5 WITH (NOLOCK) WHERE y'
],
"Alias with number and underscore" => [
'input' => 'SELECT * FROM {table_temp} a_5 WHERE y',
'expected' => 'SELECT * FROM {table_temp} a_5 WITH (NOLOCK) WHERE y'
],
"Temp table in subquery" => [
'input' => 'select * FROM (SELECT DISTINCT * FROM {table_temp})',
'expected' => 'select * FROM (SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK))'
],
"Temp table in subquery, with following commands" => [
'input' => 'select * FROM (SELECT DISTINCT * FROM {table_temp} ) WHERE y',
'expected' => 'select * FROM (SELECT DISTINCT * FROM {table_temp} WITH (NOLOCK) ) WHERE y'
],
"Temp table in subquery, with alias" => [
'input' => 'select * FROM (SELECT DISTINCT * FROM {table_temp} x) WHERE y',
'expected' => 'select * FROM (SELECT DISTINCT * FROM {table_temp} x WITH (NOLOCK)) WHERE y'
],
];
}
/**
* Test add_no_lock_to_temp_tables
*
* @param string $input The input SQL query
* @param string $expected The expected resultant query
* @dataProvider add_no_lock_to_temp_tables_provider
*/
public function test_add_no_lock_to_temp_tables($input, $expected): void {
$sqlsrv = new sqlsrv_native_moodle_database();
$reflector = new \ReflectionObject($sqlsrv);
$method = $reflector->getMethod('add_no_lock_to_temp_tables');
$temptablesproperty = $reflector->getProperty('temptables');
$temptables = new temptables_tester();
$temptablesproperty->setValue($sqlsrv, $temptables);
$result = $method->invoke($sqlsrv, $input);
$temptablesproperty->setValue($sqlsrv, null);
$this->assertEquals($expected, $result);
}
/**
* Data provider for test_has_query_order_by
*
* @return array data for test_has_query_order_by
*/
public function has_query_order_by_provider() {
// Fixtures taken from https://docs.moodle.org/en/ad-hoc_contributed_reports.
return [
'User with language => FALSE' => [
'sql' => <<<EOT
SELECT username, lang
FROM prefix_user
EOT
,
'expectedmainquery' => <<<EOT
SELECT username, lang
FROM prefix_user
EOT
,
'expectedresult' => false
],
'List Users with extra info (email) in current course => FALSE' => [
'sql' => <<<EOT
SELECT u.firstname, u.lastname, u.email
FROM prefix_role_assignments AS ra
JOIN prefix_context AS context ON context.id = ra.contextid AND context.contextlevel = 50
JOIN prefix_course AS c ON c.id = context.instanceid AND c.id = %%COURSEID%%
JOIN prefix_user AS u ON u.id = ra.userid
EOT
,
'expectedmainquery' => <<<EOT
SELECT u.firstname, u.lastname, u.email
FROM prefix_role_assignments AS ra
JOIN prefix_context AS context ON context.id = ra.contextid AND context.contextlevel = 50
JOIN prefix_course AS c ON c.id = context.instanceid AND c.id = %%COURSEID%%
JOIN prefix_user AS u ON u.id = ra.userid
EOT
,
'expectedresult' => false
],
'ROW_NUMBER() OVER (ORDER BY ...) => FALSE (https://github.com/jleyva/moodle-block_configurablereports/issues/120)' => [
'sql' => <<<EOT
SELECT COUNT(*) AS 'Users who have logged in today'
FROM (
SELECT ROW_NUMBER() OVER(ORDER BY lastaccess DESC) AS Row
FROM mdl_user
WHERE lastaccess > DATEDIFF(s, '1970-01-01 02:00:00', (SELECT Convert(DateTime, DATEDIFF(DAY, 0, GETDATE()))))
) AS Logins
EOT
,
'expectedmainquery' => <<<EOT
SELECT COUNT() AS 'Users who have logged in today'
FROM () AS Logins
EOT
,
'expectedresult' => false
],
'CONTRIB-7725 workaround) => TRUE' => [
'sql' => <<<EOT
SELECT COUNT(*) AS 'Users who have logged in today'
FROM (
SELECT ROW_NUMBER() OVER(ORDER BY lastaccess DESC) AS Row
FROM mdl_user
WHERE lastaccess > DATEDIFF(s, '1970-01-01 02:00:00', (SELECT Convert(DateTime, DATEDIFF(DAY, 0, GETDATE()))))
) AS Logins ORDER BY 1
EOT
,
'expectedmainquery' => <<<EOT
SELECT COUNT() AS 'Users who have logged in today'
FROM () AS Logins ORDER BY 1
EOT
,
'expectedresult' => true
],
'Enrolment count in each Course => TRUE' => [
'sql' => <<<EOT
SELECT c.fullname, COUNT(ue.id) AS Enroled
FROM prefix_course AS c
JOIN prefix_enrol AS en ON en.courseid = c.id
JOIN prefix_user_enrolments AS ue ON ue.enrolid = en.id
GROUP BY c.id
ORDER BY c.fullname
EOT
,
'expectedmainquery' => <<<EOT
SELECT c.fullname, COUNT() AS Enroled
FROM prefix_course AS c
JOIN prefix_enrol AS en ON en.courseid = c.id
JOIN prefix_user_enrolments AS ue ON ue.enrolid = en.id
GROUP BY c.id
ORDER BY c.fullname
EOT
,
'expectedresult' => true
],
];
}
/**
* Test has_query_order_by
*
* @dataProvider has_query_order_by_provider
* @param string $sql the query
* @param string $expectedmainquery the expected main query
* @param bool $expectedresult the expected result
*/
public function test_has_query_order_by(string $sql, string $expectedmainquery, bool $expectedresult): void {
$mainquery = preg_replace('/\(((?>[^()]+)|(?R))*\)/', '()', $sql);
$this->assertSame($expectedmainquery, $mainquery);
// The has_query_order_by static method is protected. Use Reflection to call the method.
$method = new \ReflectionMethod('sqlsrv_native_moodle_database', 'has_query_order_by');
$result = $method->invoke(null, $sql);
$this->assertSame($expectedresult, $result);
}
}
/**
* Test class for testing temptables
*
* @copyright 2017 John Okely
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class temptables_tester {
/**
* Returns if one table, based in the information present in the store, is a temp table
*
* For easy testing, anything with the word 'temp' in it is considered temporary.
*
* @param string $tablename name without prefix of the table we are asking about
* @return bool true if the table is a temp table (based in the store info), false if not
*/
public function is_temptable($tablename) {
if (strpos($tablename, 'temp') === false) {
return false;
} else {
return true;
}
}
/**
* Dispose the temptables
*
* @return void
*/
public function dispose() {
}
}