libdir.'/dml/moodle_database.php'); require_once($CFG->libdir.'/dml/pdo_moodle_recordset.php'); /** * Experimental pdo database class * @package dml */ abstract class pdo_moodle_database extends moodle_database { protected $pdb; protected $lastError = null; /** * Contructor - 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 * @param string $dbuser * @param string $dbpass * @param string $dbname * @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) { $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) { 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 */ protected function get_dsn() { return 'mysql:host='.$this->dbhost.';dbname='.$this->dbname; } /** * 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 adodb, 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($this->get_dbtype() . '_pdo', 'install'); } /** * Returns localised database description * Note: can be used before connect() * @return string */ public function get_configuration_hints() { return get_string('databasesettingssub_' . $this->get_dbtype() . '_pdo', 'install'); } /** * Returns database server info array * @return array */ 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 bitmask */ protected function allowed_param_types() { return SQL_PARAMS_QM | SQL_PARAMS_NAMED; } /** * Returns last error reported by database engine. */ public function get_last_error() { return $this->lastError; } /** * Function to print/save/ignore debuging messages related to SQL queries. */ protected function debug_query($sql, $params = null) { echo '
(', $this->get_dbtype(), '): ', htmlentities($sql); if($params) { echo ' (parameters '; print_r($params); echo ')'; } echo '
'; } /** * Do NOT use in code, to be used by database_manager only! * @param string $sql query * @return bool success */ public function change_database_structure($sql) { try { $this->lastError = null; if($this->debug) { $this->debug_query($sql); } $this->pdb->exec($sql); $this->reset_columns(); return true; } catch (PDOException $ex) { $this->lastError = $ex->getMessage(); return false; } } public function delete_records_select($table, $select, array $params=null) { $sql = "DELETE FROM {{$table}}"; if ($select) { $sql .= " WHERE $select"; } $this->writes++; 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 overide 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::execute_sql() instead! * @param string $sql query * @param array $params query parameters * @return bool success */ public function execute($sql, array $params=null) { try { $this->lastError = null; list($sql, $params, $type) = $this->fix_sql_params($sql, $params); if($this->debug) { $this->debug_query($sql, $params); } $sth = $this->pdb->prepare($sql); $sth->execute($params); return true; } catch (PDOException $ex) { $this->lastError = $ex->getMessage(); return false; } } /** * 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 mixed an moodle_recordset object, or false if an error occured. */ public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { try { $this->lastError = null; list($sql, $params, $type) = $this->fix_sql_params($sql, $params); $sql = $this->get_limit_clauses($sql, $limitfrom, $limitnum); if($this->debug) { $this->debug_query($sql, $params); } $this->reads++; $sth = $this->pdb->prepare($sql); $sth->execute($params); return $this->create_recordset($sth); } catch (PDOException $ex) { $this->lastError = $ex->getMessage(); return false; } } /** * 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) { return $sql; } /** * 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 or false if an error occured */ public function get_fieldset_sql($sql, array $params=null) { if(!$rs = $this->get_recordset_sql($sql, $params)) { 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 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 mixed an array of objects, or empty array if no records were found, or false if an error occured. */ public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { if(!$rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum)) { return false; } $objects = array(); $debugging = debugging('', DEBUG_DEVELOPER); foreach($rs as $value) { $key = reset($value); if ($debugging && 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 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'])) { return false; } $returnid = false; } else { unset($params['id']); } if (empty($params)) { return false; } $this->writes++; $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 $data 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 true or new id */ public function insert_record($table, $dataobject, $returnid=true, $bulk=false) { if (!is_object($dataobject)) { $dataobject = (object)$dataobject; } $columns = $this->get_columns($table); unset($dataobject->id); $cleaned = array(); foreach ($dataobject as $field=>$value) { if (!isset($columns[$field])) { continue; } $column = $columns[$field]; if (is_bool($value)) { $value = (int)$value; // prevent "false" problems } if (!empty($column->enums)) { // workaround for problem with wrong enums if (is_null($value) and !$column->not_null) { // ok - nulls allowed } else { if (!in_array((string)$value, $column->enums)) { debugging('Enum value '.s($value).' not allowed in field '.$field.' table '.$table.'.'); return false; } } } $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 mixed $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) { if (!is_array($params)) { $params = (array)$params; } if (!isset($params['id'])) { return false; } $id = $params['id']; unset($params['id']); if (empty($params)) { return false; } $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=?"; $this->writes++; 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) { if (!is_object($dataobject)) { $dataobject = (object)$dataobject; } if (!isset($dataobject->id) ) { return false; } $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__ . '.'; print_error(unknowparamtype, 'error', '', $this->lastError); } } $sql = "UPDATE {{$table}} SET $newfield $select"; $this->writes++; return $this->execute($sql, $params); } public function sql_concat() { print_error('TODO'); } public function sql_concat_join($separator="' '", $elements=array()) { print_error('TODO'); } public function begin_sql() { try { $this->pdb->beginTransaction(); return true; } catch(PDOException $ex) { return false; } } public function commit_sql() { try { $this->pdb->commit(); return true; } catch(PDOException $ex) { return false; } } public function rollback_sql() { try { $this->pdb->rollBack(); return true; } catch(PDOException $ex) { return false; } } /** * 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); } }