MDL-20734 normalise_value() - moving from private to protected everywhere and abstracting
[moodle.git] / lib / dml / oci_native_moodle_database.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19 /**
20  * Native oci class representing moodle database interface.
21  *
22  * @package    moodlecore
23  * @subpackage DML
24  * @copyright  2008 Petr Skoda (http://skodak.org)
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 require_once($CFG->libdir.'/dml/moodle_database.php');
29 require_once($CFG->libdir.'/dml/oci_native_moodle_recordset.php');
30 require_once($CFG->libdir.'/dml/oci_native_moodle_temptables.php');
32 /**
33  * Native oci class representing moodle database interface.
34  *
35  * One complete reference for PHP + OCI:
36  * http://www.oracle.com/technology/tech/php/underground-php-oracle-manual.html
37  */
38 class oci_native_moodle_database extends moodle_database {
40     protected $oci     = null;
41     private $temptables; // Control existing temptables (oci_native_moodle_temptables object)
43     private $last_stmt_error = null; // To store stmt errors and enable get_last_error() to detect them
44     private $commit_status = null;   // default value initialised in connect method, we need the driver to be present
46     private $last_error_reporting; // To handle oci driver default verbosity
47     private $unique_session_id; // To store unique_session_id. Needed for temp tables unique naming
49     private $dblocks_supported = null; // To cache locks support along the connection life
52     /**
53      * Detects if all needed PHP stuff installed.
54      * Note: can be used before connect()
55      * @return mixed true if ok, string if something
56      */
57     public function driver_installed() {
58         if (!extension_loaded('oci8')) {
59             return get_string('ociextensionisnotpresentinphp', 'install');
60         }
61         return true;
62     }
64     /**
65      * Returns database family type - describes SQL dialect
66      * Note: can be used before connect()
67      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
68      */
69     public function get_dbfamily() {
70         return 'oracle';
71     }
73     /**
74      * Returns more specific database driver type
75      * Note: can be used before connect()
76      * @return string db type mysql, oci, postgres7
77      */
78     protected function get_dbtype() {
79         return 'oci';
80     }
82     /**
83      * Returns general database library name
84      * Note: can be used before connect()
85      * @return string db type pdo, native
86      */
87     protected function get_dblibrary() {
88         return 'native';
89     }
91     /**
92      * Returns localised database type name
93      * Note: can be used before connect()
94      * @return string
95      */
96     public function get_name() {
97         return get_string('nativeoci', 'install'); // TODO: localise
98     }
100     /**
101      * Returns sql generator used for db manipulation.
102      * Used mostly in upgrade.php scripts. oci overrides it
103      * in order to share the oci_native_moodle_temptables
104      * between the driver and the generator
105      *
106      * @return object database_manager instance
107      */
108     public function get_manager() {
109         global $CFG;
111         if (!$this->database_manager) {
112             require_once($CFG->libdir.'/ddllib.php');
114             $classname = $this->get_dbfamily().'_sql_generator';
115             require_once("$CFG->libdir/ddl/$classname.php");
116             $generator = new $classname($this, $this->temptables);
118             $this->database_manager = new database_manager($this, $generator);
119         }
120         return $this->database_manager;
121     }
124     /**
125      * Returns localised database configuration help.
126      * Note: can be used before connect()
127      * @return string
128      */
129     public function get_configuration_help() {
130         return get_string('nativeocihelp', 'install');
131     }
133     /**
134      * Returns localised database description
135      * Note: can be used before connect()
136      * @return string
137      */
138     public function get_configuration_hints() {
139         return get_string('databasesettingssub_oci', 'install'); // TODO: l
140     }
142     /**
143      * Connect to db
144      * Must be called before other methods.
145      * @param string $dbhost
146      * @param string $dbuser
147      * @param string $dbpass
148      * @param string $dbname
149      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
150      * @param array $dboptions driver specific options
151      * @return bool true
152      * @throws dml_connection_exception if error
153      */
154     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
155         if ($prefix == '' and !$this->external) {
156             //Enforce prefixes for everybody but mysql
157             throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
158         }
159         if (!$this->external and strlen($prefix) > 2) {
160             //Max prefix length for Oracle is 2cc
161             $a = (object)array('dbfamily'=>'oracle', 'maxlength'=>2);
162             throw new dml_exception('prefixtoolong', $a);
163         }
165         $driverstatus = $this->driver_installed();
167         if ($driverstatus !== true) {
168             throw new dml_exception('dbdriverproblem', $driverstatus);
169         }
171         // Autocommit ON by default.
172         // Switching to OFF (OCI_DEFAULT), when playing with transactions
173         // please note this thing is not defined if oracle driver not present in PHP
174         // which means it can not be used as default value of object property!
175         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
177         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
178         unset($this->dboptions['dbsocket']);
180         $pass = addcslashes($this->dbpass, "'\\");
182         if (empty($this->dbhost)) {
183             // old style full address
184         } else {
185             if (empty($this->dboptions['dbport'])) {
186                 $this->dboptions['dbport'] = 1521;
187             }
188             $this->dbname = '//'.$this->dbhost.':'.$this->dboptions['dbport'].'/'.$this->dbname;
189         }
191         ob_start();
192         if (empty($this->dboptions['dbpersit'])) {
193             $this->oci = oci_connect($this->dbuser, $this->dbpass, $this->dbname, 'AL32UTF8');
194         } else {
195             $this->oci = oci_pconnect($this->dbuser, $this->dbpass, $this->dbname, 'AL32UTF8');
196         }
197         $dberr = ob_get_contents();
198         ob_end_clean();
201         if ($this->oci === false) {
202             $this->oci = null;
203             $e = oci_error();
204             if (isset($e['message'])) {
205                 $dberr = $e['message'];
206             }
207             throw new dml_connection_exception($dberr);
208         }
210         // get unique session id, to be used later for temp tables stuff
211         $sql = 'SELECT DBMS_SESSION.UNIQUE_SESSION_ID() FROM DUAL';
212         $this->query_start($sql, null, SQL_QUERY_AUX);
213         $stmt = $this->parse_query($sql);
214         $result = oci_execute($stmt, $this->commit_status);
215         $this->query_end($result, $stmt);
216         $records = null;
217         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
218         oci_free_statement($stmt);
219         $this->unique_session_id = reset($records[0]);
221         //note: do not send "ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'" !
222         //      instead fix our PHP code to convert "," to "." properly!
224         // Connection stabilished and configured, going to instantiate the temptables controller
225         $this->temptables = new oci_native_moodle_temptables($this, $this->unique_session_id);
227         return true;
228     }
230     /**
231      * Close database connection and release all resources
232      * and memory (especially circular memory references).
233      * Do NOT use connect() again, create a new instance if needed.
234      */
235     public function dispose() {
236         parent::dispose(); // Call parent dispose to write/close session and other common stuff before clossing conn
237         if ($this->oci) {
238             oci_close($this->oci);
239             $this->oci = null;
240         }
241     }
244     /**
245      * Called before each db query.
246      * @param string $sql
247      * @param array array of parameters
248      * @param int $type type of query
249      * @param mixed $extrainfo driver specific extra information
250      * @return void
251      */
252     protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
253         parent::query_start($sql, $params, $type, $extrainfo);
254         // oci driver tents to send debug to output, we do not need that ;-)
255         $this->last_error_reporting = error_reporting(0);
256     }
258     /**
259      * Called immediately after each db query.
260      * @param mixed db specific result
261      * @return void
262      */
263     protected function query_end($result, $stmt=null) {
264         // reset original debug level
265         error_reporting($this->last_error_reporting);
266         if ($stmt and $result === false) {
267             // Look for stmt error and store it
268             if (is_resource($stmt)) {
269                 $e = oci_error($stmt);
270                 if ($e !== false) {
271                     $this->last_stmt_error = $e['message'];
272                 }
273             }
274             oci_free_statement($stmt);
275         }
276         parent::query_end($result);
277     }
279     /**
280      * Returns database server info array
281      * @return array
282      */
283     public function get_server_info() {
284         static $info = null; // TODO: move to real object property
286         if (is_null($info)) {
287             $this->query_start("--oci_server_version()", null, SQL_QUERY_AUX);
288             $description = oci_server_version($this->oci);
289             $this->query_end(true);
290             preg_match('/(\d+\.)+\d+/', $description, $matches);
291             $info = array('description'=>$description, 'version'=>$matches[0]);
292         }
294         return $info;
295     }
297     protected function is_min_version($version) {
298         $server = $this->get_server_info();
299         $server = $server['version'];
300         return version_compare($server, $version, '>=');
301     }
303     /**
304      * Converts short table name {tablename} to real table name
305      * supporting temp tables ($this->unique_session_id based) if detected
306      *
307      * @param string sql
308      * @return string sql
309      */
310     protected function fix_table_names($sql) {
311         if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
312             foreach($matches[0] as $key=>$match) {
313                 $name = $matches[1][$key];
314                 if ($this->temptables->is_temptable($name)) {
315                     $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
316                 } else {
317                     $sql = str_replace($match, $this->prefix.$name, $sql);
318                 }
319             }
320         }
321         return $sql;
322     }
324     /**
325      * Returns supported query parameter types
326      * @return bitmask
327      */
328     protected function allowed_param_types() {
329         return SQL_PARAMS_NAMED;
330     }
332     /**
333      * Returns last error reported by database engine.
334      */
335     public function get_last_error() {
336         $error = false;
337         // First look for any previously saved stmt error
338         if (!empty($this->last_stmt_error)) {
339             $error = $this->last_stmt_error;
340             $this->last_stmt_error = null;
341         } else { // Now try connection error
342             $e = oci_error($this->oci);
343             if ($e !== false) {
344                 $error = $e['message'];
345             }
346         }
347         return $error;
348     }
350     protected function parse_query($sql) {
351         $stmt = oci_parse($this->oci, $sql);
352         if ($stmt == false) {
353             throw new dml_connection_exception('Can not parse sql query'); //TODO: maybe add better info
354         }
355         return $stmt;
356     }
358     /**
359      * Return tables in database WITHOUT current prefix
360      * @return array of table names in lowercase and without prefix
361      */
362     public function get_tables($usecache=true) {
363         if ($usecache and $this->tables !== null) {
364             return $this->tables;
365         }
366         $this->tables = array();
367         $prefix = str_replace('_', "\\_", strtoupper($this->prefix));
368         $sql = "SELECT TABLE_NAME
369                   FROM CAT
370                  WHERE TABLE_TYPE='TABLE'
371                        AND TABLE_NAME NOT LIKE 'BIN\$%'
372                        AND TABLE_NAME LIKE '$prefix%' ESCAPE '\\'";
373         $this->query_start($sql, null, SQL_QUERY_AUX);
374         $stmt = $this->parse_query($sql);
375         $result = oci_execute($stmt, $this->commit_status);
376         $this->query_end($result, $stmt);
377         $records = null;
378         oci_fetch_all($stmt, $records, 0, -1, OCI_ASSOC);
379         oci_free_statement($stmt);
380         $records = array_map('strtolower', $records['TABLE_NAME']);
381         foreach ($records as $tablename) {
382             if (strpos($tablename, $this->prefix) !== 0) {
383                 continue;
384             }
385             $tablename = substr($tablename, strlen($this->prefix));
386             $this->tables[$tablename] = $tablename;
387         }
389         // Add the currently available temptables
390         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
392         return $this->tables;
393     }
395     /**
396      * Return table indexes - everything lowercased
397      * @return array of arrays
398      */
399     public function get_indexes($table) {
400         $indexes = array();
401         $tablename = strtoupper($this->prefix.$table);
403         $sql = "SELECT i.INDEX_NAME, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, ac.CONSTRAINT_TYPE
404                   FROM ALL_INDEXES i
405                   JOIN ALL_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME
406              LEFT JOIN ALL_CONSTRAINTS ac ON (ac.TABLE_NAME=i.TABLE_NAME AND ac.CONSTRAINT_NAME=i.INDEX_NAME AND ac.CONSTRAINT_TYPE='P')
407                  WHERE i.TABLE_NAME = '$tablename'
408               ORDER BY i.INDEX_NAME, c.COLUMN_POSITION";
410         $stmt = $this->parse_query($sql);
411         $result = oci_execute($stmt, $this->commit_status);
412         $this->query_end($result, $stmt);
413         $records = null;
414         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
415         oci_free_statement($stmt);
417         foreach ($records as $record) {
418             if ($record['CONSTRAINT_TYPE'] === 'P') {
419                 //ignore for now;
420                 continue;
421             }
422             $indexname = strtolower($record['INDEX_NAME']);
423             if (!isset($indexes[$indexname])) {
424                 $indexes[$indexname] = array('primary' => ($record['CONSTRAINT_TYPE'] === 'P'),
425                                              'unique'  => ($record['UNIQUENESS'] === 'UNIQUE'),
426                                              'columns' => array());
427             }
428             $indexes[$indexname]['columns'][] = strtolower($record['COLUMN_NAME']);
429         }
431         return $indexes;
432     }
434     /**
435      * Returns datailed information about columns in table. This information is cached internally.
436      * @param string $table name
437      * @param bool $usecache
438      * @return array array of database_column_info objects indexed with column names
439      */
440     public function get_columns($table, $usecache=true) {
441         if ($usecache and isset($this->columns[$table])) {
442             return $this->columns[$table];
443         }
445         if (!$table) { // table not specified, return empty array directly
446             return array();
447         }
449         $this->columns[$table] = array();
451         $sql = "SELECT CNAME, COLTYPE, WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
452                   FROM COL
453                  WHERE TNAME = UPPER('{" . $table . "}')
454               ORDER BY COLNO";
456         list($sql, $params, $type) = $this->fix_sql_params($sql, null);
458         $this->query_start($sql, null, SQL_QUERY_AUX);
459         $stmt = $this->parse_query($sql);
460         $result = oci_execute($stmt, $this->commit_status);
461         $this->query_end($result, $stmt);
462         $records = null;
463         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
464         oci_free_statement($stmt);
466         if (!$records) {
467             return array();
468         }
469         foreach ($records as $rawcolumn) {
470             $rawcolumn = (object)$rawcolumn;
472             $info = new object();
473             $info->name = strtolower($rawcolumn->CNAME);
474             $matches = null;
476             if ($rawcolumn->COLTYPE === 'VARCHAR2'
477              or $rawcolumn->COLTYPE === 'VARCHAR'
478              or $rawcolumn->COLTYPE === 'NVARCHAR2'
479              or $rawcolumn->COLTYPE === 'NVARCHAR'
480              or $rawcolumn->COLTYPE === 'CHAR'
481              or $rawcolumn->COLTYPE === 'NCHAR') {
482                 //TODO add some basic enum support here
483                 $info->type          = $rawcolumn->COLTYPE;
484                 $info->meta_type     = 'C';
485                 $info->max_length    = $rawcolumn->WIDTH;
486                 $info->scale         = null;
487                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
488                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
489                 if ($info->has_default) {
491                     // this is hacky :-(
492                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
493                         $info->default_value = null;
494                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
495                         $info->default_value = "";
496                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
497                         $info->default_value = "";
498                     } else {
499                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
500                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
501                     }
502                 } else {
503                     $info->default_value = null;
504                 }
505                 $info->primary_key   = false;
506                 $info->binary        = false;
507                 $info->unsigned      = null;
508                 $info->auto_increment= false;
509                 $info->unique        = null;
511             } else if ($rawcolumn->COLTYPE === 'NUMBER') {
512                 $info->type       = $rawcolumn->COLTYPE;
513                 $info->max_length = $rawcolumn->PRECISION;
514                 $info->binary     = false;
515                 if (!is_null($rawcolumn->SCALE) && $rawcolumn->SCALE == 0) { // null in oracle scale allows decimals => not integer
516                     // integer
517                     if ($info->name === 'id') {
518                         $info->primary_key   = true;
519                         $info->meta_type     = 'R';
520                         $info->unique        = true;
521                         $info->auto_increment= true;
522                         $info->has_default   = false;
523                     } else {
524                         $info->primary_key   = false;
525                         $info->meta_type     = 'I';
526                         $info->unique        = null;
527                         $info->auto_increment= false;
528                     }
529                     $info->scale = null;
531                 } else {
532                     //float
533                     $info->meta_type     = 'N';
534                     $info->primary_key   = false;
535                     $info->unsigned      = null;
536                     $info->auto_increment= false;
537                     $info->unique        = null;
538                     $info->scale         = $rawcolumn->SCALE;
539                 }
540                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
541                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
542                 if ($info->has_default) {
543                     $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
544                 } else {
545                     $info->default_value = null;
546                 }
548             } else if ($rawcolumn->COLTYPE === 'FLOAT') {
549                 $info->type       = $rawcolumn->COLTYPE;
550                 $info->max_length = (int)($rawcolumn->PRECISION * 3.32193);
551                 $info->primary_key   = false;
552                 $info->meta_type     = 'N';
553                 $info->unique        = null;
554                 $info->auto_increment= false;
555                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
556                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
557                 if ($info->has_default) {
558                     $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
559                 } else {
560                     $info->default_value = null;
561                 }
563             } else if ($rawcolumn->COLTYPE === 'CLOB'
564                     or $rawcolumn->COLTYPE === 'NCLOB') {
565                 $info->type          = $rawcolumn->COLTYPE;
566                 $info->meta_type     = 'X';
567                 $info->max_length    = -1;
568                 $info->scale         = null;
569                 $info->scale         = null;
570                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
571                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
572                 if ($info->has_default) {
573                     // this is hacky :-(
574                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
575                         $info->default_value = null;
576                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
577                         $info->default_value = "";
578                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Other times it's stored without trailing space
579                         $info->default_value = "";
580                     } else {
581                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
582                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
583                     }
584                 } else {
585                     $info->default_value = null;
586                 }
587                 $info->primary_key   = false;
588                 $info->binary        = false;
589                 $info->unsigned      = null;
590                 $info->auto_increment= false;
591                 $info->unique        = null;
593             } else if ($rawcolumn->COLTYPE === 'BLOB') {
594                 $info->type          = $rawcolumn->COLTYPE;
595                 $info->meta_type     = 'B';
596                 $info->max_length    = -1;
597                 $info->scale         = null;
598                 $info->scale         = null;
599                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
600                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
601                 if ($info->has_default) {
602                     // this is hacky :-(
603                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
604                         $info->default_value = null;
605                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
606                         $info->default_value = "";
607                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
608                         $info->default_value = "";
609                     } else {
610                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
611                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
612                     }
613                 } else {
614                     $info->default_value = null;
615                 }
616                 $info->primary_key   = false;
617                 $info->binary        = true;
618                 $info->unsigned      = null;
619                 $info->auto_increment= false;
620                 $info->unique        = null;
622             } else {
623                 // unknown type - sorry
624                 $info->type          = $rawcolumn->COLTYPE;
625                 $info->meta_type     = '?';
626             }
628             $this->columns[$table][$info->name] = new database_column_info($info);
629         }
631         return $this->columns[$table];
632     }
634     /**
635      * Normalise values based in RDBMS dependencies (booleans, LOBs...)
636      *
637      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
638      * @param mixed $value value we are going to normalise
639      * @return mixed the normalised value
640      */
641     protected function normalise_value($column, $value) {
642         if (is_bool($value)) { // Always, convert boolean to int
643             $value = (int)$value;
645         } else if ($column->meta_type == 'B') { // CLOB detected, we return 'blob' array instead of raw value to allow
646             if (!is_null($value)) {             // binding/executing code later to know about its nature
647                 $value = array('blob' => $value);
648             }
650         } else if ($column->meta_type == 'X' && strlen($value) > 4000) { // CLOB detected (>4000 optimisation), we return 'clob'
651             if (!is_null($value)) {                                      // array instead of raw value to allow binding/
652                 $value = array('clob' => (string)$value);                // executing code later to know about its nature
653             }
655         } else if ($value === '') {
656             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
657                 $value = 0; // prevent '' problems in numeric fields
658             }
659         }
660         return $value;
661     }
663     /**
664      * Transforms the sql and params in order to emulate the LIMIT clause available in other DBs
665      *
666      * @param string $sql the SQL select query to execute.
667      * @param array $params array of sql parameters
668      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
669      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
670      * @return array with the transformed sql and params updated
671      */
672     private function get_limit_sql($sql, array $params = null, $limitfrom=0, $limitnum=0) {
674         $limitfrom = (int)$limitfrom;
675         $limitnum  = (int)$limitnum;
676         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
677         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
679         // TODO: Add the /*+ FIRST_ROWS */ hint if there isn't another hint
681         if ($limitfrom and $limitnum) {
682             $sql = "SELECT oracle_o.*
683                       FROM (SELECT oracle_i.*, rownum AS oracle_rownum
684                               FROM ($sql) oracle_i
685                              WHERE rownum <= :oracle_num_rows
686                             ) oracle_o
687                      WHERE oracle_rownum > :oracle_skip_rows";
688             $params['oracle_num_rows'] = $limitfrom + $limitnum;
689             $params['oracle_skip_rows'] = $limitfrom;
691         } else if ($limitfrom and !$limitnum) {
692             $sql = "SELECT oracle_o.*
693                       FROM (SELECT oracle_i.*, rownum AS oracle_rownum
694                               FROM ($sql) oracle_i
695                             ) oracle_o
696                      WHERE oracle_rownum > :oracle_skip_rows";
697             $params['oracle_skip_rows'] = $limitfrom;
699         } else if (!$limitfrom and $limitnum) {
700             $sql = "SELECT *
701                       FROM ($sql)
702                      WHERE rownum <= :oracle_num_rows";
703             $params['oracle_num_rows'] = $limitnum;
704         }
706         return array($sql, $params);
707     }
709     /**
710      * This function will handle all the column values before being inserted/updated to DB for Oracle
711      * installations. This is because the "special feature" of Oracle where the empty string is
712      * equal to NULL and this presents a problem with all our currently NOT NULL default '' fields.
713      * (and with empties handling in general)
714      *
715      * Note that this function is 100% private and should be used, exclusively by DML functions
716      * in this file. Also, this is considered a DIRTY HACK to be removed when possible.
717      *
718      * This function is private and must not be used outside this driver at all
719      *
720      * @param $table string the table where the record is going to be inserted/updated (without prefix)
721      * @param $field string the field where the record is going to be inserted/updated
722      * @param $value mixed the value to be inserted/updated
723      */
724     private function oracle_dirty_hack ($table, $field, $value) {
726         // Get metadata
727         $columns = $this->get_columns($table);
728         if (!isset($columns[$field])) {
729             return $value;
730         }
731         $column = $columns[$field];
733         // !! This paragraph explains behaviour before Moodle 2.0:
734         //
735         // For Oracle DB, empty strings are converted to NULLs in DB
736         // and this breaks a lot of NOT NULL columns currenty Moodle. In the future it's
737         // planned to move some of them to NULL, if they must accept empty values and this
738         // piece of code will become less and less used. But, for now, we need it.
739         // What we are going to do is to examine all the data being inserted and if it's
740         // an empty string (NULL for Oracle) and the field is defined as NOT NULL, we'll modify
741         // such data in the best form possible ("0" for booleans and numbers and " " for the
742         // rest of strings. It isn't optimal, but the only way to do so.
743         // In the oppsite, when retrieving records from Oracle, we'll decode " " back to
744         // empty strings to allow everything to work properly. DIRTY HACK.
746         // !! These paragraphs explain the rationale about the change for Moodle 2.0:
747         //
748         // Before Moodle 2.0, we only used to apply this DIRTY HACK to NOT NULL columns, as
749         // stated above, but it causes one problem in NULL columns where both empty strings
750         // and real NULLs are stored as NULLs, being impossible to diferentiate them when
751         // being retrieved from DB.
752         //
753         // So, starting with Moodle 2.0, we are going to apply the DIRTY HACK to all the
754         // CHAR/CLOB columns no matter of their nullability. That way, when retrieving
755         // NULLABLE fields we'll get proper empties and NULLs diferentiated, so we'll be able
756         // to rely in NULL/empty/content contents without problems, until now that wasn't
757         // possible at all.
758         //
759         // No breackage with old data is expected as long as at the time of writing this
760         // (20090922) all the current uses of both sql_empty() and sql_isempty() has been
761         // revised in 2.0 and all them were being performed against NOT NULL columns,
762         // where nothing has changed (the DIRTY HACK was already being applied).
763         //
764         // !! Conclusions:
765         //
766         // From Moodle 2.0 onwards, ALL empty strings in Oracle DBs will be stored as
767         // 1-whitespace char, ALL NULLs as NULLs and, obviously, content as content. And
768         // those 1-whitespace chars will be converted back to empty strings by all the
769         // get_field/record/set() functions transparently and any SQL needing direct handling
770         // of empties will need to use the sql_empty() and sql_isempty() helper functions.
771         // MDL-17491.
773         // If the field ins't VARCHAR or CLOB, skip
774         if ($column->meta_type != 'C' and $column->meta_type != 'X') {
775             return $value;
776         }
778         // If the value isn't empty, skip
779         if (!empty($value)) {
780             return $value;
781         }
783         // Now, we have one empty value, going to be inserted to one VARCHAR2 or CLOB field
784         // Try to get the best value to be inserted
786         // The '0' string doesn't need any transformation, skip
787         if ($value === '0') {
788             return $value;
789         }
791         // Transformations start
792         if (gettype($value) == 'boolean') {
793             return '0'; // Transform false to '0' that evaluates the same for PHP
795         } else if (gettype($value) == 'integer') {
796             return '0'; // Transform 0 to '0' that evaluates the same for PHP
798         } else if ($value === '') {
799             return ' '; // Transform '' to ' ' that DONT'T EVALUATE THE SAME
800                         // (we'll transform back again on get_records_XXX functions and others)!!
801         }
803         // Fail safe to original value
804         return $value;
805     }
807     /**
808      * Is db in unicode mode?
809      * @return bool
810      */
811     public function setup_is_unicodedb() {
812         $sql = "SELECT VALUE
813                   FROM NLS_DATABASE_PARAMETERS
814                  WHERE PARAMETER = 'NLS_CHARACTERSET'";
815         $this->query_start($sql, null, SQL_QUERY_AUX);
816         $stmt = $this->parse_query($sql);
817         $result = oci_execute($stmt, $this->commit_status);
818         $this->query_end($result, $stmt);
819         $records = null;
820         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
821         oci_free_statement($stmt);
823         return (isset($records['VALUE'][0]) and $records['VALUE'][0] === 'AL32UTF8');
824     }
826     /**
827      * Do NOT use in code, to be used by database_manager only!
828      * @param string $sql query
829      * @return bool true
830      * @throws dml_exception if error
831      */
832     public function change_database_structure($sql) {
833         $this->reset_caches();
835         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
836         $stmt = $this->parse_query($sql);
837         $result = oci_execute($stmt, $this->commit_status);
838         $this->query_end($result, $stmt);
839         oci_free_statement($stmt);
841         return true;
842     }
844     protected function bind_params($stmt, array $params=null, $tablename=null) {
845         $descriptors = array();
846         if ($params) {
847             $columns = array();
848             if ($tablename) {
849                 $columns = $this->get_columns($tablename);
850             }
851             foreach($params as $key => $value) {
852                 // First of all, handle already detected LOBs
853                 if (is_array($value)) { // Let's go to bind special cases (lob descriptors)
854                     if (isset($value['clob'])) {
855                         $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
856                         oci_bind_by_name($stmt, $key, $lob, -1, SQLT_CLOB);
857                         $lob->writeTemporary($this->oracle_dirty_hack($tablename, $key, $params[$key]['clob']), OCI_TEMP_CLOB);
858                         $descriptors[] = $lob;
859                         continue; // Column binding finished, go to next one
860                     } else if (isset($value['blob'])) {
861                         $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
862                         oci_bind_by_name($stmt, $key, $lob, -1, SQLT_BLOB);
863                         $lob->writeTemporary($params[$key]['blob'], OCI_TEMP_BLOB);
864                         $descriptors[] = $lob;
865                         continue; // Column binding finished, go to next one
866                     }
867                 }
868                 // TODO: Put propper types and length is possible (enormous speedup)
869                 // Arrived here, continue with standard processing, using metadata if possible
870                 if (isset($columns[$key])) {
871                     $type = $columns[$key]->meta_type;
872                     $maxlength = $columns[$key]->max_length;
873                 } else {
874                     $type = '?';
875                     $maxlength = -1;
876                 }
877                 switch ($type) {
878                     case 'I':
879                     case 'R':
880                         // TODO: Optimise
881                         oci_bind_by_name($stmt, $key, $params[$key]);
882                         break;
884                     case 'N':
885                     case 'F':
886                         // TODO: Optimise
887                         oci_bind_by_name($stmt, $key, $params[$key]);
888                         break;
890                     case 'B':
891                         // TODO: Only arrive here if BLOB is null: Bind if so, else exception!
892                         // don't break here
894                     case 'X':
895                         // TODO: Only arrive here if CLOB is null or <= 4000 cc, else exception
896                         // don't break here
898                     default: // Bind as CHAR (applying dirty hack)
899                         // TODO: Optimise
900                         oci_bind_by_name($stmt, $key, $this->oracle_dirty_hack($tablename, $key, $params[$key]));
901                 }
902             }
903         }
904         return $descriptors;
905     }
907     protected function free_descriptors($descriptors) {
908         foreach ($descriptors as $descriptor) {
909             oci_free_descriptor($descriptor);
910         }
911     }
913     /**
914      * This function is used to convert all the Oracle 1-space defaults to the empty string
915      * like a really DIRTY HACK to allow it to work better until all those NOT NULL DEFAULT ''
916      * fields will be out from Moodle.
917      * @param string the string to be converted to '' (empty string) if it's ' ' (one space)
918      * @param mixed the key of the array in case we are using this function from array_walk,
919      *              defaults to null for other (direct) uses
920      * @return boolean always true (the converted variable is returned by reference)
921      */
922     public static function onespace2empty(&$item, $key=null) {
923         $item = ($item === ' ') ? '' : $item;
924         return true;
925     }
927     /**
928      * Execute general sql query. Should be used only when no other method suitable.
929      * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead!
930      * @param string $sql query
931      * @param array $params query parameters
932      * @return bool true
933      * @throws dml_exception if error
934      */
935     public function execute($sql, array $params=null) {
936         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
938         if (strpos($sql, ';') !== false) {
939             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
940         }
942         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
943         $stmt = $this->parse_query($sql);
944         $this->bind_params($stmt, $params);
945         $result = oci_execute($stmt, $this->commit_status);
946         $this->query_end($result, $stmt);
947         oci_free_statement($stmt);
949         return true;
950     }
952     /**
953      * Get a single database record as an object using a SQL statement.
954      *
955      * The SQL statement should normally only return one record.
956      * It is recommended to use get_records_sql() if more matches possible!
957      *
958      * @param string $sql The SQL string you wish to be executed, should normally only return one record.
959      * @param array $params array of sql parameters
960      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
961      *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
962      *                        MUST_EXIST means throw exception if no record or multiple records found
963      * @return mixed a fieldset object containing the first matching record, false or exception if error not found depending on mode
964      * @throws dml_exception if error
965      */
966     public function get_record_sql($sql, array $params=null, $strictness=IGNORE_MISSING) {
967         $strictness = (int)$strictness;
968         if ($strictness == IGNORE_MULTIPLE) {
969             // do not limit here - ORA does not like that
970             if (!$rs = $this->get_recordset_sql($sql, $params)) {
971                 return false;
972             }
973             foreach ($rs as $result) {
974                 $rs->close();
975                 return $result;
976             }
977             $rs->close();
978             return false;
979         }
980         return parent::get_record_sql($sql, $params, $strictness);
981     }
983     /**
984      * Get a number of records as a moodle_recordset using a SQL statement.
985      *
986      * Since this method is a little less readable, use of it should be restricted to
987      * code where it's possible there might be large datasets being returned.  For known
988      * small datasets use get_records_sql - it leads to simpler code.
989      *
990      * The return type is as for @see function get_recordset.
991      *
992      * @param string $sql the SQL select query to execute.
993      * @param array $params array of sql parameters
994      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
995      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
996      * @return mixed an moodle_recordset object
997      * @throws dml_exception if error
998      */
999     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1001         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1003         list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1005         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1006         $stmt = $this->parse_query($rawsql);
1007         $this->bind_params($stmt, $params);
1008         $result = oci_execute($stmt, $this->commit_status);
1009         $this->query_end($result, $stmt);
1011         return $this->create_recordset($stmt);
1012     }
1014     protected function create_recordset($stmt) {
1015         return new oci_native_moodle_recordset($stmt);
1016     }
1018     /**
1019      * Get a number of records as an array of objects using a SQL statement.
1020      *
1021      * Return value as for @see function get_records.
1022      *
1023      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
1024      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
1025      *   returned array.
1026      * @param array $params array of sql parameters
1027      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1028      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1029      * @return mixed an array of objects, or empty array if no records were found
1030      * @throws dml_exception if error
1031      */
1032     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1034         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1036         list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1038         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1039         $stmt = $this->parse_query($rawsql);
1040         $this->bind_params($stmt, $params);
1041         $result = oci_execute($stmt, $this->commit_status);
1042         $this->query_end($result, $stmt);
1044         $records = null;
1045         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1046         oci_free_statement($stmt);
1048         $return = array();
1050         foreach ($records as $row) {
1051             $row = array_change_key_case($row, CASE_LOWER);
1052             unset($row['oracle_rownum']);
1053             array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
1054             $id = reset($row);
1055             if (isset($return[$id])) {
1056                 $colname = key($row);
1057                 debugging("Did you remember to make the first column something unique in your call to get_records? Duplicate value '$id' found in column '$colname'.", DEBUG_DEVELOPER);
1058             }
1059             $return[$id] = (object)$row;
1060         }
1062         return $return;
1063     }
1065     /**
1066      * Selects records and return values (first field) as an array using a SQL statement.
1067      *
1068      * @param string $sql The SQL query
1069      * @param array $params array of sql parameters
1070      * @return array of values
1071      * @throws dml_exception if error
1072      */
1073     public function get_fieldset_sql($sql, array $params=null) {
1074         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1076         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1077         $stmt = $this->parse_query($sql);
1078         $this->bind_params($stmt, $params);
1079         $result = oci_execute($stmt, $this->commit_status);
1080         $this->query_end($result, $stmt);
1082         $records = null;
1083         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
1084         oci_free_statement($stmt);
1086         $return = reset($records);
1087         array_walk($return, array('oci_native_moodle_database', 'onespace2empty'));
1089         return $return;
1090     }
1092     /**
1093      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1094      * @param string $table name
1095      * @param mixed $params data record as object or array
1096      * @param bool $returnit return it of inserted record
1097      * @param bool $bulk true means repeated inserts expected
1098      * @param bool $customsequence true if 'id' included in $params, disables $returnid
1099      * @return true or new id
1100      * @throws dml_exception if error
1101      */
1102     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1103         if (!is_array($params)) {
1104             $params = (array)$params;
1105         }
1107         $returning = "";
1109         if ($customsequence) {
1110             if (!isset($params['id'])) {
1111                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1112             }
1113             $returnid = false;
1114         } else {
1115             unset($params['id']);
1116             if ($returnid) {
1117                 $returning = " RETURNING id INTO :oracle_id"; // crazy name nobody is ever going to use or parameter ;-)
1118             }
1119         }
1121         if (empty($params)) {
1122             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1123         }
1125         $fields = implode(',', array_keys($params));
1126         $values = array();
1127         foreach ($params as $pname => $value) {
1128             $values[] = ":$pname";
1129         }
1130         $values = implode(',', $values);
1132         $sql = "INSERT INTO {" . $table . "} ($fields) VALUES ($values)";
1133         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1134         $sql .= $returning;
1136         $id = null;
1138         $this->query_start($sql, $params, SQL_QUERY_INSERT);
1139         $stmt = $this->parse_query($sql);
1140         $descriptors = $this->bind_params($stmt, $params, $table);
1141         if ($returning) {
1142             oci_bind_by_name($stmt, ":oracle_id", $id, -1, SQLT_INT);
1143         }
1144         $result = oci_execute($stmt, $this->commit_status);
1145         $this->free_descriptors($descriptors);
1146         $this->query_end($result, $stmt);
1147         oci_free_statement($stmt);
1149         if (!$returnid) {
1150             return true;
1151         }
1153         if (!$returning) {
1154             die('TODO - implement oracle 9.2 insert support'); //TODO
1155         }
1157         return (int)$id;
1158     }
1160     /**
1161      * Insert a record into a table and return the "id" field if required.
1162      *
1163      * Some conversions and safety checks are carried out. Lobs are supported.
1164      * If the return ID isn't required, then this just reports success as true/false.
1165      * $data is an object containing needed data
1166      * @param string $table The database table to be inserted into
1167      * @param object $data A data object with values for one or more fields in the record
1168      * @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.
1169      * @return true or new id
1170      * @throws dml_exception if error
1171      */
1172     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1173         if (!is_object($dataobject)) {
1174             $dataobject = (object)$dataobject;
1175         }
1177         unset($dataobject->id);
1179         $columns = $this->get_columns($table);
1180         $cleaned = array();
1182         foreach ($dataobject as $field=>$value) {
1183             if (!isset($columns[$field])) { // Non-existing table field, skip it
1184                 continue;
1185             }
1186             $column = $columns[$field];
1187             $cleaned[$field] = $this->normalise_value($column, $value);
1188         }
1190         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1191     }
1193     /**
1194      * Import a record into a table, id field is required.
1195      * Safety checks are NOT carried out. Lobs are supported.
1196      *
1197      * @param string $table name of database table to be inserted into
1198      * @param object $dataobject A data object with values for one or more fields in the record
1199      * @return bool true
1200      * @throws dml_exception if error
1201      */
1202     public function import_record($table, $dataobject) {
1203         if (!is_object($dataobject)) {
1204             $dataobject = (object)$dataobject;
1205         }
1207         $columns = $this->get_columns($table);
1208         $cleaned = array();
1210         foreach ($dataobject as $field=>$value) {
1211             if (!isset($columns[$field])) {
1212                 continue;
1213             }
1214             $column = $columns[$field];
1215             $cleaned[$field] = $this->normalise_value($column, $value);
1216         }
1218         return $this->insert_record_raw($table, $cleaned, false, true, true);
1219     }
1221     /**
1222      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1223      * @param string $table name
1224      * @param mixed $params data record as object or array
1225      * @param bool true means repeated updates expected
1226      * @return bool true
1227      * @throws dml_exception if error
1228      */
1229     public function update_record_raw($table, $params, $bulk=false) {
1230         if (!is_array($params)) {
1231             $params = (array)$params;
1232         }
1233         if (!isset($params['id'])) {
1234             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1235         }
1237         if (empty($params)) {
1238             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1239         }
1241         $sets = array();
1242         foreach ($params as $field=>$value) {
1243             if ($field == 'id') {
1244                 continue;
1245             }
1246             $sets[] = "$field = :$field";
1247         }
1249         $sets = implode(',', $sets);
1250         $sql = "UPDATE {" . $table . "} SET $sets WHERE id=:id";
1251         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1253         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1254         $stmt = $this->parse_query($sql);
1255         $descriptors = $this->bind_params($stmt, $params, $table);
1256         $result = oci_execute($stmt, $this->commit_status);
1257         $this->free_descriptors($descriptors);
1258         $this->query_end($result, $stmt);
1259         oci_free_statement($stmt);
1261         return true;
1262     }
1264     /**
1265      * Update a record in a table
1266      *
1267      * $dataobject is an object containing needed data
1268      * Relies on $dataobject having a variable "id" to
1269      * specify the record to update
1270      *
1271      * @param string $table The database table to be checked against.
1272      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1273      * @param bool true means repeated updates expected
1274      * @return bool true
1275      * @throws dml_exception if error
1276      */
1277     public function update_record($table, $dataobject, $bulk=false) {
1278         if (!is_object($dataobject)) {
1279             $dataobject = (object)$dataobject;
1280         }
1282         $columns = $this->get_columns($table);
1283         $cleaned = array();
1285         foreach ($dataobject as $field=>$value) {
1286             if (!isset($columns[$field])) {
1287                 continue;
1288             }
1289             $column = $columns[$field];
1290             $cleaned[$field] = $this->normalise_value($column, $value);
1291         }
1293         $this->update_record_raw($table, $cleaned, $bulk);
1295         return true;
1296     }
1298     /**
1299      * Set a single field in every table record which match a particular WHERE clause.
1300      *
1301      * @param string $table The database table to be checked against.
1302      * @param string $newfield the field to set.
1303      * @param string $newvalue the value to set the field to.
1304      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1305      * @param array $params array of sql parameters
1306      * @return bool true
1307      * @throws dml_exception if error
1308      */
1309     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1311         if ($select) {
1312             $select = "WHERE $select";
1313         }
1314         if (is_null($params)) {
1315             $params = array();
1316         }
1318         // Get column metadata
1319         $columns = $this->get_columns($table);
1320         $column = $columns[$newfield];
1322         $newvalue = $this->normalise_value($column, $newvalue);
1324         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1326         if (is_bool($newvalue)) {
1327             $newvalue = (int)$newvalue; // prevent "false" problems
1328         }
1329         if (is_null($newvalue)) {
1330             $newsql = "$newfield = NULL";
1331         } else {
1332             $params[$newfield] = $newvalue;
1333             $newsql = "$newfield = :$newfield";
1334         }
1335         $sql = "UPDATE {" . $table . "} SET $newsql $select";
1336         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1338         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1339         $stmt = $this->parse_query($sql);
1340         $descriptors = $this->bind_params($stmt, $params, $table);
1341         $result = oci_execute($stmt, $this->commit_status);
1342         $this->free_descriptors($descriptors);
1343         $this->query_end($result, $stmt);
1344         oci_free_statement($stmt);
1346         return true;
1347     }
1349     /**
1350      * Delete one or more records from a table which match a particular WHERE clause.
1351      *
1352      * @param string $table The database table to be checked against.
1353      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1354      * @param array $params array of sql parameters
1355      * @return bool true
1356      * @throws dml_exception if error
1357      */
1358     public function delete_records_select($table, $select, array $params=null) {
1360         if ($select) {
1361             $select = "WHERE $select";
1362         }
1364         $sql = "DELETE FROM {" . $table . "} $select";
1366         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1368         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1369         $stmt = $this->parse_query($sql);
1370         $this->bind_params($stmt, $params);
1371         $result = oci_execute($stmt, $this->commit_status);
1372         $this->query_end($result, $stmt);
1373         oci_free_statement($stmt);
1375         return true;
1376     }
1378     function sql_null_from_clause() {
1379         return ' FROM dual';
1380     }
1382     public function sql_bitand($int1, $int2) {
1383         return 'bitand((' . $int1 . '), (' . $int2 . '))';
1384     }
1386     public function sql_bitnot($int1) {
1387         return '((0 - (' . $int1 . ')) - 1)';
1388     }
1390     public function sql_bitor($int1, $int2) {
1391         return '((' . $int1 . ') + (' . $int2 . ') - ' . $this->sql_bitand($int1, $int2) . ')';
1392     }
1394     public function sql_bitxor($int1, $int2) {
1395         return '(' . $this->sql_bitor($int1, $int2) . ' - ' . $this->sql_bitand($int1, $int2) . ')';
1396     }
1398     /**
1399      * Returns the SQL text to be used in order to perform module '%'
1400      * opration - remainder after division
1401      *
1402      * @param integer int1 first integer in the operation
1403      * @param integer int2 second integer in the operation
1404      * @return string the piece of SQL code to be used in your statement.
1405      */
1406     public function sql_modulo($int1, $int2) {
1407         return 'MOD(' . $int1 . ', ' . $int2 . ')';
1408     }
1410     public function sql_cast_char2int($fieldname, $text=false) {
1411         if (!$text) {
1412             return ' CAST(' . $fieldname . ' AS INT) ';
1413         } else {
1414             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1415         }
1416     }
1418     // TODO: Change this function and uses to support 2 parameters: fieldname and value
1419     // that way we can use REGEXP_LIKE(x, y, 'i') to provide case-insensitive like searches
1420     // to lower() comparison or whatever
1421     public function sql_ilike() {
1422         // TODO: add some ilike workaround
1423         return 'LIKE';
1424     }
1426     // NOTE: Oracle concat implementation isn't ANSI compliant when using NULLs (the result of
1427     // any concatenation with NULL must return NULL) because of his inability to diferentiate
1428     // NULLs and empty strings. So this function will cause some tests to fail. Hopefully
1429     // it's only a side case and it won't affect normal concatenation operations in Moodle.
1430     public function sql_concat() {
1431         $arr = func_get_args();
1432         $s = implode(' || ', $arr);
1433         if ($s === '') {
1434             return " '' ";
1435         }
1436         return " $s ";
1437     }
1439     public function sql_concat_join($separator="' '", $elements=array()) {
1440         for ($n=count($elements)-1; $n > 0 ; $n--) {
1441             array_splice($elements, $n, 0, $separator);
1442         }
1443         $s = implode(' || ', $elements);
1444         if ($s === '') {
1445             return " '' ";
1446         }
1447         return " $s ";
1448     }
1450     /**
1451      * Returns the SQL for returning searching one string for the location of another.
1452      */
1453     public function sql_position($needle, $haystack) {
1454         return "INSTR(($haystack), ($needle))";
1455     }
1457     public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1458         if ($textfield) {
1459             return " ".$this->sql_compare_text($fieldname)." = '".$this->sql_empty()."' ";
1460         } else {
1461             return " $fieldname = '".$this->sql_empty()."' ";
1462         }
1463     }
1465     public function sql_empty() {
1466         return ' ';
1467     }
1469     public function sql_order_by_text($fieldname, $numchars=32) {
1470         return 'dbms_lob.substr(' . $fieldname . ', ' . $numchars . ',1)';
1471     }
1473 /// session locking
1474     public function session_lock_supported() {
1475         if (isset($this->dblocks_supported)) { // Use cached value if available
1476             return $this->dblocks_supported;
1477         }
1478         $sql = "SELECT 1
1479                 FROM user_objects
1480                 WHERE object_type = 'PACKAGE BODY'
1481                   AND object_name = 'MOODLE_LOCKS'
1482                   AND status = 'VALID'";
1483         $this->query_start($sql, null, SQL_QUERY_AUX);
1484         $stmt = $this->parse_query($sql);
1485         $result = oci_execute($stmt, $this->commit_status);
1486         $this->query_end($result, $stmt);
1487         $records = null;
1488         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1489         oci_free_statement($stmt);
1490         $this->dblocks_supported = isset($records[0]) && reset($records[0]) ? true : false;
1491         return $this->dblocks_supported;;
1492     }
1494     public function get_session_lock($rowid) {
1495         if (!$this->session_lock_supported()) {
1496             return;
1497         }
1498         parent::get_session_lock($rowid);
1500         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1501         $sql = 'SELECT MOODLE_LOCKS.GET_LOCK(:lockname, :locktimeout) FROM DUAL';
1502         $params = array('lockname' => $fullname , 'locktimeout' => 120);
1503         $this->query_start($sql, $params, SQL_QUERY_AUX);
1504         $stmt = $this->parse_query($sql);
1505         $this->bind_params($stmt, $params);
1506         $result = oci_execute($stmt, $this->commit_status);
1507         $this->query_end($result, $stmt);
1508         oci_free_statement($stmt);
1509     }
1511     public function release_session_lock($rowid) {
1512         if (!$this->session_lock_supported()) {
1513             return;
1514         }
1515         parent::release_session_lock($rowid);
1517         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1518         $params = array('lockname' => $fullname);
1519         $sql = 'SELECT MOODLE_LOCKS.RELEASE_LOCK(:lockname) FROM DUAL';
1520         $this->query_start($sql, $params, SQL_QUERY_AUX);
1521         $stmt = $this->parse_query($sql);
1522         $this->bind_params($stmt, $params);
1523         $result = oci_execute($stmt, $this->commit_status);
1524         $this->query_end($result, $stmt);
1525         oci_free_statement($stmt);
1526     }
1528 /// transactions
1529     /**
1530      * on DBs that support it, switch to transaction mode and begin a transaction
1531      * you'll need to ensure you call commit_sql() or your changes *will* be lost.
1532      *
1533      * this is _very_ useful for massive updates
1534      */
1535     public function begin_sql() {
1536         if (!parent::begin_sql()) {
1537             return false;
1538         }
1539         $this->commit_status = OCI_DEFAULT; //Done! ;-)
1540         return true;
1541     }
1543     /**
1544      * on DBs that support it, commit the transaction
1545      */
1546     public function commit_sql() {
1547         if (!parent::commit_sql()) {
1548             return false;
1549         }
1551         $this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX);
1552         $result = oci_commit($this->oci);
1553         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1554         $this->query_end($result);
1555         return true;
1556     }
1558     /**
1559      * on DBs that support it, rollback the transaction
1560      */
1561     public function rollback_sql() {
1562         if (!parent::rollback_sql()) {
1563             return false;
1564         }
1566         $this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX);
1567         $result = oci_rollback($this->oci);
1568         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1569         $this->query_end($result);
1570         return true;
1571     }