0c57c867 |
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 2 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 | * Native sqlsrv class representing moodle database interface. |
20 | * |
21 | * @package moodlecore |
22 | * @subpackage DML |
23 | * @copyright 2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v2 or later |
25 | */ |
26 | |
27 | require_once($CFG->libdir.'/dml/moodle_database.php'); |
28 | require_once($CFG->libdir.'/dml/sqlsrv_native_moodle_recordset.php'); |
29 | require_once($CFG->libdir.'/dml/sqlsrv_native_moodle_temptables.php'); |
30 | |
31 | /** |
32 | * Native sqlsrv class representing moodle database interface. |
33 | */ |
34 | class sqlsrv_native_moodle_database extends moodle_database { |
35 | |
36 | protected $sqlsrv = null; |
37 | protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity |
38 | protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object) |
39 | |
40 | /** |
41 | * Contructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB) |
42 | * note this has effect to decide if prefix checks must be performed or no |
43 | * @param bool true means external database used |
44 | */ |
45 | public function __construct($external=false) { |
46 | parent::__construct($external); |
47 | } |
48 | |
49 | /** |
50 | * Setup the driver configuration. As a TODO, might want to put some levele of debugging to |
51 | * see / log more info, warnings, errors. |
52 | } |
53 | |
54 | /** |
55 | * Detects if all needed PHP stuff installed. |
56 | * Note: can be used before connect() |
57 | * @return mixed true if ok, string if something |
58 | */ |
59 | public function driver_installed() { |
60 | // use 'function_exists()' rather than 'extension_loaded()' because |
61 | // the name used by 'extension_loaded()' is case specific! The extention |
62 | // therefor *could be* mixed case and hence not found. |
63 | if (!function_exists('sqlsrv_num_rows')) { |
64 | return get_string('sqlsrvextensionisnotpresentinphp', 'install'); |
65 | } |
66 | return true; |
67 | } |
68 | |
69 | /** |
70 | * Returns database family type - describes SQL dialect |
71 | * Note: can be used before connect() |
72 | * @return string db family name (mysql, postgres, mssql, sqlsrv, oracle, etc.) |
73 | */ |
74 | public function get_dbfamily() { |
75 | /** |
76 | * NOTE: If this is changed to use the mssql family of |
77 | * databases, then sqlsrv_sql_generator.php can be deleted |
78 | * because it will not be used. |
79 | */ |
80 | return 'mssql'; |
81 | } |
82 | |
83 | /** |
84 | * Returns more specific database driver type |
85 | * Note: can be used before connect() |
86 | * @return string db type mysql, pgsql, postgres7, mssql, sqlsrv |
87 | */ |
88 | protected function get_dbtype() { |
89 | return 'sqlsrv'; |
90 | } |
91 | |
92 | /** |
93 | * Returns general database library name |
94 | * Note: can be used before connect() |
95 | * @return string db type pdo, native |
96 | */ |
97 | protected function get_dblibrary() { |
98 | return 'native'; |
99 | } |
100 | |
101 | /** |
102 | * Returns localised database type name |
103 | * Note: can be used before connect() |
104 | * @return string |
105 | */ |
106 | public function get_name() { |
107 | return get_string('nativesqlsrv', 'install'); |
108 | } |
109 | |
110 | /** |
111 | * Returns localised database configuration help. |
112 | * Note: can be used before connect() |
113 | * @return string |
114 | */ |
115 | public function get_configuration_help() { |
116 | return get_string('nativesqlsrvhelp', 'install'); |
117 | } |
118 | |
119 | /** |
120 | * Returns localised database description |
121 | * Note: can be used before connect() |
122 | * @return string |
123 | */ |
124 | public function get_configuration_hints() { |
125 | $str = get_string('databasesettingssub_sqlsrv', 'install'); |
126 | $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" "; |
127 | $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Installing_sqlsrv_for_PHP')\""; |
128 | $str .= ">"; |
129 | $str .= '<img src="pix/docs.gif'.'" alt="Docs" class="iconhelp" />'; |
130 | $str .= get_string('moodledocslink', 'install').'</a></p>'; |
131 | return $str; |
132 | } |
133 | |
134 | /** |
135 | * Connect to db |
136 | * Must be called before other methods. |
137 | * @param string $dbhost |
138 | * @param string $dbuser |
139 | * @param string $dbpass |
140 | * @param string $dbname |
141 | * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used |
142 | * @param array $dboptions driver specific options |
143 | * @return bool true |
144 | * @throws dml_connection_exception if error |
145 | */ |
146 | public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) { |
147 | $driverstatus = $this->driver_installed(); |
148 | |
149 | if ($driverstatus !== true) { |
150 | throw new dml_exception('dbdriverproblem', $driverstatus); |
151 | } |
152 | |
153 | /* |
154 | * Log all Errors. |
155 | */ |
156 | sqlsrv_configure("WarningsReturnAsErrors", FALSE); |
157 | sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_ALL); |
158 | sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); |
159 | |
160 | $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); |
161 | $this->sqlsrv = sqlsrv_connect($this->dbhost, array |
162 | ( |
163 | 'UID' => $this->dbuser, |
164 | 'PWD' => $this->dbpass, |
165 | 'Database' => $this->dbname, |
166 | 'CharacterSet' => 'UTF-8', |
167 | 'MultipleActiveResultSets' => true, |
168 | 'ConnectionPooling' => $dboptions['dbpersist'], |
169 | 'ReturnDatesAsStrings' => true, |
170 | )); |
171 | |
172 | if ($this->sqlsrv === false) { |
173 | $this->sqlsrv = null; |
174 | $dberr = $this->get_last_error(); |
175 | |
176 | throw new dml_connection_exception($dberr); |
177 | } |
178 | |
179 | // Allow quoted identifiers |
180 | $sql = "SET QUOTED_IDENTIFIER ON"; |
181 | $this->query_start($sql, null, SQL_QUERY_AUX); |
182 | $result = sqlsrv_query($this->sqlsrv, $sql); |
183 | $this->query_end($result); |
184 | |
185 | $this->free_result($result); |
186 | |
187 | // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL |
188 | // instead of equal(=) and distinct(<>) symbols |
189 | $sql = "SET ANSI_NULLS ON"; |
190 | $this->query_start($sql, null, SQL_QUERY_AUX); |
191 | $result = sqlsrv_query($this->sqlsrv, $sql); |
192 | $this->query_end($result); |
193 | |
194 | $this->free_result($result); |
195 | |
196 | // Force ANSI warnings so arithmetic/string overflows will be |
197 | // returning error instead of transparently truncating data |
198 | $sql = "SET ANSI_WARNINGS ON"; |
199 | $this->query_start($sql, null, SQL_QUERY_AUX); |
200 | $result = sqlsrv_query($this->sqlsrv, $sql); |
201 | $this->query_end($result); |
202 | |
203 | // Concatenating null with anything MUST return NULL |
204 | $sql = "SET CONCAT_NULL_YIELDS_NULL ON"; |
205 | $this->query_start($sql, null, SQL_QUERY_AUX); |
206 | $result = sqlsrv_query($this->sqlsrv, $sql); |
207 | $this->query_end($result); |
208 | |
209 | $this->free_result($result); |
210 | |
211 | // Set transactions isolation level to READ_COMMITTED |
212 | // prevents dirty reads when using transactions + |
213 | // is the default isolation level of sqlsrv |
214 | $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"; |
215 | $this->query_start($sql, NULL, SQL_QUERY_AUX); |
216 | $result = sqlsrv_query($this->sqlsrv, $sql); |
217 | $this->query_end($result); |
218 | |
219 | $this->free_result($result); |
220 | |
221 | // Connection established and configured, going to instantiate the temptables controller |
222 | $this->temptables = new sqlsrv_native_moodle_temptables($this); |
223 | |
224 | return true; |
225 | } |
226 | |
227 | /** |
228 | * Close database connection and release all resources |
229 | * and memory (especially circular memory references). |
230 | * Do NOT use connect() again, create a new instance if needed. |
231 | */ |
232 | public function dispose() { |
233 | parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection |
234 | |
235 | if ($this->sqlsrv) { |
236 | sqlsrv_close($this->sqlsrv); |
237 | $this->sqlsrv = null; |
238 | } |
239 | } |
240 | |
241 | /** |
242 | * Called before each db query. |
243 | * @param string $sql |
244 | * @param array array of parameters |
245 | * @param int $type type of query |
246 | * @param mixed $extrainfo driver specific extra information |
247 | * @return void |
248 | */ |
249 | protected function query_start($sql, array $params = null, $type, $extrainfo = null) { |
250 | parent::query_start($sql, $params, $type, $extrainfo); |
251 | } |
252 | |
253 | /** |
254 | * Called immediately after each db query. |
255 | * @param mixed db specific result |
256 | * @return void |
257 | */ |
258 | protected function query_end($result) { |
259 | parent::query_end($result); |
260 | } |
261 | |
262 | /** |
263 | * Returns database server info array |
264 | * @return array |
265 | */ |
266 | public function get_server_info() { |
267 | static $info; |
268 | |
269 | if (!$info) { |
270 | $server_info = sqlsrv_server_info($this->sqlsrv); |
271 | |
272 | if ($server_info) { |
273 | $info['server'] = $server_info['SQLServerName']; |
274 | $info['version'] = $server_info['SQLServerVersion']; |
275 | $info['database'] = $server_info['CurrentDatabase']; |
276 | } |
277 | } |
278 | return $info; |
279 | } |
280 | |
281 | /** |
282 | * Get the minimum SQL allowed |
283 | * |
284 | * @param mixed $version |
285 | * @return mixed |
286 | */ |
287 | protected function is_min_version($version) { |
288 | $server = $this->get_server_info(); |
289 | $server = $server['version']; |
290 | return version_compare($server, $version, '>='); |
291 | } |
292 | |
293 | /** |
294 | * Override: Converts short table name {tablename} to real table name |
295 | * supporting temp tables (#) if detected |
296 | * |
297 | * @param string sql |
298 | * @return string sql |
299 | */ |
300 | protected function fix_table_names($sql) { |
301 | if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/i', $sql, $matches)) { |
302 | foreach ($matches[0] as $key => $match) { |
303 | $name = $matches[1][$key]; |
304 | |
305 | if ($this->temptables->is_temptable($name)) { |
306 | $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql); |
307 | } else { |
308 | $sql = str_replace($match, $this->prefix.$name, $sql); |
309 | } |
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; // sqlsrv 1.1 can bind |
321 | } |
322 | |
323 | /** |
324 | * Returns last error reported by database engine. |
325 | */ |
326 | public function get_last_error() { |
327 | $retErrors = sqlsrv_errors(SQLSRV_ERR_ALL); |
328 | $errorMessage = 'No errors found'; |
329 | |
330 | if ($retErrors != null) { |
331 | $errorMessage = ''; |
332 | |
333 | foreach ($retErrors as $arrError) { |
334 | $errorMessage .= "SQLState: ".$arrError['SQLSTATE']."<br>\n"; |
335 | $errorMessage .= "Error Code: ".$arrError['code']."<br>\n"; |
336 | $errorMessage .= "Message: ".$arrError['message']."<br>\n"; |
337 | } |
338 | } |
339 | |
340 | return $errorMessage; |
341 | } |
342 | |
343 | /*** |
344 | * Bound variables *are* supported. Until I can get it to work, emulate the bindings |
345 | * The challenge/problem/bug is that allthough they work, doing a SELECT SCOPE_IDENTITY() |
346 | * doesnt return a value (no result set) |
347 | */ |
348 | |
349 | /** |
350 | * Prepare the query binding and do the actual query. |
351 | * |
352 | * @param string $sql The sql statment |
353 | * @param mixed $params array of params for binding. If NULL, they are ignored. |
354 | * @param mixed $sql_query_type - Type of operation |
355 | * @param mixed $free_result - Default true, transaction query will be freed. |
356 | */ |
357 | private function do_query($sql, $params, $sql_query_type, $free_result = true) { |
358 | list($sql, $params, $type) = $this->fix_sql_params($sql, $params); |
359 | |
360 | $sql = $this->emulate_bound_params($sql, $params); |
361 | $this->query_start($sql, $params, $sql_query_type); |
362 | $result = sqlsrv_query($this->sqlsrv, $sql); |
363 | |
364 | if ($result === false) { |
365 | // TODO do something with error or just use if DEV or DEBUG? |
366 | $dberr = $this->get_last_error(); |
367 | } |
368 | |
369 | $this->query_end($result); |
370 | |
371 | if ($free_result) { |
372 | $this->free_result($result); |
373 | return true; |
374 | } |
375 | return $result; |
376 | } |
377 | |
378 | /** |
379 | * Return tables in database WITHOUT current prefix |
380 | * @return array of table names in lowercase and without prefix |
381 | */ |
382 | public function get_tables($usecache = true) { |
383 | if ($usecache and count($this->tables) > 0) { |
384 | return $this->tables; |
385 | } |
386 | $this->tables = array (); |
387 | $sql = "SELECT table_name ". |
388 | "FROM information_schema.tables ". |
389 | "WHERE table_name LIKE '$this->prefix%' ". |
390 | "AND table_type = 'BASE TABLE'"; |
391 | |
392 | $this->query_start($sql, null, SQL_QUERY_AUX); |
393 | $result = sqlsrv_query($this->sqlsrv, $sql); |
394 | $this->query_end($result); |
395 | |
396 | if ($result) { |
397 | while ($row = sqlsrv_fetch_array($result)) { |
398 | $tablename = reset($row); |
399 | if (strpos($tablename, $this->prefix) !== 0) { |
400 | continue; |
401 | } |
402 | $tablename = substr($tablename, strlen($this->prefix)); |
403 | $this->tables[$tablename] = $tablename; |
404 | } |
405 | $this->free_result($result); |
406 | } |
407 | |
408 | // Add the currently available temptables |
409 | $this->tables = array_merge($this->tables, $this->temptables->get_temptables()); |
410 | return $this->tables; |
411 | } |
412 | |
413 | /** |
414 | * Return table indexes - everything lowercased |
415 | * @return array of arrays |
416 | */ |
417 | public function get_indexes($table) { |
418 | $indexes = array (); |
419 | $tablename = $this->prefix.$table; |
420 | |
421 | // Indexes aren't covered by information_schema metatables, so we need to |
422 | // go to sys ones. Skipping primary key indexes on purpose. |
423 | $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name |
424 | FROM sys.indexes i |
425 | JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id |
426 | JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id |
427 | JOIN sys.tables t ON i.object_id = t.object_id |
428 | WHERE t.name = '$tablename' |
429 | AND i.is_primary_key = 0 |
430 | ORDER BY i.name, i.index_id, ic.index_column_id"; |
431 | |
432 | $this->query_start($sql, null, SQL_QUERY_AUX); |
433 | $result = sqlsrv_query($this->sqlsrv, $sql); |
434 | $this->query_end($result); |
435 | |
436 | if ($result) { |
437 | $lastindex = ''; |
438 | $unique = false; |
439 | $columns = array (); |
440 | |
441 | while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { |
442 | if ($lastindex and $lastindex != $row['index_name']) |
443 | { // Save lastindex to $indexes and reset info |
444 | $indexes[$lastindex] = array |
445 | ( |
446 | 'unique' => $unique, |
447 | 'columns' => $columns |
448 | ); |
449 | |
450 | $unique = false; |
451 | $columns = array (); |
452 | } |
453 | $lastindex = $row['index_name']; |
454 | $unique = empty($row['is_unique']) ? false : true; |
455 | $columns[] = $row['column_name']; |
456 | } |
457 | |
458 | if ($lastindex) { // Add the last one if exists |
459 | $indexes[$lastindex] = array |
460 | ( |
461 | 'unique' => $unique, |
462 | 'columns' => $columns |
463 | ); |
464 | } |
465 | |
466 | $this->free_result($result); |
467 | } |
468 | return $indexes; |
469 | } |
470 | |
471 | /** |
472 | * Returns datailed information about columns in table. This information is cached internally. |
473 | * @param string $table name |
474 | * @param bool $usecache |
475 | * @return array array of database_column_info objects indexed with column names |
476 | */ |
477 | public function get_columns($table, $usecache = true) { |
478 | if ($usecache and isset($this->columns[$table])) { |
479 | return $this->columns[$table]; |
480 | } |
481 | |
482 | $this->columns[$table] = array (); |
483 | |
484 | if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema |
485 | $sql = "SELECT column_name AS name, |
486 | data_type AS type, |
487 | numeric_precision AS max_length, |
488 | character_maximum_length AS char_max_length, |
489 | numeric_scale AS scale, |
490 | is_nullable AS is_nullable, |
491 | columnproperty(object_id(quotename(table_schema) + '.' + |
492 | quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, |
493 | column_default AS default_value |
494 | FROM information_schema.columns |
495 | WHERE table_name = '{".$table."}' |
496 | ORDER BY ordinal_position"; |
497 | } else { // temp table, get metadata from tempdb schema |
498 | $sql = "SELECT column_name AS name, |
499 | data_type AS type, |
500 | numeric_precision AS max_length, |
501 | character_maximum_length AS char_max_length, |
502 | numeric_scale AS scale, |
503 | is_nullable AS is_nullable, |
504 | columnproperty(object_id(quotename(table_schema) + '.' + |
505 | quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, |
506 | column_default AS default_value |
507 | FROM tempdb.information_schema.columns ". |
508 | // check this statement |
509 | // JOIN tempdb..sysobjects ON name = table_name |
510 | // WHERE id = object_id('tempdb..{".$table."}') |
511 | "WHERE table_name like '{".$table."}__________%' |
512 | ORDER BY ordinal_position"; |
513 | } |
514 | |
515 | list($sql, $params, $type) = $this->fix_sql_params($sql, null); |
516 | |
517 | $this->query_start($sql, null, SQL_QUERY_AUX); |
518 | $result = sqlsrv_query($this->sqlsrv, $sql); |
519 | $this->query_end($result); |
520 | |
521 | if (!$result) { |
522 | return array (); |
523 | } |
524 | |
525 | while ($rawcolumn = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { |
526 | |
527 | $rawcolumn = (object)$rawcolumn; |
528 | |
529 | $info = new object(); |
530 | $info->name = $rawcolumn->name; |
531 | $info->type = $rawcolumn->type; |
532 | $info->meta_type = $this->sqlsrvtype2moodletype($info->type); |
533 | |
534 | // Prepare auto_increment info |
535 | $info->auto_increment = $rawcolumn->auto_increment ? true : false; |
536 | |
537 | // Define type for auto_increment columns |
538 | $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type; |
539 | |
540 | // id columns being auto_incremnt are PK by definition |
541 | $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment); |
542 | |
543 | // Put correct length for character and LOB types |
544 | $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length; |
545 | $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length; |
546 | |
547 | // Scale |
548 | $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false; |
549 | |
550 | // Prepare not_null info |
551 | $info->not_null = $rawcolumn->is_nullable == 'NO' ? true : false; |
552 | |
553 | // Process defaults |
554 | $info->has_default = !empty($rawcolumn->default_value); |
555 | $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value); |
556 | |
557 | // Process binary |
558 | $info->binary = $info->meta_type == 'B' ? true : false; |
559 | |
560 | $this->columns[$table][$info->name] = new database_column_info($info); |
561 | } |
562 | $this->free_result($result); |
563 | |
564 | return $this->columns[$table]; |
565 | } |
566 | |
567 | /** |
568 | * Normalise values based in RDBMS dependencies (booleans, LOBs...) |
569 | * |
570 | * @param database_column_info $column column metadata corresponding with the value we are going to normalise |
571 | * @param mixed $value value we are going to normalise |
572 | * @return mixed the normalised value |
573 | */ |
574 | protected function normalise_value($column, $value) { |
575 | if (is_bool($value)) { /// Always, convert boolean to int |
576 | $value = (int)$value; |
577 | } // And continue processing because text columns with numeric info need special handling below |
578 | |
579 | if ($column->meta_type == 'B') |
580 | { // BLOBs need to be properly "packed", but can be inserted directly if so. |
581 | if (!is_null($value)) { // If value not null, unpack it to unquoted hexadecimal byte-string format |
582 | $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it |
583 | } // easily and "bind" the param ok. |
584 | |
585 | } else if ($column->meta_type == 'X') { // sqlsrv doesn't cast from int to text, so if text column |
586 | if (is_numeric($value)) { // and is numeric value then cast to string |
587 | $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how |
588 | } // to "bind" the param ok, avoiding reverse conversion to number |
589 | } else if ($value === '') { |
590 | |
591 | if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') { |
592 | $value = 0; // prevent '' problems in numeric fields |
593 | } |
594 | } |
595 | return $value; |
596 | } |
597 | |
598 | /** |
599 | * Selectively call sqlsrv_free_stmt(), avoiding some warnings without using the horrible @ |
600 | * |
601 | * @param sqlsrv_resource $resource resource to be freed if possible |
602 | */ |
603 | private function free_result($resource) { |
604 | if (!is_bool($resource)) { // true/false resources cannot be freed |
605 | return sqlsrv_free_stmt($resource); |
606 | } |
607 | } |
608 | |
609 | /** |
610 | * Provides mapping between sqlsrv native data types and moodle_database - database_column_info - ones) |
611 | * |
612 | * @param string $sqlsrv_type native sqlsrv data type |
613 | * @return string 1-char database_column_info data type |
614 | */ |
615 | private function sqlsrvtype2moodletype($sqlsrv_type) { |
616 | $type = null; |
617 | |
618 | switch (strtoupper($sqlsrv_type)) { |
619 | case 'BIT': |
620 | $type = 'L'; |
621 | break; |
622 | |
623 | case 'INT': |
624 | case 'SMALLINT': |
625 | case 'INTEGER': |
626 | case 'BIGINT': |
627 | $type = 'I'; |
628 | break; |
629 | |
630 | case 'DECIMAL': |
631 | case 'REAL': |
632 | case 'FLOAT': |
633 | $type = 'N'; |
634 | break; |
635 | |
636 | case 'VARCHAR': |
637 | case 'NVARCHAR': |
638 | $type = 'C'; |
639 | break; |
640 | |
641 | case 'TEXT': |
642 | case 'NTEXT': |
643 | case 'VARCHAR(MAX)': |
644 | case 'NVARCHAR(MAX)': |
645 | $type = 'X'; |
646 | break; |
647 | |
648 | case 'IMAGE': |
649 | case 'VARBINARY(MAX)': |
650 | $type = 'B'; |
651 | break; |
652 | |
653 | case 'DATETIME': |
654 | $type = 'D'; |
655 | break; |
656 | } |
657 | |
658 | if (!$type) { |
659 | throw new dml_exception('invalidsqlsrvnativetype', $sqlsrv_type); |
660 | } |
661 | return $type; |
662 | } |
663 | |
664 | /** |
665 | * Do NOT use in code, to be used by database_manager only! |
666 | * @param string $sql query |
667 | * @return bool true |
668 | * @throws dml_exception if error |
669 | */ |
670 | public function change_database_structure($sql) { |
671 | $this->reset_caches(); |
672 | |
673 | $this->query_start($sql, null, SQL_QUERY_STRUCTURE); |
674 | $result = sqlsrv_query($this->sqlsrv, $sql); |
675 | $this->query_end($result); |
676 | |
677 | return true; |
678 | } |
679 | |
680 | /** |
681 | * Prepare the array of params for native binding |
682 | */ |
683 | protected function build_native_bound_params(array $params = null) { |
684 | |
685 | return null; |
686 | } |
687 | |
688 | |
689 | /** |
690 | * Workaround for SQL*Server Native driver similar to MSSQL driver for |
691 | * consistent behavior. |
692 | */ |
693 | protected function emulate_bound_params($sql, array $params = null) { |
694 | |
695 | if (empty($params)) { |
696 | return $sql; |
697 | } |
698 | /// ok, we have verified sql statement with ? and correct number of params |
699 | $return = strtok($sql, '?'); |
700 | |
701 | foreach ($params as $param) { |
702 | if (is_bool($param)) { |
703 | $return .= (int)$param; |
704 | } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially |
705 | $return .= '0x'.$param['hex']; |
706 | } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not* |
707 | $return .= "N'{$param['numstr']}'"; // be converted back to number params, but bound as strings |
708 | } else if (is_null($param)) { |
709 | $return .= 'NULL'; |
710 | |
711 | } else if ($this->is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646 |
712 | $return .= $param; |
713 | } else if (is_float($param)) { |
714 | $return .= $param; |
715 | } else { |
716 | $param = str_replace("'", "''", $param); |
717 | $return .= "N'$param'"; |
718 | } |
719 | |
720 | $return .= strtok('?'); |
721 | } |
722 | return $return; |
723 | } |
724 | |
725 | /* this function exists in modlib, so i would need to include that.. |
726 | */ |
727 | /** |
728 | * Return true if given value is integer or string with integer value |
729 | * |
730 | * @param mixed $value String or Int |
731 | * @return bool true if number, false if not |
732 | */ |
733 | private static function is_number($value) { |
734 | if (is_int($value)) { |
735 | return true; |
736 | } else if (is_string($value)) { |
737 | return ((string)(int)$value) === $value; |
738 | } else { |
739 | return false; |
740 | } |
741 | } |
742 | |
743 | /** |
744 | * Execute general sql query. Should be used only when no other method suitable. |
745 | * Do NOT use this to make changes in db structure, use database_manager::execute_sql() instead! |
746 | * @param string $sql query |
747 | * @param array $params query parameters |
748 | * @return bool true |
749 | * @throws dml_exception if error |
750 | */ |
751 | public function execute($sql, array $params = null) { |
752 | if (strpos($sql, ';') !== false) { |
753 | throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!'); |
754 | } |
755 | $this->do_query($sql, $params, SQL_QUERY_UPDATE); |
756 | return true; |
757 | } |
758 | |
759 | /** |
760 | * Get a number of records as a moodle_recordset using a SQL statement. |
761 | * |
762 | * Since this method is a little less readable, use of it should be restricted to |
763 | * code where it's possible there might be large datasets being returned. For known |
764 | * small datasets use get_records_sql - it leads to simpler code. |
765 | * |
766 | * The return type is as for @see function get_recordset. |
767 | * |
768 | * @param string $sql the SQL select query to execute. |
769 | * @param array $params array of sql parameters |
770 | * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). |
771 | * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). |
772 | * @return mixed an moodle_recordset object |
773 | * @throws dml_exception if error |
774 | */ |
775 | public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) { |
776 | $limitfrom = (int)$limitfrom; |
777 | $limitnum = (int)$limitnum; |
778 | $limitfrom = max(0, $limitfrom); |
779 | $limitnum = max(0, $limitnum); |
780 | |
781 | if ($limitfrom or $limitnum) { |
782 | $sql = $this->limit_to_top_n($sql, $limitfrom, $limitnum); |
783 | } |
784 | $result = $this->do_query($sql, $params, SQL_QUERY_SELECT, false); |
785 | return $this->create_recordset($result); |
786 | } |
787 | |
788 | /** |
789 | * Preform a emulation for LIMIT(offset, limit) |
790 | * |
791 | * @param mixed $sql |
792 | * @param mixed $offset |
793 | * @param mixed $limit |
794 | * @return mixed |
795 | */ |
796 | private function limit_to_top_n($sql, $offset, $limit) { |
797 | if ($limit < 1 && $offset < 1) { |
798 | return $sql; |
799 | } |
800 | $limit = max(0, $limit); |
801 | $offset = max(0, $offset); |
802 | |
803 | if ($limit > 0 && $offset == 0) { |
804 | $sql1 = preg_replace('/^([\s(])*SELECT( DISTINCT | ALL)?(?!\s*TOP\s*\()/i', |
805 | "\\1SELECT\\2 TOP $limit", $sql); |
806 | } else { |
807 | // Only apply TOP clause if we have any limitnum (limitfrom offset is hadled later) |
808 | if ($limit < 1) { |
809 | $limit = "9223372036854775806"; // MAX BIGINT -1 |
810 | } |
811 | if (preg_match('/\w*FROM[\s|{]*([\w|.]*)[\s|}]?/i', $sql, $match)) { |
812 | $from_table = $match[1]; |
813 | if (preg_match('/SELECT[\w|\s]*(\*)[\w|\s]*FROM/i', $sql)) { |
814 | // Need all the columns as the emulation returns some temp cols |
815 | $cols = array_keys($this->get_columns($from_table)); |
816 | $cols = implode(', ', $cols); |
817 | } else { |
818 | $cols = '*'; |
819 | } |
820 | $sql1 = 'SELECT '.$cols.' FROM ( ' |
821 | .'SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( ' |
822 | .'SELECT 1 AS line2, sub1.* FROM ' |
823 | .'{'.$from_table.'} AS sub1 ' |
824 | .') AS sub2 ' |
825 | .') AS sub3 ' |
826 | .'WHERE line3 BETWEEN '.($offset+1).' AND ' |
827 | .($offset + $limit); |
828 | } else { |
829 | $sql1 = "SELECT 'Invalid table'"; |
830 | } |
831 | } |
832 | |
833 | return $sql1; |
834 | } |
835 | |
836 | /** |
837 | * Create a record set and initialize with first row |
838 | * |
839 | * @param mixed $result |
840 | * @return sqlsrv_native_moodle_recordset |
841 | */ |
842 | protected function create_recordset($result) { |
843 | return new sqlsrv_native_moodle_recordset($result); |
844 | } |
845 | |
846 | /** |
847 | * Get a number of records as an array of objects using a SQL statement. |
848 | * |
849 | * Return value as for @see function get_records. |
850 | * |
851 | * @param string $sql the SQL select query to execute. The first column of this SELECT statement |
852 | * must be a unique value (usually the 'id' field), as it will be used as the key of the |
853 | * returned array. |
854 | * @param array $params array of sql parameters |
855 | * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set). |
856 | * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set). |
857 | * @return mixed an array of objects, or empty array if no records were found |
858 | * @throws dml_exception if error |
859 | */ |
860 | public function get_records_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) { |
861 | |
862 | $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum); |
863 | |
864 | $results = array(); |
865 | |
866 | foreach ($rs as $row) { |
867 | $id = reset($row); |
868 | |
869 | if (isset($return[$id])) { |
870 | $colname = key($row); |
871 | 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); |
872 | } |
873 | $results[$id] = (object)$row; |
874 | } |
875 | $rs->close(); |
876 | |
877 | return $results; |
878 | } |
879 | |
880 | /** |
881 | * Selects records and return values (first field) as an array using a SQL statement. |
882 | * |
883 | * @param string $sql The SQL query |
884 | * @param array $params array of sql parameters |
885 | * @return mixed array of values |
886 | * @throws dml_exception if error |
887 | */ |
888 | public function get_fieldset_sql($sql, array $params = null) { |
889 | |
890 | $rs = $this->get_recordset_sql($sql, $params); |
891 | |
892 | $results = array (); |
893 | |
894 | foreach ($rs as $row) { |
895 | $results[] = reset($row); |
896 | } |
897 | $rs->close(); |
898 | |
899 | return $results; |
900 | } |
901 | |
902 | /** |
903 | * Insert new record into database, as fast as possible, no safety checks, lobs not supported. |
904 | * @param string $table name |
905 | * @param mixed $params data record as object or array |
906 | * @param bool $returnit return it of inserted record |
907 | * @param bool $bulk true means repeated inserts expected |
908 | * @param bool $customsequence true if 'id' included in $params, disables $returnid |
909 | * @return true or new id |
910 | * @throws dml_exception if error |
911 | */ |
912 | public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) { |
913 | if (!is_array($params)) { |
914 | $params = (array)$params; |
915 | } |
916 | if ($customsequence) { |
917 | if (!isset($params['id'])) { |
918 | throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.'); |
919 | } |
920 | $returnid = false; |
921 | } else { |
922 | unset($params['id']); |
923 | } |
924 | |
925 | if (empty($params)) { |
926 | throw new coding_exception('moodle_database::insert_record_raw() no fields found.'); |
927 | } |
928 | $fields = implode(',', array_keys($params)); |
929 | $qms = array_fill(0, count($params), '?'); |
930 | $qms = implode(',', $qms); |
931 | $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms)"; |
932 | $query_id = $this->do_query($sql, $params, SQL_QUERY_INSERT); |
933 | if ($returnid) { |
934 | $id = $this->sqlsrv_fetch_id(); |
935 | return $id; |
936 | } else { |
937 | return true; |
938 | } |
939 | } |
940 | |
941 | /** |
942 | * Get the ID of the current action |
943 | * |
944 | * @return mixed ID |
945 | */ |
946 | private function sqlsrv_fetch_id() { |
947 | $query_id = sqlsrv_query($this->sqlsrv, 'SELECT SCOPE_IDENTITY()'); |
948 | if ($query_id === false) { |
949 | $dberr = $this->get_last_error(); |
950 | return false; |
951 | } |
952 | $row = $this->sqlsrv_fetchrow($query_id); |
953 | return (int)$row[0]; |
954 | } |
955 | |
956 | /** |
957 | * Fetch a single row into an numbered array |
958 | * |
959 | * @param mixed $query_id |
960 | */ |
961 | private function sqlsrv_fetchrow($query_id) { |
962 | $row = sqlsrv_fetch_array($query_id, SQLSRV_FETCH_NUMERIC); |
963 | if ($row === false) { |
964 | $dberr = $this->get_last_error(); |
965 | return false; |
966 | } |
967 | |
968 | foreach ($row as $key => $value) { |
969 | $row[$key] = ($value === ' ' || $value === NULL) ? '' : $value; |
970 | } |
971 | return $row; |
972 | } |
973 | |
974 | /** |
975 | * Insert a record into a table and return the "id" field if required. |
976 | * |
977 | * Some conversions and safety checks are carried out. Lobs are supported. |
978 | * If the return ID isn't required, then this just reports success as true/false. |
979 | * $data is an object containing needed data |
980 | * @param string $table The database table to be inserted into |
981 | * @param object $data A data object with values for one or more fields in the record |
982 | * @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. |
983 | * @return true or new id |
984 | * @throws dml_exception if error |
985 | */ |
986 | public function insert_record($table, $dataobject, $returnid = true, $bulk = false) { |
987 | if (!is_object($dataobject)) { |
988 | $dataobject = (object)$dataobject; |
989 | } |
990 | |
991 | unset($dataobject->id); |
992 | |
993 | $columns = $this->get_columns($table); |
994 | $cleaned = array (); |
995 | |
996 | foreach ($dataobject as $field => $value) { |
997 | if (!isset($columns[$field])) { |
998 | continue; |
999 | } |
1000 | $column = $columns[$field]; |
1001 | $cleaned[$field] = $this->normalise_value($column, $value); |
1002 | } |
1003 | |
1004 | return $this->insert_record_raw($table, $cleaned, $returnid, $bulk); |
1005 | } |
1006 | |
1007 | /** |
1008 | * Import a record into a table, id field is required. |
1009 | * Safety checks are NOT carried out. Lobs are supported. |
1010 | * |
1011 | * @param string $table name of database table to be inserted into |
1012 | * @param object $dataobject A data object with values for one or more fields in the record |
1013 | * @return bool true |
1014 | * @throws dml_exception if error |
1015 | */ |
1016 | public function import_record($table, $dataobject) { |
1017 | if (!is_object($dataobject)) { |
1018 | $dataobject = (object)$dataobject; |
1019 | } |
1020 | |
1021 | $columns = $this->get_columns($table); |
1022 | $cleaned = array (); |
1023 | |
1024 | foreach ($dataobject as $field => $value) { |
1025 | if (!isset($columns[$field])) { |
1026 | continue; |
1027 | } |
1028 | $column = $columns[$field]; |
1029 | $cleaned[$field] = $this->normalise_value($column, $value); |
1030 | } |
1031 | |
1032 | // Disable IDENTITY column before inserting record with id |
1033 | $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!! |
1034 | $this->do_query($sql, null, SQL_QUERY_AUX); |
1035 | |
1036 | $insertresult = $this->insert_record_raw($table, $cleaned, false, false, true); |
1037 | |
1038 | // Enable IDENTITY column after inserting record with id |
1039 | $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!! |
1040 | |
1041 | $this->do_query($sql, null, SQL_QUERY_AUX); |
1042 | |
1043 | return $insertresult; |
1044 | } |
1045 | |
1046 | /** |
1047 | * Update record in database, as fast as possible, no safety checks, lobs not supported. |
1048 | * @param string $table name |
1049 | * @param mixed $params data record as object or array |
1050 | * @param bool true means repeated updates expected |
1051 | * @return bool true |
1052 | * @throws dml_exception if error |
1053 | */ |
1054 | public function update_record_raw($table, $params, $bulk = false) { |
1055 | if (!is_array($params)) { |
1056 | $params = (array)$params; |
1057 | } |
1058 | |
1059 | if (!isset($params['id'])) { |
1060 | throw new coding_exception('moodle_database::update_record_raw() id field must be specified.'); |
1061 | } |
1062 | $id = $params['id']; |
1063 | unset($params['id']); |
1064 | |
1065 | if (empty($params)) { |
1066 | throw new coding_exception('moodle_database::update_record_raw() no fields found.'); |
1067 | } |
1068 | |
1069 | $sets = array (); |
1070 | |
1071 | foreach ($params as $field => $value) { |
1072 | $sets[] = "$field = ?"; |
1073 | } |
1074 | |
1075 | $params[] = $id; // last ? in WHERE condition |
1076 | |
1077 | $sets = implode(',', $sets); |
1078 | $sql = "UPDATE {".$table."} SET $sets WHERE id = ?"; |
1079 | |
1080 | $this->do_query($sql, $params, SQL_QUERY_UPDATE); |
1081 | |
1082 | return true; |
1083 | } |
1084 | |
1085 | /** |
1086 | * Update a record in a table |
1087 | * |
1088 | * $dataobject is an object containing needed data |
1089 | * Relies on $dataobject having a variable "id" to |
1090 | * specify the record to update |
1091 | * |
1092 | * @param string $table The database table to be checked against. |
1093 | * @param object $dataobject An object with contents equal to fieldname=>fieldvalue. Must have an entry for 'id' to map to the table specified. |
1094 | * @param bool true means repeated updates expected |
1095 | * @return bool true |
1096 | * @throws dml_exception if error |
1097 | */ |
1098 | public function update_record($table, $dataobject, $bulk = false) { |
1099 | if (!is_object($dataobject)) { |
1100 | $dataobject = (object)$dataobject; |
1101 | } |
1102 | |
1103 | $columns = $this->get_columns($table); |
1104 | $cleaned = array (); |
1105 | |
1106 | foreach ($dataobject as $field => $value) { |
1107 | if (!isset($columns[$field])) { |
1108 | continue; |
1109 | } |
1110 | $column = $columns[$field]; |
1111 | $cleaned[$field] = $this->normalise_value($column, $value); |
1112 | } |
1113 | |
1114 | return $this->update_record_raw($table, $cleaned, $bulk); |
1115 | } |
1116 | |
1117 | /** |
1118 | * Set a single field in every table record which match a particular WHERE clause. |
1119 | * |
1120 | * @param string $table The database table to be checked against. |
1121 | * @param string $newfield the field to set. |
1122 | * @param string $newvalue the value to set the field to. |
1123 | * @param string $select A fragment of SQL to be used in a where clause in the SQL call. |
1124 | * @param array $params array of sql parameters |
1125 | * @return bool true |
1126 | * @throws dml_exception if error |
1127 | */ |
1128 | public function set_field_select($table, $newfield, $newvalue, $select, array $params = null) { |
1129 | if ($select) { |
1130 | $select = "WHERE $select"; |
1131 | } |
1132 | |
1133 | if (is_null($params)) { |
1134 | $params = array (); |
1135 | } |
1136 | |
1137 | /// Get column metadata |
1138 | $columns = $this->get_columns($table); |
1139 | $column = $columns[$newfield]; |
1140 | |
1141 | $newvalue = $this->normalise_value($column, $newvalue); |
1142 | |
1143 | if (is_null($newvalue)) { |
1144 | $newfield = "$newfield = NULL"; |
1145 | } else { |
1146 | $newfield = "$newfield = ?"; |
1147 | array_unshift($params, $newvalue); |
1148 | } |
1149 | $sql = "UPDATE {".$table."} SET $newfield $select"; |
1150 | |
1151 | $this->do_query($sql, $params, SQL_QUERY_UPDATE); |
1152 | |
1153 | return true; |
1154 | } |
1155 | |
1156 | /** |
1157 | * Delete one or more records from a table which match a particular WHERE clause. |
1158 | * |
1159 | * @param string $table The database table to be checked against. |
1160 | * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria). |
1161 | * @param array $params array of sql parameters |
1162 | * @return bool true |
1163 | * @throws dml_exception if error |
1164 | */ |
1165 | public function delete_records_select($table, $select, array $params = null) { |
1166 | if ($select) { |
1167 | $select = "WHERE $select"; |
1168 | } |
1169 | |
1170 | $sql = "DELETE FROM {".$table."} $select"; |
1171 | |
1172 | $this->do_query($sql, $params, SQL_QUERY_UPDATE); // msk probably should be SQL_QUERY_DELETE |
1173 | |
1174 | return true; |
1175 | } |
1176 | |
1177 | |
1178 | /// SQL helper functions |
1179 | |
1180 | public function sql_bitxor($int1, $int2) { |
1181 | return '('.$this->sql_bitor($int1, $int2).' - '.$this->sql_bitand($int1, $int2).')'; |
1182 | } |
1183 | |
1184 | public function sql_cast_char2int($fieldname, $text = false) { |
1185 | return ' CAST('.$fieldname.' AS INT) '; |
1186 | } |
1187 | |
1188 | public function sql_ceil($fieldname) { |
1189 | return ' CEILING('.$fieldname.')'; |
1190 | } |
1191 | |
1192 | public function sql_concat() { |
1193 | $arr = func_get_args(); |
1194 | |
1195 | foreach ($arr as $key => $ele) { |
1196 | $arr[$key] = ' CAST('.$ele.' AS VARCHAR(255)) '; |
1197 | } |
1198 | $s = implode(' + ', $arr); |
1199 | |
1200 | if ($s === '') { |
1201 | return " '' "; |
1202 | } |
1203 | return " $s "; |
1204 | } |
1205 | |
1206 | public function sql_concat_join($separator = "' '", $elements = array ()) { |
1207 | for ($n = count($elements) - 1; $n > 0; $n--) { |
1208 | array_splice($elements, $n, 0, $separator); |
1209 | } |
1210 | $s = implode(' + ', $elements); |
1211 | |
1212 | if ($s === '') { |
1213 | return " '' "; |
1214 | } |
1215 | return " $s "; |
1216 | } |
1217 | |
1218 | public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) { |
1219 | if ($textfield) { |
1220 | return ' ('.$this->sql_compare_text($fieldname)." = '') "; |
1221 | } else { |
1222 | return " ($fieldname = '') "; |
1223 | } |
1224 | } |
1225 | |
1226 | /** |
1227 | * Returns the SQL text to be used to calculate the length in characters of one expression. |
1228 | * @param string fieldname or expression to calculate its length in characters. |
1229 | * @return string the piece of SQL code to be used in the statement. |
1230 | */ |
1231 | public function sql_length($fieldname) { |
1232 | return ' LEN('.$fieldname.')'; |
1233 | } |
1234 | |
1235 | public function sql_order_by_text($fieldname, $numchars = 32) { |
1236 | return ' CONVERT(varchar, '.$fieldname.', '.$numchars.')'; |
1237 | } |
1238 | |
1239 | /** |
1240 | * Returns the SQL for returning searching one string for the location of another. |
1241 | */ |
1242 | public function sql_position($needle, $haystack) { |
1243 | return "CHARINDEX(($needle), ($haystack))"; |
1244 | } |
1245 | |
1246 | /** |
1247 | * Returns the proper substr() SQL text used to extract substrings from DB |
1248 | * NOTE: this was originally returning only function name |
1249 | * |
1250 | * @param string $expr some string field, no aggregates |
1251 | * @param mixed $start integer or expresion evaluating to int |
1252 | * @param mixed $length optional integer or expresion evaluating to int |
1253 | * @return string sql fragment |
1254 | */ |
1255 | public function sql_substr($expr, $start, $length = false) { |
1256 | if (count(func_get_args()) < 2) { |
1257 | throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa |
1258 | s only returning name of SQL substring function, it now requires all parameters.'); |
1259 | } |
1260 | |
1261 | if ($length === false) { |
1262 | return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))"; |
1263 | } else { |
1264 | return "SUBSTRING($expr, $start, $length)"; |
1265 | } |
1266 | } |
1267 | |
1268 | /// session locking |
1269 | |
1270 | public function session_lock_supported() { |
1271 | return true; |
1272 | } |
1273 | |
1274 | public function get_session_lock($rowid) { |
1275 | if (!$this->session_lock_supported()) { |
1276 | return; |
1277 | } |
1278 | parent::get_session_lock($rowid); |
1279 | |
1280 | $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; |
1281 | $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', 120000"; |
1282 | $this->query_start($sql, null, SQL_QUERY_AUX); |
1283 | $result = sqlsrv_query($this->sqlsrv, $sql); |
1284 | $this->query_end($result); |
1285 | $this->free_result($result); |
1286 | } |
1287 | |
1288 | public function release_session_lock($rowid) { |
1289 | if (!$this->session_lock_supported()) { |
1290 | return; |
1291 | } |
1292 | parent::release_session_lock($rowid); |
1293 | |
1294 | $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; |
1295 | $sql = "sp_releaseapplock '$fullname', 'Session'"; |
1296 | $this->query_start($sql, null, SQL_QUERY_AUX); |
1297 | $result = sqlsrv_query($this->sqlsrv, $sql); |
1298 | $this->query_end($result); |
1299 | $this->free_result($result); |
1300 | } |
1301 | |
1302 | |
1303 | /// transactions |
1304 | |
1305 | // NOTE: |
1306 | // TODO -- should these be wrapped in query start/end? They arnt a query |
1307 | // but information and error capture is nice. msk |
1308 | |
1309 | |
1310 | /** |
1311 | * Driver specific start of real database transaction, |
1312 | * this can not be used directly in code. |
1313 | * @return void |
1314 | */ |
1315 | protected function begin_transaction() { |
1316 | $this->query_start('native sqlsrv_begin_transaction', NULL, SQL_QUERY_AUX); |
1317 | $result = sqlsrv_begin_transaction($this->sqlsrv); |
1318 | $this->query_end($result); |
1319 | } |
1320 | |
1321 | /** |
1322 | * Driver specific commit of real database transaction, |
1323 | * this can not be used directly in code. |
1324 | * @return void |
1325 | */ |
1326 | protected function commit_transaction() { |
1327 | $this->query_start('native sqlsrv_commit', NULL, SQL_QUERY_AUX); |
1328 | $result = sqlsrv_commit($this->sqlsrv); |
1329 | $this->query_end($result); |
1330 | } |
1331 | |
1332 | /** |
1333 | * Driver specific abort of real database transaction, |
1334 | * this can not be used directly in code. |
1335 | * @return void |
1336 | */ |
1337 | protected function rollback_transaction() { |
1338 | $this->query_start('native sqlsrv_rollback', NULL, SQL_QUERY_AUX); |
1339 | $result = sqlsrv_rollback($this->sqlsrv); |
1340 | $this->query_end($result); |
1341 | } |
1342 | } |