Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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   * Experimental pdo database class.
  19   *
  20   * @package    core_dml
  21   * @copyright  2008 Andrei Bautu
  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__.'/pdo_moodle_database.php');
  28  
  29  /**
  30   * Experimental pdo database class
  31   *
  32   * @package    core_dml
  33   * @copyright  2008 Andrei Bautu
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class sqlite3_pdo_moodle_database extends pdo_moodle_database {
  37      protected $database_file_extension = '.sq3.php';
  38      /**
  39       * Detects if all needed PHP stuff installed.
  40       * Note: can be used before connect()
  41       * @return mixed true if ok, string if something
  42       */
  43      public function driver_installed() {
  44          if (!extension_loaded('pdo_sqlite') || !extension_loaded('pdo')){
  45              return get_string('sqliteextensionisnotpresentinphp', 'install');
  46          }
  47          return true;
  48      }
  49  
  50      /**
  51       * Returns database family type - describes SQL dialect
  52       * Note: can be used before connect()
  53       * @return string db family name (mysql, postgres, mssql, oracle, etc.)
  54       */
  55      public function get_dbfamily() {
  56          return 'sqlite';
  57      }
  58  
  59      /**
  60       * Returns more specific database driver type
  61       * Note: can be used before connect()
  62       * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
  63       */
  64      protected function get_dbtype() {
  65          return 'sqlite3';
  66      }
  67  
  68      protected function configure_dbconnection() {
  69          // try to protect database file against web access;
  70          // this is required in case that the moodledata folder is web accessible and
  71          // .htaccess is not in place; requires that the database file extension is php
  72          $this->pdb->exec('CREATE TABLE IF NOT EXISTS "<?php die?>" (id int)');
  73          $this->pdb->exec('PRAGMA synchronous=OFF');
  74          $this->pdb->exec('PRAGMA short_column_names=1');
  75          $this->pdb->exec('PRAGMA encoding="UTF-8"');
  76          $this->pdb->exec('PRAGMA case_sensitive_like=0');
  77          $this->pdb->exec('PRAGMA locking_mode=NORMAL');
  78      }
  79  
  80      /**
  81       * Attempt to create the database
  82       * @param string $dbhost
  83       * @param string $dbuser
  84       * @param string $dbpass
  85       * @param string $dbname
  86       *
  87       * @return bool success
  88       */
  89      public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
  90          global $CFG;
  91  
  92          $this->dbhost = $dbhost;
  93          $this->dbuser = $dbuser;
  94          $this->dbpass = $dbpass;
  95          $this->dbname = $dbname;
  96          $filepath = $this->get_dbfilepath();
  97          $dirpath = dirname($filepath);
  98          @mkdir($dirpath, $CFG->directorypermissions, true);
  99          return touch($filepath);
 100      }
 101  
 102      /**
 103       * Returns the driver-dependent DSN for PDO based on members stored by connect.
 104       * Must be called after connect (or after $dbname, $dbhost, etc. members have been set).
 105       * @return string driver-dependent DSN
 106       */
 107      protected function get_dsn() {
 108          return 'sqlite:'.$this->get_dbfilepath();
 109      }
 110  
 111      /**
 112       * Returns the file path for the database file, computed from dbname and/or dboptions.
 113       * If dboptions['file'] is set, then it is used (use :memory: for in memory database);
 114       * else if dboptions['path'] is set, then the file will be <dboptions path>/<dbname>.sq3.php;
 115       * else if dbhost is set and not localhost, then the file will be <dbhost>/<dbname>.sq3.php;
 116       * else the file will be <moodle data path>/<dbname>.sq3.php
 117       * @return string file path to the SQLite database;
 118       */
 119      public function get_dbfilepath() {
 120          global $CFG;
 121          if (!empty($this->dboptions['file'])) {
 122              return $this->dboptions['file'];
 123          }
 124          if ($this->dbhost && $this->dbhost != 'localhost') {
 125              $path = $this->dbhost;
 126          } else {
 127              $path = $CFG->dataroot;
 128          }
 129          $path = rtrim($path, '\\/').'/';
 130          if (!empty($this->dbuser)) {
 131              $path .= $this->dbuser.'_';
 132          }
 133          $path .= $this->dbname.'_'.md5($this->dbpass).$this->database_file_extension;
 134          return $path;
 135      }
 136  
 137      /**
 138       * Return tables in database WITHOUT current prefix.
 139       * @param bool $usecache if true, returns list of cached tables.
 140       * @return array of table names in lowercase and without prefix
 141       */
 142      public function get_tables($usecache=true) {
 143          $tables = array();
 144  
 145          $sql = 'SELECT name FROM sqlite_master WHERE type="table" UNION ALL SELECT name FROM sqlite_temp_master WHERE type="table" ORDER BY name';
 146          if ($this->debug) {
 147              $this->debug_query($sql);
 148          }
 149          $rstables = $this->pdb->query($sql);
 150          foreach ($rstables as $table) {
 151              $table = $table['name'];
 152              $table = strtolower($table);
 153              if ($this->prefix !== false && $this->prefix !== '') {
 154                  if (strpos($table, $this->prefix) !== 0) {
 155                      continue;
 156                  }
 157                  $table = substr($table, strlen($this->prefix));
 158              }
 159              $tables[$table] = $table;
 160          }
 161          return $tables;
 162      }
 163  
 164      /**
 165       * Return table indexes - everything lowercased
 166       * @param string $table The table we want to get indexes from.
 167       * @return array of arrays
 168       */
 169      public function get_indexes($table) {
 170          $indexes = array();
 171          $sql = 'PRAGMA index_list('.$this->prefix.$table.')';
 172          if ($this->debug) {
 173              $this->debug_query($sql);
 174          }
 175          $rsindexes = $this->pdb->query($sql);
 176          foreach($rsindexes as $index) {
 177              $unique = (boolean)$index['unique'];
 178              $index = $index['name'];
 179              $sql = 'PRAGMA index_info("'.$index.'")';
 180              if ($this->debug) {
 181                  $this->debug_query($sql);
 182              }
 183              $rscolumns = $this->pdb->query($sql);
 184              $columns = array();
 185              foreach($rscolumns as $row) {
 186                  $columns[] = strtolower($row['name']);
 187              }
 188              $index = strtolower($index);
 189              $indexes[$index]['unique'] = $unique;
 190              $indexes[$index]['columns'] = $columns;
 191          }
 192          return $indexes;
 193      }
 194  
 195      /**
 196       * Returns detailed information about columns in table.
 197       *
 198       * @param string $table name
 199       * @return array array of database_column_info objects indexed with column names
 200       */
 201      protected function fetch_columns(string $table): array {
 202          $structure = array();
 203  
 204          // get table's CREATE TABLE command (we'll need it for autoincrement fields)
 205          $sql = 'SELECT sql FROM sqlite_master WHERE type="table" AND tbl_name="'.$this->prefix.$table.'"';
 206          if ($this->debug) {
 207              $this->debug_query($sql);
 208          }
 209          $createsql = $this->pdb->query($sql)->fetch();
 210          if (!$createsql) {
 211              return false;
 212          }
 213          $createsql = $createsql['sql'];
 214  
 215          $sql = 'PRAGMA table_info("'. $this->prefix.$table.'")';
 216          if ($this->debug) {
 217              $this->debug_query($sql);
 218          }
 219          $rscolumns = $this->pdb->query($sql);
 220          foreach ($rscolumns as $row) {
 221              $columninfo = array(
 222                  'name' => strtolower($row['name']), // colum names must be lowercase
 223                  'not_null' =>(boolean)$row['notnull'],
 224                  'primary_key' => (boolean)$row['pk'],
 225                  'has_default' => !is_null($row['dflt_value']),
 226                  'default_value' => $row['dflt_value'],
 227                  'auto_increment' => false,
 228                  'binary' => false,
 229                  //'unsigned' => false,
 230              );
 231              $type = explode('(', $row['type']);
 232              $columninfo['type'] = strtolower($type[0]);
 233              if (count($type) > 1) {
 234                  $size = explode(',', trim($type[1], ')'));
 235                  $columninfo['max_length'] = $size[0];
 236                  if (count($size) > 1) {
 237                      $columninfo['scale'] = $size[1];
 238                  }
 239              }
 240              // SQLite does not have a fixed set of datatypes (ie. it accepts any string as
 241              // datatype in the CREATE TABLE command. We try to guess which type is used here
 242              switch(substr($columninfo['type'], 0, 3)) {
 243                  case 'int': // int integer
 244                      if ($columninfo['primary_key'] && preg_match('/'.$columninfo['name'].'\W+integer\W+primary\W+key\W+autoincrement/im', $createsql)) {
 245                          $columninfo['meta_type'] = 'R';
 246                          $columninfo['auto_increment'] = true;
 247                      } else {
 248                          $columninfo['meta_type'] = 'I';
 249                      }
 250                      break;
 251                  case 'num': // number numeric
 252                  case 'rea': // real
 253                  case 'dou': // double
 254                  case 'flo': // float
 255                      $columninfo['meta_type'] = 'N';
 256                      break;
 257                  case 'var': // varchar
 258                  case 'cha': // char
 259                      $columninfo['meta_type'] = 'C';
 260                      break;
 261                  case 'enu': // enums
 262                      $columninfo['meta_type'] = 'C';
 263                      break;
 264                  case 'tex': // text
 265                  case 'clo': // clob
 266                      $columninfo['meta_type'] = 'X';
 267                      break;
 268                  case 'blo': // blob
 269                  case 'non': // none
 270                      $columninfo['meta_type'] = 'B';
 271                      $columninfo['binary'] = true;
 272                      break;
 273                  case 'boo': // boolean
 274                  case 'bit': // bit
 275                  case 'log': // logical
 276                      $columninfo['meta_type'] = 'L';
 277                      $columninfo['max_length'] = 1;
 278                      break;
 279                  case 'tim': // timestamp
 280                      $columninfo['meta_type'] = 'T';
 281                      break;
 282                  case 'dat': // date datetime
 283                      $columninfo['meta_type'] = 'D';
 284                      break;
 285              }
 286              if ($columninfo['has_default'] && ($columninfo['meta_type'] == 'X' || $columninfo['meta_type']== 'C')) {
 287                  // trim extra quotes from text default values
 288                  $columninfo['default_value'] = substr($columninfo['default_value'], 1, -1);
 289              }
 290              $structure[$columninfo['name']] = new database_column_info($columninfo);
 291          }
 292  
 293          return $structure;
 294      }
 295  
 296      /**
 297       * Normalise values based in RDBMS dependencies (booleans, LOBs...)
 298       *
 299       * @param database_column_info $column column metadata corresponding with the value we are going to normalise
 300       * @param mixed $value value we are going to normalise
 301       * @return mixed the normalised value
 302       */
 303      protected function normalise_value($column, $value) {
 304          return $value;
 305      }
 306  
 307      /**
 308       * Returns the sql statement with clauses to append used to limit a recordset range.
 309       * @param string $sql the SQL statement to limit.
 310       * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
 311       * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
 312       * @return string the SQL statement with limiting clauses
 313       */
 314      protected function get_limit_clauses($sql, $limitfrom=0, $limitnum=0) {
 315          if ($limitnum) {
 316              $sql .= ' LIMIT '.$limitnum;
 317              if ($limitfrom) {
 318                  $sql .= ' OFFSET '.$limitfrom;
 319              }
 320          }
 321          return $sql;
 322      }
 323  
 324      /**
 325       * Delete the records from a table where all the given conditions met.
 326       * If conditions not specified, table is truncated.
 327       *
 328       * @param string $table the table to delete from.
 329       * @param array $conditions optional array $fieldname=>requestedvalue with AND in between
 330       * @return returns success.
 331       */
 332      public function delete_records($table, array $conditions=null) {
 333          if (is_null($conditions)) {
 334              return $this->execute("DELETE FROM {{$table}}");
 335          }
 336          list($select, $params) = $this->where_clause($table, $conditions);
 337          return $this->delete_records_select($table, $select, $params);
 338      }
 339  
 340      /**
 341       * Returns the proper SQL to do CONCAT between the elements passed
 342       * Can take many parameters
 343       *
 344       * @param string $element
 345       * @return string
 346       */
 347      public function sql_concat() {
 348          $elements = func_get_args();
 349          return implode('||', $elements);
 350      }
 351  
 352      /**
 353       * Returns the proper SQL to do CONCAT between the elements passed
 354       * with a given separator
 355       *
 356       * @param string $separator
 357       * @param array  $elements
 358       * @return string
 359       */
 360      public function sql_concat_join($separator="' '", $elements=array()) {
 361          // Intersperse $elements in the array.
 362          // Add items to the array on the fly, walking it
 363          // _backwards_ splicing the elements in. The loop definition
 364          // should skip first and last positions.
 365          for ($n=count($elements)-1; $n > 0; $n--) {
 366              array_splice($elements, $n, 0, $separator);
 367          }
 368          return implode('||', $elements);
 369      }
 370  
 371      /**
 372       * Returns the SQL text to be used in order to perform one bitwise XOR operation
 373       * between 2 integers.
 374       *
 375       * @param integer int1 first integer in the operation
 376       * @param integer int2 second integer in the operation
 377       * @return string the piece of SQL code to be used in your statement.
 378       */
 379      public function sql_bitxor($int1, $int2) {
 380          return '( ~' . $this->sql_bitand($int1, $int2) . ' & ' . $this->sql_bitor($int1, $int2) . ')';
 381      }
 382  }