7da5810123556f4dfe7054889baf1bfa6362a9b6
[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    core
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 defined('MOODLE_INTERNAL') || die();
30 require_once($CFG->libdir.'/dml/moodle_database.php');
31 require_once($CFG->libdir.'/dml/oci_native_moodle_recordset.php');
32 require_once($CFG->libdir.'/dml/oci_native_moodle_temptables.php');
34 /**
35  * Native oci class representing moodle database interface.
36  *
37  * One complete reference for PHP + OCI:
38  * http://www.oracle.com/technology/tech/php/underground-php-oracle-manual.html
39  */
40 class oci_native_moodle_database extends moodle_database {
42     protected $oci     = null;
44     private $last_stmt_error = null; // To store stmt errors and enable get_last_error() to detect them
45     private $commit_status = null;   // default value initialised in connect method, we need the driver to be present
47     private $last_error_reporting; // To handle oci driver default verbosity
48     private $unique_session_id; // To store unique_session_id. Needed for temp tables unique naming
50     private $dblocks_supported = null; // To cache locks support along the connection life
51     private $bitwise_supported = null; // To cache bitwise operations support along the connection life
53     /**
54      * Detects if all needed PHP stuff installed.
55      * Note: can be used before connect()
56      * @return mixed true if ok, string if something
57      */
58     public function driver_installed() {
59         if (!extension_loaded('oci8')) {
60             return get_string('ociextensionisnotpresentinphp', 'install');
61         }
62         return true;
63     }
65     /**
66      * Returns database family type - describes SQL dialect
67      * Note: can be used before connect()
68      * @return string db family name (mysql, postgres, mssql, oracle, etc.)
69      */
70     public function get_dbfamily() {
71         return 'oracle';
72     }
74     /**
75      * Returns more specific database driver type
76      * Note: can be used before connect()
77      * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
78      */
79     protected function get_dbtype() {
80         return 'oci';
81     }
83     /**
84      * Returns general database library name
85      * Note: can be used before connect()
86      * @return string db type pdo, native
87      */
88     protected function get_dblibrary() {
89         return 'native';
90     }
92     /**
93      * Returns localised database type name
94      * Note: can be used before connect()
95      * @return string
96      */
97     public function get_name() {
98         return get_string('nativeoci', 'install');
99     }
101     /**
102      * Returns localised database configuration help.
103      * Note: can be used before connect()
104      * @return string
105      */
106     public function get_configuration_help() {
107         return get_string('nativeocihelp', 'install');
108     }
110     /**
111      * Returns localised database description
112      * Note: can be used before connect()
113      * @return string
114      */
115     public function get_configuration_hints() {
116         return get_string('databasesettingssub_oci', 'install');
117     }
119     /**
120      * Diagnose database and tables, this function is used
121      * to verify database and driver settings, db engine types, etc.
122      *
123      * @return string null means everything ok, string means problem found.
124      */
125     public function diagnose() {
126         if (!$this->bitwise_supported() or !$this->session_lock_supported()) {
127             return 'Oracle PL/SQL Moodle support packages are not installed! Database administrator has to execute /lib/dml/oci_native_moodle_package.sql script.';
128         }
129         return null;
130     }
132     /**
133      * Connect to db
134      * Must be called before other methods.
135      * @param string $dbhost
136      * @param string $dbuser
137      * @param string $dbpass
138      * @param string $dbname
139      * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
140      * @param array $dboptions driver specific options
141      * @return bool true
142      * @throws dml_connection_exception if error
143      */
144     public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
145         if ($prefix == '' and !$this->external) {
146             //Enforce prefixes for everybody but mysql
147             throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
148         }
149         if (!$this->external and strlen($prefix) > 2) {
150             //Max prefix length for Oracle is 2cc
151             $a = (object)array('dbfamily'=>'oracle', 'maxlength'=>2);
152             throw new dml_exception('prefixtoolong', $a);
153         }
155         $driverstatus = $this->driver_installed();
157         if ($driverstatus !== true) {
158             throw new dml_exception('dbdriverproblem', $driverstatus);
159         }
161         // Autocommit ON by default.
162         // Switching to OFF (OCI_DEFAULT), when playing with transactions
163         // please note this thing is not defined if oracle driver not present in PHP
164         // which means it can not be used as default value of object property!
165         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
167         $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
168         unset($this->dboptions['dbsocket']);
170         // NOTE: use of ', ", /¬†and \ is very problematic, even native oracle tools seem to have
171         //       problems with these, so just forget them and do not report problems into tracker...
173         if (empty($this->dbhost)) {
174             // old style full address (TNS)
175             $dbstring = $this->dbname;
176         } else {
177             if (empty($this->dboptions['dbport'])) {
178                 $this->dboptions['dbport'] = 1521;
179             }
180             $dbstring = '//'.$this->dbhost.':'.$this->dboptions['dbport'].'/'.$this->dbname;
181         }
183         ob_start();
184         if (empty($this->dboptions['dbpersist'])) {
185             $this->oci = oci_new_connect($this->dbuser, $this->dbpass, $dbstring, 'AL32UTF8');
186         } else {
187             $this->oci = oci_pconnect($this->dbuser, $this->dbpass, $dbstring, 'AL32UTF8');
188         }
189         $dberr = ob_get_contents();
190         ob_end_clean();
193         if ($this->oci === false) {
194             $this->oci = null;
195             $e = oci_error();
196             if (isset($e['message'])) {
197                 $dberr = $e['message'];
198             }
199             throw new dml_connection_exception($dberr);
200         }
202         // get unique session id, to be used later for temp tables stuff
203         $sql = 'SELECT DBMS_SESSION.UNIQUE_SESSION_ID() FROM DUAL';
204         $this->query_start($sql, null, SQL_QUERY_AUX);
205         $stmt = $this->parse_query($sql);
206         $result = oci_execute($stmt, $this->commit_status);
207         $this->query_end($result, $stmt);
208         $records = null;
209         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
210         oci_free_statement($stmt);
211         $this->unique_session_id = reset($records[0]);
213         //note: do not send "ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'" !
214         //      instead fix our PHP code to convert "," to "." properly!
216         // Connection stabilised and configured, going to instantiate the temptables controller
217         $this->temptables = new oci_native_moodle_temptables($this, $this->unique_session_id);
219         return true;
220     }
222     /**
223      * Close database connection and release all resources
224      * and memory (especially circular memory references).
225      * Do NOT use connect() again, create a new instance if needed.
226      */
227     public function dispose() {
228         parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
229         if ($this->oci) {
230             oci_close($this->oci);
231             $this->oci = null;
232         }
233     }
236     /**
237      * Called before each db query.
238      * @param string $sql
239      * @param array array of parameters
240      * @param int $type type of query
241      * @param mixed $extrainfo driver specific extra information
242      * @return void
243      */
244     protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
245         parent::query_start($sql, $params, $type, $extrainfo);
246         // oci driver tents to send debug to output, we do not need that ;-)
247         $this->last_error_reporting = error_reporting(0);
248     }
250     /**
251      * Called immediately after each db query.
252      * @param mixed db specific result
253      * @return void
254      */
255     protected function query_end($result, $stmt=null) {
256         // reset original debug level
257         error_reporting($this->last_error_reporting);
258         if ($stmt and $result === false) {
259             // Look for stmt error and store it
260             if (is_resource($stmt)) {
261                 $e = oci_error($stmt);
262                 if ($e !== false) {
263                     $this->last_stmt_error = $e['message'];
264                 }
265             }
266             oci_free_statement($stmt);
267         }
268         parent::query_end($result);
269     }
271     /**
272      * Returns database server info array
273      * @return array
274      */
275     public function get_server_info() {
276         static $info = null; // TODO: move to real object property
278         if (is_null($info)) {
279             $this->query_start("--oci_server_version()", null, SQL_QUERY_AUX);
280             $description = oci_server_version($this->oci);
281             $this->query_end(true);
282             preg_match('/(\d+\.)+\d+/', $description, $matches);
283             $info = array('description'=>$description, 'version'=>$matches[0]);
284         }
286         return $info;
287     }
289     protected function is_min_version($version) {
290         $server = $this->get_server_info();
291         $server = $server['version'];
292         return version_compare($server, $version, '>=');
293     }
295     /**
296      * Converts short table name {tablename} to real table name
297      * supporting temp tables ($this->unique_session_id based) if detected
298      *
299      * @param string sql
300      * @return string sql
301      */
302     protected function fix_table_names($sql) {
303         if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
304             foreach($matches[0] as $key=>$match) {
305                 $name = $matches[1][$key];
306                 if ($this->temptables->is_temptable($name)) {
307                     $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
308                 } else {
309                     $sql = str_replace($match, $this->prefix.$name, $sql);
310                 }
311             }
312         }
313         return $sql;
314     }
316     /**
317      * Returns supported query parameter types
318      * @return int bitmask
319      */
320     protected function allowed_param_types() {
321         return SQL_PARAMS_NAMED;
322     }
324     /**
325      * Returns last error reported by database engine.
326      * @return string error message
327      */
328     public function get_last_error() {
329         $error = false;
330         // First look for any previously saved stmt error
331         if (!empty($this->last_stmt_error)) {
332             $error = $this->last_stmt_error;
333             $this->last_stmt_error = null;
334         } else { // Now try connection error
335             $e = oci_error($this->oci);
336             if ($e !== false) {
337                 $error = $e['message'];
338             }
339         }
340         return $error;
341     }
343     protected function parse_query($sql) {
344         $stmt = oci_parse($this->oci, $sql);
345         if ($stmt == false) {
346             throw new dml_connection_exception('Can not parse sql query'); //TODO: maybe add better info
347         }
348         return $stmt;
349     }
351     /**
352      * Return tables in database WITHOUT current prefix
353      * @return array of table names in lowercase and without prefix
354      */
355     public function get_tables($usecache=true) {
356         if ($usecache and $this->tables !== null) {
357             return $this->tables;
358         }
359         $this->tables = array();
360         $prefix = str_replace('_', "\\_", strtoupper($this->prefix));
361         $sql = "SELECT TABLE_NAME
362                   FROM CAT
363                  WHERE TABLE_TYPE='TABLE'
364                        AND TABLE_NAME NOT LIKE 'BIN\$%'
365                        AND TABLE_NAME LIKE '$prefix%' ESCAPE '\\'";
366         $this->query_start($sql, null, SQL_QUERY_AUX);
367         $stmt = $this->parse_query($sql);
368         $result = oci_execute($stmt, $this->commit_status);
369         $this->query_end($result, $stmt);
370         $records = null;
371         oci_fetch_all($stmt, $records, 0, -1, OCI_ASSOC);
372         oci_free_statement($stmt);
373         $records = array_map('strtolower', $records['TABLE_NAME']);
374         foreach ($records as $tablename) {
375             if (strpos($tablename, $this->prefix) !== 0) {
376                 continue;
377             }
378             $tablename = substr($tablename, strlen($this->prefix));
379             $this->tables[$tablename] = $tablename;
380         }
382         // Add the currently available temptables
383         $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
385         return $this->tables;
386     }
388     /**
389      * Return table indexes - everything lowercased
390      * @return array of arrays
391      */
392     public function get_indexes($table) {
393         $indexes = array();
394         $tablename = strtoupper($this->prefix.$table);
396         $sql = "SELECT i.INDEX_NAME, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, ac.CONSTRAINT_TYPE
397                   FROM ALL_INDEXES i
398                   JOIN ALL_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME
399              LEFT JOIN ALL_CONSTRAINTS ac ON (ac.TABLE_NAME=i.TABLE_NAME AND ac.CONSTRAINT_NAME=i.INDEX_NAME AND ac.CONSTRAINT_TYPE='P')
400                  WHERE i.TABLE_NAME = '$tablename'
401               ORDER BY i.INDEX_NAME, c.COLUMN_POSITION";
403         $stmt = $this->parse_query($sql);
404         $result = oci_execute($stmt, $this->commit_status);
405         $this->query_end($result, $stmt);
406         $records = null;
407         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
408         oci_free_statement($stmt);
410         foreach ($records as $record) {
411             if ($record['CONSTRAINT_TYPE'] === 'P') {
412                 //ignore for now;
413                 continue;
414             }
415             $indexname = strtolower($record['INDEX_NAME']);
416             if (!isset($indexes[$indexname])) {
417                 $indexes[$indexname] = array('primary' => ($record['CONSTRAINT_TYPE'] === 'P'),
418                                              'unique'  => ($record['UNIQUENESS'] === 'UNIQUE'),
419                                              'columns' => array());
420             }
421             $indexes[$indexname]['columns'][] = strtolower($record['COLUMN_NAME']);
422         }
424         return $indexes;
425     }
427     /**
428      * Returns detailed information about columns in table. This information is cached internally.
429      * @param string $table name
430      * @param bool $usecache
431      * @return array array of database_column_info objects indexed with column names
432      */
433     public function get_columns($table, $usecache=true) {
434         if ($usecache and isset($this->columns[$table])) {
435             return $this->columns[$table];
436         }
438         if (!$table) { // table not specified, return empty array directly
439             return array();
440         }
442         $this->columns[$table] = array();
444         $sql = "SELECT CNAME, COLTYPE, WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL
445                   FROM COL
446                  WHERE TNAME = UPPER('{" . $table . "}')
447               ORDER BY COLNO";
449         list($sql, $params, $type) = $this->fix_sql_params($sql, null);
451         $this->query_start($sql, null, SQL_QUERY_AUX);
452         $stmt = $this->parse_query($sql);
453         $result = oci_execute($stmt, $this->commit_status);
454         $this->query_end($result, $stmt);
455         $records = null;
456         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
457         oci_free_statement($stmt);
459         if (!$records) {
460             return array();
461         }
462         foreach ($records as $rawcolumn) {
463             $rawcolumn = (object)$rawcolumn;
465             $info = new stdClass();
466             $info->name = strtolower($rawcolumn->CNAME);
467             $matches = null;
469             if ($rawcolumn->COLTYPE === 'VARCHAR2'
470              or $rawcolumn->COLTYPE === 'VARCHAR'
471              or $rawcolumn->COLTYPE === 'NVARCHAR2'
472              or $rawcolumn->COLTYPE === 'NVARCHAR'
473              or $rawcolumn->COLTYPE === 'CHAR'
474              or $rawcolumn->COLTYPE === 'NCHAR') {
475                 //TODO add some basic enum support here
476                 $info->type          = $rawcolumn->COLTYPE;
477                 $info->meta_type     = 'C';
478                 $info->max_length    = $rawcolumn->WIDTH;
479                 $info->scale         = null;
480                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
481                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
482                 if ($info->has_default) {
484                     // this is hacky :-(
485                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
486                         $info->default_value = null;
487                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
488                         $info->default_value = "";
489                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
490                         $info->default_value = "";
491                     } else {
492                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
493                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
494                     }
495                 } else {
496                     $info->default_value = null;
497                 }
498                 $info->primary_key   = false;
499                 $info->binary        = false;
500                 $info->unsigned      = null;
501                 $info->auto_increment= false;
502                 $info->unique        = null;
504             } else if ($rawcolumn->COLTYPE === 'NUMBER') {
505                 $info->type       = $rawcolumn->COLTYPE;
506                 $info->max_length = $rawcolumn->PRECISION;
507                 $info->binary     = false;
508                 if (!is_null($rawcolumn->SCALE) && $rawcolumn->SCALE == 0) { // null in oracle scale allows decimals => not integer
509                     // integer
510                     if ($info->name === 'id') {
511                         $info->primary_key   = true;
512                         $info->meta_type     = 'R';
513                         $info->unique        = true;
514                         $info->auto_increment= true;
515                         $info->has_default   = false;
516                     } else {
517                         $info->primary_key   = false;
518                         $info->meta_type     = 'I';
519                         $info->unique        = null;
520                         $info->auto_increment= false;
521                     }
522                     $info->scale = null;
524                 } else {
525                     //float
526                     $info->meta_type     = 'N';
527                     $info->primary_key   = false;
528                     $info->unsigned      = null;
529                     $info->auto_increment= false;
530                     $info->unique        = null;
531                     $info->scale         = $rawcolumn->SCALE;
532                 }
533                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
534                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
535                 if ($info->has_default) {
536                     $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
537                 } else {
538                     $info->default_value = null;
539                 }
541             } else if ($rawcolumn->COLTYPE === 'FLOAT') {
542                 $info->type       = $rawcolumn->COLTYPE;
543                 $info->max_length = (int)($rawcolumn->PRECISION * 3.32193);
544                 $info->primary_key   = false;
545                 $info->meta_type     = 'N';
546                 $info->unique        = null;
547                 $info->auto_increment= false;
548                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
549                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
550                 if ($info->has_default) {
551                     $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
552                 } else {
553                     $info->default_value = null;
554                 }
556             } else if ($rawcolumn->COLTYPE === 'CLOB'
557                     or $rawcolumn->COLTYPE === 'NCLOB') {
558                 $info->type          = $rawcolumn->COLTYPE;
559                 $info->meta_type     = 'X';
560                 $info->max_length    = -1;
561                 $info->scale         = null;
562                 $info->scale         = null;
563                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
564                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
565                 if ($info->has_default) {
566                     // this is hacky :-(
567                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
568                         $info->default_value = null;
569                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
570                         $info->default_value = "";
571                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Other times it's stored without trailing space
572                         $info->default_value = "";
573                     } else {
574                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
575                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
576                     }
577                 } else {
578                     $info->default_value = null;
579                 }
580                 $info->primary_key   = false;
581                 $info->binary        = false;
582                 $info->unsigned      = null;
583                 $info->auto_increment= false;
584                 $info->unique        = null;
586             } else if ($rawcolumn->COLTYPE === 'BLOB') {
587                 $info->type          = $rawcolumn->COLTYPE;
588                 $info->meta_type     = 'B';
589                 $info->max_length    = -1;
590                 $info->scale         = null;
591                 $info->scale         = null;
592                 $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
593                 $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
594                 if ($info->has_default) {
595                     // this is hacky :-(
596                     if ($rawcolumn->DEFAULTVAL === 'NULL') {
597                         $info->default_value = null;
598                     } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
599                         $info->default_value = "";
600                     } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
601                         $info->default_value = "";
602                     } else {
603                         $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
604                         $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
605                     }
606                 } else {
607                     $info->default_value = null;
608                 }
609                 $info->primary_key   = false;
610                 $info->binary        = true;
611                 $info->unsigned      = null;
612                 $info->auto_increment= false;
613                 $info->unique        = null;
615             } else {
616                 // unknown type - sorry
617                 $info->type          = $rawcolumn->COLTYPE;
618                 $info->meta_type     = '?';
619             }
621             $this->columns[$table][$info->name] = new database_column_info($info);
622         }
624         return $this->columns[$table];
625     }
627     /**
628      * Normalise values based in RDBMS dependencies (booleans, LOBs...)
629      *
630      * @param database_column_info $column column metadata corresponding with the value we are going to normalise
631      * @param mixed $value value we are going to normalise
632      * @return mixed the normalised value
633      */
634     protected function normalise_value($column, $value) {
635         if (is_bool($value)) { // Always, convert boolean to int
636             $value = (int)$value;
638         } else if ($column->meta_type == 'B') { // CLOB detected, we return 'blob' array instead of raw value to allow
639             if (!is_null($value)) {             // binding/executing code later to know about its nature
640                 $value = array('blob' => $value);
641             }
643         } else if ($column->meta_type == 'X' && strlen($value) > 4000) { // CLOB detected (>4000 optimisation), we return 'clob'
644             if (!is_null($value)) {                                      // array instead of raw value to allow binding/
645                 $value = array('clob' => (string)$value);                // executing code later to know about its nature
646             }
648         } else if ($value === '') {
649             if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
650                 $value = 0; // prevent '' problems in numeric fields
651             }
652         }
653         return $value;
654     }
656     /**
657      * Transforms the sql and params in order to emulate the LIMIT clause available in other DBs
658      *
659      * @param string $sql the SQL select query to execute.
660      * @param array $params array of sql parameters
661      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
662      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
663      * @return array with the transformed sql and params updated
664      */
665     private function get_limit_sql($sql, array $params = null, $limitfrom=0, $limitnum=0) {
667         $limitfrom = (int)$limitfrom;
668         $limitnum  = (int)$limitnum;
669         $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
670         $limitnum  = ($limitnum < 0)  ? 0 : $limitnum;
672         // TODO: Add the /*+ FIRST_ROWS */ hint if there isn't another hint
674         if ($limitfrom and $limitnum) {
675             $sql = "SELECT oracle_o.*
676                       FROM (SELECT oracle_i.*, rownum AS oracle_rownum
677                               FROM ($sql) oracle_i
678                              WHERE rownum <= :oracle_num_rows
679                             ) oracle_o
680                      WHERE oracle_rownum > :oracle_skip_rows";
681             $params['oracle_num_rows'] = $limitfrom + $limitnum;
682             $params['oracle_skip_rows'] = $limitfrom;
684         } else if ($limitfrom and !$limitnum) {
685             $sql = "SELECT oracle_o.*
686                       FROM (SELECT oracle_i.*, rownum AS oracle_rownum
687                               FROM ($sql) oracle_i
688                             ) oracle_o
689                      WHERE oracle_rownum > :oracle_skip_rows";
690             $params['oracle_skip_rows'] = $limitfrom;
692         } else if (!$limitfrom and $limitnum) {
693             $sql = "SELECT *
694                       FROM ($sql)
695                      WHERE rownum <= :oracle_num_rows";
696             $params['oracle_num_rows'] = $limitnum;
697         }
699         return array($sql, $params);
700     }
702     /**
703      * This function will handle all the column values before being inserted/updated to DB for Oracle
704      * installations. This is because the "special feature" of Oracle where the empty string is
705      * equal to NULL and this presents a problem with all our currently NOT NULL default '' fields.
706      * (and with empties handling in general)
707      *
708      * Note that this function is 100% private and should be used, exclusively by DML functions
709      * in this file. Also, this is considered a DIRTY HACK to be removed when possible.
710      *
711      * This function is private and must not be used outside this driver at all
712      *
713      * @param $table string the table where the record is going to be inserted/updated (without prefix)
714      * @param $field string the field where the record is going to be inserted/updated
715      * @param $value mixed the value to be inserted/updated
716      */
717     private function oracle_dirty_hack ($table, $field, $value) {
719         // Get metadata
720         $columns = $this->get_columns($table);
721         if (!isset($columns[$field])) {
722             return $value;
723         }
724         $column = $columns[$field];
726         // !! This paragraph explains behaviour before Moodle 2.0:
727         //
728         // For Oracle DB, empty strings are converted to NULLs in DB
729         // and this breaks a lot of NOT NULL columns currently Moodle. In the future it's
730         // planned to move some of them to NULL, if they must accept empty values and this
731         // piece of code will become less and less used. But, for now, we need it.
732         // What we are going to do is to examine all the data being inserted and if it's
733         // an empty string (NULL for Oracle) and the field is defined as NOT NULL, we'll modify
734         // such data in the best form possible ("0" for booleans and numbers and " " for the
735         // rest of strings. It isn't optimal, but the only way to do so.
736         // In the opposite, when retrieving records from Oracle, we'll decode " " back to
737         // empty strings to allow everything to work properly. DIRTY HACK.
739         // !! These paragraphs explain the rationale about the change for Moodle 2.0:
740         //
741         // Before Moodle 2.0, we only used to apply this DIRTY HACK to NOT NULL columns, as
742         // stated above, but it causes one problem in NULL columns where both empty strings
743         // and real NULLs are stored as NULLs, being impossible to differentiate them when
744         // being retrieved from DB.
745         //
746         // So, starting with Moodle 2.0, we are going to apply the DIRTY HACK to all the
747         // CHAR/CLOB columns no matter of their nullability. That way, when retrieving
748         // NULLABLE fields we'll get proper empties and NULLs differentiated, so we'll be able
749         // to rely in NULL/empty/content contents without problems, until now that wasn't
750         // possible at all.
751         //
752         // No breakage with old data is expected as long as at the time of writing this
753         // (20090922) all the current uses of both sql_empty() and sql_isempty() has been
754         // revised in 2.0 and all them were being performed against NOT NULL columns,
755         // where nothing has changed (the DIRTY HACK was already being applied).
756         //
757         // !! Conclusions:
758         //
759         // From Moodle 2.0 onwards, ALL empty strings in Oracle DBs will be stored as
760         // 1-whitespace char, ALL NULLs as NULLs and, obviously, content as content. And
761         // those 1-whitespace chars will be converted back to empty strings by all the
762         // get_field/record/set() functions transparently and any SQL needing direct handling
763         // of empties will need to use the sql_empty() and sql_isempty() helper functions.
764         // MDL-17491.
766         // If the field ins't VARCHAR or CLOB, skip
767         if ($column->meta_type != 'C' and $column->meta_type != 'X') {
768             return $value;
769         }
771         // If the value isn't empty, skip
772         if (!empty($value)) {
773             return $value;
774         }
776         // Now, we have one empty value, going to be inserted to one VARCHAR2 or CLOB field
777         // Try to get the best value to be inserted
779         // The '0' string doesn't need any transformation, skip
780         if ($value === '0') {
781             return $value;
782         }
784         // Transformations start
785         if (gettype($value) == 'boolean') {
786             return '0'; // Transform false to '0' that evaluates the same for PHP
788         } else if (gettype($value) == 'integer') {
789             return '0'; // Transform 0 to '0' that evaluates the same for PHP
791         } else if ($value === '') {
792             return ' '; // Transform '' to ' ' that DONT'T EVALUATE THE SAME
793                         // (we'll transform back again on get_records_XXX functions and others)!!
794         }
796         // Fail safe to original value
797         return $value;
798     }
800     /**
801      * Is db in unicode mode?
802      * @return bool
803      */
804     public function setup_is_unicodedb() {
805         $sql = "SELECT VALUE
806                   FROM NLS_DATABASE_PARAMETERS
807                  WHERE PARAMETER = 'NLS_CHARACTERSET'";
808         $this->query_start($sql, null, SQL_QUERY_AUX);
809         $stmt = $this->parse_query($sql);
810         $result = oci_execute($stmt, $this->commit_status);
811         $this->query_end($result, $stmt);
812         $records = null;
813         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
814         oci_free_statement($stmt);
816         return (isset($records['VALUE'][0]) and $records['VALUE'][0] === 'AL32UTF8');
817     }
819     /**
820      * Do NOT use in code, to be used by database_manager only!
821      * @param string $sql query
822      * @return bool true
823      * @throws dml_exception if error
824      */
825     public function change_database_structure($sql) {
826         $this->reset_caches();
828         $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
829         $stmt = $this->parse_query($sql);
830         $result = oci_execute($stmt, $this->commit_status);
831         $this->query_end($result, $stmt);
832         oci_free_statement($stmt);
834         return true;
835     }
837     protected function bind_params($stmt, array $params=null, $tablename=null) {
838         $descriptors = array();
839         if ($params) {
840             $columns = array();
841             if ($tablename) {
842                 $columns = $this->get_columns($tablename);
843             }
844             foreach($params as $key => $value) {
845                 // Decouple column name and param name as far as sometimes they aren't the same
846                 $columnname = $key; // Default columnname (for DB introspecting is key), but...
847                 if ($key == 'newfieldtoset') { // found case where column and key diverge, handle that
848                     $columnname   = key($value);    // columnname is the key of the array
849                     $params[$key] = $value[$columnname]; // set the proper value in the $params array and
850                     $value        = $value[$columnname]; // set the proper value in the $value variable
851                 }
852                 // Continue processing
853                 // Now, handle already detected LOBs
854                 if (is_array($value)) { // Let's go to bind special cases (lob descriptors)
855                     if (isset($value['clob'])) {
856                         $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
857                         oci_bind_by_name($stmt, $key, $lob, -1, SQLT_CLOB);
858                         $lob->writeTemporary($this->oracle_dirty_hack($tablename, $columnname, $params[$key]['clob']), OCI_TEMP_CLOB);
859                         $descriptors[] = $lob;
860                         continue; // Column binding finished, go to next one
861                     } else if (isset($value['blob'])) {
862                         $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
863                         oci_bind_by_name($stmt, $key, $lob, -1, SQLT_BLOB);
864                         $lob->writeTemporary($params[$key]['blob'], OCI_TEMP_BLOB);
865                         $descriptors[] = $lob;
866                         continue; // Column binding finished, go to next one
867                     }
868                 }
869                 // TODO: Put proper types and length is possible (enormous speedup)
870                 // Arrived here, continue with standard processing, using metadata if possible
871                 if (isset($columns[$columnname])) {
872                     $type = $columns[$columnname]->meta_type;
873                     $maxlength = $columns[$columnname]->max_length;
874                 } else {
875                     $type = '?';
876                     $maxlength = -1;
877                 }
878                 switch ($type) {
879                     case 'I':
880                     case 'R':
881                         // TODO: Optimise
882                         oci_bind_by_name($stmt, $key, $params[$key]);
883                         break;
885                     case 'N':
886                     case 'F':
887                         // TODO: Optimise
888                         oci_bind_by_name($stmt, $key, $params[$key]);
889                         break;
891                     case 'B':
892                         // TODO: Only arrive here if BLOB is null: Bind if so, else exception!
893                         // don't break here
895                     case 'X':
896                         // TODO: Only arrive here if CLOB is null or <= 4000 cc, else exception
897                         // don't break here
899                     default: // Bind as CHAR (applying dirty hack)
900                         // TODO: Optimise
901                         oci_bind_by_name($stmt, $key, $this->oracle_dirty_hack($tablename, $columnname, $params[$key]));
902                 }
903             }
904         }
905         return $descriptors;
906     }
908     protected function free_descriptors($descriptors) {
909         foreach ($descriptors as $descriptor) {
910             oci_free_descriptor($descriptor);
911         }
912     }
914     /**
915      * This function is used to convert all the Oracle 1-space defaults to the empty string
916      * like a really DIRTY HACK to allow it to work better until all those NOT NULL DEFAULT ''
917      * fields will be out from Moodle.
918      * @param string the string to be converted to '' (empty string) if it's ' ' (one space)
919      * @param mixed the key of the array in case we are using this function from array_walk,
920      *              defaults to null for other (direct) uses
921      * @return boolean always true (the converted variable is returned by reference)
922      */
923     public static function onespace2empty(&$item, $key=null) {
924         $item = ($item === ' ') ? '' : $item;
925         return true;
926     }
928     /**
929      * Execute general sql query. Should be used only when no other method suitable.
930      * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead!
931      * @param string $sql query
932      * @param array $params query parameters
933      * @return bool true
934      * @throws dml_exception if error
935      */
936     public function execute($sql, array $params=null) {
937         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
939         if (strpos($sql, ';') !== false) {
940             throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
941         }
943         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
944         $stmt = $this->parse_query($sql);
945         $this->bind_params($stmt, $params);
946         $result = oci_execute($stmt, $this->commit_status);
947         $this->query_end($result, $stmt);
948         oci_free_statement($stmt);
950         return true;
951     }
953     /**
954      * Get a single database record as an object using a SQL statement.
955      *
956      * The SQL statement should normally only return one record.
957      * It is recommended to use get_records_sql() if more matches possible!
958      *
959      * @param string $sql The SQL string you wish to be executed, should normally only return one record.
960      * @param array $params array of sql parameters
961      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
962      *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
963      *                        MUST_EXIST means throw exception if no record or multiple records found
964      * @return mixed a fieldset object containing the first matching record, false or exception if error not found depending on mode
965      * @throws dml_exception if error
966      */
967     public function get_record_sql($sql, array $params=null, $strictness=IGNORE_MISSING) {
968         $strictness = (int)$strictness;
969         if ($strictness == IGNORE_MULTIPLE) {
970             // do not limit here - ORA does not like that
971             if (!$rs = $this->get_recordset_sql($sql, $params)) {
972                 return false;
973             }
974             foreach ($rs as $result) {
975                 $rs->close();
976                 return $result;
977             }
978             $rs->close();
979             return false;
980         }
981         return parent::get_record_sql($sql, $params, $strictness);
982     }
984     /**
985      * Get a number of records as a moodle_recordset using a SQL statement.
986      *
987      * Since this method is a little less readable, use of it should be restricted to
988      * code where it's possible there might be large datasets being returned.  For known
989      * small datasets use get_records_sql - it leads to simpler code.
990      *
991      * The return type is as for @see function get_recordset.
992      *
993      * @param string $sql the SQL select query to execute.
994      * @param array $params array of sql parameters
995      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
996      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
997      * @return moodle_recordset instance
998      * @throws dml_exception if error
999      */
1000     public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1002         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1004         list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1006         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1007         $stmt = $this->parse_query($rawsql);
1008         $this->bind_params($stmt, $params);
1009         $result = oci_execute($stmt, $this->commit_status);
1010         $this->query_end($result, $stmt);
1012         return $this->create_recordset($stmt);
1013     }
1015     protected function create_recordset($stmt) {
1016         return new oci_native_moodle_recordset($stmt);
1017     }
1019     /**
1020      * Get a number of records as an array of objects using a SQL statement.
1021      *
1022      * Return value as for @see function get_records.
1023      *
1024      * @param string $sql the SQL select query to execute. The first column of this SELECT statement
1025      *   must be a unique value (usually the 'id' field), as it will be used as the key of the
1026      *   returned array.
1027      * @param array $params array of sql parameters
1028      * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1029      * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1030      * @return array of objects, or empty array if no records were found
1031      * @throws dml_exception if error
1032      */
1033     public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1035         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1037         list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1039         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1040         $stmt = $this->parse_query($rawsql);
1041         $this->bind_params($stmt, $params);
1042         $result = oci_execute($stmt, $this->commit_status);
1043         $this->query_end($result, $stmt);
1045         $records = null;
1046         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1047         oci_free_statement($stmt);
1049         $return = array();
1051         foreach ($records as $row) {
1052             $row = array_change_key_case($row, CASE_LOWER);
1053             unset($row['oracle_rownum']);
1054             array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
1055             $id = reset($row);
1056             if (isset($return[$id])) {
1057                 $colname = key($row);
1058                 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);
1059             }
1060             $return[$id] = (object)$row;
1061         }
1063         return $return;
1064     }
1066     /**
1067      * Selects records and return values (first field) as an array using a SQL statement.
1068      *
1069      * @param string $sql The SQL query
1070      * @param array $params array of sql parameters
1071      * @return array of values
1072      * @throws dml_exception if error
1073      */
1074     public function get_fieldset_sql($sql, array $params=null) {
1075         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1077         $this->query_start($sql, $params, SQL_QUERY_SELECT);
1078         $stmt = $this->parse_query($sql);
1079         $this->bind_params($stmt, $params);
1080         $result = oci_execute($stmt, $this->commit_status);
1081         $this->query_end($result, $stmt);
1083         $records = null;
1084         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
1085         oci_free_statement($stmt);
1087         $return = reset($records);
1088         array_walk($return, array('oci_native_moodle_database', 'onespace2empty'));
1090         return $return;
1091     }
1093     /**
1094      * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1095      * @param string $table name
1096      * @param mixed $params data record as object or array
1097      * @param bool $returnit return it of inserted record
1098      * @param bool $bulk true means repeated inserts expected
1099      * @param bool $customsequence true if 'id' included in $params, disables $returnid
1100      * @return bool|int true or new id
1101      * @throws dml_exception if error
1102      */
1103     public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1104         if (!is_array($params)) {
1105             $params = (array)$params;
1106         }
1108         $returning = "";
1110         if ($customsequence) {
1111             if (!isset($params['id'])) {
1112                 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1113             }
1114             $returnid = false;
1115         } else {
1116             unset($params['id']);
1117             if ($returnid) {
1118                 $returning = " RETURNING id INTO :oracle_id"; // crazy name nobody is ever going to use or parameter ;-)
1119             }
1120         }
1122         if (empty($params)) {
1123             throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1124         }
1126         $fields = implode(',', array_keys($params));
1127         $values = array();
1128         foreach ($params as $pname => $value) {
1129             $values[] = ":$pname";
1130         }
1131         $values = implode(',', $values);
1133         $sql = "INSERT INTO {" . $table . "} ($fields) VALUES ($values)";
1134         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1135         $sql .= $returning;
1137         $id = null;
1139         $this->query_start($sql, $params, SQL_QUERY_INSERT);
1140         $stmt = $this->parse_query($sql);
1141         $descriptors = $this->bind_params($stmt, $params, $table);
1142         if ($returning) {
1143             oci_bind_by_name($stmt, ":oracle_id", $id, 10, SQLT_INT);
1144         }
1145         $result = oci_execute($stmt, $this->commit_status);
1146         $this->free_descriptors($descriptors);
1147         $this->query_end($result, $stmt);
1148         oci_free_statement($stmt);
1150         if (!$returnid) {
1151             return true;
1152         }
1154         if (!$returning) {
1155             die('TODO - implement oracle 9.2 insert support'); //TODO
1156         }
1158         return (int)$id;
1159     }
1161     /**
1162      * Insert a record into a table and return the "id" field if required.
1163      *
1164      * Some conversions and safety checks are carried out. Lobs are supported.
1165      * If the return ID isn't required, then this just reports success as true/false.
1166      * $data is an object containing needed data
1167      * @param string $table The database table to be inserted into
1168      * @param object $data A data object with values for one or more fields in the record
1169      * @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.
1170      * @return bool|int true or new id
1171      * @throws dml_exception if error
1172      */
1173     public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1174         $dataobject = (array)$dataobject;
1176         $columns = $this->get_columns($table);
1177         $cleaned = array();
1179         foreach ($dataobject as $field=>$value) {
1180             if ($field === 'id') {
1181                 continue;
1182             }
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         $dataobject = (array)$dataobject;
1205         $columns = $this->get_columns($table);
1206         $cleaned = array();
1208         foreach ($dataobject as $field=>$value) {
1209             if (!isset($columns[$field])) {
1210                 continue;
1211             }
1212             $column = $columns[$field];
1213             $cleaned[$field] = $this->normalise_value($column, $value);
1214         }
1216         return $this->insert_record_raw($table, $cleaned, false, true, true);
1217     }
1219     /**
1220      * Update record in database, as fast as possible, no safety checks, lobs not supported.
1221      * @param string $table name
1222      * @param mixed $params data record as object or array
1223      * @param bool true means repeated updates expected
1224      * @return bool true
1225      * @throws dml_exception if error
1226      */
1227     public function update_record_raw($table, $params, $bulk=false) {
1228         $params = (array)$params;
1230         if (!isset($params['id'])) {
1231             throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1232         }
1234         if (empty($params)) {
1235             throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1236         }
1238         $sets = array();
1239         foreach ($params as $field=>$value) {
1240             if ($field == 'id') {
1241                 continue;
1242             }
1243             $sets[] = "$field = :$field";
1244         }
1246         $sets = implode(',', $sets);
1247         $sql = "UPDATE {" . $table . "} SET $sets WHERE id=:id";
1248         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1250         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1251         $stmt = $this->parse_query($sql);
1252         $descriptors = $this->bind_params($stmt, $params, $table);
1253         $result = oci_execute($stmt, $this->commit_status);
1254         $this->free_descriptors($descriptors);
1255         $this->query_end($result, $stmt);
1256         oci_free_statement($stmt);
1258         return true;
1259     }
1261     /**
1262      * Update a record in a table
1263      *
1264      * $dataobject is an object containing needed data
1265      * Relies on $dataobject having a variable "id" to
1266      * specify the record to update
1267      *
1268      * @param string $table The database table to be checked against.
1269      * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1270      * @param bool true means repeated updates expected
1271      * @return bool true
1272      * @throws dml_exception if error
1273      */
1274     public function update_record($table, $dataobject, $bulk=false) {
1275         $dataobject = (array)$dataobject;
1277         $columns = $this->get_columns($table);
1278         $cleaned = array();
1280         foreach ($dataobject as $field=>$value) {
1281             if (!isset($columns[$field])) {
1282                 continue;
1283             }
1284             $column = $columns[$field];
1285             $cleaned[$field] = $this->normalise_value($column, $value);
1286         }
1288         $this->update_record_raw($table, $cleaned, $bulk);
1290         return true;
1291     }
1293     /**
1294      * Set a single field in every table record which match a particular WHERE clause.
1295      *
1296      * @param string $table The database table to be checked against.
1297      * @param string $newfield the field to set.
1298      * @param string $newvalue the value to set the field to.
1299      * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1300      * @param array $params array of sql parameters
1301      * @return bool true
1302      * @throws dml_exception if error
1303      */
1304     public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1306         if ($select) {
1307             $select = "WHERE $select";
1308         }
1309         if (is_null($params)) {
1310             $params = array();
1311         }
1313         // Get column metadata
1314         $columns = $this->get_columns($table);
1315         $column = $columns[$newfield];
1317         $newvalue = $this->normalise_value($column, $newvalue);
1319         list($select, $params, $type) = $this->fix_sql_params($select, $params);
1321         if (is_bool($newvalue)) {
1322             $newvalue = (int)$newvalue; // prevent "false" problems
1323         }
1324         if (is_null($newvalue)) {
1325             $newsql = "$newfield = NULL";
1326         } else {
1327             // Set the param to array ($newfield => $newvalue) and key to 'newfieldtoset'
1328             // name in the build sql. Later, bind_params() will detect the value array and
1329             // perform the needed modifications to allow the query to work. Note that
1330             // 'newfieldtoset' is one arbitrary name that hopefully won't be used ever
1331             // in order to avoid problems where the same field is used both in the set clause and in
1332             // the conditions. This was breaking badly in drivers using NAMED params like oci.
1333             $params['newfieldtoset'] = array($newfield => $newvalue);
1334             $newsql = "$newfield = :newfieldtoset";
1335         }
1336         $sql = "UPDATE {" . $table . "} SET $newsql $select";
1337         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1339         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1340         $stmt = $this->parse_query($sql);
1341         $descriptors = $this->bind_params($stmt, $params, $table);
1342         $result = oci_execute($stmt, $this->commit_status);
1343         $this->free_descriptors($descriptors);
1344         $this->query_end($result, $stmt);
1345         oci_free_statement($stmt);
1347         return true;
1348     }
1350     /**
1351      * Delete one or more records from a table which match a particular WHERE clause.
1352      *
1353      * @param string $table The database table to be checked against.
1354      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1355      * @param array $params array of sql parameters
1356      * @return bool true
1357      * @throws dml_exception if error
1358      */
1359     public function delete_records_select($table, $select, array $params=null) {
1361         if ($select) {
1362             $select = "WHERE $select";
1363         }
1365         $sql = "DELETE FROM {" . $table . "} $select";
1367         list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1369         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1370         $stmt = $this->parse_query($sql);
1371         $this->bind_params($stmt, $params);
1372         $result = oci_execute($stmt, $this->commit_status);
1373         $this->query_end($result, $stmt);
1374         oci_free_statement($stmt);
1376         return true;
1377     }
1379     function sql_null_from_clause() {
1380         return ' FROM dual';
1381     }
1383 // Bitwise operations
1384    protected function bitwise_supported() {
1385         if (isset($this->bitwise_supported)) { // Use cached value if available
1386             return $this->bitwise_supported;
1387         }
1388         $sql = "SELECT 1
1389                 FROM user_objects
1390                 WHERE object_type = 'PACKAGE BODY'
1391                   AND object_name = 'MOODLE_BITS'
1392                   AND status = 'VALID'";
1393         $this->query_start($sql, null, SQL_QUERY_AUX);
1394         $stmt = $this->parse_query($sql);
1395         $result = oci_execute($stmt, $this->commit_status);
1396         $this->query_end($result, $stmt);
1397         $records = null;
1398         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1399         oci_free_statement($stmt);
1400         $this->bitwise_supported = isset($records[0]) && reset($records[0]) ? true : false;
1401         return $this->bitwise_supported;
1402     }
1404     public function sql_bitand($int1, $int2) {
1405         return 'bitand((' . $int1 . '), (' . $int2 . '))';
1406     }
1408     public function sql_bitnot($int1) {
1409         return '((0 - (' . $int1 . ')) - 1)';
1410     }
1412     public function sql_bitor($int1, $int2) {
1413         // Use the MOODLE_BITS package if available
1414         if ($this->bitwise_supported()) {
1415             return 'MOODLE_BITS.BITOR(' . $int1 . ', ' . $int2 . ')';
1416         }
1417         // fallback to PHP bool operations, can break if using placeholders
1418         return '((' . $int1 . ') + (' . $int2 . ') - ' . $this->sql_bitand($int1, $int2) . ')';
1419     }
1421     public function sql_bitxor($int1, $int2) {
1422         // Use the MOODLE_BITS package if available
1423         if ($this->bitwise_supported()) {
1424             return 'MOODLE_BITS.BITXOR(' . $int1 . ', ' . $int2 . ')';
1425         }
1426         // fallback to PHP bool operations, can break if using placeholders
1427         return '(' . $this->sql_bitor($int1, $int2) . ' - ' . $this->sql_bitand($int1, $int2) . ')';
1428     }
1430     /**
1431      * Returns the SQL text to be used in order to perform module '%'
1432      * operation - remainder after division
1433      *
1434      * @param integer int1 first integer in the operation
1435      * @param integer int2 second integer in the operation
1436      * @return string the piece of SQL code to be used in your statement.
1437      */
1438     public function sql_modulo($int1, $int2) {
1439         return 'MOD(' . $int1 . ', ' . $int2 . ')';
1440     }
1442     public function sql_cast_char2int($fieldname, $text=false) {
1443         if (!$text) {
1444             return ' CAST(' . $fieldname . ' AS INT) ';
1445         } else {
1446             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1447         }
1448     }
1450     public function sql_cast_char2real($fieldname, $text=false) {
1451         if (!$text) {
1452             return ' CAST(' . $fieldname . ' AS FLOAT) ';
1453         } else {
1454             return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS FLOAT) ';
1455         }
1456     }
1458     /**
1459      * Returns 'LIKE' part of a query.
1460      *
1461      * @param string $fieldname usually name of the table column
1462      * @param string $param usually bound query parameter (?, :named)
1463      * @param bool $casesensitive use case sensitive search
1464      * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1465      * @param bool $notlike true means "NOT LIKE"
1466      * @param string $escapechar escape char for '%' and '_'
1467      * @return string SQL code fragment
1468      */
1469     public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1470         if (strpos($param, '%') !== false) {
1471             debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)');
1472         }
1474         $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1476         // no accent sensitiveness here for now, sorry
1478         if ($casesensitive) {
1479             return "$fieldname $LIKE $param ESCAPE '$escapechar'";
1480         } else {
1481             return "LOWER($fieldname) $LIKE LOWER($param) ESCAPE '$escapechar'";
1482         }
1483     }
1485     // NOTE: Oracle concat implementation isn't ANSI compliant when using NULLs (the result of
1486     // any concatenation with NULL must return NULL) because of his inability to differentiate
1487     // NULLs and empty strings. So this function will cause some tests to fail. Hopefully
1488     // it's only a side case and it won't affect normal concatenation operations in Moodle.
1489     public function sql_concat() {
1490         $arr = func_get_args();
1491         $s = implode(' || ', $arr);
1492         if ($s === '') {
1493             return " '' ";
1494         }
1495         return " $s ";
1496     }
1498     public function sql_concat_join($separator="' '", $elements=array()) {
1499         for ($n=count($elements)-1; $n > 0 ; $n--) {
1500             array_splice($elements, $n, 0, $separator);
1501         }
1502         $s = implode(' || ', $elements);
1503         if ($s === '') {
1504             return " '' ";
1505         }
1506         return " $s ";
1507     }
1509     /**
1510      * Returns the SQL for returning searching one string for the location of another.
1511      */
1512     public function sql_position($needle, $haystack) {
1513         return "INSTR(($haystack), ($needle))";
1514     }
1516     public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1517         if ($textfield) {
1518             return " (".$this->sql_compare_text($fieldname)." = '".$this->sql_empty()."') ";
1519         } else {
1520             return " ($fieldname = '".$this->sql_empty()."') ";
1521         }
1522     }
1524     public function sql_empty() {
1525         return ' ';
1526     }
1528     public function sql_order_by_text($fieldname, $numchars=32) {
1529         return 'dbms_lob.substr(' . $fieldname . ', ' . $numchars . ',1)';
1530     }
1532 /// session locking
1533     public function session_lock_supported() {
1534         if (isset($this->dblocks_supported)) { // Use cached value if available
1535             return $this->dblocks_supported;
1536         }
1537         $sql = "SELECT 1
1538                 FROM user_objects
1539                 WHERE object_type = 'PACKAGE BODY'
1540                   AND object_name = 'MOODLE_LOCKS'
1541                   AND status = 'VALID'";
1542         $this->query_start($sql, null, SQL_QUERY_AUX);
1543         $stmt = $this->parse_query($sql);
1544         $result = oci_execute($stmt, $this->commit_status);
1545         $this->query_end($result, $stmt);
1546         $records = null;
1547         oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1548         oci_free_statement($stmt);
1549         $this->dblocks_supported = isset($records[0]) && reset($records[0]) ? true : false;
1550         return $this->dblocks_supported;
1551     }
1553     public function get_session_lock($rowid) {
1554         if (!$this->session_lock_supported()) {
1555             return;
1556         }
1557         parent::get_session_lock($rowid);
1559         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1560         $sql = 'SELECT MOODLE_LOCKS.GET_LOCK(:lockname, :locktimeout) FROM DUAL';
1561         $params = array('lockname' => $fullname , 'locktimeout' => 120);
1562         $this->query_start($sql, $params, SQL_QUERY_AUX);
1563         $stmt = $this->parse_query($sql);
1564         $this->bind_params($stmt, $params);
1565         $result = oci_execute($stmt, $this->commit_status);
1566         $this->query_end($result, $stmt);
1567         oci_free_statement($stmt);
1568     }
1570     public function release_session_lock($rowid) {
1571         if (!$this->session_lock_supported()) {
1572             return;
1573         }
1574         parent::release_session_lock($rowid);
1576         $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1577         $params = array('lockname' => $fullname);
1578         $sql = 'SELECT MOODLE_LOCKS.RELEASE_LOCK(:lockname) FROM DUAL';
1579         $this->query_start($sql, $params, SQL_QUERY_AUX);
1580         $stmt = $this->parse_query($sql);
1581         $this->bind_params($stmt, $params);
1582         $result = oci_execute($stmt, $this->commit_status);
1583         $this->query_end($result, $stmt);
1584         oci_free_statement($stmt);
1585     }
1587 /// transactions
1588     /**
1589      * Driver specific start of real database transaction,
1590      * this can not be used directly in code.
1591      * @return void
1592      */
1593     protected function begin_transaction() {
1594         $this->commit_status = OCI_DEFAULT; //Done! ;-)
1595     }
1597     /**
1598      * Driver specific commit of real database transaction,
1599      * this can not be used directly in code.
1600      * @return void
1601      */
1602     protected function commit_transaction() {
1603         $this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX);
1604         $result = oci_commit($this->oci);
1605         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1606         $this->query_end($result);
1607     }
1609     /**
1610      * Driver specific abort of real database transaction,
1611      * this can not be used directly in code.
1612      * @return void
1613      */
1614     protected function rollback_transaction() {
1615         $this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX);
1616         $result = oci_rollback($this->oci);
1617         $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1618         $this->query_end($result);
1619     }