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