520b730c |
1 | <?php |
2 | |
3 | // This file is part of Moodle - http://moodle.org/ |
4 | // |
5 | // Moodle is free software: you can redistribute it and/or modify |
6 | // it under the terms of the GNU General Public License as published by |
7 | // the Free Software Foundation, either version 3 of the License, or |
8 | // (at your option) any later version. |
9 | // |
10 | // Moodle is distributed in the hope that it will be useful, |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | // GNU General Public License for more details. |
14 | // |
15 | // You should have received a copy of the GNU General Public License |
16 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. |
17 | |
18 | |
19 | /** |
20 | * Native mssql class representing moodle database interface. |
21 | * |
22 | * @package moodlecore |
23 | * @subpackage DML |
24 | * @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} |
25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
26 | */ |
27 | |
28 | require_once($CFG->libdir.'/dml/moodle_database.php'); |
29 | require_once($CFG->libdir.'/dml/mssql_native_moodle_recordset.php'); |
30 | require_once($CFG->libdir.'/dml/mssql_native_moodle_temptables.php'); |
31 | |
32 | /** |
33 | * Native mssql class representing moodle database interface. |
34 | */ |
35 | class mssql_native_moodle_database extends moodle_database { |
36 | |
37 | protected $mssql = null; |
38 | private $temptables; // Control existing temptables (mssql_moodle_temptables object) |
39 | |
40 | /** |
41 | * Detects if all needed PHP stuff installed. |
42 | * Note: can be used before connect() |
43 | * @return mixed true if ok, string if something |
44 | */ |
45 | public function driver_installed() { |
46 | if (!function_exists('mssql_connect')) { |
47 | return get_string('mssqlextensionisnotpresentinphp', 'install'); |
48 | } |
49 | return true; |
50 | } |
51 | |
52 | /** |
53 | * Returns database family type - describes SQL dialect |
54 | * Note: can be used before connect() |
55 | * @return string db family name (mysql, postgres, mssql, oracle, etc.) |
56 | */ |
57 | public function get_dbfamily() { |
58 | return 'mssql'; |
59 | } |
60 | |
61 | /** |
62 | * Returns more specific database driver type |
63 | * Note: can be used before connect() |
64 | * @return string db type mysql, pgsql, postgres7 |
65 | */ |
66 | protected function get_dbtype() { |
67 | return 'mssql'; |
68 | } |
69 | |
70 | /** |
71 | * Returns general database library name |
72 | * Note: can be used before connect() |
73 | * @return string db type adodb, pdo, native |
74 | */ |
75 | protected function get_dblibrary() { |
76 | return 'native'; |
77 | } |
78 | |
79 | /** |
80 | * Returns localised database type name |
81 | * Note: can be used before connect() |
82 | * @return string |
83 | */ |
84 | public function get_name() { |
85 | return get_string('nativemssql', 'install'); |
86 | } |
87 | |
88 | /** |
89 | * Returns sql generator used for db manipulation. |
90 | * Used mostly in upgrade.php scripts. mssql overrides it |
91 | * in order to share the mssql_moodle_temptables |
92 | * between the driver and the generator |
93 | * |
94 | * @return object database_manager instance |
95 | */ |
96 | public function get_manager() { |
97 | global $CFG; |
98 | |
99 | if (!$this->database_manager) { |
100 | require_once($CFG->libdir.'/ddllib.php'); |
101 | |
102 | $classname = $this->get_dbfamily().'_sql_generator'; |
103 | require_once("$CFG->libdir/ddl/$classname.php"); |
104 | $generator = new $classname($this, $this->temptables); |
105 | |
106 | $this->database_manager = new database_manager($this, $generator); |
107 | } |
108 | return $this->database_manager; |
109 | } |
110 | |
111 | /** |
112 | * Returns localised database configuration help. |
113 | * Note: can be used before connect() |
114 | * @return string |
115 | */ |
116 | public function get_configuration_help() { |
117 | return get_string('nativemssqlhelp', 'install'); |
118 | } |
119 | |
120 | /** |
121 | * Returns localised database description |
122 | * Note: can be used before connect() |
123 | * @return string |
124 | */ |
125 | public function get_configuration_hints() { |
126 | $str = get_string('databasesettingssub_mssql', 'install'); |
127 | $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" "; |
128 | $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Installing_MSSQL_for_PHP')\""; |
129 | $str .= ">"; |
130 | $str .= '<img src="pix/docs.gif' . '" alt="Docs" class="iconhelp" />'; |
131 | $str .= get_string('moodledocslink', 'install') . '</a></p>'; |
132 | return $str; |
133 | } |
134 | |
135 | /** |
136 | * Connect to db |
137 | * Must be called before other methods. |
138 | * @param string $dbhost |
139 | * @param string $dbuser |
140 | * @param string $dbpass |
141 | * @param string $dbname |
142 | * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used |
143 | * @param array $dboptions driver specific options |
144 | * @return bool true |
145 | * @throws dml_connection_exception if error |
146 | */ |
147 | public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) { |
148 | if ($prefix == '' and !$this->external) { |
149 | //Enforce prefixes for everybody but mysql |
150 | throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily()); |
151 | } |
152 | |
153 | $driverstatus = $this->driver_installed(); |
154 | |
155 | if ($driverstatus !== true) { |
156 | throw new dml_exception('dbdriverproblem', $driverstatus); |
157 | } |
158 | |
159 | $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); |
160 | |
161 | ob_start(); |
162 | if (!empty($this->dboptions['dbpersit'])) { // persistent connection |
163 | $this->mssql = mssql_pconnect($this->dbhost, $this->dbuser, $this->dbpass, true); |
164 | } else { |
165 | $this->mssql = mssql_connect($this->dbhost, $this->dbuser, $this->dbpass, true); |
166 | } |
167 | $dberr = ob_get_contents(); |
168 | ob_end_clean(); |
169 | |
170 | if ($this->mssql === false) { |
171 | $this->mssql = null; |
172 | throw new dml_connection_exception($dberr); |
173 | } |
174 | |
175 | // already connected, select database and set some env. variables |
176 | $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX); |
177 | $result = mssql_select_db($this->dbname, $this->mssql); |
178 | $this->query_end($result); |
179 | |
180 | // No need to set charset. It's UTF8, with transparent conversions |
181 | // back and forth performed both by FreeTDS or ODBTP |
182 | |
183 | // Allow quoted identifiers |
184 | $sql = "SET QUOTED_IDENTIFIER ON"; |
185 | $this->query_start($sql, null, SQL_QUERY_AUX); |
186 | $result = mssql_query($sql, $this->mssql); |
187 | $this->query_end($result); |
188 | |
189 | $this->free_result($result); |
190 | |
191 | // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL |
192 | // instead of equal(=) and distinct(<>) simbols |
193 | $sql = "SET ANSI_NULLS ON"; |
194 | $this->query_start($sql, null, SQL_QUERY_AUX); |
195 | $result = mssql_query($sql, $this->mssql); |
196 | $this->query_end($result); |
197 | |
198 | $this->free_result($result); |
199 | |
200 | // Force ANSI warnings so arithmetic/string overflows will be |
201 | // returning error instead of transparently truncating data |
202 | $sql = "SET ANSI_WARNINGS ON"; |
203 | $this->query_start($sql, null, SQL_QUERY_AUX); |
204 | $result = mssql_query($sql, $this->mssql); |
205 | $this->query_end($result); |
206 | |
207 | // Concatenating null with anything MUST return NULL |
208 | $sql = "SET CONCAT_NULL_YIELDS_NULL ON"; |
209 | $this->query_start($sql, null, SQL_QUERY_AUX); |
210 | $result = mssql_query($sql, $this->mssql); |
211 | $this->query_end($result); |
212 | |
213 | $this->free_result($result); |
214 | |
215 | // Set transactions isolation level to READ_COMMITTED |
216 | // prevents dirty reads when using transactions + |
217 | // is the default isolation level of MSSQL |
218 | $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"; |
219 | $this->query_start($sql, NULL, SQL_QUERY_AUX); |
220 | $result = mssql_query($sql, $this->mssql); |
221 | $this->query_end($result); |
222 | |
223 | $this->free_result($result); |
224 | |
225 | // Connection stabilished and configured, going to instantiate the temptables controller |
226 | $this->temptables = new mssql_native_moodle_temptables($this); |
227 | |
228 | return true; |
229 | } |
230 | |
231 | /** |
232 | * Close database connection and release all resources |
233 | * and memory (especially circular memory references). |
234 | * Do NOT use connect() again, create a new instance if needed. |
235 | */ |
236 | public function dispose() { |
237 | parent::dispose(); // Call parent dispose to write/close session and other common stuff before clossing conn |
238 | if ($this->mssql) { |
239 | mssql_close($this->mssql); |
240 | $this->mssql = null; |
241 | } |
242 | } |
243 | |
244 | /** |
245 | * Called before each db query. |
246 | * @param string $sql |
247 | * @param array array of parameters |
248 | * @param int $type type of query |
249 | * @param mixed $extrainfo driver specific extra information |
250 | * @return void |
251 | */ |
252 | protected function query_start($sql, array $params=null, $type, $extrainfo=null) { |
253 | parent::query_start($sql, $params, $type, $extrainfo); |
254 | } |
255 | |
256 | /** |
257 | * Called immediately after each db query. |
258 | * @param mixed db specific result |
259 | * @return void |
260 | */ |
261 | protected function query_end($result) { |
262 | parent::query_end($result); |
263 | } |
264 | |
265 | /** |
266 | * Returns database server info array |
267 | * @return array |
268 | */ |
269 | public function get_server_info() { |
270 | static $info; |
271 | if (!$info) { |
272 | $info = array(); |
273 | $sql = 'sp_server_info 2'; |
274 | $this->query_start($sql, null, SQL_QUERY_AUX); |
275 | $result = mssql_query($sql, $this->mssql); |
276 | $this->query_end($result); |
277 | $row = mssql_fetch_row($result); |
278 | $info['server'] = $row[2]; |
279 | $this->free_result($result); |
280 | |
281 | $sql = 'sp_server_info 500'; |
282 | $this->query_start($sql, null, SQL_QUERY_AUX); |
283 | $result = mssql_query($sql, $this->mssql); |
284 | $this->query_end($result); |
285 | $row = mssql_fetch_row($result); |
286 | $info['version'] = $row[2]; |
287 | $this->free_result($result); |
288 | } |
289 | return $info; |
290 | } |
291 | |
292 | protected function is_min_version($version) { |
293 | $server = $this->get_server_info(); |
294 | $server = $server['version']; |
295 | return version_compare($server, $version, '>='); |
296 | } |
297 | |
298 | /** |
299 | * Converts short table name {tablename} to real table name |
300 | * supporting temp tables (#) if detected |
301 | * |
302 | * @param string sql |
303 | * @return string sql |
304 | */ |
305 | protected function fix_table_names($sql) { |
306 | if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) { |
307 | foreach($matches[0] as $key=>$match) { |
308 | $name = $matches[1][$key]; |
309 | $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql); |
310 | } |
311 | } |
312 | return $sql; |
313 | } |
314 | |
315 | /** |
316 | * Returns supported query parameter types |
317 | * @return bitmask |
318 | */ |
319 | protected function allowed_param_types() { |
320 | return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params() |
321 | } |
322 | |
323 | /** |
324 | * Returns last error reported by database engine. |
325 | */ |
326 | public function get_last_error() { |
327 | return mssql_get_last_message(); |
328 | } |
329 | |
330 | /** |
331 | * Return tables in database WITHOUT current prefix |
332 | * @return array of table names in lowercase and without prefix |
333 | */ |
334 | public function get_tables($usecache=true) { |
335 | if ($usecache and $this->tables !== null) { |
336 | return $this->tables; |
337 | } |
338 | $this->tables = array(); |
339 | $sql = "SELECT table_name |
340 | FROM information_schema.tables |
341 | WHERE table_name LIKE '$this->prefix%' |
342 | AND table_type = 'BASE TABLE'"; |
343 | $this->query_start($sql, null, SQL_QUERY_AUX); |
344 | $result = mssql_query($sql, $this->mssql); |
345 | $this->query_end($result); |
346 | |
347 | if ($result) { |
348 | while ($row = mssql_fetch_row($result)) { |
349 | $tablename = reset($row); |
350 | if (strpos($tablename, $this->prefix) !== 0) { |
351 | continue; |
352 | } |
353 | $tablename = substr($tablename, strlen($this->prefix)); |
354 | $this->tables[$tablename] = $tablename; |
355 | } |
356 | $this->free_result($result); |
357 | } |
358 | |
359 | // Add the currently available temptables |
360 | $this->tables = array_merge($this->tables, $this->temptables->get_temptables()); |
361 | return $this->tables; |
362 | } |
363 | |
364 | /** |
365 | * Return table indexes - everything lowercased |
366 | * @return array of arrays |
367 | */ |
368 | public function get_indexes($table) { |
369 | $indexes = array(); |
370 | $tablename = $this->prefix.$table; |
371 | |
372 | // Indexes aren't covered by information_schema metatables, so we need to |
373 | // go to sys ones. Skipping primary key indexes on purpose. |
374 | $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name |
375 | FROM sys.indexes i |
376 | JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id |
377 | JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id |
378 | JOIN sys.tables t ON i.object_id = t.object_id |
379 | WHERE t.name = '$tablename' |
380 | AND i.is_primary_key = 0 |
381 | ORDER BY i.name, i.index_id, ic.index_column_id"; |
382 | |
383 | $this->query_start($sql, null, SQL_QUERY_AUX); |
384 | $result = mssql_query($sql, $this->mssql); |
385 | $this->query_end($result); |
386 | |
387 | if ($result) { |
388 | $lastindex = ''; |
389 | $unique = false; |
390 | $columns = array(); |
391 | while ($row = mssql_fetch_assoc($result)) { |
392 | if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info |
393 | $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns); |
394 | $unique = false; |
395 | $columns = array(); |
396 | } |
397 | $lastindex = $row['index_name']; |
398 | $unique = empty($row['is_unique']) ? false : true; |
399 | $columns[] = $row['column_name']; |
400 | } |
401 | if ($lastindex ) { // Add the last one if exists |
402 | $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns); |
403 | } |
404 | $this->free_result($result); |
405 | } |
406 | return $indexes; |
407 | } |
408 | |
409 | /** |
410 | * Returns datailed information about columns in table. This information is cached internally. |
411 | * @param string $table name |
412 | * @param bool $usecache |
413 | * @return array array of database_column_info objects indexed with column names |
414 | */ |
415 | public function get_columns($table, $usecache=true) { |
416 | if ($usecache and isset($this->columns[$table])) { |
417 | return $this->columns[$table]; |
418 | } |
419 | |
420 | $this->columns[$table] = array(); |
421 | |
422 | $tablename = $this->temptables->get_correct_name($table); |
423 | |
424 | if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema |
425 | $sql = "SELECT column_name AS name, |
426 | data_type AS type, |
427 | numeric_precision AS max_length, |
428 | character_maximum_length AS char_max_length, |
429 | numeric_scale AS scale, |
430 | is_nullable AS is_nullable, |
431 | columnproperty(object_id(quotename(table_schema) + '.' + |
432 | quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, |
433 | column_default AS default_value |
434 | FROM information_schema.columns |
435 | WHERE table_name = '$tablename' |
436 | ORDER BY ordinal_position"; |
437 | } else { // temp table, get metadata from tempdb schema |
438 | $sql = "SELECT column_name AS name, |
439 | data_type AS type, |
440 | numeric_precision AS max_length, |
441 | character_maximum_length AS char_max_length, |
442 | numeric_scale AS scale, |
443 | is_nullable AS is_nullable, |
444 | columnproperty(object_id(quotename(table_schema) + '.' + |
445 | quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, |
446 | column_default AS default_value |
447 | FROM tempdb.information_schema.columns |
448 | WHERE table_name like '{$tablename}__________%' |
449 | ORDER BY ordinal_position"; |
450 | } |
451 | |
452 | $this->query_start($sql, null, SQL_QUERY_AUX); |
453 | $result = mssql_query($sql, $this->mssql); |
454 | $this->query_end($result); |
455 | |
456 | if (!$result) { |
457 | return array(); |
458 | } |
459 | |
460 | while ($rawcolumn = mssql_fetch_assoc($result)) { |
461 | |
462 | $rawcolumn = (object)$rawcolumn; |
463 | |
464 | $info = new object(); |
465 | $info->name = $rawcolumn->name; |
466 | $info->type = $rawcolumn->type; |
467 | $info->meta_type = $this->mssqltype2moodletype($info->type); |
468 | |
469 | // Prepare auto_increment info |
470 | $info->auto_increment = $rawcolumn->auto_increment ? true : false; |
471 | |
472 | // Define type for auto_increment columns |
473 | $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type; |
474 | |
475 | // id columns being auto_incremnt are PK by definition |
476 | $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment); |
477 | |
478 | // Put correct length for character and LOB types |
479 | $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length; |
480 | $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length; |
481 | |
482 | // Scale |
483 | $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false; |
484 | |
485 | // Prepare not_null info |
486 | $info->not_null = $rawcolumn->is_nullable == 'NO' ? true : false; |
487 | |
488 | // Process defaults |
489 | $info->has_default = !empty($rawcolumn->default_value); |
490 | $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value); |
491 | |
492 | // Process binary |
493 | $info->binary = $info->meta_type == 'B' ? true : false; |
494 | |
495 | $this->columns[$table][$info->name] = new database_column_info($info); |
496 | } |
497 | $this->free_result($result); |
498 | |
499 | return $this->columns[$table]; |
500 | } |
501 | |
502 | /** |
503 | * Normalise values based in RDBMS dependencies (booleans, LOBs...) |
504 | * |
505 | * @param database_column_info $column column metadata corresponding with the value we are going to normalise |
506 | * @param mixed $value value we are going to normalise |
507 | * @return mixed the normalised value |
508 | */ |
509 | private function normalise_value($column, $value) { |
510 | if (is_bool($value)) { /// Always, convert boolean to int |
511 | $value = (int)$value; |
512 | |
513 | } else if ($column->meta_type == 'B') { // BLOBs need to be properly "packed", but can be inserted directly if so. |
514 | if (!is_null($value)) { // If value not null, unpack it to unquoted hexadecimal byte-string format |
515 | $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it |
516 | } // easily and "bind" the param ok. |
517 | |
518 | } else if ($column->meta_type == 'X') { // MSSQL doesn't cast from int to text, so if text column |
519 | if (is_numeric($value)) { // and is numeric value then cast to string |
520 | $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how |
521 | } // to "bind" the param ok, avoiding reverse conversion to number |
522 | |
523 | } else if ($value === '') { |
524 | if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') { |
525 | $value = 0; // prevent '' problems in numeric fields |
526 | } |
527 | } |
528 | return $value; |
529 | } |
530 | |
531 | /** |
532 | * Selectively call mssql_free_result(), avoiding some warnings without using the horrible @ |
533 | * |
534 | * @param mssql_resource $resource resource to be freed if possible |
535 | */ |
536 | private function free_result($resource) { |
537 | if (!is_bool($resource)) { // true/false resources cannot be freed |
538 | mssql_free_result($resource); |
539 | } |
540 | } |
541 | |
542 | /** |
543 | * Provides mapping between mssql native data types and moodle_database - database_column_info - ones) |
544 | * |
545 | * @param string $mssql_type native mssql data type |
546 | * @return string 1-char database_column_info data type |
547 | */ |
548 | private function mssqltype2moodletype($mssql_type) { |
549 | $type = null; |
550 | switch (strtoupper($mssql_type)) { |
551 | case 'BIT': |
552 | $type = 'L'; |
553 | break; |
554 | case 'INT': |
555 | case 'SMALLINT': |
556 | case 'INTEGER': |
557 | case 'BIGINT': |
558 | $type = 'I'; |
559 | break; |
560 | case 'DECIMAL': |
561 | case 'REAL': |
562 | case 'FLOAT': |
563 | $type = 'N'; |
564 | break; |
565 | case 'VARCHAR': |
566 | case 'NVARCHAR': |
567 | $type = 'C'; |
568 | break; |
569 | case 'TEXT': |
570 | case 'NTEXT': |
571 | case 'VARCHAR(MAX)': |
572 | case 'NVARCHAR(MAX)': |
573 | $type = 'X'; |
574 | break; |
575 | case 'IMAGE': |
576 | case 'VARBINARY(MAX)': |
577 | $type = 'B'; |
578 | break; |
579 | case 'DATETIME': |
580 | $type = 'D'; |
581 | break; |
582 | } |
583 | if (!$type) { |
584 | throw new dml_exception('invalidmssqlnativetype', $mssql_type); |
585 | } |
586 | return $type; |
587 | } |
588 | |
589 | /** |
590 | * Do NOT use in code, to be used by database_manager only! |
591 | * @param string $sql query |
592 | * @return bool true |
593 | * @throws dml_exception if error |
594 | */ |
595 | public function change_database_structure($sql) { |
596 | $this->reset_caches(); |
597 | |
598 | $this->query_start($sql, null, SQL_QUERY_STRUCTURE); |
599 | $result = @mssql_query($sql, $this->mssql); |
600 | $this->query_end($result); |
601 | |
602 | return true; |
603 | } |
604 | |
605 | /** |
606 | * Very ugly hack which emulates bound parameters in queries |
607 | * because the mssql driver doesn't support placeholders natively at all |
608 | */ |
609 | protected function emulate_bound_params($sql, array $params=null) { |
610 | if (empty($params)) { |
611 | return $sql; |
612 | } |
613 | /// ok, we have verified sql statement with ? and correct number of params |
614 | $return = strtok($sql, '?'); |
615 | foreach ($params as $param) { |
616 | if (is_bool($param)) { |
617 | $return .= (int)$param; |
618 | |
619 | } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially |
620 | $return .= '0x' . $param['hex']; |
621 | |
622 | } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not* |
623 | $return .= "N'{$param['numstr']}'"; // be converted back to number params, but bound as strings |
624 | |
625 | } else if (is_null($param)) { |
626 | $return .= 'NULL'; |
627 | |
628 | } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646 |
629 | $return .= $param; |
630 | |
631 | } else if (is_float($param)) { |
632 | $return .= $param; |
633 | |
634 | } else { |
635 | $param = str_replace("'", "''", $param); |
636 | $return .= "N'$param'"; |
637 | } |
638 | |
639 | $return .= strtok('?'); |
640 | } |
641 | return $return; |
642 | } |
643 | |
644 | /** |
645 | * Execute general sql query. Should be used only when no other method suitable. |
646 | * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead! |
647 | * @param string $sql query |
648 | * @param array $params query parameters |
649 | * @return bool true |
650 | * @throws dml_exception if error |
651 | */ |
652 | public function execute($sql, array $params=null) { |
653 | |
654 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
655 | $rawsql = $this->emulate_bound_params($sql, $params); |
656 | |
657 | if (strpos($sql, ';') !== false) { |
658 | throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!'); |
659 | } |
660 | |
661 | $this->query_start($sql, $params, SQL_QUERY_UPDATE); |
662 | $result = @mssql_query($rawsql, $this->mssql); |
663 | $this->query_end($result); |
664 | $this->free_result($result); |
665 | |
666 | return true; |
667 | } |
668 | |
669 | /** |
670 | * Get a number of records as a moodle_recordset using a SQL statement. |
671 | * |
672 | * Since this method is a little less readable, use of it should be restricted to |
673 | * code where it's possible there might be large datasets being returned. For known |
674 | * small datasets use get_records_sql - it leads to simpler code. |
675 | * |
676 | * The return type is as for @see function get_recordset. |
677 | * |
678 | * @param string $sql the SQL select query to execute. |
679 | * @param array $params array of sql parameters |
680 | * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). |
681 | * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). |
682 | * @return mixed an moodle_recordset object |
683 | * @throws dml_exception if error |
684 | */ |
685 | public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { |
686 | $limitfrom = (int)$limitfrom; |
687 | $limitnum = (int)$limitnum; |
688 | $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom; |
689 | $limitnum = ($limitnum < 0) ? 0 : $limitnum; |
690 | if ($limitfrom or $limitnum) { |
691 | if ($limitnum < 1) { |
692 | $limitnum = "18446744073709551615"; |
693 | } |
694 | $fetch = $limitfrom + $limitnum; |
695 | $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i', |
696 | "\\1SELECT\\2 TOP $fetch", $sql); |
697 | } |
698 | |
699 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
700 | $rawsql = $this->emulate_bound_params($sql, $params); |
701 | |
702 | $this->query_start($sql, $params, SQL_QUERY_SELECT); |
703 | $result = @mssql_query($rawsql, $this->mssql); |
704 | $this->query_end($result); |
705 | |
706 | if ($limitfrom) { |
707 | @mssql_data_seek($result, $limitfrom); |
708 | } |
709 | |
710 | return $this->create_recordset($result); |
711 | } |
712 | |
713 | protected function create_recordset($result) { |
714 | return new mssql_native_moodle_recordset($result); |
715 | } |
716 | |
717 | /** |
718 | * Get a number of records as an array of objects using a SQL statement. |
719 | * |
720 | * Return value as for @see function get_records. |
721 | * |
722 | * @param string $sql the SQL select query to execute. The first column of this SELECT statement |
723 | * must be a unique value (usually the 'id' field), as it will be used as the key of the |
724 | * returned array. |
725 | * @param array $params array of sql parameters |
726 | * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). |
727 | * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). |
728 | * @return mixed an array of objects, or empty array if no records were found |
729 | * @throws dml_exception if error |
730 | */ |
731 | public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { |
732 | |
733 | $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum); |
734 | |
735 | $results = array(); |
736 | |
737 | foreach ($rs as $row) { |
738 | $id = reset($row); |
739 | if (isset($return[$id])) { |
740 | $colname = key($row); |
741 | 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); |
742 | } |
743 | $results[$id] = (object)$row; |
744 | } |
745 | $rs->close(); |
746 | |
747 | return $results; |
748 | } |
749 | |
750 | /** |
751 | * Selects records and return values (first field) as an array using a SQL statement. |
752 | * |
753 | * @param string $sql The SQL query |
754 | * @param array $params array of sql parameters |
755 | * @return mixed array of values |
756 | * @throws dml_exception if error |
757 | */ |
758 | public function get_fieldset_sql($sql, array $params=null) { |
759 | |
760 | $rs = $this->get_recordset_sql($sql, $params); |
761 | |
762 | $results = array(); |
763 | |
764 | foreach ($rs as $row) { |
765 | $results[] = reset($row); |
766 | } |
767 | $rs->close(); |
768 | |
769 | return $results; |
770 | } |
771 | |
772 | /** |
773 | * Insert new record into database, as fast as possible, no safety checks, lobs not supported. |
774 | * @param string $table name |
775 | * @param mixed $params data record as object or array |
776 | * @param bool $returnit return it of inserted record |
777 | * @param bool $bulk true means repeated inserts expected |
778 | * @param bool $customsequence true if 'id' included in $params, disables $returnid |
779 | * @return true or new id |
780 | * @throws dml_exception if error |
781 | */ |
782 | public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) { |
783 | if (!is_array($params)) { |
784 | $params = (array)$params; |
785 | } |
786 | |
787 | $returning = ""; |
788 | |
789 | if ($customsequence) { |
790 | if (!isset($params['id'])) { |
791 | throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.'); |
792 | } |
793 | $returnid = false; |
794 | } else { |
795 | unset($params['id']); |
796 | if ($returnid) { |
797 | $returning = "; SELECT SCOPE_IDENTITY()"; |
798 | } |
799 | } |
800 | |
801 | if (empty($params)) { |
802 | throw new coding_exception('moodle_database::insert_record_raw() no fields found.'); |
803 | } |
804 | |
805 | $fields = implode(',', array_keys($params)); |
806 | $qms = array_fill(0, count($params), '?'); |
807 | $qms = implode(',', $qms); |
808 | |
809 | $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms) $returning"; |
810 | |
811 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
812 | $rawsql = $this->emulate_bound_params($sql, $params); |
813 | |
814 | $this->query_start($sql, $params, SQL_QUERY_INSERT); |
815 | $result = @mssql_query($rawsql, $this->mssql); |
816 | $this->query_end($result); |
817 | |
818 | if ($returning !== "") { |
819 | $row = mssql_fetch_assoc($result); |
820 | $params['id'] = reset($row); |
821 | } |
822 | $this->free_result($result); |
823 | |
824 | if (!$returnid) { |
825 | return true; |
826 | } |
827 | |
828 | return (int)$params['id']; |
829 | } |
830 | |
831 | /** |
832 | * Insert a record into a table and return the "id" field if required. |
833 | * |
834 | * Some conversions and safety checks are carried out. Lobs are supported. |
835 | * If the return ID isn't required, then this just reports success as true/false. |
836 | * $data is an object containing needed data |
837 | * @param string $table The database table to be inserted into |
838 | * @param object $data A data object with values for one or more fields in the record |
839 | * @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. |
840 | * @return true or new id |
841 | * @throws dml_exception if error |
842 | */ |
843 | public function insert_record($table, $dataobject, $returnid=true, $bulk=false) { |
844 | if (!is_object($dataobject)) { |
845 | $dataobject = (object)$dataobject; |
846 | } |
847 | |
848 | unset($dataobject->id); |
849 | |
850 | $columns = $this->get_columns($table); |
851 | $cleaned = array(); |
852 | |
853 | foreach ($dataobject as $field => $value) { |
854 | if (!isset($columns[$field])) { |
855 | continue; |
856 | } |
857 | $column = $columns[$field]; |
858 | $cleaned[$field] = $this->normalise_value($column, $value); |
859 | } |
860 | |
861 | return $this->insert_record_raw($table, $cleaned, $returnid, $bulk); |
862 | } |
863 | |
864 | /** |
865 | * Import a record into a table, id field is required. |
866 | * Safety checks are NOT carried out. Lobs are supported. |
867 | * |
868 | * @param string $table name of database table to be inserted into |
869 | * @param object $dataobject A data object with values for one or more fields in the record |
870 | * @return bool true |
871 | * @throws dml_exception if error |
872 | */ |
873 | public function import_record($table, $dataobject) { |
874 | $dataobject = (object)$dataobject; |
875 | |
876 | $columns = $this->get_columns($table); |
877 | $cleaned = array(); |
878 | |
879 | foreach ($dataobject as $field=>$value) { |
880 | if (!isset($columns[$field])) { |
881 | continue; |
882 | } |
883 | $cleaned[$field] = $value; |
884 | } |
885 | |
886 | return $this->insert_record_raw($table, $cleaned, false, true, true); |
887 | } |
888 | |
889 | /** |
890 | * Update record in database, as fast as possible, no safety checks, lobs not supported. |
891 | * @param string $table name |
892 | * @param mixed $params data record as object or array |
893 | * @param bool true means repeated updates expected |
894 | * @return bool true |
895 | * @throws dml_exception if error |
896 | */ |
897 | public function update_record_raw($table, $params, $bulk=false) { |
898 | if (!is_array($params)) { |
899 | $params = (array)$params; |
900 | } |
901 | if (!isset($params['id'])) { |
902 | throw new coding_exception('moodle_database::update_record_raw() id field must be specified.'); |
903 | } |
904 | $id = $params['id']; |
905 | unset($params['id']); |
906 | |
907 | if (empty($params)) { |
908 | throw new coding_exception('moodle_database::update_record_raw() no fields found.'); |
909 | } |
910 | |
911 | $sets = array(); |
912 | foreach ($params as $field=>$value) { |
913 | $sets[] = "$field = ?"; |
914 | } |
915 | |
916 | $params[] = $id; // last ? in WHERE condition |
917 | |
918 | $sets = implode(',', $sets); |
919 | $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?"; |
920 | |
921 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
922 | $rawsql = $this->emulate_bound_params($sql, $params); |
923 | |
924 | $this->query_start($sql, $params, SQL_QUERY_UPDATE); |
925 | $result = @mssql_query($rawsql, $this->mssql); |
926 | $this->query_end($result); |
927 | |
928 | $this->free_result($result); |
929 | return true; |
930 | } |
931 | |
932 | /** |
933 | * Update a record in a table |
934 | * |
935 | * $dataobject is an object containing needed data |
936 | * Relies on $dataobject having a variable "id" to |
937 | * specify the record to update |
938 | * |
939 | * @param string $table The database table to be checked against. |
940 | * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified. |
941 | * @param bool true means repeated updates expected |
942 | * @return bool true |
943 | * @throws dml_exception if error |
944 | */ |
945 | public function update_record($table, $dataobject, $bulk=false) { |
946 | if (!is_object($dataobject)) { |
947 | $dataobject = (object)$dataobject; |
948 | } |
949 | |
950 | $columns = $this->get_columns($table); |
951 | $cleaned = array(); |
952 | |
953 | foreach ($dataobject as $field => $value) { |
954 | if (!isset($columns[$field])) { |
955 | continue; |
956 | } |
957 | $column = $columns[$field]; |
958 | $cleaned[$field] = $this->normalise_value($column, $value); |
959 | } |
960 | |
961 | return $this->update_record_raw($table, $cleaned, $bulk); |
962 | } |
963 | |
964 | /** |
965 | * Set a single field in every table record which match a particular WHERE clause. |
966 | * |
967 | * @param string $table The database table to be checked against. |
968 | * @param string $newfield the field to set. |
969 | * @param string $newvalue the value to set the field to. |
970 | * @param string $select A fragment of SQL to be used in a where clause in the SQL call. |
971 | * @param array $params array of sql parameters |
972 | * @return bool true |
973 | * @throws dml_exception if error |
974 | */ |
975 | public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) { |
976 | |
977 | if ($select) { |
978 | $select = "WHERE $select"; |
979 | } |
980 | if (is_null($params)) { |
981 | $params = array(); |
982 | } |
983 | |
984 | /// Get column metadata |
985 | $columns = $this->get_columns($table); |
986 | $column = $columns[$newfield]; |
987 | |
988 | $newvalue = $this->normalise_value($column, $newvalue); |
989 | |
990 | if (is_null($newvalue)) { |
991 | $newfield = "$newfield = NULL"; |
992 | } else { |
993 | $newfield = "$newfield = ?"; |
994 | array_unshift($params, $newvalue); |
995 | } |
996 | $sql = "UPDATE {" . $table . "} SET $newfield $select"; |
997 | |
998 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
999 | $rawsql = $this->emulate_bound_params($sql, $params); |
1000 | |
1001 | $this->query_start($sql, $params, SQL_QUERY_UPDATE); |
1002 | $result = @mssql_query($rawsql, $this->mssql); |
1003 | $this->query_end($result); |
1004 | |
1005 | $this->free_result($result); |
1006 | |
1007 | return true; |
1008 | } |
1009 | |
1010 | /** |
1011 | * Delete one or more records from a table which match a particular WHERE clause. |
1012 | * |
1013 | * @param string $table The database table to be checked against. |
1014 | * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria). |
1015 | * @param array $params array of sql parameters |
1016 | * @return bool true |
1017 | * @throws dml_exception if error |
1018 | */ |
1019 | public function delete_records_select($table, $select, array $params=null) { |
1020 | |
1021 | if ($select) { |
1022 | $select = "WHERE $select"; |
1023 | } |
1024 | |
1025 | $sql = "DELETE FROM {" . $table . "} $select"; |
1026 | |
1027 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
1028 | $rawsql = $this->emulate_bound_params($sql, $params); |
1029 | |
1030 | $this->query_start($sql, $params, SQL_QUERY_UPDATE); |
1031 | $result = @mssql_query($rawsql, $this->mssql); |
1032 | $this->query_end($result); |
1033 | |
1034 | $this->free_result($result); |
1035 | |
1036 | return true; |
1037 | } |
1038 | |
1039 | /// SQL helper functions |
1040 | |
1041 | public function sql_bitxor($int1, $int2) { |
1042 | return '(' . $this->sql_bitor($int1, $int2) . ' - ' . $this->sql_bitand($int1, $int2) . ')'; |
1043 | } |
1044 | |
1045 | public function sql_cast_char2int($fieldname, $text=false) { |
1046 | return ' CAST(' . $fieldname . ' AS INT) '; |
1047 | } |
1048 | |
1049 | public function sql_ceil($fieldname) { |
1050 | return ' CEILING(' . $fieldname . ')'; |
1051 | } |
1052 | |
1053 | public function sql_concat() { |
1054 | $arr = func_get_args(); |
1055 | foreach ($arr as $key => $ele) { |
1056 | $arr[$key] = ' CAST(' . $ele . ' AS VARCHAR(255)) '; |
1057 | } |
1058 | $s = implode(' + ', $arr); |
1059 | if ($s === '') { |
1060 | return " '' "; |
1061 | } |
1062 | return " $s "; |
1063 | } |
1064 | |
1065 | public function sql_concat_join($separator="' '", $elements=array()) { |
1066 | for ($n=count($elements)-1; $n > 0 ; $n--) { |
1067 | array_splice($elements, $n, 0, $separator); |
1068 | } |
1069 | $s = implode(' + ', $elements); |
1070 | if ($s === '') { |
1071 | return " '' "; |
1072 | } |
1073 | return " $s "; |
1074 | } |
1075 | |
1076 | public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) { |
1077 | if ($textfield) { |
1078 | return $this->sql_compare_text($fieldname)." = '' "; |
1079 | } else { |
1080 | return " $fieldname = '' "; |
1081 | } |
1082 | } |
1083 | |
1084 | /** |
1085 | * Returns the SQL text to be used to calculate the length in characters of one expression. |
1086 | * @param string fieldname or expression to calculate its length in characters. |
1087 | * @return string the piece of SQL code to be used in the statement. |
1088 | */ |
1089 | public function sql_length($fieldname) { |
1090 | return ' LEN(' . $fieldname . ')'; |
1091 | } |
1092 | |
1093 | public function sql_order_by_text($fieldname, $numchars=32) { |
1094 | return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')'; |
1095 | } |
1096 | |
1097 | /** |
1098 | * Returns the SQL for returning searching one string for the location of another. |
1099 | */ |
1100 | public function sql_position($needle, $haystack) { |
1101 | return "CHARINDEX(($needle), ($haystack))"; |
1102 | } |
1103 | |
1104 | /** |
1105 | * Returns the proper substr() function for each DB. |
1106 | * NOTE: this was originally returning only function name |
1107 | * |
1108 | * @param string $expr some string field, no aggregates |
1109 | * @param mixed $start integer or expresion evaluating to int |
1110 | * @param mixed $length optional integer or expresion evaluating to int |
1111 | * @return string sql fragment |
1112 | */ |
1113 | public function sql_substr($expr, $start, $length=false) { |
1114 | if (count(func_get_args()) < 2) { |
1115 | throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa |
1116 | s only returning name of SQL substring function, it now requires all parameters.'); |
1117 | } |
1118 | if ($length === false) { |
1119 | return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))"; |
1120 | } else { |
1121 | return "SUBSTRING($expr, $start, $length)"; |
1122 | } |
1123 | } |
1124 | |
1125 | /// session locking |
1126 | |
1127 | public function session_lock_supported() { |
1128 | return false; |
1129 | } |
1130 | |
1131 | public function get_session_lock($rowid) { |
1132 | // NOTE: there is a potential locking problem for database running |
1133 | // multiple instances of moodle, we could try to use pg_advisory_lock(int, int), |
1134 | // luckily there is not a big chance that they would collide |
1135 | if (!$this->session_lock_supported()) { |
1136 | return; |
1137 | } |
1138 | |
1139 | parent::get_session_lock($rowid); |
1140 | $sql = "SELECT pg_advisory_lock($rowid)"; |
1141 | $this->query_start($sql, null, SQL_QUERY_AUX); |
1142 | $result = pg_query($this->pgsql, $sql); |
1143 | $this->query_end($result); |
1144 | |
1145 | if ($result) { |
1146 | pg_free_result($result); |
1147 | } |
1148 | } |
1149 | |
1150 | public function release_session_lock($rowid) { |
1151 | if (!$this->session_lock_supported()) { |
1152 | return; |
1153 | } |
1154 | parent::release_session_lock($rowid); |
1155 | |
1156 | $sql = "SELECT pg_advisory_unlock($rowid)"; |
1157 | $this->query_start($sql, null, SQL_QUERY_AUX); |
1158 | $result = pg_query($this->pgsql, $sql); |
1159 | $this->query_end($result); |
1160 | |
1161 | if ($result) { |
1162 | pg_free_result($result); |
1163 | } |
1164 | } |
1165 | |
1166 | /// transactions |
1167 | |
1168 | /** |
1169 | * on DBs that support it, switch to transaction mode and begin a transaction |
1170 | * you'll need to ensure you call commit_sql() or your changes *will* be lost. |
1171 | * |
1172 | * this is _very_ useful for massive updates |
1173 | */ |
1174 | public function begin_sql() { |
1175 | if (!parent::begin_sql()) { |
1176 | return false; |
1177 | } |
1178 | $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation |
1179 | $this->query_start($sql, NULL, SQL_QUERY_AUX); |
1180 | $result = mssql_query($sql, $this->mssql); |
1181 | $this->query_end($result); |
1182 | |
1183 | $this->free_result($result); |
1184 | |
1185 | return true; |
1186 | } |
1187 | |
1188 | /** |
1189 | * on DBs that support it, commit the transaction |
1190 | */ |
1191 | public function commit_sql() { |
1192 | if (!parent::commit_sql()) { |
1193 | return false; |
1194 | } |
1195 | $sql = "COMMIT TRANSACTION"; |
1196 | $this->query_start($sql, NULL, SQL_QUERY_AUX); |
1197 | $result = mssql_query($sql, $this->mssql); |
1198 | $this->query_end($result); |
1199 | |
1200 | $this->free_result($result); |
1201 | |
1202 | return true; |
1203 | } |
1204 | |
1205 | /** |
1206 | * on DBs that support it, rollback the transaction |
1207 | */ |
1208 | public function rollback_sql() { |
1209 | if (!parent::rollback_sql()) { |
1210 | return false; |
1211 | } |
1212 | $sql = "ROLLBACK TRANSACTION"; |
1213 | $this->query_start($sql, NULL, SQL_QUERY_AUX); |
1214 | $result = mssql_query($sql, $this->mssql); |
1215 | $this->query_end($result); |
1216 | |
1217 | $this->free_result($result); |
1218 | |
1219 | return true; |
1220 | } |
1221 | } |