mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 06:18:28 +01:00
591 lines
21 KiB
PHP
591 lines
21 KiB
PHP
<?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/>.
|
|
|
|
|
|
/**
|
|
* General adodb database driver.
|
|
*
|
|
* TODO: delete before branching 2.0
|
|
*
|
|
* @package moodlecore
|
|
* @subpackage DML
|
|
* @copyright 2008 Petr Skoda (http://skodak.org)
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
require_once($CFG->libdir.'/dml/moodle_database.php');
|
|
require_once($CFG->libdir.'/dml/adodb_moodle_recordset.php');
|
|
|
|
/**
|
|
* Abstract moodle database class
|
|
*/
|
|
abstract class adodb_moodle_database extends moodle_database {
|
|
|
|
protected $adodb;
|
|
|
|
/**
|
|
* Returns general database library name
|
|
* Note: can be used before connect()
|
|
* @return string db type adodb, pdo, native
|
|
*/
|
|
protected function get_dblibrary() {
|
|
return 'adodb';
|
|
}
|
|
|
|
/**
|
|
* Returns localised database type name
|
|
* Note: can be used before connect()
|
|
* @return string
|
|
*/
|
|
public function get_name() {
|
|
$dbtype = $this->get_dbtype();
|
|
return get_string($dbtype, 'install');
|
|
}
|
|
|
|
/**
|
|
* Returns localised database configuration help.
|
|
* Note: can be used before connect()
|
|
* @return string
|
|
*/
|
|
public function get_configuration_help() {
|
|
$dbtype = $this->get_dbtype();
|
|
return get_string($dbtype.'help', 'install');
|
|
}
|
|
|
|
/**
|
|
* Adodb preconnection routines, ususally sets up needed defines;
|
|
*/
|
|
protected abstract function preconfigure_dbconnection();
|
|
|
|
public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
|
|
global $CFG;
|
|
|
|
$driverstatus = $this->driver_installed();
|
|
|
|
if ($driverstatus !== true) {
|
|
throw new dml_exception('dbdriverproblem', $driverstatus);
|
|
}
|
|
|
|
ob_start();
|
|
|
|
$this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
|
|
|
|
$this->preconfigure_dbconnection();
|
|
|
|
require_once($CFG->libdir.'/adodb/adodb.inc.php');
|
|
|
|
$this->adodb = ADONewConnection($this->get_dbtype());
|
|
|
|
// See MDL-6760 for why this is necessary. In Moodle 1.8, once we start using NULLs properly,
|
|
// we probably want to change this value to ''.
|
|
$this->adodb->null2null = 'A long random string that will never, ever match something we want to insert into the database, I hope. \'';
|
|
|
|
|
|
if (!empty($this->dboptions['dbpersist'])) { // Use persistent connection
|
|
$connected = $this->adodb->PConnect($this->dbhost, $this->dbuser, $this->dbpass, $this->dbname);
|
|
} else { // Use single connection
|
|
$connected = $this->adodb->Connect($this->dbhost, $this->dbuser, $this->dbpass, $this->dbname);
|
|
}
|
|
|
|
$dberr = ob_get_contents();
|
|
ob_end_clean();
|
|
|
|
if (!$connected) {
|
|
throw new dml_connection_exception($dberr);
|
|
}
|
|
|
|
$this->configure_dbconnection();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Adodb post connection routines, usually sets up encoding,e tc.
|
|
*/
|
|
protected abstract function configure_dbconnection();
|
|
|
|
/**
|
|
* Close database connection and release all resources
|
|
* and memory (especially circular memory references).
|
|
* Do NOT use connect() again, create a new instance if needed.
|
|
*/
|
|
public function dispose() {
|
|
parent::dispose(); // Call parent dispose to write/close session and other common stuff before clossing conn
|
|
if ($this->adodb) {
|
|
$this->adodb->Close();
|
|
}
|
|
}
|
|
|
|
public function query_log($error=false) {
|
|
// no logging for obsoleted adodb - it would require a lot more fixing
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Returns database server info array
|
|
* @return array
|
|
*/
|
|
public function get_server_info() {
|
|
$this->query_start("--adodb-ServerInfo", null, SQL_QUERY_AUX);
|
|
$info = $this->adodb->ServerInfo();
|
|
$this->query_end(true);
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Return tables in database WITHOUT current prefix
|
|
* @return array of table names in lowercase and without prefix
|
|
*/
|
|
public function get_tables($usecache=true) {
|
|
$this->query_start("--adodb-MetaTables", null, SQL_QUERY_AUX);
|
|
$metatables = $this->adodb->MetaTables();
|
|
$this->query_end(true);
|
|
|
|
$tables = array();
|
|
|
|
foreach ($metatables as $table) {
|
|
$table = strtolower($table);
|
|
if (empty($this->prefix) || strpos($table, $this->prefix) === 0) {
|
|
$tablename = substr($table, strlen($this->prefix));
|
|
$tables[$tablename] = $tablename;
|
|
}
|
|
}
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Return table indexes - everything lowercased
|
|
* @return array of arrays
|
|
*/
|
|
public function get_indexes($table) {
|
|
$this->query_start("--adodb-MetaIndexes", null, SQL_QUERY_AUX);
|
|
$indexes = $this->adodb->MetaIndexes($this->prefix.$table);
|
|
$this->query_end(true);
|
|
|
|
if (!$indexes) {
|
|
return array();
|
|
}
|
|
$indexes = array_change_key_case($indexes, CASE_LOWER);
|
|
foreach ($indexes as $indexname => $index) {
|
|
$columns = $index['columns'];
|
|
/// column names always lowercase
|
|
$columns = array_map('strtolower', $columns);
|
|
$indexes[$indexname]['columns'] = $columns;
|
|
}
|
|
|
|
return $indexes;
|
|
}
|
|
|
|
public function get_columns($table, $usecache=true) {
|
|
if ($usecache and isset($this->columns[$table])) {
|
|
return $this->columns[$table];
|
|
}
|
|
|
|
$this->query_start("--adodb-MetaColumns", null, SQL_QUERY_AUX);
|
|
$columns = $this->adodb->MetaColumns($this->prefix.$table);
|
|
$this->query_end(true);
|
|
|
|
if (!$columns) {
|
|
return array();
|
|
}
|
|
|
|
$this->columns[$table] = array();
|
|
|
|
foreach ($columns as $column) {
|
|
// colum names must be lowercase
|
|
$column->meta_type = substr($this->adodb->MetaType($column), 0 ,1); // only 1 character
|
|
if (!empty($column->enums)) {
|
|
// hack: fix the 'quotes' surrounding the values itroduced by adodb
|
|
foreach ($column->enums as $key=>$value) {
|
|
if (strpos($value, "'") === 0 and strlen($value) > 2) {
|
|
$column->enums[$key] = substr($value, 1, strlen($value)-2);
|
|
}
|
|
}
|
|
}
|
|
$this->columns[$table][$column->name] = new database_column_info($column);
|
|
}
|
|
|
|
return $this->columns[$table];
|
|
}
|
|
|
|
public function get_last_error() {
|
|
return $this->adodb->ErrorMsg();
|
|
}
|
|
|
|
/**
|
|
* Do NOT use in code, to be used by database_manager only!
|
|
* @param string $sql query
|
|
* @return bool true
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function change_database_structure($sql) {
|
|
$this->reset_caches();
|
|
|
|
$this->query_start($sql, null, SQL_QUERY_STRUCTURE);
|
|
$rs = $this->adodb->Execute($sql);
|
|
$this->query_end($rs);
|
|
|
|
$rs->Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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::execute_sql() instead!
|
|
* @param string $sql query
|
|
* @param array $params query parameters
|
|
* @return bool true
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function execute($sql, array $params=null) {
|
|
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
|
|
|
|
if (strpos($sql, ';') !== false) {
|
|
throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
|
|
}
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_UPDATE);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
|
|
$rs->Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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 mixed true or new id
|
|
* @throws dml_exception if error
|
|
*/
|
|
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 {$this->prefix}$table ($fields) VALUES($qms)";
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_INSERT);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
$rs->Close();
|
|
|
|
if (!$returnid) {
|
|
return true;
|
|
}
|
|
|
|
if (!$id = $this->adodb->Insert_ID()) {
|
|
throw new dml_write_exception('unknown error fetching inserted id');
|
|
}
|
|
|
|
return (int)$id;
|
|
}
|
|
|
|
/**
|
|
* Update record in 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 true means repeated updates expected
|
|
* @return bool true
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function update_record_raw($table, $params, $bulk=false) {
|
|
if (!is_array($params)) {
|
|
$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 {$this->prefix}$table SET $sets WHERE id=?";
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_UPDATE);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
$rs->Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete one or more records from a table
|
|
*
|
|
* @param string $table The database table to be checked against.
|
|
* @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
|
|
* @param array $params array of sql parameters
|
|
* @return bool true
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function delete_records_select($table, $select, array $params=null) {
|
|
if ($select) {
|
|
$select = "WHERE $select";
|
|
}
|
|
$sql = "DELETE FROM {$this->prefix}$table $select";
|
|
|
|
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_UPDATE);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
$rs->Close();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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 as for @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 object moodle_recordset instance
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
|
|
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
|
|
|
|
if ($limitfrom || $limitnum) {
|
|
///Special case, 0 must be -1 for ADOdb
|
|
$limitfrom = empty($limitfrom) ? -1 : $limitfrom;
|
|
$limitnum = empty($limitnum) ? -1 : $limitnum;
|
|
$this->query_start($sql." --LIMIT $limitfrom, $limitnum", $params, SQL_QUERY_SELECT);
|
|
$rs = $this->adodb->SelectLimit($sql, $limitnum, $limitfrom, $params);
|
|
$this->query_end($rs);
|
|
return $this->create_recordset($rs);
|
|
}
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_SELECT);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
return $this->create_recordset($rs);
|
|
}
|
|
|
|
protected function create_recordset($rs) {
|
|
return new adodb_moodle_recordset($rs);
|
|
}
|
|
|
|
/**
|
|
* Get a number of records as an array of objects.
|
|
*
|
|
* Return value as for @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 indexed by first column
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
|
|
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
|
|
|
|
$rs = null;
|
|
if ($limitfrom || $limitnum) {
|
|
///Special case, 0 must be -1 for ADOdb
|
|
$limitfrom = empty($limitfrom) ? -1 : $limitfrom;
|
|
$limitnum = empty($limitnum) ? -1 : $limitnum;
|
|
$this->query_start($sql." --LIMIT $limitfrom, $limitnum", $params, SQL_QUERY_SELECT);
|
|
$rs = $this->adodb->SelectLimit($sql, $limitnum, $limitfrom, $params);
|
|
$this->query_end($rs);
|
|
} else {
|
|
$this->query_start($sql, $params, SQL_QUERY_SELECT);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
}
|
|
$return = $this->adodb_recordset_to_array($rs);
|
|
$rs->Close();
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Selects rows and return values of first column as array.
|
|
*
|
|
* @param string $sql The SQL query
|
|
* @param array $params array of sql parameters
|
|
* @return mixed array of values
|
|
* @throws dml_exception if error
|
|
*/
|
|
public function get_fieldset_sql($sql, array $params=null) {
|
|
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
|
|
|
|
$this->query_start($sql, $params, SQL_QUERY_SELECT);
|
|
$rs = $this->adodb->Execute($sql, $params);
|
|
$this->query_end($rs);
|
|
|
|
$results = array();
|
|
while (!$rs->EOF) {
|
|
$res = reset($rs->fields);
|
|
$results[] = $res;
|
|
$rs->MoveNext();
|
|
}
|
|
$rs->Close();
|
|
return $results;
|
|
}
|
|
|
|
protected function adodb_recordset_to_array($rs) {
|
|
$debugging = debugging('', DEBUG_DEVELOPER);
|
|
|
|
if ($rs->EOF) {
|
|
// BIIIG change here - return empty array() if nothing found (2.0)
|
|
return array();
|
|
}
|
|
|
|
$objects = array();
|
|
/// First of all, we are going to get the name of the first column
|
|
/// to introduce it back after transforming the recordset to assoc array
|
|
/// See http://docs.moodle.org/en/XMLDB_Problems, fetch mode problem.
|
|
$firstcolumn = $rs->FetchField(0);
|
|
/// Get the whole associative array
|
|
if ($records = $rs->GetAssoc(true)) {
|
|
foreach ($records as $key => $record) {
|
|
$record = array($firstcolumn->name=>$key) + $record; /// Re-add the assoc field (as FIRST element since 2.0)
|
|
$objects[$key] = (object) $record; /// To object
|
|
}
|
|
if ($debugging) {
|
|
if (count($objects) != $rs->_numOfRows) {
|
|
debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate values found in column '".$firstcolumn->name."'.", DEBUG_DEVELOPER);
|
|
}
|
|
}
|
|
return $objects;
|
|
/// Fallback in case we only have 1 field in the recordset. MDL-5877
|
|
} else if ($rs->_numOfFields == 1 and $records = $rs->GetRows()) {
|
|
foreach ($records as $key => $record) {
|
|
if ($debugging && array_key_exists($record[$firstcolumn->name], $objects)) {
|
|
debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '".$record[$firstcolumn->name]."' found in column '".$firstcolumn->name."'.", DEBUG_DEVELOPER);
|
|
}
|
|
$objects[$record[$firstcolumn->name]] = (object) $record; /// The key is the first column value (like Assoc)
|
|
}
|
|
return $objects;
|
|
} else {
|
|
// weird error?
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function sql_concat() {
|
|
$args = func_get_args();
|
|
return call_user_func_array(array($this->adodb, 'Concat'), $args);
|
|
}
|
|
|
|
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 call_user_func_array(array($this->adodb, 'Concat'), $elements);
|
|
}
|
|
|
|
public function begin_sql() {
|
|
if (!parent::begin_sql()) {
|
|
return false;
|
|
}
|
|
$this->adodb->BeginTrans();
|
|
return true;
|
|
}
|
|
public function commit_sql() {
|
|
if (!parent::commit_sql()) {
|
|
return false;
|
|
}
|
|
$this->adodb->CommitTrans();
|
|
return true;
|
|
}
|
|
public function rollback_sql() {
|
|
if (!parent::rollback_sql()) {
|
|
return false;
|
|
}
|
|
$this->adodb->RollbackTrans();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Very ugly hack which emulates bound parameters in mssql queries
|
|
* where params not supported (UpdateBlob) :-(
|
|
*/
|
|
protected function emulate_bound_params($sql, array $params=null) {
|
|
if (empty($params)) {
|
|
return $sql;
|
|
}
|
|
/// ok, we have verified sql statement with ? and correct number of params
|
|
$return = strtok($sql, '?');
|
|
foreach ($params as $param) {
|
|
if (is_bool($param)) {
|
|
$return .= (int)$param;
|
|
} else if (is_null($param)) {
|
|
$return .= 'NULL';
|
|
} else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
|
|
$return .= $param;
|
|
} else if (is_float($param)) {
|
|
$return .= $param;
|
|
} else {
|
|
$param = $this->adodb->qstr($param);
|
|
$return .= "$param";
|
|
}
|
|
$return .= strtok('?');
|
|
}
|
|
return $return;
|
|
}
|
|
}
|