PageRenderTime 52ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/dml/mysqli_native_moodle_database.php

https://bitbucket.org/kudutest1/moodlegit
PHP | 1327 lines | 976 code | 123 blank | 228 comment | 113 complexity | 5a211702214189d508b20a763961033f MD5 | raw file
  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. * Native mysqli class representing moodle database interface.
  18. *
  19. * @package core_dml
  20. * @copyright 2008 Petr Skoda (http://skodak.org)
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. require_once(__DIR__.'/moodle_database.php');
  25. require_once(__DIR__.'/mysqli_native_moodle_recordset.php');
  26. require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
  27. /**
  28. * Native mysqli class representing moodle database interface.
  29. *
  30. * @package core_dml
  31. * @copyright 2008 Petr Skoda (http://skodak.org)
  32. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33. */
  34. class mysqli_native_moodle_database extends moodle_database {
  35. protected $mysqli = null;
  36. private $transactions_supported = null;
  37. /**
  38. * Attempt to create the database
  39. * @param string $dbhost
  40. * @param string $dbuser
  41. * @param string $dbpass
  42. * @param string $dbname
  43. * @return bool success
  44. * @throws dml_exception A DML specific exception is thrown for any errors.
  45. */
  46. public function create_database($dbhost, $dbuser, $dbpass, $dbname, array $dboptions=null) {
  47. $driverstatus = $this->driver_installed();
  48. if ($driverstatus !== true) {
  49. throw new dml_exception('dbdriverproblem', $driverstatus);
  50. }
  51. if (!empty($dboptions['dbsocket'])
  52. and (strpos($dboptions['dbsocket'], '/') !== false or strpos($dboptions['dbsocket'], '\\') !== false)) {
  53. $dbsocket = $dboptions['dbsocket'];
  54. } else {
  55. $dbsocket = ini_get('mysqli.default_socket');
  56. }
  57. if (empty($dboptions['dbport'])) {
  58. $dbport = (int)ini_get('mysqli.default_port');
  59. } else {
  60. $dbport = (int)$dboptions['dbport'];
  61. }
  62. // verify ini.get does not return nonsense
  63. if (empty($dbport)) {
  64. $dbport = 3306;
  65. }
  66. ob_start();
  67. $conn = new mysqli($dbhost, $dbuser, $dbpass, '', $dbport, $dbsocket); // Connect without db
  68. $dberr = ob_get_contents();
  69. ob_end_clean();
  70. $errorno = @$conn->connect_errno;
  71. if ($errorno !== 0) {
  72. throw new dml_connection_exception($dberr);
  73. }
  74. if (isset($dboptions['dbcollation']) and strpos($dboptions['dbcollation'], 'utf8_') === 0) {
  75. $collation = $dboptions['dbcollation'];
  76. } else {
  77. $collation = 'utf8_unicode_ci';
  78. }
  79. $result = $conn->query("CREATE DATABASE $dbname DEFAULT CHARACTER SET utf8 DEFAULT COLLATE ".$collation);
  80. $conn->close();
  81. if (!$result) {
  82. throw new dml_exception('cannotcreatedb');
  83. }
  84. return true;
  85. }
  86. /**
  87. * Detects if all needed PHP stuff installed.
  88. * Note: can be used before connect()
  89. * @return mixed true if ok, string if something
  90. */
  91. public function driver_installed() {
  92. if (!extension_loaded('mysqli')) {
  93. return get_string('mysqliextensionisnotpresentinphp', 'install');
  94. }
  95. return true;
  96. }
  97. /**
  98. * Returns database family type - describes SQL dialect
  99. * Note: can be used before connect()
  100. * @return string db family name (mysql, postgres, mssql, oracle, etc.)
  101. */
  102. public function get_dbfamily() {
  103. return 'mysql';
  104. }
  105. /**
  106. * Returns more specific database driver type
  107. * Note: can be used before connect()
  108. * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
  109. */
  110. protected function get_dbtype() {
  111. return 'mysqli';
  112. }
  113. /**
  114. * Returns general database library name
  115. * Note: can be used before connect()
  116. * @return string db type pdo, native
  117. */
  118. protected function get_dblibrary() {
  119. return 'native';
  120. }
  121. /**
  122. * Returns the current MySQL db engine.
  123. *
  124. * This is an ugly workaround for MySQL default engine problems,
  125. * Moodle is designed to work best on ACID compliant databases
  126. * with full transaction support. Do not use MyISAM.
  127. *
  128. * @return string or null MySQL engine name
  129. */
  130. public function get_dbengine() {
  131. if (isset($this->dboptions['dbengine'])) {
  132. return $this->dboptions['dbengine'];
  133. }
  134. if ($this->external) {
  135. return null;
  136. }
  137. $engine = null;
  138. // Look for current engine of our config table (the first table that gets created),
  139. // so that we create all tables with the same engine.
  140. $sql = "SELECT engine
  141. FROM INFORMATION_SCHEMA.TABLES
  142. WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config'";
  143. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  144. $result = $this->mysqli->query($sql);
  145. $this->query_end($result);
  146. if ($rec = $result->fetch_assoc()) {
  147. $engine = $rec['engine'];
  148. }
  149. $result->close();
  150. if ($engine) {
  151. // Cache the result to improve performance.
  152. $this->dboptions['dbengine'] = $engine;
  153. return $engine;
  154. }
  155. // get the default database engine
  156. $sql = "SELECT @@storage_engine";
  157. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  158. $result = $this->mysqli->query($sql);
  159. $this->query_end($result);
  160. if ($rec = $result->fetch_assoc()) {
  161. $engine = $rec['@@storage_engine'];
  162. }
  163. $result->close();
  164. if ($engine === 'MyISAM') {
  165. // we really do not want MyISAM for Moodle, InnoDB or XtraDB is a reasonable defaults if supported
  166. $sql = "SHOW STORAGE ENGINES";
  167. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  168. $result = $this->mysqli->query($sql);
  169. $this->query_end($result);
  170. $engines = array();
  171. while ($res = $result->fetch_assoc()) {
  172. if ($res['Support'] === 'YES' or $res['Support'] === 'DEFAULT') {
  173. $engines[$res['Engine']] = true;
  174. }
  175. }
  176. $result->close();
  177. if (isset($engines['InnoDB'])) {
  178. $engine = 'InnoDB';
  179. }
  180. if (isset($engines['XtraDB'])) {
  181. $engine = 'XtraDB';
  182. }
  183. }
  184. // Cache the result to improve performance.
  185. $this->dboptions['dbengine'] = $engine;
  186. return $engine;
  187. }
  188. /**
  189. * Returns the current MySQL db collation.
  190. *
  191. * This is an ugly workaround for MySQL default collation problems.
  192. *
  193. * @return string or null MySQL collation name
  194. */
  195. public function get_dbcollation() {
  196. if (isset($this->dboptions['dbcollation'])) {
  197. return $this->dboptions['dbcollation'];
  198. }
  199. if ($this->external) {
  200. return null;
  201. }
  202. $collation = null;
  203. // Look for current collation of our config table (the first table that gets created),
  204. // so that we create all tables with the same collation.
  205. $sql = "SELECT collation_name
  206. FROM INFORMATION_SCHEMA.COLUMNS
  207. WHERE table_schema = DATABASE() AND table_name = '{$this->prefix}config' AND column_name = 'value'";
  208. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  209. $result = $this->mysqli->query($sql);
  210. $this->query_end($result);
  211. if ($rec = $result->fetch_assoc()) {
  212. $collation = $rec['collation_name'];
  213. }
  214. $result->close();
  215. if (!$collation) {
  216. // Get the default database collation, but only if using UTF-8.
  217. $sql = "SELECT @@collation_database";
  218. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  219. $result = $this->mysqli->query($sql);
  220. $this->query_end($result);
  221. if ($rec = $result->fetch_assoc()) {
  222. if (strpos($rec['@@collation_database'], 'utf8_') === 0) {
  223. $collation = $rec['@@collation_database'];
  224. }
  225. }
  226. $result->close();
  227. }
  228. if (!$collation) {
  229. // We want only utf8 compatible collations.
  230. $collation = null;
  231. $sql = "SHOW COLLATION WHERE Collation LIKE 'utf8\_%' AND Charset = 'utf8'";
  232. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  233. $result = $this->mysqli->query($sql);
  234. $this->query_end($result);
  235. while ($res = $result->fetch_assoc()) {
  236. $collation = $res['Collation'];
  237. if (strtoupper($res['Default']) === 'YES') {
  238. $collation = $res['Collation'];
  239. break;
  240. }
  241. }
  242. $result->close();
  243. }
  244. // Cache the result to improve performance.
  245. $this->dboptions['dbcollation'] = $collation;
  246. return $collation;
  247. }
  248. /**
  249. * Returns localised database type name
  250. * Note: can be used before connect()
  251. * @return string
  252. */
  253. public function get_name() {
  254. return get_string('nativemysqli', 'install');
  255. }
  256. /**
  257. * Returns localised database configuration help.
  258. * Note: can be used before connect()
  259. * @return string
  260. */
  261. public function get_configuration_help() {
  262. return get_string('nativemysqlihelp', 'install');
  263. }
  264. /**
  265. * Returns localised database description
  266. * Note: can be used before connect()
  267. * @return string
  268. */
  269. public function get_configuration_hints() {
  270. return get_string('databasesettingssub_mysqli', 'install');
  271. }
  272. /**
  273. * Diagnose database and tables, this function is used
  274. * to verify database and driver settings, db engine types, etc.
  275. *
  276. * @return string null means everything ok, string means problem found.
  277. */
  278. public function diagnose() {
  279. $sloppymyisamfound = false;
  280. $prefix = str_replace('_', '\\_', $this->prefix);
  281. $sql = "SELECT COUNT('x')
  282. FROM INFORMATION_SCHEMA.TABLES
  283. WHERE table_schema = DATABASE()
  284. AND table_name LIKE BINARY '$prefix%'
  285. AND Engine = 'MyISAM'";
  286. $this->query_start($sql, null, SQL_QUERY_AUX);
  287. $result = $this->mysqli->query($sql);
  288. $this->query_end($result);
  289. if ($result) {
  290. if ($arr = $result->fetch_assoc()) {
  291. $count = reset($arr);
  292. if ($count) {
  293. $sloppymyisamfound = true;
  294. }
  295. }
  296. $result->close();
  297. }
  298. if ($sloppymyisamfound) {
  299. return get_string('myisamproblem', 'error');
  300. } else {
  301. return null;
  302. }
  303. }
  304. /**
  305. * Connect to db
  306. * Must be called before other methods.
  307. * @param string $dbhost The database host.
  308. * @param string $dbuser The database username.
  309. * @param string $dbpass The database username's password.
  310. * @param string $dbname The name of the database being connected to.e
  311. * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
  312. * @param array $dboptions driver specific options
  313. * @return bool success
  314. */
  315. public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
  316. $driverstatus = $this->driver_installed();
  317. if ($driverstatus !== true) {
  318. throw new dml_exception('dbdriverproblem', $driverstatus);
  319. }
  320. $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
  321. // dbsocket is used ONLY if host is NULL or 'localhost',
  322. // you can not disable it because it is always tried if dbhost is 'localhost'
  323. if (!empty($this->dboptions['dbsocket'])
  324. and (strpos($this->dboptions['dbsocket'], '/') !== false or strpos($this->dboptions['dbsocket'], '\\') !== false)) {
  325. $dbsocket = $this->dboptions['dbsocket'];
  326. } else {
  327. $dbsocket = ini_get('mysqli.default_socket');
  328. }
  329. if (empty($this->dboptions['dbport'])) {
  330. $dbport = (int)ini_get('mysqli.default_port');
  331. } else {
  332. $dbport = (int)$this->dboptions['dbport'];
  333. }
  334. // verify ini.get does not return nonsense
  335. if (empty($dbport)) {
  336. $dbport = 3306;
  337. }
  338. if ($dbhost and !empty($this->dboptions['dbpersist'])) {
  339. $dbhost = "p:$dbhost";
  340. }
  341. ob_start();
  342. $this->mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
  343. $dberr = ob_get_contents();
  344. ob_end_clean();
  345. $errorno = @$this->mysqli->connect_errno;
  346. if ($errorno !== 0) {
  347. $this->mysqli = null;
  348. throw new dml_connection_exception($dberr);
  349. }
  350. $this->query_start("--set_charset()", null, SQL_QUERY_AUX);
  351. $this->mysqli->set_charset('utf8');
  352. $this->query_end(true);
  353. // If available, enforce strict mode for the session. That guaranties
  354. // standard behaviour under some situations, avoiding some MySQL nasty
  355. // habits like truncating data or performing some transparent cast losses.
  356. // With strict mode enforced, Moodle DB layer will be consistently throwing
  357. // the corresponding exceptions as expected.
  358. $si = $this->get_server_info();
  359. if (version_compare($si['version'], '5.0.2', '>=')) {
  360. $sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES'";
  361. $this->query_start($sql, null, SQL_QUERY_AUX);
  362. $result = $this->mysqli->query($sql);
  363. $this->query_end($result);
  364. }
  365. // Connection stabilised and configured, going to instantiate the temptables controller
  366. $this->temptables = new mysqli_native_moodle_temptables($this);
  367. return true;
  368. }
  369. /**
  370. * Close database connection and release all resources
  371. * and memory (especially circular memory references).
  372. * Do NOT use connect() again, create a new instance if needed.
  373. */
  374. public function dispose() {
  375. parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
  376. if ($this->mysqli) {
  377. $this->mysqli->close();
  378. $this->mysqli = null;
  379. }
  380. }
  381. /**
  382. * Returns database server info array
  383. * @return array Array containing 'description' and 'version' info
  384. */
  385. public function get_server_info() {
  386. return array('description'=>$this->mysqli->server_info, 'version'=>$this->mysqli->server_info);
  387. }
  388. /**
  389. * Returns supported query parameter types
  390. * @return int bitmask of accepted SQL_PARAMS_*
  391. */
  392. protected function allowed_param_types() {
  393. return SQL_PARAMS_QM;
  394. }
  395. /**
  396. * Returns last error reported by database engine.
  397. * @return string error message
  398. */
  399. public function get_last_error() {
  400. return $this->mysqli->error;
  401. }
  402. /**
  403. * Return tables in database WITHOUT current prefix
  404. * @param bool $usecache if true, returns list of cached tables.
  405. * @return array of table names in lowercase and without prefix
  406. */
  407. public function get_tables($usecache=true) {
  408. if ($usecache and $this->tables !== null) {
  409. return $this->tables;
  410. }
  411. $this->tables = array();
  412. $prefix = str_replace('_', '\\_', $this->prefix);
  413. $sql = "SHOW TABLES LIKE '$prefix%'";
  414. $this->query_start($sql, null, SQL_QUERY_AUX);
  415. $result = $this->mysqli->query($sql);
  416. $this->query_end($result);
  417. $len = strlen($this->prefix);
  418. if ($result) {
  419. while ($arr = $result->fetch_assoc()) {
  420. $tablename = reset($arr);
  421. $tablename = substr($tablename, $len);
  422. $this->tables[$tablename] = $tablename;
  423. }
  424. $result->close();
  425. }
  426. // Add the currently available temptables
  427. $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
  428. return $this->tables;
  429. }
  430. /**
  431. * Return table indexes - everything lowercased.
  432. * @param string $table The table we want to get indexes from.
  433. * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
  434. */
  435. public function get_indexes($table) {
  436. $indexes = array();
  437. $sql = "SHOW INDEXES FROM {$this->prefix}$table";
  438. $this->query_start($sql, null, SQL_QUERY_AUX);
  439. $result = $this->mysqli->query($sql);
  440. try {
  441. $this->query_end($result);
  442. } catch (dml_read_exception $e) {
  443. return $indexes; // table does not exist - no indexes...
  444. }
  445. if ($result) {
  446. while ($res = $result->fetch_object()) {
  447. if ($res->Key_name === 'PRIMARY') {
  448. continue;
  449. }
  450. if (!isset($indexes[$res->Key_name])) {
  451. $indexes[$res->Key_name] = array('unique'=>empty($res->Non_unique), 'columns'=>array());
  452. }
  453. $indexes[$res->Key_name]['columns'][$res->Seq_in_index-1] = $res->Column_name;
  454. }
  455. $result->close();
  456. }
  457. return $indexes;
  458. }
  459. /**
  460. * Returns detailed information about columns in table. This information is cached internally.
  461. * @param string $table name
  462. * @param bool $usecache
  463. * @return array array of database_column_info objects indexed with column names
  464. */
  465. public function get_columns($table, $usecache=true) {
  466. if ($usecache) {
  467. $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
  468. $cache = cache::make('core', 'databasemeta', $properties);
  469. if ($data = $cache->get($table)) {
  470. return $data;
  471. }
  472. }
  473. $structure = array();
  474. $sql = "SELECT column_name, data_type, character_maximum_length, numeric_precision,
  475. numeric_scale, is_nullable, column_type, column_default, column_key, extra
  476. FROM information_schema.columns
  477. WHERE table_name = '" . $this->prefix.$table . "'
  478. AND table_schema = '" . $this->dbname . "'
  479. ORDER BY ordinal_position";
  480. $this->query_start($sql, null, SQL_QUERY_AUX);
  481. $result = $this->mysqli->query($sql);
  482. $this->query_end(true); // Don't want to throw anything here ever. MDL-30147
  483. if ($result === false) {
  484. return array();
  485. }
  486. if ($result->num_rows > 0) {
  487. // standard table exists
  488. while ($rawcolumn = $result->fetch_assoc()) {
  489. $info = (object)$this->get_column_info((object)$rawcolumn);
  490. $structure[$info->name] = new database_column_info($info);
  491. }
  492. $result->close();
  493. } else {
  494. // temporary tables are not in information schema, let's try it the old way
  495. $result->close();
  496. $sql = "SHOW COLUMNS FROM {$this->prefix}$table";
  497. $this->query_start($sql, null, SQL_QUERY_AUX);
  498. $result = $this->mysqli->query($sql);
  499. $this->query_end(true);
  500. if ($result === false) {
  501. return array();
  502. }
  503. while ($rawcolumn = $result->fetch_assoc()) {
  504. $rawcolumn = (object)array_change_key_case($rawcolumn, CASE_LOWER);
  505. $rawcolumn->column_name = $rawcolumn->field; unset($rawcolumn->field);
  506. $rawcolumn->column_type = $rawcolumn->type; unset($rawcolumn->type);
  507. $rawcolumn->character_maximum_length = null;
  508. $rawcolumn->numeric_precision = null;
  509. $rawcolumn->numeric_scale = null;
  510. $rawcolumn->is_nullable = $rawcolumn->null; unset($rawcolumn->null);
  511. $rawcolumn->column_default = $rawcolumn->default; unset($rawcolumn->default);
  512. $rawcolumn->column_key = $rawcolumn->key; unset($rawcolumn->default);
  513. if (preg_match('/(enum|varchar)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
  514. $rawcolumn->data_type = $matches[1];
  515. $rawcolumn->character_maximum_length = $matches[2];
  516. } else if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
  517. $rawcolumn->data_type = $matches[1];
  518. $rawcolumn->numeric_precision = $matches[2];
  519. $rawcolumn->max_length = $rawcolumn->numeric_precision;
  520. $type = strtoupper($matches[1]);
  521. if ($type === 'BIGINT') {
  522. $maxlength = 18;
  523. } else if ($type === 'INT' or $type === 'INTEGER') {
  524. $maxlength = 9;
  525. } else if ($type === 'MEDIUMINT') {
  526. $maxlength = 6;
  527. } else if ($type === 'SMALLINT') {
  528. $maxlength = 4;
  529. } else if ($type === 'TINYINT') {
  530. $maxlength = 2;
  531. } else {
  532. // This should not happen.
  533. $maxlength = 0;
  534. }
  535. if ($maxlength < $rawcolumn->max_length) {
  536. $rawcolumn->max_length = $maxlength;
  537. }
  538. } else if (preg_match('/(decimal)\((\d+),(\d+)\)/i', $rawcolumn->column_type, $matches)) {
  539. $rawcolumn->data_type = $matches[1];
  540. $rawcolumn->numeric_precision = $matches[2];
  541. $rawcolumn->numeric_scale = $matches[3];
  542. } else if (preg_match('/(double|float)(\((\d+),(\d+)\))?/i', $rawcolumn->column_type, $matches)) {
  543. $rawcolumn->data_type = $matches[1];
  544. $rawcolumn->numeric_precision = isset($matches[3]) ? $matches[3] : null;
  545. $rawcolumn->numeric_scale = isset($matches[4]) ? $matches[4] : null;
  546. } else if (preg_match('/([a-z]*text)/i', $rawcolumn->column_type, $matches)) {
  547. $rawcolumn->data_type = $matches[1];
  548. $rawcolumn->character_maximum_length = -1; // unknown
  549. } else if (preg_match('/([a-z]*blob)/i', $rawcolumn->column_type, $matches)) {
  550. $rawcolumn->data_type = $matches[1];
  551. } else {
  552. $rawcolumn->data_type = $rawcolumn->column_type;
  553. }
  554. $info = $this->get_column_info($rawcolumn);
  555. $structure[$info->name] = new database_column_info($info);
  556. }
  557. $result->close();
  558. }
  559. if ($usecache) {
  560. $result = $cache->set($table, $structure);
  561. }
  562. return $structure;
  563. }
  564. /**
  565. * Returns moodle column info for raw column from information schema.
  566. * @param stdClass $rawcolumn
  567. * @return stdClass standardised colum info
  568. */
  569. private function get_column_info(stdClass $rawcolumn) {
  570. $rawcolumn = (object)$rawcolumn;
  571. $info = new stdClass();
  572. $info->name = $rawcolumn->column_name;
  573. $info->type = $rawcolumn->data_type;
  574. $info->meta_type = $this->mysqltype2moodletype($rawcolumn->data_type);
  575. $info->default_value = $rawcolumn->column_default;
  576. $info->has_default = !is_null($rawcolumn->column_default);
  577. $info->not_null = ($rawcolumn->is_nullable === 'NO');
  578. $info->primary_key = ($rawcolumn->column_key === 'PRI');
  579. $info->binary = false;
  580. $info->unsigned = null;
  581. $info->auto_increment = false;
  582. $info->unique = null;
  583. $info->scale = null;
  584. if ($info->meta_type === 'C') {
  585. $info->max_length = $rawcolumn->character_maximum_length;
  586. } else if ($info->meta_type === 'I') {
  587. if ($info->primary_key) {
  588. $info->meta_type = 'R';
  589. $info->unique = true;
  590. }
  591. // Return number of decimals, not bytes here.
  592. $info->max_length = $rawcolumn->numeric_precision;
  593. if (preg_match('/([a-z]*int[a-z]*)\((\d+)\)/i', $rawcolumn->column_type, $matches)) {
  594. $type = strtoupper($matches[1]);
  595. if ($type === 'BIGINT') {
  596. $maxlength = 18;
  597. } else if ($type === 'INT' or $type === 'INTEGER') {
  598. $maxlength = 9;
  599. } else if ($type === 'MEDIUMINT') {
  600. $maxlength = 6;
  601. } else if ($type === 'SMALLINT') {
  602. $maxlength = 4;
  603. } else if ($type === 'TINYINT') {
  604. $maxlength = 2;
  605. } else {
  606. // This should not happen.
  607. $maxlength = 0;
  608. }
  609. // It is possible that display precision is different from storage type length,
  610. // always use the smaller value to make sure our data fits.
  611. if ($maxlength < $info->max_length) {
  612. $info->max_length = $maxlength;
  613. }
  614. }
  615. $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
  616. $info->auto_increment= (strpos($rawcolumn->extra, 'auto_increment') !== false);
  617. } else if ($info->meta_type === 'N') {
  618. $info->max_length = $rawcolumn->numeric_precision;
  619. $info->scale = $rawcolumn->numeric_scale;
  620. $info->unsigned = (stripos($rawcolumn->column_type, 'unsigned') !== false);
  621. } else if ($info->meta_type === 'X') {
  622. if ("$rawcolumn->character_maximum_length" === '4294967295') { // watch out for PHP max int limits!
  623. // means maximum moodle size for text column, in other drivers it may also mean unknown size
  624. $info->max_length = -1;
  625. } else {
  626. $info->max_length = $rawcolumn->character_maximum_length;
  627. }
  628. $info->primary_key = false;
  629. } else if ($info->meta_type === 'B') {
  630. $info->max_length = -1;
  631. $info->primary_key = false;
  632. $info->binary = true;
  633. }
  634. return $info;
  635. }
  636. /**
  637. * Normalise column type.
  638. * @param string $mysql_type
  639. * @return string one character
  640. * @throws dml_exception
  641. */
  642. private function mysqltype2moodletype($mysql_type) {
  643. $type = null;
  644. switch(strtoupper($mysql_type)) {
  645. case 'BIT':
  646. $type = 'L';
  647. break;
  648. case 'TINYINT':
  649. case 'SMALLINT':
  650. case 'MEDIUMINT':
  651. case 'INT':
  652. case 'INTEGER':
  653. case 'BIGINT':
  654. $type = 'I';
  655. break;
  656. case 'FLOAT':
  657. case 'DOUBLE':
  658. case 'DECIMAL':
  659. $type = 'N';
  660. break;
  661. case 'CHAR':
  662. case 'ENUM':
  663. case 'SET':
  664. case 'VARCHAR':
  665. $type = 'C';
  666. break;
  667. case 'TINYTEXT':
  668. case 'TEXT':
  669. case 'MEDIUMTEXT':
  670. case 'LONGTEXT':
  671. $type = 'X';
  672. break;
  673. case 'BINARY':
  674. case 'VARBINARY':
  675. case 'BLOB':
  676. case 'TINYBLOB':
  677. case 'MEDIUMBLOB':
  678. case 'LONGBLOB':
  679. $type = 'B';
  680. break;
  681. case 'DATE':
  682. case 'TIME':
  683. case 'DATETIME':
  684. case 'TIMESTAMP':
  685. case 'YEAR':
  686. $type = 'D';
  687. break;
  688. }
  689. if (!$type) {
  690. throw new dml_exception('invalidmysqlnativetype', $mysql_type);
  691. }
  692. return $type;
  693. }
  694. /**
  695. * Normalise values based in RDBMS dependencies (booleans, LOBs...)
  696. *
  697. * @param database_column_info $column column metadata corresponding with the value we are going to normalise
  698. * @param mixed $value value we are going to normalise
  699. * @return mixed the normalised value
  700. */
  701. protected function normalise_value($column, $value) {
  702. $this->detect_objects($value);
  703. if (is_bool($value)) { // Always, convert boolean to int
  704. $value = (int)$value;
  705. } else if ($value === '') {
  706. if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') {
  707. $value = 0; // prevent '' problems in numeric fields
  708. }
  709. // Any float value being stored in varchar or text field is converted to string to avoid
  710. // any implicit conversion by MySQL
  711. } else if (is_float($value) and ($column->meta_type == 'C' or $column->meta_type == 'X')) {
  712. $value = "$value";
  713. }
  714. return $value;
  715. }
  716. /**
  717. * Is this database compatible with utf8?
  718. * @return bool
  719. */
  720. public function setup_is_unicodedb() {
  721. // All new tables are created with this collation, we just have to make sure it is utf8 compatible,
  722. // if config table already exists it has this collation too.
  723. $collation = $this->get_dbcollation();
  724. $sql = "SHOW COLLATION WHERE Collation ='$collation' AND Charset = 'utf8'";
  725. $this->query_start($sql, NULL, SQL_QUERY_AUX);
  726. $result = $this->mysqli->query($sql);
  727. $this->query_end($result);
  728. if ($result->fetch_assoc()) {
  729. $return = true;
  730. } else {
  731. $return = false;
  732. }
  733. $result->close();
  734. return $return;
  735. }
  736. /**
  737. * Do NOT use in code, to be used by database_manager only!
  738. * @param string $sql query
  739. * @return bool true
  740. * @throws dml_exception A DML specific exception is thrown for any errors.
  741. */
  742. public function change_database_structure($sql) {
  743. $this->reset_caches();
  744. $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
  745. $result = $this->mysqli->query($sql);
  746. $this->query_end($result);
  747. return true;
  748. }
  749. /**
  750. * Very ugly hack which emulates bound parameters in queries
  751. * because prepared statements do not use query cache.
  752. */
  753. protected function emulate_bound_params($sql, array $params=null) {
  754. if (empty($params)) {
  755. return $sql;
  756. }
  757. // ok, we have verified sql statement with ? and correct number of params
  758. $parts = explode('?', $sql);
  759. $return = array_shift($parts);
  760. foreach ($params as $param) {
  761. if (is_bool($param)) {
  762. $return .= (int)$param;
  763. } else if (is_null($param)) {
  764. $return .= 'NULL';
  765. } else if (is_number($param)) {
  766. $return .= "'".$param."'"; // we have to always use strings because mysql is using weird automatic int casting
  767. } else if (is_float($param)) {
  768. $return .= $param;
  769. } else {
  770. $param = $this->mysqli->real_escape_string($param);
  771. $return .= "'$param'";
  772. }
  773. $return .= array_shift($parts);
  774. }
  775. return $return;
  776. }
  777. /**
  778. * Execute general sql query. Should be used only when no other method suitable.
  779. * Do NOT use this to make changes in db structure, use database_manager methods instead!
  780. * @param string $sql query
  781. * @param array $params query parameters
  782. * @return bool true
  783. * @throws dml_exception A DML specific exception is thrown for any errors.
  784. */
  785. public function execute($sql, array $params=null) {
  786. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  787. if (strpos($sql, ';') !== false) {
  788. throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!');
  789. }
  790. $rawsql = $this->emulate_bound_params($sql, $params);
  791. $this->query_start($sql, $params, SQL_QUERY_UPDATE);
  792. $result = $this->mysqli->query($rawsql);
  793. $this->query_end($result);
  794. if ($result === true) {
  795. return true;
  796. } else {
  797. $result->close();
  798. return true;
  799. }
  800. }
  801. /**
  802. * Get a number of records as a moodle_recordset using a SQL statement.
  803. *
  804. * Since this method is a little less readable, use of it should be restricted to
  805. * code where it's possible there might be large datasets being returned. For known
  806. * small datasets use get_records_sql - it leads to simpler code.
  807. *
  808. * The return type is like:
  809. * @see function get_recordset.
  810. *
  811. * @param string $sql the SQL select query to execute.
  812. * @param array $params array of sql parameters
  813. * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
  814. * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
  815. * @return moodle_recordset instance
  816. * @throws dml_exception A DML specific exception is thrown for any errors.
  817. */
  818. public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
  819. $limitfrom = (int)$limitfrom;
  820. $limitnum = (int)$limitnum;
  821. $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
  822. $limitnum = ($limitnum < 0) ? 0 : $limitnum;
  823. if ($limitfrom or $limitnum) {
  824. if ($limitnum < 1) {
  825. $limitnum = "18446744073709551615";
  826. }
  827. $sql .= " LIMIT $limitfrom, $limitnum";
  828. }
  829. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  830. $rawsql = $this->emulate_bound_params($sql, $params);
  831. $this->query_start($sql, $params, SQL_QUERY_SELECT);
  832. // no MYSQLI_USE_RESULT here, it would block write ops on affected tables
  833. $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
  834. $this->query_end($result);
  835. return $this->create_recordset($result);
  836. }
  837. /**
  838. * Get all records from a table.
  839. *
  840. * This method works around potential memory problems and may improve performance,
  841. * this method may block access to table until the recordset is closed.
  842. *
  843. * @param string $table Name of database table.
  844. * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
  845. * @throws dml_exception A DML specific exception is thrown for any errors.
  846. */
  847. public function export_table_recordset($table) {
  848. $sql = $this->fix_table_names("SELECT * FROM {{$table}}");
  849. $this->query_start($sql, array(), SQL_QUERY_SELECT);
  850. // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries.
  851. $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT);
  852. $this->query_end($result);
  853. return $this->create_recordset($result);
  854. }
  855. protected function create_recordset($result) {
  856. return new mysqli_native_moodle_recordset($result);
  857. }
  858. /**
  859. * Get a number of records as an array of objects using a SQL statement.
  860. *
  861. * Return value is like:
  862. * @see function get_records.
  863. *
  864. * @param string $sql the SQL select query to execute. The first column of this SELECT statement
  865. * must be a unique value (usually the 'id' field), as it will be used as the key of the
  866. * returned array.
  867. * @param array $params array of sql parameters
  868. * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
  869. * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
  870. * @return array of objects, or empty array if no records were found
  871. * @throws dml_exception A DML specific exception is thrown for any errors.
  872. */
  873. public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) {
  874. $limitfrom = (int)$limitfrom;
  875. $limitnum = (int)$limitnum;
  876. $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom;
  877. $limitnum = ($limitnum < 0) ? 0 : $limitnum;
  878. if ($limitfrom or $limitnum) {
  879. if ($limitnum < 1) {
  880. $limitnum = "18446744073709551615";
  881. }
  882. $sql .= " LIMIT $limitfrom, $limitnum";
  883. }
  884. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  885. $rawsql = $this->emulate_bound_params($sql, $params);
  886. $this->query_start($sql, $params, SQL_QUERY_SELECT);
  887. $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
  888. $this->query_end($result);
  889. $return = array();
  890. while($row = $result->fetch_assoc()) {
  891. $row = array_change_key_case($row, CASE_LOWER);
  892. $id = reset($row);
  893. if (isset($return[$id])) {
  894. $colname = key($row);
  895. 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);
  896. }
  897. $return[$id] = (object)$row;
  898. }
  899. $result->close();
  900. return $return;
  901. }
  902. /**
  903. * Selects records and return values (first field) as an array using a SQL statement.
  904. *
  905. * @param string $sql The SQL query
  906. * @param array $params array of sql parameters
  907. * @return array of values
  908. * @throws dml_exception A DML specific exception is thrown for any errors.
  909. */
  910. public function get_fieldset_sql($sql, array $params=null) {
  911. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  912. $rawsql = $this->emulate_bound_params($sql, $params);
  913. $this->query_start($sql, $params, SQL_QUERY_SELECT);
  914. $result = $this->mysqli->query($rawsql, MYSQLI_STORE_RESULT);
  915. $this->query_end($result);
  916. $return = array();
  917. while($row = $result->fetch_assoc()) {
  918. $return[] = reset($row);
  919. }
  920. $result->close();
  921. return $return;
  922. }
  923. /**
  924. * Insert new record into database, as fast as possible, no safety checks, lobs not supported.
  925. * @param string $table name
  926. * @param mixed $params data record as object or array
  927. * @param bool $returnit return it of inserted record
  928. * @param bool $bulk true means repeated inserts expected
  929. * @param bool $customsequence true if 'id' included in $params, disables $returnid
  930. * @return bool|int true or new id
  931. * @throws dml_exception A DML specific exception is thrown for any errors.
  932. */
  933. public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) {
  934. if (!is_array($params)) {
  935. $params = (array)$params;
  936. }
  937. if ($customsequence) {
  938. if (!isset($params['id'])) {
  939. throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.');
  940. }
  941. $returnid = false;
  942. } else {
  943. unset($params['id']);
  944. }
  945. if (empty($params)) {
  946. throw new coding_exception('moodle_database::insert_record_raw() no fields found.');
  947. }
  948. $fields = implode(',', array_keys($params));
  949. $qms = array_fill(0, count($params), '?');
  950. $qms = implode(',', $qms);
  951. $sql = "INSERT INTO {$this->prefix}$table ($fields) VALUES($qms)";
  952. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  953. $rawsql = $this->emulate_bound_params($sql, $params);
  954. $this->query_start($sql, $params, SQL_QUERY_INSERT);
  955. $result = $this->mysqli->query($rawsql);
  956. $id = @$this->mysqli->insert_id; // must be called before query_end() which may insert log into db
  957. $this->query_end($result);
  958. if (!$customsequence and !$id) {
  959. throw new dml_write_exception('unknown error fetching inserted id');
  960. }
  961. if (!$returnid) {
  962. return true;
  963. } else {
  964. return (int)$id;
  965. }
  966. }
  967. /**
  968. * Insert a record into a table and return the "id" field if required.
  969. *
  970. * Some conversions and safety checks are carried out. Lobs are supported.
  971. * If the return ID isn't required, then this just reports success as true/false.
  972. * $data is an object containing needed data
  973. * @param string $table The database table to be inserted into
  974. * @param object $data A data object with values for one or more fields in the record
  975. * @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.
  976. * @return bool|int true or new id
  977. * @throws dml_exception A DML specific exception is thrown for any errors.
  978. */
  979. public function insert_record($table, $dataobject, $returnid=true, $bulk=false) {
  980. $dataobject = (array)$dataobject;
  981. $columns = $this->get_columns($table);
  982. $cleaned = array();
  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. return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
  994. }
  995. /**
  996. * Import a record into a table, id field is required.
  997. * Safety checks are NOT carried out. Lobs are supported.
  998. *
  999. * @param string $table name of database table to be inserted into
  1000. * @param object $dataobject A data object with values for one or more fields in the record
  1001. * @return bool true
  1002. * @throws dml_exception A DML specific exception is thrown for any errors.
  1003. */
  1004. public function import_record($table, $dataobject) {
  1005. $dataobject = (array)$dataobject;
  1006. $columns = $this->get_columns($table);
  1007. $cleaned = array();
  1008. foreach ($dataobject as $field=>$value) {
  1009. if (!isset($columns[$field])) {
  1010. continue;
  1011. }
  1012. $cleaned[$field] = $value;
  1013. }
  1014. return $this->insert_record_raw($table, $cleaned, false, true, true);
  1015. }
  1016. /**
  1017. * Update record in database, as fast as possible, no safety checks, lobs not supported.
  1018. * @param string $table name
  1019. * @param mixed $params data record as object or array
  1020. * @param bool true means repeated updates expected
  1021. * @return bool true
  1022. * @throws dml_exception A DML specific exception is thrown for any errors.
  1023. */
  1024. public function update_record_raw($table, $params, $bulk=false) {
  1025. $params = (array)$params;
  1026. if (!isset($params['id'])) {
  1027. throw new coding_exception('moodle_database::update_record_raw() id field must be specified.');
  1028. }
  1029. $id = $params['id'];
  1030. unset($params['id']);
  1031. if (empty($params)) {
  1032. throw new coding_exception('moodle_database::update_record_raw() no fields found.');
  1033. }
  1034. $sets = array();
  1035. foreach ($params as $field=>$value) {
  1036. $sets[] = "$field = ?";
  1037. }
  1038. $params[] = $id; // last ? in WHERE condition
  1039. $sets = implode(',', $sets);
  1040. $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=?";
  1041. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  1042. $rawsql = $this->emulate_bound_params($sql, $params);
  1043. $this->query_start($sql, $params, SQL_QUERY_UPDATE);
  1044. $result = $this->mysqli->query($rawsql);
  1045. $this->query_end($result);
  1046. return true;
  1047. }
  1048. /**
  1049. * Update a record in a table
  1050. *
  1051. * $dataobject is an object containing needed data
  1052. * Relies on $dataobject having a variable "id" to
  1053. * specify the record to update
  1054. *
  1055. * @param string $table The database table to be checked against.
  1056. * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified.
  1057. * @param bool true means repeated updates expected
  1058. * @return bool true
  1059. * @throws dml_exception A DML specific exception is thrown for any errors.
  1060. */
  1061. public function update_record($table, $dataobject, $bulk=false) {
  1062. $dataobject = (array)$dataobject;
  1063. $columns = $this->get_columns($table);
  1064. $cleaned = array();
  1065. foreach ($dataobject as $field=>$value) {
  1066. if (!isset($columns[$field])) {
  1067. continue;
  1068. }
  1069. $column = $columns[$field];
  1070. $cleaned[$field] = $this->normalise_value($column, $value);
  1071. }
  1072. return $this->update_record_raw($table, $cleaned, $bulk);
  1073. }
  1074. /**
  1075. * Set a single field in every table record which match a particular WHERE clause.
  1076. *
  1077. * @param string $table The database table to be checked against.
  1078. * @param string $newfield the field to set.
  1079. * @param string $newvalue the value to set the field to.
  1080. * @param string $select A fragment of SQL to be used in a where clause in the SQL call.
  1081. * @param array $params array of sql parameters
  1082. * @return bool true
  1083. * @throws dml_exception A DML specific exception is thrown for any errors.
  1084. */
  1085. public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) {
  1086. if ($select) {
  1087. $select = "WHERE $select";
  1088. }
  1089. if (is_null($params)) {
  1090. $params = array();
  1091. }
  1092. list($select, $params, $type) = $this->fix_sql_params($select, $params);
  1093. // Get column metadata
  1094. $columns = $this->get_columns($table);
  1095. $column = $columns[$newfield];
  1096. $normalised_value = $this->normalise_value($column, $newvalue);
  1097. if (is_null($normalised_value)) {
  1098. $newfield = "$newfield = NULL";
  1099. } else {
  1100. $newfield = "$newfield = ?";
  1101. array_unshift($params, $normalised_value);
  1102. }
  1103. $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
  1104. $rawsql = $this->emulate_bound_params($sql, $params);
  1105. $this->query_start($sql, $params, SQL_QUERY_UPDATE);
  1106. $result = $this->mysqli->query($rawsql);
  1107. $this->query_end($result);
  1108. return true;
  1109. }
  1110. /**
  1111. * Delete one or more records from a table which match a particular WHERE clause.
  1112. *
  1113. * @param string $table The database table to be checked against.
  1114. * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
  1115. * @param array $params array of sql parameters
  1116. * @return bool true
  1117. * @throws dml_exception A DML specific exception is thrown for any errors.
  1118. */
  1119. public function delete_records_select($table, $select, array $params=null) {
  1120. if ($select) {
  1121. $select = "WHERE $select";
  1122. }
  1123. $sql = "DELETE FROM {$this->prefix}$table $select";
  1124. list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
  1125. $rawsql = $this->emulate_bound_params($sql, $params);
  1126. $this->query_start($sql, $params, SQL_QUERY_UPDATE);
  1127. $result = $this->mysqli->query($rawsql);
  1128. $this->query_end($result);
  1129. return true;
  1130. }
  1131. public function sql_cast_char2int($fieldname, $text=false) {
  1132. return ' CAST(' . $fieldname . ' AS SIGNED) ';
  1133. }
  1134. public function sql_cast_char2real($fieldname, $text=false) {
  1135. return ' CAST(' . $fieldname . ' AS DECIMAL) ';
  1136. }
  1137. /**
  1138. * Returns 'LIKE' part of a query.
  1139. *
  1140. * @param string $fieldname usually name of the table column
  1141. * @param string $param usually bound query parameter (?, :named)
  1142. * @param bool $casesensitive use case sensitive search
  1143. * @param bool $accensensitive use accent sensitive search (not all databases support accent insensitive)
  1144. * @param bool $notlike true means "NOT LIKE"
  1145. * @param string $escapechar escape char for '%' and '_'
  1146. * @return string SQL code fragment
  1147. */
  1148. public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') {
  1149. if (strpos($param, '%') !== false) {
  1150. debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
  1151. }
  1152. $escapechar = $this->mysqli->real_escape_string($escapechar); // prevents problems with C-style escapes of enclosing '\'
  1153. $LIKE = $notlike ? 'NOT LIKE' : 'LIKE';
  1154. if ($casesensitive) {
  1155. return "$fieldname $LIKE $param COLLATE utf8_bin ESCAPE '$escapechar'";
  1156. } else {
  1157. if ($accentsensitive) {
  1158. return "LOWER($fieldnam