[moodle] / moodle / lib / dml / pdo_moodle_database.php Repository:

View of /moodle/lib/dml/pdo_moodle_database.php

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.21 - (download) (annotate)
Fri Oct 9 06:34:35 2009 WST (6 weeks, 4 days ago) by skodak
Branch: MAIN
Changes since 1.20: +1 -1 lines
MDL-19474 One year and a half ago we started designing and implementing our new databse abstraction layer which included brand new moodle native drivers. Today we are finally removing old legacy drivers that were still using adodb internally. From now on the adodb will be used only in some authentication and enrolment plugins, we are probably going to drop adodb there too later in 2.x. Thanks everybody who helped test and improve our new drivers, even bigger thanks to all the great developers who created ADOdb!
<?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    moodlecore
 * @subpackage DML
 * @copyright  2008 Andrei Bautu
 * @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/pdo_moodle_recordset.php');

/**
 * Experimental pdo database class
 */
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) {
        $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 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 '<hr /> (', $this->get_dbtype(), '): ',  htmlentities($sql);
        if($params) {
            echo ' (parameters ';
            print_r($params);
            echo ')';
        }
        echo '<hr />';
    }

    /**
     * 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) {
        $result = true;
        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);

        try {
            $this->pdb->exec($sql);
            $this->reset_caches();
        } catch (PDOException $ex) {
            $this->lastError = $ex->getMessage();
            $result = false;
        }
        $this->query_end($result);
        return $result;
    }

    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 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) {
        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 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) {

        $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 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'])) {
                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 $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'])) {
            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) {
        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";
        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() {
        if (!parent::begin_sql()) {
            return false;
        }

        $this->query_start('', NULL, SQL_QUERY_AUX);
        $result = true;

        try {
            $this->pdb->beginTransaction();
        } catch(PDOException $ex) {
            $this->lastError = $ex->getMessage();
            $result = false;
        }
        $this->query_end($result);
        return $result;
    }
    public function commit_sql() {
        if (!parent::commit_sql()) {
            return false;
        }

        $this->query_start('', NULL, SQL_QUERY_AUX);
        $result = true;

        try {
            $this->pdb->commit();
        } catch(PDOException $ex) {
            $this->lastError = $ex->getMessage();
            $result = false;
        }
        $this->query_end($result);
        return $result;
    }

    public function rollback_sql() {
        if (!parent::rollback_sql()) {
            return false;
        }

        $this->query_start('', NULL, SQL_QUERY_AUX);
        $result = true;

        try {
            $this->pdb->rollBack();
        } catch(PDOException $ex) {
            $this->lastError = $ex->getMessage();
            $result = false;
        }
        $this->query_end($result);
        return $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.
     *
     * Overriden to ensure $this->lastErorr is reset each query
     *
     * @param string $sql
     * @param array 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=null, $type, $extrainfo=null) {
        $this->lastError = null;
        parent::query_start($sql, $params, $type, $extrainfo);
    }
}

Moodle CVS Admin
ViewVC Help
Powered by ViewVC 1.0.7