[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/dml/ -> mssql_native_moodle_database.php (source)

   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 mssql class representing moodle database interface.
  19   *
  20   * @package    core_dml
  21   * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  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__.'/mssql_native_moodle_recordset.php');
  29  require_once (__DIR__.'/mssql_native_moodle_temptables.php');
  30  
  31  /**
  32   * Native mssql class representing moodle database interface.
  33   *
  34   * @package    core_dml
  35   * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class mssql_native_moodle_database extends moodle_database {
  39  
  40      protected $mssql     = null;
  41      protected $last_error_reporting; // To handle mssql driver default verbosity
  42      protected $collation;  // current DB collation cache
  43      /**
  44       * Does the used db version support ANSI way of limiting (2012 and higher)
  45       * @var bool
  46       */
  47      protected $supportsoffsetfetch;
  48  
  49      /**
  50       * Detects if all needed PHP stuff installed.
  51       * Note: can be used before connect()
  52       * @return mixed true if ok, string if something
  53       */
  54      public function driver_installed() {
  55          if (!function_exists('mssql_connect')) {
  56              return get_string('mssqlextensionisnotpresentinphp', 'install');
  57          }
  58          return true;
  59      }
  60  
  61      /**
  62       * Returns database family type - describes SQL dialect
  63       * Note: can be used before connect()
  64       * @return string db family name (mysql, postgres, mssql, oracle, etc.)
  65       */
  66      public function get_dbfamily() {
  67          return 'mssql';
  68      }
  69  
  70      /**
  71       * Returns more specific database driver type
  72       * Note: can be used before connect()
  73       * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
  74       */
  75      protected function get_dbtype() {
  76          return 'mssql';
  77      }
  78  
  79      /**
  80       * Returns general database library name
  81       * Note: can be used before connect()
  82       * @return string db type pdo, native
  83       */
  84      protected function get_dblibrary() {
  85          return 'native';
  86      }
  87  
  88      /**
  89       * Returns localised database type name
  90       * Note: can be used before connect()
  91       * @return string
  92       */
  93      public function get_name() {
  94          return get_string('nativemssql', 'install');
  95      }
  96  
  97      /**
  98       * Returns localised database configuration help.
  99       * Note: can be used before connect()
 100       * @return string
 101       */
 102      public function get_configuration_help() {
 103          return get_string('nativemssqlhelp', 'install');
 104      }
 105  
 106      /**
 107       * Diagnose database and tables, this function is used
 108       * to verify database and driver settings, db engine types, etc.
 109       *
 110       * @return string null means everything ok, string means problem found.
 111       */
 112      public function diagnose() {
 113          // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
 114          // (that's required to get snapshots/row versioning on READ_COMMITED mode).
 115          $correctrcsmode = false;
 116          $sql = "SELECT is_read_committed_snapshot_on
 117                    FROM sys.databases
 118                   WHERE name = '{$this->dbname}'";
 119          $this->query_start($sql, null, SQL_QUERY_AUX);
 120          $result = mssql_query($sql, $this->mssql);
 121          $this->query_end($result);
 122          if ($result) {
 123              if ($row = mssql_fetch_assoc($result)) {
 124                  $correctrcsmode = (bool)reset($row);
 125              }
 126          }
 127          $this->free_result($result);
 128  
 129          if (!$correctrcsmode) {
 130              return get_string('mssqlrcsmodemissing', 'error');
 131          }
 132  
 133          // Arrived here, all right.
 134          return null;
 135      }
 136  
 137      /**
 138       * Connect to db
 139       * Must be called before other methods.
 140       * @param string $dbhost The database host.
 141       * @param string $dbuser The database username.
 142       * @param string $dbpass The database username's password.
 143       * @param string $dbname The name of the database being connected to.
 144       * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
 145       * @param array $dboptions driver specific options
 146       * @return bool true
 147       * @throws dml_connection_exception if error
 148       */
 149      public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
 150          if ($prefix == '' and !$this->external) {
 151              //Enforce prefixes for everybody but mysql
 152              throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
 153          }
 154  
 155          $driverstatus = $this->driver_installed();
 156  
 157          if ($driverstatus !== true) {
 158              throw new dml_exception('dbdriverproblem', $driverstatus);
 159          }
 160  
 161          $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
 162  
 163          $dbhost = $this->dbhost;
 164          // Zero shouldn't be used as a port number so doing a check with empty() should be fine.
 165          if (!empty($dboptions['dbport'])) {
 166              if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
 167                  $dbhost .= ','.$dboptions['dbport'];
 168              } else {
 169                  $dbhost .= ':'.$dboptions['dbport'];
 170              }
 171          }
 172          ob_start();
 173          if (!empty($this->dboptions['dbpersist'])) { // persistent connection
 174              $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
 175          } else {
 176              $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
 177          }
 178          $dberr = ob_get_contents();
 179          ob_end_clean();
 180  
 181          if ($this->mssql === false) {
 182              $this->mssql = null;
 183              throw new dml_connection_exception($dberr);
 184          }
 185  
 186          // already connected, select database and set some env. variables
 187          $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
 188          $result = mssql_select_db($this->dbname, $this->mssql);
 189          $this->query_end($result);
 190  
 191          // No need to set charset. It's UTF8, with transparent conversions
 192          // back and forth performed both by FreeTDS or ODBTP
 193  
 194          // Allow quoted identifiers
 195          $sql = "SET QUOTED_IDENTIFIER ON";
 196          $this->query_start($sql, null, SQL_QUERY_AUX);
 197          $result = mssql_query($sql, $this->mssql);
 198          $this->query_end($result);
 199  
 200          $this->free_result($result);
 201  
 202          // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
 203          // instead of equal(=) and distinct(<>) symbols
 204          $sql = "SET ANSI_NULLS ON";
 205          $this->query_start($sql, null, SQL_QUERY_AUX);
 206          $result = mssql_query($sql, $this->mssql);
 207          $this->query_end($result);
 208  
 209          $this->free_result($result);
 210  
 211          // Force ANSI warnings so arithmetic/string overflows will be
 212          // returning error instead of transparently truncating data
 213          $sql = "SET ANSI_WARNINGS ON";
 214          $this->query_start($sql, null, SQL_QUERY_AUX);
 215          $result = mssql_query($sql, $this->mssql);
 216          $this->query_end($result);
 217  
 218          // Concatenating null with anything MUST return NULL
 219          $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
 220          $this->query_start($sql, null, SQL_QUERY_AUX);
 221          $result = mssql_query($sql, $this->mssql);
 222          $this->query_end($result);
 223  
 224          $this->free_result($result);
 225  
 226          // Set transactions isolation level to READ_COMMITTED
 227          // prevents dirty reads when using transactions +
 228          // is the default isolation level of MSSQL
 229          // Requires database to run with READ_COMMITTED_SNAPSHOT ON
 230          $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
 231          $this->query_start($sql, NULL, SQL_QUERY_AUX);
 232          $result = mssql_query($sql, $this->mssql);
 233          $this->query_end($result);
 234  
 235          $this->free_result($result);
 236  
 237          $serverinfo = $this->get_server_info();
 238          // Fetch/offset is supported staring from SQL Server 2012.
 239          $this->supportsoffsetfetch = $serverinfo['version'] > '11';
 240  
 241          // Connection stabilised and configured, going to instantiate the temptables controller
 242          $this->temptables = new mssql_native_moodle_temptables($this);
 243  
 244          return true;
 245      }
 246  
 247      /**
 248       * Close database connection and release all resources
 249       * and memory (especially circular memory references).
 250       * Do NOT use connect() again, create a new instance if needed.
 251       */
 252      public function dispose() {
 253          parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
 254          if ($this->mssql) {
 255              mssql_close($this->mssql);
 256              $this->mssql = null;
 257          }
 258      }
 259  
 260      /**
 261       * Called before each db query.
 262       * @param string $sql
 263       * @param array array of parameters
 264       * @param int $type type of query
 265       * @param mixed $extrainfo driver specific extra information
 266       * @return void
 267       */
 268      protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
 269          parent::query_start($sql, $params, $type, $extrainfo);
 270          // mssql driver tends to send debug to output, we do not need that ;-)
 271          $this->last_error_reporting = error_reporting(0);
 272      }
 273  
 274      /**
 275       * Called immediately after each db query.
 276       * @param mixed db specific result
 277       * @return void
 278       */
 279      protected function query_end($result) {
 280          // reset original debug level
 281          error_reporting($this->last_error_reporting);
 282          parent::query_end($result);
 283      }
 284  
 285      /**
 286       * Returns database server info array
 287       * @return array Array containing 'description' and 'version' info
 288       */
 289      public function get_server_info() {
 290          static $info;
 291          if (!$info) {
 292              $info = array();
 293              $sql = 'sp_server_info 2';
 294              $this->query_start($sql, null, SQL_QUERY_AUX);
 295              $result = mssql_query($sql, $this->mssql);
 296              $this->query_end($result);
 297              $row = mssql_fetch_row($result);
 298              $info['description'] = $row[2];
 299              $this->free_result($result);
 300  
 301              $sql = 'sp_server_info 500';
 302              $this->query_start($sql, null, SQL_QUERY_AUX);
 303              $result = mssql_query($sql, $this->mssql);
 304              $this->query_end($result);
 305              $row = mssql_fetch_row($result);
 306              $info['version'] = $row[2];
 307              $this->free_result($result);
 308          }
 309          return $info;
 310      }
 311  
 312      /**
 313       * Converts short table name {tablename} to real table name
 314       * supporting temp tables (#) if detected
 315       *
 316       * @param string sql
 317       * @return string sql
 318       */
 319      protected function fix_table_names($sql) {
 320          if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
 321              foreach($matches[0] as $key=>$match) {
 322                  $name = $matches[1][$key];
 323                  if ($this->temptables->is_temptable($name)) {
 324                      $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
 325                  } else {
 326                      $sql = str_replace($match, $this->prefix.$name, $sql);
 327                  }
 328              }
 329          }
 330          return $sql;
 331      }
 332  
 333      /**
 334       * Returns supported query parameter types
 335       * @return int bitmask of accepted SQL_PARAMS_*
 336       */
 337      protected function allowed_param_types() {
 338          return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
 339      }
 340  
 341      /**
 342       * Returns last error reported by database engine.
 343       * @return string error message
 344       */
 345      public function get_last_error() {
 346          return mssql_get_last_message();
 347      }
 348  
 349      /**
 350       * Return tables in database WITHOUT current prefix
 351       * @param bool $usecache if true, returns list of cached tables.
 352       * @return array of table names in lowercase and without prefix
 353       */
 354      public function get_tables($usecache=true) {
 355          if ($usecache and $this->tables !== null) {
 356              return $this->tables;
 357          }
 358          $this->tables = array();
 359          $sql = "SELECT table_name
 360                    FROM INFORMATION_SCHEMA.TABLES
 361                   WHERE table_name LIKE '$this->prefix%'
 362                     AND table_type = 'BASE TABLE'";
 363          $this->query_start($sql, null, SQL_QUERY_AUX);
 364          $result = mssql_query($sql, $this->mssql);
 365          $this->query_end($result);
 366  
 367          if ($result) {
 368              while ($row = mssql_fetch_row($result)) {
 369                  $tablename = reset($row);
 370                  if ($this->prefix !== false && $this->prefix !== '') {
 371                      if (strpos($tablename, $this->prefix) !== 0) {
 372                          continue;
 373                      }
 374                      $tablename = substr($tablename, strlen($this->prefix));
 375                  }
 376                  $this->tables[$tablename] = $tablename;
 377              }
 378              $this->free_result($result);
 379          }
 380  
 381          // Add the currently available temptables
 382          $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
 383          return $this->tables;
 384      }
 385  
 386      /**
 387       * Return table indexes - everything lowercased.
 388       * @param string $table The table we want to get indexes from.
 389       * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
 390       */
 391      public function get_indexes($table) {
 392          $indexes = array();
 393          $tablename = $this->prefix.$table;
 394  
 395          // Indexes aren't covered by information_schema metatables, so we need to
 396          // go to sys ones. Skipping primary key indexes on purpose.
 397          $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
 398                    FROM sys.indexes i
 399                    JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
 400                    JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
 401                    JOIN sys.tables t ON i.object_id = t.object_id
 402                   WHERE t.name = '$tablename'
 403                     AND i.is_primary_key = 0
 404                ORDER BY i.name, i.index_id, ic.index_column_id";
 405  
 406          $this->query_start($sql, null, SQL_QUERY_AUX);
 407          $result = mssql_query($sql, $this->mssql);
 408          $this->query_end($result);
 409  
 410          if ($result) {
 411              $lastindex = '';
 412              $unique = false;
 413              $columns = array();
 414              while ($row = mssql_fetch_assoc($result)) {
 415                  if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
 416                      $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
 417                      $unique = false;
 418                      $columns = array();
 419                  }
 420                  $lastindex = $row['index_name'];
 421                  $unique = empty($row['is_unique']) ? false : true;
 422                  $columns[] = $row['column_name'];
 423              }
 424              if ($lastindex ) { // Add the last one if exists
 425                  $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
 426              }
 427              $this->free_result($result);
 428          }
 429          return $indexes;
 430      }
 431  
 432      /**
 433       * Returns datailed information about columns in table. This information is cached internally.
 434       * @param string $table name
 435       * @param bool $usecache
 436       * @return array array of database_column_info objects indexed with column names
 437       */
 438      public function get_columns($table, $usecache=true) {
 439  
 440          if ($usecache) {
 441              if ($this->temptables->is_temptable($table)) {
 442                  if ($data = $this->get_temp_tables_cache()->get($table)) {
 443                      return $data;
 444                  }
 445              } else {
 446                  if ($data = $this->get_metacache()->get($table)) {
 447                      return $data;
 448                  }
 449              }
 450          }
 451  
 452          $structure = array();
 453  
 454          if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
 455              $sql = "SELECT column_name AS name,
 456                             data_type AS type,
 457                             numeric_precision AS max_length,
 458                             character_maximum_length AS char_max_length,
 459                             numeric_scale AS scale,
 460                             is_nullable AS is_nullable,
 461                             columnproperty(object_id(quotename(table_schema) + '.' +
 462                                 quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
 463                             column_default AS default_value
 464                        FROM INFORMATION_SCHEMA.COLUMNS
 465                       WHERE table_name = '{" . $table . "}'
 466                    ORDER BY ordinal_position";
 467          } else { // temp table, get metadata from tempdb schema
 468              $sql = "SELECT column_name AS name,
 469                             data_type AS type,
 470                             numeric_precision AS max_length,
 471                             character_maximum_length AS char_max_length,
 472                             numeric_scale AS scale,
 473                             is_nullable AS is_nullable,
 474                             columnproperty(object_id(quotename(table_schema) + '.' +
 475                                 quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
 476                             column_default AS default_value
 477                        FROM tempdb.INFORMATION_SCHEMA.COLUMNS
 478                        JOIN tempdb..sysobjects ON name = table_name
 479                       WHERE id = object_id('tempdb..{" . $table . "}')
 480                    ORDER BY ordinal_position";
 481          }
 482  
 483          list($sql, $params, $type) = $this->fix_sql_params($sql, null);
 484  
 485          $this->query_start($sql, null, SQL_QUERY_AUX);
 486          $result = mssql_query($sql, $this->mssql);
 487          $this->query_end($result);
 488  
 489          if (!$result) {
 490              return array();
 491          }
 492  
 493          while ($rawcolumn = mssql_fetch_assoc($result)) {
 494  
 495              $rawcolumn = (object)$rawcolumn;
 496  
 497              $info = new stdClass();
 498              $info->name = $rawcolumn->name;
 499              $info->type = $rawcolumn->type;
 500              $info->meta_type = $this->mssqltype2moodletype($info->type);
 501  
 502              // Prepare auto_increment info
 503              $info->auto_increment = $rawcolumn->auto_increment ? true : false;
 504  
 505              // Define type for auto_increment columns
 506              $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
 507  
 508              // id columns being auto_incremnt are PK by definition
 509              $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
 510  
 511              if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
 512                  // This is NVARCHAR(MAX), not a normal NVARCHAR.
 513                  $info->max_length = -1;
 514                  $info->meta_type = 'X';
 515              } else {
 516                  // Put correct length for character and LOB types
 517                  $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
 518                  $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length;
 519              }
 520  
 521              // Scale
 522              $info->scale = $rawcolumn->scale;
 523  
 524              // Prepare not_null info
 525              $info->not_null = $rawcolumn->is_nullable == 'NO'  ? true : false;
 526  
 527              // Process defaults
 528              $info->has_default = !empty($rawcolumn->default_value);
 529              if ($rawcolumn->default_value === NULL) {
 530                  $info->default_value = NULL;
 531              } else {
 532                  $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value);
 533              }
 534  
 535              // Process binary
 536              $info->binary = $info->meta_type == 'B' ? true : false;
 537  
 538              $structure[$info->name] = new database_column_info($info);
 539          }
 540          $this->free_result($result);
 541  
 542          if ($usecache) {
 543              if ($this->temptables->is_temptable($table)) {
 544                  $this->get_temp_tables_cache()->set($table, $structure);
 545              } else {
 546                  $this->get_metacache()->set($table, $structure);
 547              }
 548          }
 549  
 550          return $structure;
 551      }
 552  
 553      /**
 554       * Normalise values based on varying RDBMS's dependencies (booleans, LOBs...)
 555       *
 556       * @param database_column_info $column column metadata corresponding with the value we are going to normalise
 557       * @param mixed $value value we are going to normalise
 558       * @return mixed the normalised value
 559       */
 560      protected function normalise_value($column, $value) {
 561          $this->detect_objects($value);
 562  
 563          if (is_bool($value)) { // Always, convert boolean to int
 564              $value = (int)$value;
 565          } // And continue processing because text columns with numeric info need special handling below
 566  
 567          if ($column->meta_type == 'B') {   // BLOBs need to be properly "packed", but can be inserted directly if so.
 568              if (!is_null($value)) {               // If value not null, unpack it to unquoted hexadecimal byte-string format
 569                  $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it
 570              }                                     // easily and "bind" the param ok.
 571  
 572          } else if ($column->meta_type == 'X') {             // MSSQL doesn't cast from int to text, so if text column
 573              if (is_numeric($value)) {                       // and is numeric value then cast to string
 574                  $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how
 575              }                                               // to "bind" the param ok, avoiding reverse conversion to number
 576  
 577          } else if ($value === '') {
 578              if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
 579                  $value = 0; // prevent '' problems in numeric fields
 580              }
 581          }
 582          return $value;
 583      }
 584  
 585      /**
 586       * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @
 587       *
 588       * @param mssql_resource $resource resource to be freed if possible
 589       */
 590      private function free_result($resource) {
 591          if (!is_bool($resource)) { // true/false resources cannot be freed
 592              mssql_free_result($resource);
 593          }
 594      }
 595  
 596      /**
 597       * Provides mapping between mssql native data types and moodle_database - database_column_info - ones)
 598       *
 599       * @param string $mssql_type native mssql data type
 600       * @return string 1-char database_column_info data type
 601       */
 602      private function mssqltype2moodletype($mssql_type) {
 603          $type = null;
 604          switch (strtoupper($mssql_type)) {
 605              case 'BIT':
 606                  $type = 'L';
 607                  break;
 608              case 'INT':
 609              case 'SMALLINT':
 610              case 'INTEGER':
 611              case 'BIGINT':
 612                  $type = 'I';
 613                  break;
 614              case 'DECIMAL':
 615              case 'REAL':
 616              case 'FLOAT':
 617                  $type = 'N';
 618                  break;
 619              case 'VARCHAR':
 620              case 'NVARCHAR':
 621                  $type = 'C';
 622                  break;
 623              case 'TEXT':
 624              case 'NTEXT':
 625              case 'VARCHAR(MAX)':
 626              case 'NVARCHAR(MAX)':
 627                  $type = 'X';
 628                  break;
 629              case 'IMAGE':
 630              case 'VARBINARY':
 631              case 'VARBINARY(MAX)':
 632                  $type = 'B';
 633                  break;
 634              case 'DATETIME':
 635                  $type = 'D';
 636                  break;
 637          }
 638          if (!$type) {
 639              throw new dml_exception('invalidmssqlnativetype', $mssql_type);
 640          }
 641          return $type;
 642      }
 643  
 644      /**
 645       * Do NOT use in code, to be used by database_manager only!
 646       * @param string|array $sql query
 647       * @param array|null $tablenames an array of xmldb table names affected by this request.
 648       * @return bool true
 649       * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
 650       */
 651      public function change_database_structure($sql, $tablenames = null) {
 652          $this->get_manager(); // Includes DDL exceptions classes ;-)
 653          $sqls = (array)$sql;
 654  
 655          try {
 656              foreach ($sqls as $sql) {
 657                  $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
 658                  $result = mssql_query($sql, $this->mssql);
 659                  $this->query_end($result);
 660              }
 661          } catch (ddl_change_structure_exception $e) {
 662              $this->reset_caches($tablenames);
 663              throw $e;
 664          }
 665  
 666          $this->reset_caches($tablenames);
 667          return true;
 668      }
 669  
 670      /**
 671       * Very ugly hack which emulates bound parameters in queries
 672       * because the mssql driver doesn't support placeholders natively at all
 673       */
 674      protected function emulate_bound_params($sql, array $params=null) {
 675          if (empty($params)) {
 676              return $sql;
 677          }
 678          // ok, we have verified sql statement with ? and correct number of params
 679          $parts = array_reverse(explode('?', $sql));
 680          $return = array_pop($parts);
 681          foreach ($params as $param) {
 682              if (is_bool($param)) {
 683                  $return .= (int)$param;
 684  
 685              } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially
 686                  $return .= '0x' . $param['hex'];
 687  
 688              } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not*
 689                  $return .= "N'{$param['numstr']}'";                   // be converted back to number params, but bound as strings
 690  
 691              } else if (is_null($param)) {
 692                  $return .= 'NULL';
 693  
 694              } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646
 695                  $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int.
 696  
 697              } else if (is_float($param)) {
 698                  $return .= $param;
 699  
 700              } else {
 701                  $param = str_replace("'", "''", $param);
 702                  $param = str_replace("\0", "", $param);
 703                  $return .= "N'$param'";
 704              }
 705  
 706              $return .= array_pop($parts);
 707          }
 708          return $return;
 709      }
 710  
 711      /**
 712       * Execute general sql query. Should be used only when no other method suitable.
 713       * Do NOT use this to make changes in db structure, use database_manager methods instead!
 714       * @param string $sql query
 715       * @param array $params query parameters
 716       * @return bool true
 717       * @throws dml_exception A DML specific exception is thrown for any errors.
 718       */
 719      public function execute($sql, array $params=null) {
 720  
 721          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 722          $rawsql = $this->emulate_bound_params($sql, $params);
 723  
 724          if (strpos($sql, ';') !== false) {
 725              throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
 726          }
 727  
 728          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
 729          $result = mssql_query($rawsql, $this->mssql);
 730          $this->query_end($result);
 731          $this->free_result($result);
 732  
 733          return true;
 734      }
 735  
 736      /**
 737       * Get a number of records as a moodle_recordset using a SQL statement.
 738       *
 739       * Since this method is a little less readable, use of it should be restricted to
 740       * code where it's possible there might be large datasets being returned.  For known
 741       * small datasets use get_records_sql - it leads to simpler code.
 742       *
 743       * The return type is like:
 744       * @see function get_recordset.
 745       *
 746       * @param string $sql the SQL select query to execute.
 747       * @param array $params array of sql parameters
 748       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 749       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 750       * @return moodle_recordset instance
 751       * @throws dml_exception A DML specific exception is thrown for any errors.
 752       */
 753      public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
 754  
 755          list($limitfrom, $limitnum) = $this->normalise_limit_from_num($limitfrom, $limitnum);
 756  
 757          if ($limitfrom or $limitnum) {
 758              if (!$this->supportsoffsetfetch) {
 759                  if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later).
 760                      $fetch = $limitfrom + $limitnum;
 761                      if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow.
 762                          $fetch = PHP_INT_MAX;
 763                      }
 764                      $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i',
 765                                          "\\1SELECT\\2 TOP $fetch", $sql);
 766                  }
 767              } else {
 768                  $sql = (substr($sql, -1) === ';') ? substr($sql, 0, -1) : $sql;
 769                  // We need order by to use FETCH/OFFSET.
 770                  // Ordering by first column shouldn't break anything if there was no order in the first place.
 771                  if (!strpos(strtoupper($sql), "ORDER BY")) {
 772                      $sql .= " ORDER BY 1";
 773                  }
 774  
 775                  $sql .= " OFFSET ".$limitfrom." ROWS ";
 776  
 777                  if ($limitnum > 0) {
 778                      $sql .= " FETCH NEXT ".$limitnum." ROWS ONLY";
 779                  }
 780              }
 781          }
 782  
 783          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 784          $rawsql = $this->emulate_bound_params($sql, $params);
 785  
 786          $this->query_start($sql, $params, SQL_QUERY_SELECT);
 787          $result = mssql_query($rawsql, $this->mssql);
 788          $this->query_end($result);
 789  
 790          if ($limitfrom && !$this->supportsoffsetfetch) { // Skip $limitfrom records.
 791              if (!@mssql_data_seek($result, $limitfrom)) {
 792                  // Nothing, most probably seek past the end.
 793                  mssql_free_result($result);
 794                  $result = null;
 795              }
 796          }
 797  
 798          return $this->create_recordset($result);
 799      }
 800  
 801      protected function create_recordset($result) {
 802          return new mssql_native_moodle_recordset($result);
 803      }
 804  
 805      /**
 806       * Get a number of records as an array of objects using a SQL statement.
 807       *
 808       * Return value is like:
 809       * @see function get_records.
 810       *
 811       * @param string $sql the SQL select query to execute. The first column of this SELECT statement
 812       *   must be a unique value (usually the 'id' field), as it will be used as the key of the
 813       *   returned array.
 814       * @param array $params array of sql parameters
 815       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 816       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 817       * @return array of objects, or empty array if no records were found
 818       * @throws dml_exception A DML specific exception is thrown for any errors.
 819       */
 820      public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
 821  
 822          $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 823  
 824          $results = array();
 825  
 826          foreach ($rs as $row) {
 827              $id = reset($row);
 828              if (isset($results[$id])) {
 829                  $colname = key($row);
 830                  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);
 831              }
 832              $results[$id] = $row;
 833          }
 834          $rs->close();
 835  
 836          return $results;
 837      }
 838  
 839      /**
 840       * Selects records and return values (first field) as an array using a SQL statement.
 841       *
 842       * @param string $sql The SQL query
 843       * @param array $params array of sql parameters
 844       * @return array of values
 845       * @throws dml_exception A DML specific exception is thrown for any errors.
 846       */
 847      public function get_fieldset_sql($sql, array $params=null) {
 848  
 849          $rs = $this->get_recordset_sql($sql, $params);
 850  
 851          $results = array();
 852  
 853          foreach ($rs as $row) {
 854              $results[] = reset($row);
 855          }
 856          $rs->close();
 857  
 858          return $results;
 859      }
 860  
 861      /**
 862       * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
 863       * @param string $table name
 864       * @param mixed $params data record as object or array
 865       * @param bool $returnit return it of inserted record
 866       * @param bool $bulk true means repeated inserts expected
 867       * @param bool $customsequence true if 'id' included in $params, disables $returnid
 868       * @return bool|int true or new id
 869       * @throws dml_exception A DML specific exception is thrown for any errors.
 870       */
 871      public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
 872          if (!is_array($params)) {
 873              $params = (array)$params;
 874          }
 875  
 876          $returning = "";
 877          $isidentity = false;
 878  
 879          if ($customsequence) {
 880              if (!isset($params['id'])) {
 881                  throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
 882              }
 883              $returnid = false;
 884  
 885              $columns = $this->get_columns($table);
 886              if (isset($columns['id']) and $columns['id']->auto_increment) {
 887                  $isidentity = true;
 888              }
 889  
 890              // Disable IDENTITY column before inserting record with id, only if the
 891              // column is identity, from meta information.
 892              if ($isidentity) {
 893                  $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!!
 894                  list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
 895                  $this->query_start($sql, null, SQL_QUERY_AUX);
 896                  $result = mssql_query($sql, $this->mssql);
 897                  $this->query_end($result);
 898                  $this->free_result($result);
 899              }
 900  
 901          } else {
 902              unset($params['id']);
 903              if ($returnid) {
 904                  $returning = "OUTPUT inserted.id";
 905              }
 906          }
 907  
 908          if (empty($params)) {
 909              throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
 910          }
 911  
 912          $fields = implode(',', array_keys($params));
 913          $qms    = array_fill(0, count($params), '?');
 914          $qms    = implode(',', $qms);
 915  
 916          $sql = "INSERT INTO {" . $table . "} ($fields) $returning VALUES ($qms)";
 917  
 918          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
 919          $rawsql = $this->emulate_bound_params($sql, $params);
 920  
 921          $this->query_start($sql, $params, SQL_QUERY_INSERT);
 922          $result = mssql_query($rawsql, $this->mssql);
 923          // Expected results are:
 924          //     - true: insert ok and there isn't returned information.
 925          //     - false: insert failed and there isn't returned information.
 926          //     - resource: insert executed, need to look for returned (output)
 927          //           values to know if the insert was ok or no. Posible values
 928          //           are false = failed, integer = insert ok, id returned.
 929          $end = false;
 930          if (is_bool($result)) {
 931              $end = $result;
 932          } else if (is_resource($result)) {
 933              $end = mssql_result($result, 0, 0); // Fetch 1st column from 1st row.
 934          }
 935          $this->query_end($end); // End the query with the calculated $end.
 936  
 937          if ($returning !== "") {
 938              $params['id'] = $end;
 939          }
 940          $this->free_result($result);
 941  
 942          if ($customsequence) {
 943              // Enable IDENTITY column after inserting record with id, only if the
 944              // column is identity, from meta information.
 945              if ($isidentity) {
 946                  $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!!
 947                  list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null);
 948                  $this->query_start($sql, null, SQL_QUERY_AUX);
 949                  $result = mssql_query($sql, $this->mssql);
 950                  $this->query_end($result);
 951                  $this->free_result($result);
 952              }
 953          }
 954  
 955          if (!$returnid) {
 956              return true;
 957          }
 958  
 959          return (int)$params['id'];
 960      }
 961  
 962      /**
 963       * Insert a record into a table and return the "id" field if required.
 964       *
 965       * Some conversions and safety checks are carried out. Lobs are supported.
 966       * If the return ID isn't required, then this just reports success as true/false.
 967       * $data is an object containing needed data
 968       * @param string $table The database table to be inserted into
 969       * @param object $data A data object with values for one or more fields in the record
 970       * @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.
 971       * @return bool|int true or new id
 972       * @throws dml_exception A DML specific exception is thrown for any errors.
 973       */
 974      public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
 975          $dataobject = (array)$dataobject;
 976  
 977          $columns = $this->get_columns($table);
 978          if (empty($columns)) {
 979              throw new dml_exception('ddltablenotexist', $table);
 980          }
 981          $cleaned = array();
 982  
 983          foreach ($dataobject as $field => $value) {
 984              if ($field === 'id') {
 985                  continue;
 986              }
 987              if (!isset($columns[$field])) {
 988                  continue;
 989              }
 990              $column = $columns[$field];
 991              $cleaned[$field] = $this->normalise_value($column, $value);
 992          }
 993  
 994          return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
 995      }
 996  
 997      /**
 998       * Import a record into a table, id field is required.
 999       * Safety checks are NOT carried out. Lobs are supported.
1000       *
1001       * @param string $table name of database table to be inserted into
1002       * @param object $dataobject A data object with values for one or more fields in the record
1003       * @return bool true
1004       * @throws dml_exception A DML specific exception is thrown for any errors.
1005       */
1006      public function import_record($table, $dataobject) {
1007          $dataobject = (array)$dataobject;
1008  
1009          $columns = $this->get_columns($table);
1010          $cleaned = array();
1011  
1012          foreach ($dataobject as $field => $value) {
1013              if (!isset($columns[$field])) {
1014                  continue;
1015              }
1016              $column = $columns[$field];
1017              $cleaned[$field] = $this->normalise_value($column, $value);
1018          }
1019  
1020          $this->insert_record_raw($table, $cleaned, false, false, true);
1021  
1022          return true;
1023      }
1024  
1025      /**
1026       * Update record in database, as fast as possible, no safety checks, lobs not supported.
1027       * @param string $table name
1028       * @param mixed $params data record as object or array
1029       * @param bool true means repeated updates expected
1030       * @return bool true
1031       * @throws dml_exception A DML specific exception is thrown for any errors.
1032       */
1033      public function update_record_raw($table, $params, $bulk=false) {
1034          $params = (array)$params;
1035  
1036          if (!isset($params['id'])) {
1037              throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
1038          }
1039          $id = $params['id'];
1040          unset($params['id']);
1041  
1042          if (empty($params)) {
1043              throw new coding_exception('moodle_database::update_record_raw() no fields found.');
1044          }
1045  
1046          $sets = array();
1047          foreach ($params as $field=>$value) {
1048              $sets[] = "$field = ?";
1049          }
1050  
1051          $params[] = $id; // last ? in WHERE condition
1052  
1053          $sets = implode(',', $sets);
1054          $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?";
1055  
1056          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1057          $rawsql = $this->emulate_bound_params($sql, $params);
1058  
1059          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1060          $result = mssql_query($rawsql, $this->mssql);
1061          $this->query_end($result);
1062  
1063          $this->free_result($result);
1064          return true;
1065      }
1066  
1067      /**
1068       * Update a record in a table
1069       *
1070       * $dataobject is an object containing needed data
1071       * Relies on $dataobject having a variable "id" to
1072       * specify the record to update
1073       *
1074       * @param string $table The database table to be checked against.
1075       * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
1076       * @param bool true means repeated updates expected
1077       * @return bool true
1078       * @throws dml_exception A DML specific exception is thrown for any errors.
1079       */
1080      public function update_record($table, $dataobject, $bulk=false) {
1081          $dataobject = (array)$dataobject;
1082  
1083          $columns = $this->get_columns($table);
1084          $cleaned = array();
1085  
1086          foreach ($dataobject as $field => $value) {
1087              if (!isset($columns[$field])) {
1088                  continue;
1089              }
1090              $column = $columns[$field];
1091              $cleaned[$field] = $this->normalise_value($column, $value);
1092          }
1093  
1094          return $this->update_record_raw($table, $cleaned, $bulk);
1095      }
1096  
1097      /**
1098       * Set a single field in every table record which match a particular WHERE clause.
1099       *
1100       * @param string $table The database table to be checked against.
1101       * @param string $newfield the field to set.
1102       * @param string $newvalue the value to set the field to.
1103       * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
1104       * @param array $params array of sql parameters
1105       * @return bool true
1106       * @throws dml_exception A DML specific exception is thrown for any errors.
1107       */
1108      public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
1109  
1110          if ($select) {
1111              $select = "WHERE $select";
1112          }
1113          if (is_null($params)) {
1114              $params = array();
1115          }
1116  
1117          // convert params to ? types
1118          list($select, $params, $type) = $this->fix_sql_params($select, $params);
1119  
1120          // Get column metadata
1121          $columns = $this->get_columns($table);
1122          $column = $columns[$newfield];
1123  
1124          $newvalue = $this->normalise_value($column, $newvalue);
1125  
1126          if (is_null($newvalue)) {
1127              $newfield = "$newfield = NULL";
1128          } else {
1129              $newfield = "$newfield = ?";
1130              array_unshift($params, $newvalue);
1131          }
1132          $sql = "UPDATE {" . $table . "} SET $newfield $select";
1133  
1134          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1135          $rawsql = $this->emulate_bound_params($sql, $params);
1136  
1137          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1138          $result = mssql_query($rawsql, $this->mssql);
1139          $this->query_end($result);
1140  
1141          $this->free_result($result);
1142  
1143          return true;
1144      }
1145  
1146      /**
1147       * Delete one or more records from a table which match a particular WHERE clause.
1148       *
1149       * @param string $table The database table to be checked against.
1150       * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
1151       * @param array $params array of sql parameters
1152       * @return bool true
1153       * @throws dml_exception A DML specific exception is thrown for any errors.
1154       */
1155      public function delete_records_select($table, $select, array $params=null) {
1156  
1157          if ($select) {
1158              $select = "WHERE $select";
1159          }
1160  
1161          $sql = "DELETE FROM {" . $table . "} $select";
1162  
1163          list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
1164          $rawsql = $this->emulate_bound_params($sql, $params);
1165  
1166          $this->query_start($sql, $params, SQL_QUERY_UPDATE);
1167          $result = mssql_query($rawsql, $this->mssql);
1168          $this->query_end($result);
1169  
1170          $this->free_result($result);
1171  
1172          return true;
1173      }
1174  
1175      public function sql_cast_char2int($fieldname, $text=false) {
1176          if (!$text) {
1177              return ' CAST(' . $fieldname . ' AS INT) ';
1178          } else {
1179              return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) ';
1180          }
1181      }
1182  
1183      public function sql_cast_char2real($fieldname, $text=false) {
1184          if (!$text) {
1185              return ' CAST(' . $fieldname . ' AS REAL) ';
1186          } else {
1187              return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) ';
1188          }
1189      }
1190  
1191      public function sql_ceil($fieldname) {
1192          return ' CEILING(' . $fieldname . ')';
1193      }
1194  
1195  
1196      protected function get_collation() {
1197          if (isset($this->collation)) {
1198              return $this->collation;
1199          }
1200          if (!empty($this->dboptions['dbcollation'])) {
1201              // perf speedup
1202              $this->collation = $this->dboptions['dbcollation'];
1203              return $this->collation;
1204          }
1205  
1206          // make some default
1207          $this->collation = 'Latin1_General_CI_AI';
1208  
1209          $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation";
1210          $this->query_start($sql, null, SQL_QUERY_AUX);
1211          $result = mssql_query($sql, $this->mssql);
1212          $this->query_end($result);
1213  
1214          if ($result) {
1215              if ($rawcolumn = mssql_fetch_assoc($result)) {
1216                  $this->collation = reset($rawcolumn);
1217              }
1218              $this->free_result($result);
1219          }
1220  
1221          return $this->collation;
1222      }
1223  
1224      /**
1225       * Returns 'LIKE' part of a query.
1226       *
1227       * @param string $fieldname usually name of the table column
1228       * @param string $param usually bound query parameter (?, :named)
1229       * @param bool $casesensitive use case sensitive search
1230       * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
1231       * @param bool $notlike true means "NOT LIKE"
1232       * @param string $escapechar escape char for '%' and '_'
1233       * @return string SQL code fragment
1234       */
1235      public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
1236          if (strpos($param, '%') !== false) {
1237              debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
1238          }
1239  
1240          $collation = $this->get_collation();
1241  
1242          if ($casesensitive) {
1243              $collation = str_replace('_CI', '_CS', $collation);
1244          } else {
1245              $collation = str_replace('_CS', '_CI', $collation);
1246          }
1247          if ($accentsensitive) {
1248              $collation = str_replace('_AI', '_AS', $collation);
1249          } else {
1250              $collation = str_replace('_AS', '_AI', $collation);
1251          }
1252  
1253          $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
1254  
1255          return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'";
1256      }
1257  
1258      public function sql_concat() {
1259          $arr = func_get_args();
1260          foreach ($arr as $key => $ele) {
1261              $arr[$key] = ' CAST(' . $ele . ' AS NVARCHAR(255)) ';
1262          }
1263          $s = implode(' + ', $arr);
1264          if ($s === '') {
1265              return " '' ";
1266          }
1267          return " $s ";
1268      }
1269  
1270      public function sql_concat_join($separator="' '", $elements=array()) {
1271          for ($n=count($elements)-1; $n > 0 ; $n--) {
1272              array_splice($elements, $n, 0, $separator);
1273          }
1274          return call_user_func_array(array($this, 'sql_concat'), $elements);
1275      }
1276  
1277     public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) {
1278          if ($textfield) {
1279              return ' (' . $this->sql_compare_text($fieldname) . " = '') ";
1280          } else {
1281              return " ($fieldname = '') ";
1282          }
1283      }
1284  
1285     /**
1286       * Returns the SQL text to be used to calculate the length in characters of one expression.
1287       * @param string fieldname or expression to calculate its length in characters.
1288       * @return string the piece of SQL code to be used in the statement.
1289       */
1290      public function sql_length($fieldname) {
1291          return ' LEN(' . $fieldname . ')';
1292      }
1293  
1294      public function sql_order_by_text($fieldname, $numchars=32) {
1295          return " CONVERT(varchar({$numchars}), {$fieldname})";
1296      }
1297  
1298     /**
1299       * Returns the SQL for returning searching one string for the location of another.
1300       */
1301      public function sql_position($needle, $haystack) {
1302          return "CHARINDEX(($needle), ($haystack))";
1303      }
1304  
1305      /**
1306       * Returns the proper substr() SQL text used to extract substrings from DB
1307       * NOTE: this was originally returning only function name
1308       *
1309       * @param string $expr some string field, no aggregates
1310       * @param mixed $start integer or expression evaluating to int
1311       * @param mixed $length optional integer or expression evaluating to int
1312       * @return string sql fragment
1313       */
1314      public function sql_substr($expr, $start, $length=false) {
1315          if (count(func_get_args()) < 2) {
1316              throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa
1317  s only returning name of SQL substring function, it now requires all parameters.');
1318          }
1319          if ($length === false) {
1320              return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", 2^31-1)";
1321          } else {
1322              return "SUBSTRING($expr, " . $this->sql_cast_char2int($start) . ", " . $this->sql_cast_char2int($length) . ")";
1323          }
1324      }
1325  
1326      /**
1327       * Does this driver support tool_replace?
1328       *
1329       * @since Moodle 2.6.1
1330       * @return bool
1331       */
1332      public function replace_all_text_supported() {
1333          return true;
1334      }
1335  
1336      public function session_lock_supported() {
1337          return true;
1338      }
1339  
1340      /**
1341       * Obtain session lock
1342       * @param int $rowid id of the row with session record
1343       * @param int $timeout max allowed time to wait for the lock in seconds
1344       * @return bool success
1345       */
1346      public function get_session_lock($rowid, $timeout) {
1347          if (!$this->session_lock_supported()) {
1348              return;
1349          }
1350          parent::get_session_lock($rowid, $timeout);
1351  
1352          $timeoutmilli = $timeout * 1000;
1353  
1354          $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1355          // There is one bug in PHP/freetds (both reproducible with mssql_query()
1356          // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
1357          // stored procedures, causing scalar results of the execution
1358          // to be cast to boolean (true/fals). Here there is one
1359          // workaround that forces the return of one recordset resource.
1360          // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session',  $timeoutmilli";
1361          $sql = "BEGIN
1362                      DECLARE @result INT
1363                      EXECUTE @result = sp_getapplock @Resource='$fullname',
1364                                                      @LockMode='Exclusive',
1365                                                      @LockOwner='Session',
1366                                                      @LockTimeout='$timeoutmilli'
1367                      SELECT @result
1368                  END";
1369          $this->query_start($sql, null, SQL_QUERY_AUX);
1370          $result = mssql_query($sql, $this->mssql);
1371          $this->query_end($result);
1372  
1373          if ($result) {
1374              $row = mssql_fetch_row($result);
1375              if ($row[0] < 0) {
1376                  throw new dml_sessionwait_exception();
1377              }
1378          }
1379  
1380          $this->free_result($result);
1381      }
1382  
1383      public function release_session_lock($rowid) {
1384          if (!$this->session_lock_supported()) {
1385              return;
1386          }
1387          if (!$this->used_for_db_sessions) {
1388              return;
1389          }
1390  
1391          parent::release_session_lock($rowid);
1392  
1393          $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
1394          $sql = "sp_releaseapplock '$fullname', 'Session'";
1395          $this->query_start($sql, null, SQL_QUERY_AUX);
1396          $result = mssql_query($sql, $this->mssql);
1397          $this->query_end($result);
1398  
1399          $this->free_result($result);
1400      }
1401  
1402      /**
1403       * Driver specific start of real database transaction,
1404       * this can not be used directly in code.
1405       * @return void
1406       */
1407      protected function begin_transaction() {
1408          // requires database to run with READ_COMMITTED_SNAPSHOT ON
1409          $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
1410          $this->query_start($sql, NULL, SQL_QUERY_AUX);
1411          $result = mssql_query($sql, $this->mssql);
1412          $this->query_end($result);
1413  
1414          $this->free_result($result);
1415      }
1416  
1417      /**
1418       * Driver specific commit of real database transaction,
1419       * this can not be used directly in code.
1420       * @return void
1421       */
1422      protected function commit_transaction() {
1423          $sql = "COMMIT TRANSACTION";
1424          $this->query_start($sql, NULL, SQL_QUERY_AUX);
1425          $result = mssql_query($sql, $this->mssql);
1426          $this->query_end($result);
1427  
1428          $this->free_result($result);
1429      }
1430  
1431      /**
1432       * Driver specific abort of real database transaction,
1433       * this can not be used directly in code.
1434       * @return void
1435       */
1436      protected function rollback_transaction() {
1437          $sql = "ROLLBACK TRANSACTION";
1438          $this->query_start($sql, NULL, SQL_QUERY_AUX);
1439          $result = mssql_query($sql, $this->mssql);
1440          $this->query_end($result);
1441  
1442          $this->free_result($result);
1443      }
1444  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1