Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Native oci class representing moodle database interface.
  19   *
  20   * @package    core_dml
  21   * @copyright  2008 Petr Skoda (http://skodak.org)
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once (__DIR__.'/moodle_database.php');
  28  require_once (__DIR__.'/oci_native_moodle_recordset.php');
  29  require_once (__DIR__.'/oci_native_moodle_temptables.php');
  30  
  31  /**
  32   * Native oci class representing moodle database interface.
  33   *
  34   * One complete reference for PHP + OCI:
  35   * http://www.oracle.com/technology/tech/php/underground-php-oracle-manual.html
  36   *
  37   * @package    core_dml
  38   * @copyright  2008 Petr Skoda (http://skodak.org)
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class oci_native_moodle_database extends moodle_database {
  42  
  43      protected $oci     = null;
  44  
  45      /** @var To store stmt errors and enable get_last_error() to detect them.*/
  46      private $last_stmt_error = null;
  47      /** @var Default value initialised in connect method, we need the driver to be present.*/
  48      private $commit_status = null;
  49  
  50      /** @var To handle oci driver default verbosity.*/
  51      private $last_error_reporting;
  52      /** @var To store unique_session_id. Needed for temp tables unique naming.*/
  53      private $unique_session_id;
  54  
  55      /**
  56       * Detects if all needed PHP stuff installed.
  57       * Note: can be used before connect()
  58       * @return mixed true if ok, string if something
  59       */
  60      public function driver_installed() {
  61          if (!extension_loaded('oci8')) {
  62              return get_string('ociextensionisnotpresentinphp', 'install');
  63          }
  64          return true;
  65      }
  66  
  67      /**
  68       * Returns database family type - describes SQL dialect
  69       * Note: can be used before connect()
  70       * @return string db family name (mysql, postgres, mssql, oracle, etc.)
  71       */
  72      public function get_dbfamily() {
  73          return 'oracle';
  74      }
  75  
  76      /**
  77       * Returns more specific database driver type
  78       * Note: can be used before connect()
  79       * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
  80       */
  81      protected function get_dbtype() {
  82          return 'oci';
  83      }
  84  
  85      /**
  86       * Returns general database library name
  87       * Note: can be used before connect()
  88       * @return string db type pdo, native
  89       */
  90      protected function get_dblibrary() {
  91          return 'native';
  92      }
  93  
  94      /**
  95       * Returns localised database type name
  96       * Note: can be used before connect()
  97       * @return string
  98       */
  99      public function get_name() {
 100          return get_string('nativeoci', 'install');
 101      }
 102  
 103      /**
 104       * Returns localised database configuration help.
 105       * Note: can be used before connect()
 106       * @return string
 107       */
 108      public function get_configuration_help() {
 109          return get_string('nativeocihelp', 'install');
 110      }
 111  
 112      /**
 113       * Diagnose database and tables, this function is used
 114       * to verify database and driver settings, db engine types, etc.
 115       *
 116       * @return string null means everything ok, string means problem found.
 117       */
 118      public function diagnose() {
 119          return null;
 120      }
 121  
 122      /**
 123       * Connect to db
 124       * Must be called before other methods.
 125       * @param string $dbhost The database host.
 126       * @param string $dbuser The database username.
 127       * @param string $dbpass The database username's password.
 128       * @param string $dbname The name of the database being connected to.
 129       * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
 130       * @param array $dboptions driver specific options
 131       * @return bool true
 132       * @throws dml_connection_exception if error
 133       */
 134      public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
 135          if ($prefix == '' and !$this->external) {
 136              //Enforce prefixes for everybody but mysql
 137              throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
 138          }
 139          if (!$this->external and strlen($prefix) > 2) {
 140              //Max prefix length for Oracle is 2cc
 141              $a = (object)array('dbfamily'=>'oracle', 'maxlength'=>2);
 142              throw new dml_exception('prefixtoolong', $a);
 143          }
 144  
 145          $driverstatus = $this->driver_installed();
 146  
 147          if ($driverstatus !== true) {
 148              throw new dml_exception('dbdriverproblem', $driverstatus);
 149          }
 150  
 151          // Autocommit ON by default.
 152          // Switching to OFF (OCI_DEFAULT), when playing with transactions
 153          // please note this thing is not defined if oracle driver not present in PHP
 154          // which means it can not be used as default value of object property!
 155          $this->commit_status = OCI_COMMIT_ON_SUCCESS;
 156  
 157          $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
 158          unset($this->dboptions['dbsocket']);
 159  
 160          // NOTE: use of ', ", / and \ is very problematic, even native oracle tools seem to have
 161          //       problems with these, so just forget them and do not report problems into tracker...
 162  
 163          if (empty($this->dbhost)) {
 164              // old style full address (TNS)
 165              $dbstring = $this->dbname;
 166          } else {
 167              if (empty($this->dboptions['dbport'])) {
 168                  $this->dboptions['dbport'] = 1521;
 169              }
 170              $dbstring = '//'.$this->dbhost.':'.$this->dboptions['dbport'].'/'.$this->dbname;
 171          }
 172  
 173          ob_start();
 174          if (empty($this->dboptions['dbpersist'])) {
 175              $this->oci = oci_new_connect($this->dbuser, $this->dbpass, $dbstring, 'AL32UTF8');
 176          } else {
 177              $this->oci = oci_pconnect($this->dbuser, $this->dbpass, $dbstring, 'AL32UTF8');
 178          }
 179          $dberr = ob_get_contents();
 180          ob_end_clean();
 181  
 182  
 183          if ($this->oci === false) {
 184              $this->oci = null;
 185              $e = oci_error();
 186              if (isset($e['message'])) {
 187                  $dberr = $e['message'];
 188              }
 189              throw new dml_connection_exception($dberr);
 190          }
 191  
 192          // Disable logging until we are fully setup.
 193          $this->query_log_prevent();
 194  
 195          // Make sure moodle package is installed - now required.
 196          if (!$this->oci_package_installed()) {
 197              try {
 198                  $this->attempt_oci_package_install();
 199              } catch (Exception $e) {
 200                  // Ignore problems, only the result counts,
 201                  // admins have to fix it manually if necessary.
 202              }
 203              if (!$this->oci_package_installed()) {
 204                  throw new dml_exception('dbdriverproblem', 'Oracle PL/SQL Moodle support package MOODLELIB is not installed! Database administrator has to execute /lib/dml/oci_native_moodle_package.sql script.');
 205              }
 206          }
 207  
 208          // get unique session id, to be used later for temp tables stuff
 209          $sql = 'SELECT DBMS_SESSION.UNIQUE_SESSION_ID() FROM DUAL';
 210          $this->query_start($sql, null, SQL_QUERY_AUX);
 211          $stmt = $this->parse_query($sql);
 212          $result = oci_execute($stmt, $this->commit_status);
 213          $this->query_end($result, $stmt);
 214          $records = null;
 215          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
 216          oci_free_statement($stmt);
 217          $this->unique_session_id = reset($records[0]);
 218  
 219          //note: do not send "ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'" !
 220          //      instead fix our PHP code to convert "," to "." properly!
 221  
 222          // We can enable logging now.
 223          $this->query_log_allow();
 224  
 225          // Connection stabilised and configured, going to instantiate the temptables controller
 226          $this->temptables = new oci_native_moodle_temptables($this, $this->unique_session_id);
 227  
 228          return true;
 229      }
 230  
 231      /**
 232       * Close database connection and release all resources
 233       * and memory (especially circular memory references).
 234       * Do NOT use connect() again, create a new instance if needed.
 235       */
 236      public function dispose() {
 237          parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
 238          if ($this->oci) {
 239              oci_close($this->oci);
 240              $this->oci = null;
 241          }
 242      }
 243  
 244  
 245      /**
 246       * Called before each db query.
 247       * @param string $sql
 248       * @param array|null $params An array of parameters.
 249       * @param int $type type of query
 250       * @param mixed $extrainfo driver specific extra information
 251       * @return void
 252       */
 253      protected function query_start($sql, ?array $params, $type, $extrainfo=null) {
 254          parent::query_start($sql, $params, $type, $extrainfo);
 255          // oci driver tents to send debug to output, we do not need that ;-)
 256          $this->last_error_reporting = error_reporting(0);
 257      }
 258  
 259      /**
 260       * Called immediately after each db query.
 261       * @param mixed db specific result
 262       * @return void
 263       */
 264      protected function query_end($result, $stmt=null) {
 265          // reset original debug level
 266          error_reporting($this->last_error_reporting);
 267          if ($stmt and $result === false) {
 268              // Look for stmt error and store it
 269              if (is_resource($stmt)) {
 270                  $e = oci_error($stmt);
 271                  if ($e !== false) {
 272                      $this->last_stmt_error = $e['message'];
 273                  }
 274              }
 275              oci_free_statement($stmt);
 276          }
 277          parent::query_end($result);
 278      }
 279  
 280      /**
 281       * Returns database server info array
 282       * @return array Array containing 'description' and 'version' info
 283       */
 284      public function get_server_info() {
 285          static $info = null; // TODO: move to real object property
 286  
 287          if (is_null($info)) {
 288              $this->query_start("--oci_server_version()", null, SQL_QUERY_AUX);
 289              $description = oci_server_version($this->oci);
 290              $this->query_end(true);
 291              preg_match('/(\d+\.)+\d+/', $description, $matches);
 292              $info = array('description'=>$description, 'version'=>$matches[0]);
 293          }
 294  
 295          return $info;
 296      }
 297  
 298      /**
 299       * Converts short table name {tablename} to real table name
 300       * supporting temp tables ($this->unique_session_id based) if detected
 301       *
 302       * @param string sql
 303       * @return string sql
 304       */
 305      protected function fix_table_names($sql) {
 306          if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
 307              foreach($matches[0] as $key=>$match) {
 308                  $name = $matches[1][$key];
 309                  if ($this->temptables && $this->temptables->is_temptable($name)) {
 310                      $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
 311                  } else {
 312                      $sql = str_replace($match, $this->prefix.$name, $sql);
 313                  }
 314              }
 315          }
 316          return $sql;
 317      }
 318  
 319      /**
 320       * Returns supported query parameter types
 321       * @return int bitmask of accepted SQL_PARAMS_*
 322       */
 323      protected function allowed_param_types() {
 324          return SQL_PARAMS_NAMED;
 325      }
 326  
 327      /**
 328       * Returns last error reported by database engine.
 329       * @return string error message
 330       */
 331      public function get_last_error() {
 332          $error = false;
 333          // First look for any previously saved stmt error
 334          if (!empty($this->last_stmt_error)) {
 335              $error = $this->last_stmt_error;
 336              $this->last_stmt_error = null;
 337          } else { // Now try connection error
 338              $e = oci_error($this->oci);
 339              if ($e !== false) {
 340                  $error = $e['message'];
 341              }
 342          }
 343          return $error;
 344      }
 345  
 346      /**
 347       * Prepare the statement for execution
 348       *
 349       * @param string $sql
 350       * @return resource
 351       *
 352       * @throws dml_exception
 353       */
 354      protected function parse_query($sql) {
 355          $stmt = oci_parse($this->oci, $sql);
 356          if ($stmt == false) {
 357              throw new dml_exception('dmlparseexception', null, $this->get_last_error());
 358          }
 359          return $stmt;
 360      }
 361  
 362      /**
 363       * Make sure there are no reserved words in param names...
 364       * @param string $sql
 365       * @param array $params
 366       * @return array ($sql, $params) updated query and parameters
 367       */
 368      protected function tweak_param_names($sql, array $params) {
 369          if (empty($params)) {
 370              return array($sql, $params);
 371          }
 372  
 373          $newparams = array();
 374          $searcharr = array(); // search => replace pairs
 375          foreach ($params as $name => $value) {
 376              // Keep the name within the 30 chars limit always (prefixing/replacing)
 377              if (strlen($name) <= 28) {
 378                  $newname = 'o_' . $name;
 379              } else {
 380                  $newname = 'o_' . substr($name, 2);
 381              }
 382              $newparams[$newname] = $value;
 383              $searcharr[':' . $name] = ':' . $newname;
 384          }
 385          // sort by length desc to avoid potential str_replace() overlap
 386          uksort($searcharr, array('oci_native_moodle_database', 'compare_by_length_desc'));
 387  
 388          $sql = str_replace(array_keys($searcharr), $searcharr, $sql);
 389          return array($sql, $newparams);
 390      }
 391  
 392      /**
 393       * Return tables in database WITHOUT current prefix
 394       * @param bool $usecache if true, returns list of cached tables.
 395       * @return array of table names in lowercase and without prefix
 396       */
 397      public function get_tables($usecache=true) {
 398          if ($usecache and $this->tables !== null) {
 399              return $this->tables;
 400          }
 401          $this->tables = array();
 402          $prefix = str_replace('_', "\\_", strtoupper($this->prefix));
 403          $sql = "SELECT TABLE_NAME
 404                    FROM CAT
 405                   WHERE TABLE_TYPE='TABLE'
 406                         AND TABLE_NAME NOT LIKE 'BIN\$%'
 407                         AND TABLE_NAME LIKE '$prefix%' ESCAPE '\\'";
 408          $this->query_start($sql, null, SQL_QUERY_AUX);
 409          $stmt = $this->parse_query($sql);
 410          $result = oci_execute($stmt, $this->commit_status);
 411          $this->query_end($result, $stmt);
 412          $records = null;
 413          oci_fetch_all($stmt, $records, 0, -1, OCI_ASSOC);
 414          oci_free_statement($stmt);
 415          $records = array_map('strtolower', $records['TABLE_NAME']);
 416          foreach ($records as $tablename) {
 417              if ($this->prefix !== false && $this->prefix !== '') {
 418                  if (strpos($tablename, $this->prefix) !== 0) {
 419                      continue;
 420                  }
 421                  $tablename = substr($tablename, strlen($this->prefix));
 422              }
 423              $this->tables[$tablename] = $tablename;
 424          }
 425  
 426          // Add the currently available temptables
 427          $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
 428  
 429          return $this->tables;
 430      }
 431  
 432      /**
 433       * Return table indexes - everything lowercased.
 434       * @param string $table The table we want to get indexes from.
 435       * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
 436       */
 437      public function get_indexes($table) {
 438          $indexes = array();
 439          $tablename = strtoupper($this->prefix.$table);
 440  
 441          $sql = "SELECT i.INDEX_NAME, i.INDEX_TYPE, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, e.COLUMN_EXPRESSION, ac.CONSTRAINT_TYPE
 442                    FROM ALL_INDEXES i
 443                    JOIN ALL_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME
 444               LEFT JOIN ALL_IND_EXPRESSIONS e ON (e.INDEX_NAME = c.INDEX_NAME AND e.COLUMN_POSITION = c.COLUMN_POSITION)
 445               LEFT JOIN ALL_CONSTRAINTS ac ON (ac.TABLE_NAME=i.TABLE_NAME AND ac.CONSTRAINT_NAME=i.INDEX_NAME AND ac.CONSTRAINT_TYPE='P')
 446                   WHERE i.TABLE_NAME = '$tablename'
 447                ORDER BY i.INDEX_NAME, c.COLUMN_POSITION";
 448  
 449          $stmt = $this->parse_query($sql);
 450          $result = oci_execute($stmt, $this->commit_status);
 451          $this->query_end($result, $stmt);
 452          $records = null;
 453          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
 454          oci_free_statement($stmt);
 455  
 456          foreach ($records as $record) {
 457              if ($record['CONSTRAINT_TYPE'] === 'P') {
 458                  //ignore for now;
 459                  continue;
 460              }
 461              $indexname = strtolower($record['INDEX_NAME']);
 462              if (!isset($indexes[$indexname])) {
 463                  $indexes[$indexname] = array('primary' => ($record['CONSTRAINT_TYPE'] === 'P'),
 464                                               'unique'  => ($record['UNIQUENESS'] === 'UNIQUE'),
 465                                               'columns' => array());
 466              }
 467  
 468              // If this is an unique, function-based, index, then we have to look to the expression
 469              // and calculate the column name by parsing it.
 470              if ($record['UNIQUENESS'] === 'UNIQUE' && $record['INDEX_TYPE'] === 'FUNCTION-BASED NORMAL') {
 471                  // Only if there is an expression to look.
 472                  if (!empty($record['COLUMN_EXPRESSION'])) {
 473                      // Let's parse the usual code used for these unique indexes.
 474                      $regex = '/^CASE *WHEN .* THEN "(?<column_name>[^"]+)" ELSE NULL END *$/';
 475                      if (preg_match($regex, $record['COLUMN_EXPRESSION'], $matches)) {
 476                          $record['COLUMN_NAME'] = $matches['column_name'] ?? $record['COLUMN_NAME'];
 477                      }
 478                  }
 479              }
 480  
 481              $indexes[$indexname]['columns'][] = strtolower($record['COLUMN_NAME']);
 482          }
 483  
 484          return $indexes;
 485      }
 486  
 487      /**
 488       * Fetches detailed information about columns in table.
 489       *
 490       * @param string $table name
 491       * @return array array of database_column_info objects indexed with column names
 492       */
 493      protected function fetch_columns(string $table): array {
 494          $structure = array();
 495  
 496          // We give precedence to CHAR_LENGTH for VARCHAR2 columns over WIDTH because the former is always
 497          // BYTE based and, for cross-db operations, we want CHAR based results. See MDL-29415
 498          // Instead of guessing sequence based exclusively on name, check tables against user_triggers to
 499          // ensure the table has a 'before each row' trigger to assume 'id' is auto_increment. MDL-32365
 500          $sql = "SELECT CNAME, COLTYPE, nvl(CHAR_LENGTH, WIDTH) AS WIDTH, SCALE, PRECISION, NULLS, DEFAULTVAL,
 501                    DECODE(NVL(TRIGGER_NAME, '0'), '0', '0', '1') HASTRIGGER
 502                    FROM COL c
 503               LEFT JOIN USER_TAB_COLUMNS u ON (u.TABLE_NAME = c.TNAME AND u.COLUMN_NAME = c.CNAME AND u.DATA_TYPE = 'VARCHAR2')
 504               LEFT JOIN USER_TRIGGERS t ON (t.TABLE_NAME = c.TNAME AND TRIGGER_TYPE = 'BEFORE EACH ROW' AND c.CNAME = 'ID')
 505                   WHERE TNAME = UPPER('{" . $table . "}')
 506                ORDER BY COLNO";
 507  
 508          list($sql, $params, $type) = $this->fix_sql_params($sql, null);
 509  
 510          $this->query_start($sql, null, SQL_QUERY_AUX);
 511          $stmt = $this->parse_query($sql);
 512          $result = oci_execute($stmt, $this->commit_status);
 513          $this->query_end($result, $stmt);
 514          $records = null;
 515          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
 516          oci_free_statement($stmt);
 517  
 518          if (!$records) {
 519              return array();
 520          }
 521          foreach ($records as $rawcolumn) {
 522              $rawcolumn = (object)$rawcolumn;
 523  
 524              $info = new stdClass();
 525              $info->name = strtolower($rawcolumn->CNAME);
 526              $info->auto_increment = ((int)$rawcolumn->HASTRIGGER) ? true : false;
 527              $matches = null;
 528  
 529              if ($rawcolumn->COLTYPE === 'VARCHAR2'
 530               or $rawcolumn->COLTYPE === 'VARCHAR'
 531               or $rawcolumn->COLTYPE === 'NVARCHAR2'
 532               or $rawcolumn->COLTYPE === 'NVARCHAR'
 533               or $rawcolumn->COLTYPE === 'CHAR'
 534               or $rawcolumn->COLTYPE === 'NCHAR') {
 535                  $info->type          = $rawcolumn->COLTYPE;
 536                  $info->meta_type     = 'C';
 537                  $info->max_length    = $rawcolumn->WIDTH;
 538                  $info->scale         = null;
 539                  $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
 540                  $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
 541                  if ($info->has_default) {
 542  
 543                      // this is hacky :-(
 544                      if ($rawcolumn->DEFAULTVAL === 'NULL') {
 545                          $info->default_value = null;
 546                      } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
 547                          $info->default_value = "";
 548                      } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
 549                          $info->default_value = "";
 550                      } else {
 551                          $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
 552                          $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
 553                      }
 554                  } else {
 555                      $info->default_value = null;
 556                  }
 557                  $info->primary_key   = false;
 558                  $info->binary        = false;
 559                  $info->unsigned      = null;
 560                  $info->unique        = null;
 561  
 562              } else if ($rawcolumn->COLTYPE === 'NUMBER') {
 563                  $info->type       = $rawcolumn->COLTYPE;
 564                  $info->max_length = $rawcolumn->PRECISION;
 565                  $info->binary     = false;
 566                  if (!is_null($rawcolumn->SCALE) && $rawcolumn->SCALE == 0) { // null in oracle scale allows decimals => not integer
 567                      // integer
 568                      if ($info->name === 'id') {
 569                          $info->primary_key   = true;
 570                          $info->meta_type     = 'R';
 571                          $info->unique        = true;
 572                          $info->has_default   = false;
 573                      } else {
 574                          $info->primary_key   = false;
 575                          $info->meta_type     = 'I';
 576                          $info->unique        = null;
 577                      }
 578                      $info->scale = 0;
 579  
 580                  } else {
 581                      //float
 582                      $info->meta_type     = 'N';
 583                      $info->primary_key   = false;
 584                      $info->unsigned      = null;
 585                      $info->unique        = null;
 586                      $info->scale         = $rawcolumn->SCALE;
 587                  }
 588                  $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
 589                  $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
 590                  if ($info->has_default) {
 591                      $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
 592                  } else {
 593                      $info->default_value = null;
 594                  }
 595  
 596              } else if ($rawcolumn->COLTYPE === 'FLOAT') {
 597                  $info->type       = $rawcolumn->COLTYPE;
 598                  $info->max_length = (int)($rawcolumn->PRECISION * 3.32193);
 599                  $info->primary_key   = false;
 600                  $info->meta_type     = 'N';
 601                  $info->unique        = null;
 602                  $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
 603                  $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
 604                  if ($info->has_default) {
 605                      $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
 606                  } else {
 607                      $info->default_value = null;
 608                  }
 609  
 610              } else if ($rawcolumn->COLTYPE === 'CLOB'
 611                      or $rawcolumn->COLTYPE === 'NCLOB') {
 612                  $info->type          = $rawcolumn->COLTYPE;
 613                  $info->meta_type     = 'X';
 614                  $info->max_length    = -1;
 615                  $info->scale         = null;
 616                  $info->scale         = null;
 617                  $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
 618                  $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
 619                  if ($info->has_default) {
 620                      // this is hacky :-(
 621                      if ($rawcolumn->DEFAULTVAL === 'NULL') {
 622                          $info->default_value = null;
 623                      } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
 624                          $info->default_value = "";
 625                      } else if ($rawcolumn->DEFAULTVAL === "' '") { // Other times it's stored without trailing space
 626                          $info->default_value = "";
 627                      } else {
 628                          $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
 629                          $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
 630                      }
 631                  } else {
 632                      $info->default_value = null;
 633                  }
 634                  $info->primary_key   = false;
 635                  $info->binary        = false;
 636                  $info->unsigned      = null;
 637                  $info->unique        = null;
 638  
 639              } else if ($rawcolumn->COLTYPE === 'BLOB') {
 640                  $info->type          = $rawcolumn->COLTYPE;
 641                  $info->meta_type     = 'B';
 642                  $info->max_length    = -1;
 643                  $info->scale         = null;
 644                  $info->scale         = null;
 645                  $info->not_null      = ($rawcolumn->NULLS === 'NOT NULL');
 646                  $info->has_default   = !is_null($rawcolumn->DEFAULTVAL);
 647                  if ($info->has_default) {
 648                      // this is hacky :-(
 649                      if ($rawcolumn->DEFAULTVAL === 'NULL') {
 650                          $info->default_value = null;
 651                      } else if ($rawcolumn->DEFAULTVAL === "' ' ") { // Sometimes it's stored with trailing space
 652                          $info->default_value = "";
 653                      } else if ($rawcolumn->DEFAULTVAL === "' '") { // Sometimes it's stored without trailing space
 654                          $info->default_value = "";
 655                      } else {
 656                          $info->default_value = trim($rawcolumn->DEFAULTVAL); // remove trailing space
 657                          $info->default_value = substr($info->default_value, 1, strlen($info->default_value)-2); //trim ''
 658                      }
 659                  } else {
 660                      $info->default_value = null;
 661                  }
 662                  $info->primary_key   = false;
 663                  $info->binary        = true;
 664                  $info->unsigned      = null;
 665                  $info->unique        = null;
 666  
 667              } else {
 668                  // unknown type - sorry
 669                  $info->type          = $rawcolumn->COLTYPE;
 670                  $info->meta_type     = '?';
 671              }
 672  
 673              $structure[$info->name] = new database_column_info($info);
 674          }
 675  
 676          return $structure;
 677      }
 678  
 679      /**
 680       * Normalise values based in RDBMS dependencies (booleans, LOBs...)
 681       *
 682       * @param database_column_info $column column metadata corresponding with the value we are going to normalise
 683       * @param mixed $value value we are going to normalise
 684       * @return mixed the normalised value
 685       */
 686      protected function normalise_value($column, $value) {
 687          $this->detect_objects($value);
 688  
 689          if (is_bool($value)) { // Always, convert boolean to int
 690              $value = (int)$value;
 691  
 692          } else if ($column->meta_type == 'B') { // BLOB detected, we return 'blob' array instead of raw value to allow
 693              if (!is_null($value)) {             // binding/executing code later to know about its nature
 694                  $value = array('blob' => $value);
 695              }
 696  
 697          } else if ($column->meta_type == 'X' && strlen($value) > 4000) { // CLOB detected (>4000 optimisation), we return 'clob'
 698              if (!is_null($value)) {                                      // array instead of raw value to allow binding/
 699                  $value = array('clob' => (string)$value);                // executing code later to know about its nature
 700              }
 701  
 702          } else if ($value === '') {
 703              if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
 704                  $value = 0; // prevent '' problems in numeric fields
 705              }
 706          }
 707          return $value;
 708      }
 709  
 710      /**
 711       * Transforms the sql and params in order to emulate the LIMIT clause available in other DBs
 712       *
 713       * @param string $sql the SQL select query to execute.
 714       * @param array $params array of sql parameters
 715       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 716       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 717       * @return array with the transformed sql and params updated
 718       */
 719      private function get_limit_sql($sql, array $params = null, $limitfrom=0, $limitnum=0) {
 720  
 721          list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 722          // TODO: Add the /*+ FIRST_ROWS */ hint if there isn't another hint
 723  
 724          if ($limitfrom and $limitnum) {
 725              $sql = "SELECT oracle_o.*
 726                        FROM (SELECT oracle_i.*, rownum AS oracle_rownum
 727                                FROM ($sql) oracle_i
 728                               WHERE rownum <= :oracle_num_rows
 729                              ) oracle_o
 730                       WHERE oracle_rownum > :oracle_skip_rows";
 731              $params['oracle_num_rows'] = $limitfrom + $limitnum;
 732              $params['oracle_skip_rows'] = $limitfrom;
 733  
 734          } else if ($limitfrom and !$limitnum) {
 735              $sql = "SELECT oracle_o.*
 736                        FROM (SELECT oracle_i.*, rownum AS oracle_rownum
 737                                FROM ($sql) oracle_i
 738                              ) oracle_o
 739                       WHERE oracle_rownum > :oracle_skip_rows";
 740              $params['oracle_skip_rows'] = $limitfrom;
 741  
 742          } else if (!$limitfrom and $limitnum) {
 743              $sql = "SELECT *
 744                        FROM ($sql)
 745                       WHERE rownum <= :oracle_num_rows";
 746              $params['oracle_num_rows'] = $limitnum;
 747          }
 748  
 749          return array($sql, $params);
 750      }
 751  
 752      /**
 753       * This function will handle all the column values before being inserted/updated to DB for Oracle
 754       * installations. This is because the "special feature" of Oracle where the empty string is
 755       * equal to NULL and this presents a problem with all our currently NOT NULL default '' fields.
 756       * (and with empties handling in general)
 757       *
 758       * Note that this function is 100% private and should be used, exclusively by DML functions
 759       * in this file. Also, this is considered a DIRTY HACK to be removed when possible.
 760       *
 761       * This function is private and must not be used outside this driver at all
 762       *
 763       * @param $table string the table where the record is going to be inserted/updated (without prefix)
 764       * @param $field string the field where the record is going to be inserted/updated
 765       * @param $value mixed the value to be inserted/updated
 766       */
 767      private function oracle_dirty_hack ($table, $field, $value) {
 768  
 769          // General bound parameter, just hack the spaces and pray it will work.
 770          if (!$table) {
 771              if ($value === '') {
 772                  return ' ';
 773              } else if (is_bool($value)) {
 774                  return (int)$value;
 775              } else {
 776                  return $value;
 777              }
 778          }
 779  
 780          // Get metadata
 781          $columns = $this->get_columns($table);
 782          if (!isset($columns[$field])) {
 783              if ($value === '') {
 784                  return ' ';
 785              } else if (is_bool($value)) {
 786                  return (int)$value;
 787              } else {
 788                  return $value;
 789              }
 790          }
 791          $column = $columns[$field];
 792  
 793          // !! This paragraph explains behaviour before Moodle 2.0:
 794          //
 795          // For Oracle DB, empty strings are converted to NULLs in DB
 796          // and this breaks a lot of NOT NULL columns currently Moodle. In the future it's
 797          // planned to move some of them to NULL, if they must accept empty values and this
 798          // piece of code will become less and less used. But, for now, we need it.
 799          // What we are going to do is to examine all the data being inserted and if it's
 800          // an empty string (NULL for Oracle) and the field is defined as NOT NULL, we'll modify
 801          // such data in the best form possible ("0" for booleans and numbers and " " for the
 802          // rest of strings. It isn't optimal, but the only way to do so.
 803          // In the opposite, when retrieving records from Oracle, we'll decode " " back to
 804          // empty strings to allow everything to work properly. DIRTY HACK.
 805  
 806          // !! These paragraphs explain the rationale about the change for Moodle 2.5:
 807          //
 808          // Before Moodle 2.0, we only used to apply this DIRTY HACK to NOT NULL columns, as
 809          // stated above, but it causes one problem in NULL columns where both empty strings
 810          // and real NULLs are stored as NULLs, being impossible to differentiate them when
 811          // being retrieved from DB.
 812          //
 813          // So, starting with Moodle 2.0, we are going to apply the DIRTY HACK to all the
 814          // CHAR/CLOB columns no matter of their nullability. That way, when retrieving
 815          // NULLABLE fields we'll get proper empties and NULLs differentiated, so we'll be able
 816          // to rely in NULL/empty/content contents without problems, until now that wasn't
 817          // possible at all.
 818          //
 819          // One space DIRTY HACK is now applied automatically for all query parameters
 820          // and results. The only problem is string concatenation where the glue must
 821          // be specified as "' '" sql fragment.
 822          //
 823          // !! Conclusions:
 824          //
 825          // From Moodle 2.5 onwards, ALL empty strings in Oracle DBs will be stored as
 826          // 1-whitespace char, ALL NULLs as NULLs and, obviously, content as content. And
 827          // those 1-whitespace chars will be converted back to empty strings by all the
 828          // get_field/record/set() functions transparently and any SQL needing direct handling
 829          // of empties will have to use placeholders or sql_isempty() helper function.
 830  
 831          // If the field isn't VARCHAR or CLOB, skip
 832          if ($column->meta_type != 'C' and $column->meta_type != 'X') {
 833              return $value;
 834          }
 835  
 836          // If the value isn't empty, skip
 837          if (!empty($value)) {
 838              return $value;
 839          }
 840  
 841          // Now, we have one empty value, going to be inserted to one VARCHAR2 or CLOB field
 842          // Try to get the best value to be inserted
 843  
 844          // The '0' string doesn't need any transformation, skip
 845          if ($value === '0') {
 846              return $value;
 847          }
 848  
 849          // Transformations start
 850          if (gettype($value) == 'boolean') {
 851              return '0'; // Transform false to '0' that evaluates the same for PHP
 852  
 853          } else if (gettype($value) == 'integer') {
 854              return '0'; // Transform 0 to '0' that evaluates the same for PHP
 855  
 856          } else if ($value === '') {
 857              return ' '; // Transform '' to ' ' that DON'T EVALUATE THE SAME
 858                          // (we'll transform back again on get_records_XXX functions and others)!!
 859          }
 860  
 861          // Fail safe to original value
 862          return $value;
 863      }
 864  
 865      /**
 866       * Helper function to order by string length desc
 867       *
 868       * @param $a string first element to compare
 869       * @param $b string second element to compare
 870       * @return int < 0 $a goes first (is less), 0 $b goes first, 0 doesn't matter
 871       */
 872      private function compare_by_length_desc($a, $b) {
 873          return strlen($b) - strlen($a);
 874      }
 875  
 876      /**
 877       * Is db in unicode mode?
 878       * @return bool
 879       */
 880      public function setup_is_unicodedb() {
 881          $sql = "SELECT VALUE
 882                    FROM NLS_DATABASE_PARAMETERS
 883                   WHERE PARAMETER = 'NLS_CHARACTERSET'";
 884          $this->query_start($sql, null, SQL_QUERY_AUX);
 885          $stmt = $this->parse_query($sql);
 886          $result = oci_execute($stmt, $this->commit_status);
 887          $this->query_end($result, $stmt);
 888          $records = null;
 889          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
 890          oci_free_statement($stmt);
 891  
 892          return (isset($records['VALUE'][0]) and $records['VALUE'][0] === 'AL32UTF8');
 893      }
 894  
 895      /**
 896       * Do NOT use in code, to be used by database_manager only!
 897       * @param string|array $sql query
 898       * @param array|null $tablenames an array of xmldb table names affected by this request.
 899       * @return bool true
 900       * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
 901       */
 902      public function change_database_structure($sql, $tablenames = null) {
 903          $this->get_manager(); // Includes DDL exceptions classes ;-)
 904          $sqls = (array)$sql;
 905  
 906          try {
 907              foreach ($sqls as $sql) {
 908                  $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
 909                  $stmt = $this->parse_query($sql);
 910                  $result = oci_execute($stmt, $this->commit_status);
 911                  $this->query_end($result, $stmt);
 912                  oci_free_statement($stmt);
 913              }
 914          } catch (ddl_change_structure_exception $e) {
 915              $this->reset_caches($tablenames);
 916              throw $e;
 917          }
 918  
 919          $this->reset_caches($tablenames);
 920          return true;
 921      }
 922  
 923      protected function bind_params($stmt, array &$params=null, $tablename=null, array &$descriptors = null) {
 924          if ($params) {
 925              $columns = array();
 926              if ($tablename) {
 927                  $columns = $this->get_columns($tablename);
 928              }
 929              foreach($params as $key => $value) {
 930                  // Decouple column name and param name as far as sometimes they aren't the same
 931                  if ($key == 'o_newfieldtoset') { // found case where column and key diverge, handle that
 932                      $columnname   = key($value);    // columnname is the key of the array
 933                      $params[$key] = $value[$columnname]; // set the proper value in the $params array and
 934                      $value        = $value[$columnname]; // set the proper value in the $value variable
 935                  } else {
 936                      $columnname = preg_replace('/^o_/', '', $key); // Default columnname (for DB introspecting is key), but...
 937                  }
 938                  // Continue processing
 939                  // Now, handle already detected LOBs
 940                  if (is_array($value)) { // Let's go to bind special cases (lob descriptors)
 941                      if (isset($value['clob'])) {
 942                          $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
 943                          if ($descriptors === null) {
 944                              throw new coding_exception('moodle_database::bind_params() $descriptors not specified for clob');
 945                          }
 946                          $descriptors[] = $lob;
 947                          oci_bind_by_name($stmt, $key, $lob, -1, SQLT_CLOB);
 948                          $lob->writeTemporary($this->oracle_dirty_hack($tablename, $columnname, $params[$key]['clob']), OCI_TEMP_CLOB);
 949                          continue; // Column binding finished, go to next one
 950                      } else if (isset($value['blob'])) {
 951                          $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
 952                          if ($descriptors === null) {
 953                              throw new coding_exception('moodle_database::bind_params() $descriptors not specified for clob');
 954                          }
 955                          $descriptors[] = $lob;
 956                          oci_bind_by_name($stmt, $key, $lob, -1, SQLT_BLOB);
 957                          $lob->writeTemporary($params[$key]['blob'], OCI_TEMP_BLOB);
 958                          continue; // Column binding finished, go to next one
 959                      }
 960                  } else {
 961                      // If, at this point, the param value > 4000 (bytes), let's assume it's a clob
 962                      // passed in an arbitrary sql (not processed by normalise_value() ever,
 963                      // and let's handle it as such. This will provide proper binding of CLOBs in
 964                      // conditions and other raw SQLs not covered by the above function.
 965                      if (strlen($value) > 4000) {
 966                          $lob = oci_new_descriptor($this->oci, OCI_DTYPE_LOB);
 967                          if ($descriptors === null) {
 968                              throw new coding_exception('moodle_database::bind_params() $descriptors not specified for clob');
 969                          }
 970                          $descriptors[] = $lob;
 971                          oci_bind_by_name($stmt, $key, $lob, -1, SQLT_CLOB);
 972                          $lob->writeTemporary($this->oracle_dirty_hack($tablename, $columnname, $params[$key]), OCI_TEMP_CLOB);
 973                          continue; // Param binding finished, go to next one.
 974                      }
 975                  }
 976                  // TODO: Put proper types and length is possible (enormous speedup)
 977                  // Arrived here, continue with standard processing, using metadata if possible
 978                  if (isset($columns[$columnname])) {
 979                      $type = $columns[$columnname]->meta_type;
 980                      $maxlength = $columns[$columnname]->max_length;
 981                  } else {
 982                      $type = '?';
 983                      $maxlength = -1;
 984                  }
 985                  switch ($type) {
 986                      case 'I':
 987                      case 'R':
 988                          // TODO: Optimise
 989                          oci_bind_by_name($stmt, $key, $params[$key]);
 990                          break;
 991  
 992                      case 'N':
 993                      case 'F':
 994                          // TODO: Optimise
 995                          oci_bind_by_name($stmt, $key, $params[$key]);
 996                          break;
 997  
 998                      case 'B':
 999                          // TODO: Only arrive here if BLOB is null: Bind if so, else exception!
1000                          // don't break here
1001  
1002                      case 'X':
1003                          // TODO: Only arrive here if CLOB is null or <= 4000 cc, else exception
1004                          // don't break here
1005  
1006                      default: // Bind as CHAR (applying dirty hack)
1007                          // TODO: Optimise
1008                          $params[$key] = $this->oracle_dirty_hack($tablename, $columnname, $params[$key]);
1009                          // Because of PHP7 bug (https://bugs.php.net/bug.php?id=72524) it seems that it's
1010                          // impossible to bind NULL values in a reliable way, let's use empty string
1011                          // instead in the mean time.
1012                          if ($params[$key] === null && version_compare(PHP_VERSION, '7.0.0', '>=')) {
1013                              $params[$key] = '';
1014                          }
1015                          oci_bind_by_name($stmt, $key, $params[$key]);
1016                  }
1017              }
1018          }
1019          return $descriptors;
1020      }
1021  
1022      protected function free_descriptors($descriptors) {
1023          foreach ($descriptors as $descriptor) {
1024              // Because all descriptors used in the driver come from LOB::writeTemporary() calls
1025              // we can safely close them here unconditionally.
1026              $descriptor->close();
1027              // Free resources.
1028              oci_free_descriptor($descriptor);
1029          }
1030      }
1031  
1032      /**
1033       * This function is used to convert all the Oracle 1-space defaults to the empty string
1034       * like a really DIRTY HACK to allow it to work better until all those NOT NULL DEFAULT ''
1035       * fields will be out from Moodle.
1036       * @param string the string to be converted to '' (empty string) if it's ' ' (one space)
1037       * @param mixed the key of the array in case we are using this function from array_walk,
1038       *              defaults to null for other (direct) uses
1039       * @return boolean always true (the converted variable is returned by reference)
1040       */
1041      public static function onespace2empty(&$item, $key=null) {
1042          $item = ($item === ' ') ? '' : $item;
1043          return true;
1044      }
1045  
1046      /**
1047       * Execute general sql query. Should be used only when no other method suitable.
1048       * Do NOT use this to make changes in db structure, use database_manager methods instead!
1049       * @param string $sql query
1050       * @param array $params query parameters
1051       * @return bool true
1052       * @throws dml_exception A DML specific exception is thrown for any errors.
1053       */
1054      public function execute($sql, array $params=null) {
1055          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1056  
1057          if (strpos($sql, ';') !== false) {
1058              throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
1059          }
1060  
1061          list($sql, $params) = $this->tweak_param_names($sql, $params);
1062          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1063          $stmt = $this->parse_query($sql);
1064          $descriptors = array();
1065          $this->bind_params($stmt, $params, null, $descriptors);
1066          $result = oci_execute($stmt, $this->commit_status);
1067          $this->free_descriptors($descriptors);
1068          $this->query_end($result, $stmt);
1069          oci_free_statement($stmt);
1070  
1071          return true;
1072      }
1073  
1074      /**
1075       * Get a single database record as an object using a SQL statement.
1076       *
1077       * The SQL statement should normally only return one record.
1078       * It is recommended to use get_records_sql() if more matches possible!
1079       *
1080       * @param string $sql The SQL string you wish to be executed, should normally only return one record.
1081       * @param array $params array of sql parameters
1082       * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
1083       *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
1084       *                        MUST_EXIST means throw exception if no record or multiple records found
1085       * @return mixed a fieldset object containing the first matching record, false or exception if error not found depending on mode
1086       * @throws dml_exception A DML specific exception is thrown for any errors.
1087       */
1088      public function get_record_sql($sql, array $params=null, $strictness=IGNORE_MISSING) {
1089          $strictness = (int)$strictness;
1090          if ($strictness == IGNORE_MULTIPLE) {
1091              // do not limit here - ORA does not like that
1092              $rs = $this->get_recordset_sql($sql, $params);
1093              $result = false;
1094              foreach ($rs as $rec) {
1095                  $result = $rec;
1096                  break;
1097              }
1098              $rs->close();
1099              return $result;
1100          }
1101          return parent::get_record_sql($sql, $params, $strictness);
1102      }
1103  
1104      /**
1105       * Get a number of records as a moodle_recordset using a SQL statement.
1106       *
1107       * Since this method is a little less readable, use of it should be restricted to
1108       * code where it's possible there might be large datasets being returned.  For known
1109       * small datasets use get_records_sql - it leads to simpler code.
1110       *
1111       * The return type is like:
1112       * @see function get_recordset.
1113       *
1114       * @param string $sql the SQL select query to execute.
1115       * @param array $params array of sql parameters
1116       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1117       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1118       * @return moodle_recordset instance
1119       * @throws dml_exception A DML specific exception is thrown for any errors.
1120       */
1121      public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1122  
1123          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1124  
1125          list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1126  
1127          list($rawsql, $params) = $this->tweak_param_names($rawsql, $params);
1128          $this->query_start($rawsql, $params, SQL_QUERY_SELECT);
1129          $stmt = $this->parse_query($rawsql);
1130          $descriptors = array();
1131          $this->bind_params($stmt, $params, null, $descriptors);
1132          $result = oci_execute($stmt, $this->commit_status);
1133          $this->free_descriptors($descriptors);
1134          $this->query_end($result, $stmt);
1135  
1136          return $this->create_recordset($stmt);
1137      }
1138  
1139      protected function create_recordset($stmt) {
1140          return new oci_native_moodle_recordset($stmt);
1141      }
1142  
1143      /**
1144       * Get a number of records as an array of objects using a SQL statement.
1145       *
1146       * Return value is like:
1147       * @see function get_records.
1148       *
1149       * @param string $sql the SQL select query to execute. The first column of this SELECT statement
1150       *   must be a unique value (usually the 'id' field), as it will be used as the key of the
1151       *   returned array.
1152       * @param array $params array of sql parameters
1153       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
1154       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
1155       * @return array of objects, or empty array if no records were found
1156       * @throws dml_exception A DML specific exception is thrown for any errors.
1157       */
1158      public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
1159  
1160          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1161  
1162          list($rawsql, $params) = $this->get_limit_sql($sql, $params, $limitfrom, $limitnum);
1163  
1164          list($rawsql, $params) = $this->tweak_param_names($rawsql, $params);
1165          $this->query_start($rawsql, $params, SQL_QUERY_SELECT);
1166          $stmt = $this->parse_query($rawsql);
1167          $descriptors = array();
1168          $this->bind_params($stmt, $params, null, $descriptors);
1169          $result = oci_execute($stmt, $this->commit_status);
1170          $this->free_descriptors($descriptors);
1171          $this->query_end($result, $stmt);
1172  
1173          $records = null;
1174          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1175          oci_free_statement($stmt);
1176  
1177          $return = array();
1178  
1179          foreach ($records as $row) {
1180              $row = array_change_key_case($row, CASE_LOWER);
1181              unset($row['oracle_rownum']);
1182              array_walk($row, array('oci_native_moodle_database', 'onespace2empty'));
1183              $id = reset($row);
1184              if (isset($return[$id])) {
1185                  $colname = key($row);
1186                  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);
1187              }
1188              $return[$id] = (object)$row;
1189          }
1190  
1191          return $return;
1192      }
1193  
1194      /**
1195       * Selects records and return values (first field) as an array using a SQL statement.
1196       *
1197       * @param string $sql The SQL query
1198       * @param array $params array of sql parameters
1199       * @return array of values
1200       * @throws dml_exception A DML specific exception is thrown for any errors.
1201       */
1202      public function get_fieldset_sql($sql, array $params=null) {
1203          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1204  
1205          list($sql, $params) = $this->tweak_param_names($sql, $params);
1206          $this->query_start($sql, $params, SQL_QUERY_SELECT);
1207          $stmt = $this->parse_query($sql);
1208          $descriptors = array();
1209          $this->bind_params($stmt, $params, null, $descriptors);
1210          $result = oci_execute($stmt, $this->commit_status);
1211          $this->free_descriptors($descriptors);
1212          $this->query_end($result, $stmt);
1213  
1214          $records = null;
1215          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_COLUMN);
1216          oci_free_statement($stmt);
1217  
1218          $return = reset($records);
1219          array_walk($return, array('oci_native_moodle_database', 'onespace2empty'));
1220  
1221          return $return;
1222      }
1223  
1224      /**
1225       * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
1226       * @param string $table name
1227       * @param mixed $params data record as object or array
1228       * @param bool $returnit return it of inserted record
1229       * @param bool $bulk true means repeated inserts expected
1230       * @param bool $customsequence true if 'id' included in $params, disables $returnid
1231       * @return bool|int true or new id
1232       * @throws dml_exception A DML specific exception is thrown for any errors.
1233       */
1234      public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
1235          if (!is_array($params)) {
1236              $params = (array)$params;
1237          }
1238  
1239          $returning = "";
1240  
1241          if ($customsequence) {
1242              if (!isset($params['id'])) {
1243                  throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
1244              }
1245              $returnid = false;
1246          } else {
1247              unset($params['id']);
1248              if ($returnid) {
1249                  $returning = " RETURNING id INTO :oracle_id"; // crazy name nobody is ever going to use or parameter ;-)
1250              }
1251          }
1252  
1253          if (empty($params)) {
1254              throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
1255          }
1256  
1257          $fields = implode(',', array_keys($params));
1258          $values = array();
1259          foreach ($params as $pname => $value) {
1260              $values[] = ":$pname";
1261          }
1262          $values = implode(',', $values);
1263  
1264          $sql = "INSERT INTO {" . $table . "} ($fields) VALUES ($values)";
1265          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1266          $sql .= $returning;
1267  
1268          $id = 0;
1269  
1270          // note we don't need tweak_param_names() here. Placeholders are safe column names. MDL-28080
1271          // list($sql, $params) = $this->tweak_param_names($sql, $params);
1272          $this->query_start($sql, $params, SQL_QUERY_INSERT);
1273          $stmt = $this->parse_query($sql);
1274          if ($returning) {
1275              oci_bind_by_name($stmt, ":oracle_id", $id, 10, SQLT_INT);
1276          }
1277          $descriptors = array();
1278          $this->bind_params($stmt, $params, $table, $descriptors);
1279          $result = oci_execute($stmt, $this->commit_status);
1280          $this->free_descriptors($descriptors);
1281          $this->query_end($result, $stmt);
1282          oci_free_statement($stmt);
1283  
1284          if (!$returnid) {
1285              return true;
1286          }
1287  
1288          if (!$returning) {
1289              die('TODO - implement oracle 9.2 insert support'); //TODO
1290          }
1291  
1292          return (int)$id;
1293      }
1294  
1295      /**
1296       * Insert a record into a table and return the "id" field if required.
1297       *
1298       * Some conversions and safety checks are carried out. Lobs are supported.
1299       * If the return ID isn't required, then this just reports success as true/false.
1300       * $data is an object containing needed data
1301       * @param string $table The database table to be inserted into
1302       * @param object|array $dataobject A data object with values for one or more fields in the record
1303       * @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.
1304       * @return bool|int true or new id
1305       * @throws dml_exception A DML specific exception is thrown for any errors.
1306       */
1307      public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
1308          $dataobject = (array)$dataobject;
1309  
1310          $columns = $this->get_columns($table);
1311          if (empty($columns)) {
1312              throw new dml_exception('ddltablenotexist', $table);
1313          }
1314  
1315          $cleaned = array();
1316  
1317          foreach ($dataobject as $field=>$value) {
1318              if ($field === 'id') {
1319                  continue;
1320              }
1321              if (!isset($columns[$field])) { // Non-existing table field, skip it
1322                  continue;
1323              }
1324              $column = $columns[$field];
1325              $cleaned[$field] = $this->normalise_value($column, $value);
1326          }
1327  
1328          return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
1329      }
1330  
1331      /**
1332       * Import a record into a table, id field is required.
1333       * Safety checks are NOT carried out. Lobs are supported.
1334       *
1335       * @param string $table name of database table to be inserted into
1336       * @param object $dataobject A data object with values for one or more fields in the record
1337       * @return bool true
1338       * @throws dml_exception A DML specific exception is thrown for any errors.
1339       */
1340      public function import_record($table, $dataobject) {
1341          $dataobject = (array)$dataobject;
1342  
1343          $columns = $this->get_columns($table);
1344          $cleaned = array();
1345  
1346          foreach ($dataobject as $field=>$value) {
1347              if (!isset($columns[$field])) {
1348                  continue;
1349              }
1350              $column = $columns[$field];
1351              $cleaned[$field] = $this->normalise_value($column, $value);
1352          }
1353  
1354          return $this->insert_record_raw($table, $cleaned, false, true, true);
1355      }
1356  
1357      /**
1358       * Update record in database, as fast as possible, no safety checks, lobs not supported.
1359       * @param string $table name
1360       * @param mixed $params data record as object or array
1361       * @param bool true means repeated updates expected
1362       * @return bool true
1363       * @throws dml_exception A DML specific exception is thrown for any errors.
1364       */
1365      public function update_record_raw($table, $params, $bulk=false) {
1366          $params = (array)$params;
1367  
1368          if (!isset($params['id'])) {
1369              throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1370          }
1371  
1372          if (empty($params)) {
1373              throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1374          }
1375  
1376          $sets = array();
1377          foreach ($params as $field=>$value) {
1378              if ($field == 'id') {
1379                  continue;
1380              }
1381              $sets[] = "$field = :$field";
1382          }
1383  
1384          $sets = implode(',', $sets);
1385          $sql = "UPDATE {" . $table . "} SET $sets WHERE id=:id";
1386          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1387  
1388          // note we don't need tweak_param_names() here. Placeholders are safe column names. MDL-28080
1389          // list($sql, $params) = $this->tweak_param_names($sql, $params);
1390          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1391          $stmt = $this->parse_query($sql);
1392          $descriptors = array();
1393          $this->bind_params($stmt, $params, $table, $descriptors);
1394          $result = oci_execute($stmt, $this->commit_status);
1395          $this->free_descriptors($descriptors);
1396          $this->query_end($result, $stmt);
1397          oci_free_statement($stmt);
1398  
1399          return true;
1400      }
1401  
1402      /**
1403       * Update a record in a table
1404       *
1405       * $dataobject is an object containing needed data
1406       * Relies on $dataobject having a variable "id" to
1407       * specify the record to update
1408       *
1409       * @param string $table The database table to be checked against.
1410       * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1411       * @param bool true means repeated updates expected
1412       * @return bool true
1413       * @throws dml_exception A DML specific exception is thrown for any errors.
1414       */
1415      public function update_record($table, $dataobject, $bulk=false) {
1416          $dataobject = (array)$dataobject;
1417  
1418          $columns = $this->get_columns($table);
1419          $cleaned = array();
1420  
1421          foreach ($dataobject as $field=>$value) {
1422              if (!isset($columns[$field])) {
1423                  continue;
1424              }
1425              $column = $columns[$field];
1426              $cleaned[$field] = $this->normalise_value($column, $value);
1427          }
1428  
1429          $this->update_record_raw($table, $cleaned, $bulk);
1430  
1431          return true;
1432      }
1433  
1434      /**
1435       * Set a single field in every table record which match a particular WHERE clause.
1436       *
1437       * @param string $table The database table to be checked against.
1438       * @param string $newfield the field to set.
1439       * @param string $newvalue the value to set the field to.
1440       * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1441       * @param array $params array of sql parameters
1442       * @return bool true
1443       * @throws dml_exception A DML specific exception is thrown for any errors.
1444       */
1445      public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1446  
1447          if ($select) {
1448              $select = "WHERE $select";
1449          }
1450          if (is_null($params)) {
1451              $params = array();
1452          }
1453  
1454          // Get column metadata
1455          $columns = $this->get_columns($table);
1456          $column = $columns[$newfield];
1457  
1458          $newvalue = $this->normalise_value($column, $newvalue);
1459  
1460          list($select, $params, $type) = $this->fix_sql_params($select, $params);
1461  
1462          if (is_bool($newvalue)) {
1463              $newvalue = (int)$newvalue; // prevent "false" problems
1464          }
1465          if (is_null($newvalue)) {
1466              $newsql = "$newfield = NULL";
1467          } else {
1468              // Set the param to array ($newfield => $newvalue) and key to 'newfieldtoset'
1469              // name in the build sql. Later, bind_params() will detect the value array and
1470              // perform the needed modifications to allow the query to work. Note that
1471              // 'newfieldtoset' is one arbitrary name that hopefully won't be used ever
1472              // in order to avoid problems where the same field is used both in the set clause and in
1473              // the conditions. This was breaking badly in drivers using NAMED params like oci.
1474              $params['newfieldtoset'] = array($newfield => $newvalue);
1475              $newsql = "$newfield = :newfieldtoset";
1476          }
1477          $sql = "UPDATE {" . $table . "} SET $newsql $select";
1478          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1479  
1480          list($sql, $params) = $this->tweak_param_names($sql, $params);
1481          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1482          $stmt = $this->parse_query($sql);
1483          $descriptors = array();
1484          $this->bind_params($stmt, $params, $table, $descriptors);
1485          $result = oci_execute($stmt, $this->commit_status);
1486          $this->free_descriptors($descriptors);
1487          $this->query_end($result, $stmt);
1488          oci_free_statement($stmt);
1489  
1490          return true;
1491      }
1492  
1493      /**
1494       * Delete one or more records from a table which match a particular WHERE clause.
1495       *
1496       * @param string $table The database table to be checked against.
1497       * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1498       * @param array $params array of sql parameters
1499       * @return bool true
1500       * @throws dml_exception A DML specific exception is thrown for any errors.
1501       */
1502      public function delete_records_select($table, $select, array $params=null) {
1503  
1504          if ($select) {
1505              $select = "WHERE $select";
1506          }
1507  
1508          $sql = "DELETE FROM {" . $table . "} $select";
1509  
1510          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1511  
1512          list($sql, $params) = $this->tweak_param_names($sql, $params);
1513          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1514          $stmt = $this->parse_query($sql);
1515          $descriptors = array();
1516          $this->bind_params($stmt, $params, null, $descriptors);
1517          $result = oci_execute($stmt, $this->commit_status);
1518          $this->free_descriptors($descriptors);
1519          $this->query_end($result, $stmt);
1520          oci_free_statement($stmt);
1521  
1522          return true;
1523      }
1524  
1525      function sql_null_from_clause() {
1526          return ' FROM dual';
1527      }
1528  
1529      public function sql_bitand($int1, $int2) {
1530          return 'bitand((' . $int1 . '), (' . $int2 . '))';
1531      }
1532  
1533      public function sql_bitnot($int1) {
1534          return '((0 - (' . $int1 . ')) - 1)';
1535      }
1536  
1537      public function sql_bitor($int1, $int2) {
1538          return 'MOODLELIB.BITOR(' . $int1 . ', ' . $int2 . ')';
1539      }
1540  
1541      public function sql_bitxor($int1, $int2) {
1542          return 'MOODLELIB.BITXOR(' . $int1 . ', ' . $int2 . ')';
1543      }
1544  
1545      /**
1546       * Returns the SQL text to be used in order to perform module '%'
1547       * operation - remainder after division
1548       *
1549       * @param integer int1 first integer in the operation
1550       * @param integer int2 second integer in the operation
1551       * @return string the piece of SQL code to be used in your statement.
1552       */
1553      public function sql_modulo($int1, $int2) {
1554          return 'MOD(' . $int1 . ', ' . $int2 . ')';
1555      }
1556  
1557      public function sql_cast_char2int($fieldname, $text=false) {
1558          if (!$text) {
1559              return ' CAST(' . $fieldname . ' AS INT) ';
1560          } else {
1561              return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1562          }
1563      }
1564  
1565      public function sql_cast_char2real($fieldname, $text=false) {
1566          if (!$text) {
1567              return ' CAST(' . $fieldname . ' AS FLOAT) ';
1568          } else {
1569              return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS FLOAT) ';
1570          }
1571      }
1572  
1573      /**
1574       * Returns 'LIKE' part of a query.
1575       *
1576       * @param string $fieldname usually name of the table column
1577       * @param string $param usually bound query parameter (?, :named)
1578       * @param bool $casesensitive use case sensitive search
1579       * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1580       * @param bool $notlike true means "NOT LIKE"
1581       * @param string $escapechar escape char for '%' and '_'
1582       * @return string SQL code fragment
1583       */
1584      public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1585          if (strpos($param, '%') !== false) {
1586              debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1587          }
1588  
1589          $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1590  
1591          // no accent sensitiveness here for now, sorry
1592  
1593          if ($casesensitive) {
1594              return "$fieldname $LIKE $param ESCAPE '$escapechar'";
1595          } else {
1596              return "LOWER($fieldname) $LIKE LOWER($param) ESCAPE '$escapechar'";
1597          }
1598      }
1599  
1600      public function sql_concat() {
1601          $arr = func_get_args();
1602          if (empty($arr)) {
1603              return " ' ' ";
1604          }
1605          foreach ($arr as $k => $v) {
1606              if ($v === "' '") {
1607                  $arr[$k] = "'*OCISP*'"; // New mega hack.
1608              }
1609          }
1610          $s = $this->recursive_concat($arr);
1611          return " MOODLELIB.UNDO_MEGA_HACK($s) ";
1612      }
1613  
1614      public function sql_concat_join($separator="' '", $elements = array()) {
1615          if ($separator === "' '") {
1616              $separator = "'*OCISP*'"; // New mega hack.
1617          }
1618          foreach ($elements as $k => $v) {
1619              if ($v === "' '") {
1620                  $elements[$k] = "'*OCISP*'"; // New mega hack.
1621              }
1622          }
1623          for ($n = count($elements)-1; $n > 0 ; $n--) {
1624              array_splice($elements, $n, 0, $separator);
1625          }
1626          if (empty($elements)) {
1627              return " ' ' ";
1628          }
1629          $s = $this->recursive_concat($elements);
1630          return " MOODLELIB.UNDO_MEGA_HACK($s) ";
1631      }
1632  
1633      /**
1634       * Return SQL for performing group concatenation on given field/expression
1635       *
1636       * @param string $field
1637       * @param string $separator
1638       * @param string $sort
1639       * @return string
1640       */
1641      public function sql_group_concat(string $field, string $separator = ', ', string $sort = ''): string {
1642          $fieldsort = $sort ?: '1';
1643          return "LISTAGG({$field}, '{$separator}') WITHIN GROUP (ORDER BY {$fieldsort})";
1644      }
1645  
1646      /**
1647       * Constructs 'IN()' or '=' sql fragment
1648       *
1649       * Method overriding {@link moodle_database::get_in_or_equal} to be able to get
1650       * more than 1000 elements working, to avoid ORA-01795. We use a pivoting technique
1651       * to be able to transform the params into virtual rows, so the original IN()
1652       * expression gets transformed into a subquery. Once more, be noted that we shouldn't
1653       * be using ever get_in_or_equal() with such number of parameters (proper subquery and/or
1654       * chunking should be used instead).
1655       *
1656       * @param mixed $items A single value or array of values for the expression.
1657       * @param int $type Parameter bounding type : SQL_PARAMS_QM or SQL_PARAMS_NAMED.
1658       * @param string $prefix Named parameter placeholder prefix (a unique counter value is appended to each parameter name).
1659       * @param bool $equal True means we want to equate to the constructed expression, false means we don't want to equate to it.
1660       * @param mixed $onemptyitems This defines the behavior when the array of items provided is empty. Defaults to false,
1661       *              meaning throw exceptions. Other values will become part of the returned SQL fragment.
1662       * @throws coding_exception | dml_exception
1663       * @return array A list containing the constructed sql fragment and an array of parameters.
1664       */
1665      public function get_in_or_equal($items, $type=SQL_PARAMS_QM, $prefix='param', $equal=true, $onemptyitems=false) {
1666          list($sql, $params) = parent::get_in_or_equal($items, $type, $prefix,  $equal, $onemptyitems);
1667  
1668          // Less than 1000 elements, nothing to do.
1669          if (count($params) < 1000) {
1670              return array($sql, $params); // Return unmodified.
1671          }
1672  
1673          // Extract the interesting parts of the sql to rewrite.
1674          if (preg_match('!(^.*IN \()([^\)]*)(.*)$!', $sql, $matches) === false) {
1675              return array($sql, $params); // Return unmodified.
1676          }
1677  
1678          $instart = $matches[1];
1679          $insql = $matches[2];
1680          $inend = $matches[3];
1681          $newsql = '';
1682  
1683          // Some basic verification about the matching going ok.
1684          $insqlarr = explode(',', $insql);
1685          if (count($insqlarr) !== count($params)) {
1686              return array($sql, $params); // Return unmodified.
1687          }
1688  
1689          // Arrived here, we need to chunk and pivot the params, building a new sql (params remain the same).
1690          $addunionclause = false;
1691          while ($chunk = array_splice($insqlarr, 0, 125)) { // Each chunk will handle up to 125 (+125 +1) elements (DECODE max is 255).
1692              $chunksize = count($chunk);
1693              if ($addunionclause) {
1694                  $newsql .= "\n    UNION ALL";
1695              }
1696              $newsql .= "\n        SELECT DECODE(pivot";
1697              $counter = 1;
1698              foreach ($chunk as $element) {
1699                  $newsql .= ",\n            {$counter}, " . trim($element);
1700                  $counter++;
1701              }
1702              $newsql .= ")";
1703              $newsql .= "\n        FROM dual";
1704              $newsql .= "\n        CROSS JOIN (SELECT LEVEL AS pivot FROM dual CONNECT BY LEVEL <= {$chunksize})";
1705              $addunionclause = true;
1706          }
1707  
1708          // Rebuild the complete IN() clause and return it.
1709          return array($instart . $newsql . $inend, $params);
1710      }
1711  
1712      /**
1713       * Mega hacky magic to work around crazy Oracle NULL concats.
1714       * @param array $args
1715       * @return string
1716       */
1717      protected function recursive_concat(array $args) {
1718          $count = count($args);
1719          if ($count == 1) {
1720              $arg = reset($args);
1721              return $arg;
1722          }
1723          if ($count == 2) {
1724              $args[] = "' '";
1725              // No return here intentionally.
1726          }
1727          $first = array_shift($args);
1728          $second = array_shift($args);
1729          $third = $this->recursive_concat($args);
1730          return "MOODLELIB.TRICONCAT($first, $second, $third)";
1731      }
1732  
1733      /**
1734       * Returns the SQL for returning searching one string for the location of another.
1735       */
1736      public function sql_position($needle, $haystack) {
1737          return "INSTR(($haystack), ($needle))";
1738      }
1739  
1740      /**
1741       * Returns the SQL to know if one field is empty.
1742       *
1743       * @param string $tablename Name of the table (without prefix). Not used for now but can be
1744       *                          necessary in the future if we want to use some introspection using
1745       *                          meta information against the DB.
1746       * @param string $fieldname Name of the field we are going to check
1747       * @param bool $nullablefield For specifying if the field is nullable (true) or no (false) in the DB.
1748       * @param bool $textfield For specifying if it is a text (also called clob) field (true) or a varchar one (false)
1749       * @return string the sql code to be added to check for empty values
1750       */
1751      public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1752          if ($textfield) {
1753              return " (".$this->sql_compare_text($fieldname)." = ' ') ";
1754          } else {
1755              return " ($fieldname = ' ') ";
1756          }
1757      }
1758  
1759      public function sql_order_by_text($fieldname, $numchars=32) {
1760          return 'dbms_lob.substr(' . $fieldname . ', ' . $numchars . ',1)';
1761      }
1762  
1763      /**
1764       * Is the required OCI server package installed?
1765       * @return bool
1766       */
1767      protected function oci_package_installed() {
1768          $sql = "SELECT 1
1769                  FROM user_objects
1770                  WHERE object_type = 'PACKAGE BODY'
1771                    AND object_name = 'MOODLELIB'
1772                    AND status = 'VALID'";
1773          $this->query_start($sql, null, SQL_QUERY_AUX);
1774          $stmt = $this->parse_query($sql);
1775          $result = oci_execute($stmt, $this->commit_status);
1776          $this->query_end($result, $stmt);
1777          $records = null;
1778          oci_fetch_all($stmt, $records, 0, -1, OCI_FETCHSTATEMENT_BY_ROW);
1779          oci_free_statement($stmt);
1780          return isset($records[0]) && reset($records[0]) ? true : false;
1781      }
1782  
1783      /**
1784       * Try to add required moodle package into oracle server.
1785       */
1786      protected function attempt_oci_package_install() {
1787          $sqls = file_get_contents(__DIR__.'/oci_native_moodle_package.sql');
1788          $sqls = preg_split('/^\/$/sm', $sqls);
1789          foreach ($sqls as $sql) {
1790              $sql = trim($sql);
1791              if ($sql === '' or $sql === 'SHOW ERRORS') {
1792                  continue;
1793              }
1794              $this->change_database_structure($sql);
1795          }
1796      }
1797  
1798      /**
1799       * Does this driver support tool_replace?
1800       *
1801       * @since Moodle 2.8
1802       * @return bool
1803       */
1804      public function replace_all_text_supported() {
1805          return true;
1806      }
1807  
1808      public function session_lock_supported() {
1809          return true;
1810      }
1811  
1812      /**
1813       * Obtain session lock
1814       * @param int $rowid id of the row with session record
1815       * @param int $timeout max allowed time to wait for the lock in seconds
1816       * @return void
1817       */
1818      public function get_session_lock($rowid, $timeout) {
1819          parent::get_session_lock($rowid, $timeout);
1820  
1821          $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1822          $sql = 'SELECT MOODLELIB.GET_LOCK(:lockname, :locktimeout) FROM DUAL';
1823          $params = array('lockname' => $fullname , 'locktimeout' => $timeout);
1824          $this->query_start($sql, $params, SQL_QUERY_AUX);
1825          $stmt = $this->parse_query($sql);
1826          $this->bind_params($stmt, $params);
1827          $result = oci_execute($stmt, $this->commit_status);
1828          if ($result === false) { // Any failure in get_lock() raises error, causing return of bool false
1829              throw new dml_sessionwait_exception();
1830          }
1831          $this->query_end($result, $stmt);
1832          oci_free_statement($stmt);
1833      }
1834  
1835      public function release_session_lock($rowid) {
1836          if (!$this->used_for_db_sessions) {
1837              return;
1838          }
1839  
1840          parent::release_session_lock($rowid);
1841  
1842          $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1843          $params = array('lockname' => $fullname);
1844          $sql = 'SELECT MOODLELIB.RELEASE_LOCK(:lockname) FROM DUAL';
1845          $this->query_start($sql, $params, SQL_QUERY_AUX);
1846          $stmt = $this->parse_query($sql);
1847          $this->bind_params($stmt, $params);
1848          $result = oci_execute($stmt, $this->commit_status);
1849          $this->query_end($result, $stmt);
1850          oci_free_statement($stmt);
1851      }
1852  
1853      /**
1854       * Driver specific start of real database transaction,
1855       * this can not be used directly in code.
1856       * @return void
1857       */
1858      protected function begin_transaction() {
1859          $this->commit_status = OCI_DEFAULT; //Done! ;-)
1860      }
1861  
1862      /**
1863       * Driver specific commit of real database transaction,
1864       * this can not be used directly in code.
1865       * @return void
1866       */
1867      protected function commit_transaction() {
1868          $this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX);
1869          $result = oci_commit($this->oci);
1870          $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1871          $this->query_end($result);
1872      }
1873  
1874      /**
1875       * Driver specific abort of real database transaction,
1876       * this can not be used directly in code.
1877       * @return void
1878       */
1879      protected function rollback_transaction() {
1880          $this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX);
1881          $result = oci_rollback($this->oci);
1882          $this->commit_status = OCI_COMMIT_ON_SUCCESS;
1883          $this->query_end($result);
1884      }
1885  }