|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 2 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once($CFG->libdir.'/dml/moodle_database.php'); 00030 require_once($CFG->libdir.'/dml/sqlsrv_native_moodle_recordset.php'); 00031 require_once($CFG->libdir.'/dml/sqlsrv_native_moodle_temptables.php'); 00032 00036 class sqlsrv_native_moodle_database extends moodle_database { 00037 00038 protected $sqlsrv = null; 00039 protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity 00040 protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object) 00041 protected $collation; // current DB collation cache 00042 00048 public function __construct($external=false) { 00049 parent::__construct($external); 00050 } 00051 00057 public function driver_installed() { 00058 // use 'function_exists()' rather than 'extension_loaded()' because 00059 // the name used by 'extension_loaded()' is case specific! The extension 00060 // therefore *could be* mixed case and hence not found. 00061 if (!function_exists('sqlsrv_num_rows')) { 00062 return get_string('sqlsrvextensionisnotpresentinphp', 'install'); 00063 } 00064 return true; 00065 } 00066 00072 public function get_dbfamily() { 00073 return 'mssql'; 00074 } 00075 00081 protected function get_dbtype() { 00082 return 'sqlsrv'; 00083 } 00084 00090 protected function get_dblibrary() { 00091 return 'native'; 00092 } 00093 00099 public function get_name() { 00100 return get_string('nativesqlsrv', 'install'); 00101 } 00102 00108 public function get_configuration_help() { 00109 return get_string('nativesqlsrvhelp', 'install'); 00110 } 00111 00117 public function get_configuration_hints() { 00118 $str = get_string('databasesettingssub_sqlsrv', 'install'); 00119 $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" "; 00120 $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Using_the_Microsoft_SQL_Server_Driver_for_PHP')\""; 00121 $str .= ">"; 00122 $str .= '<img src="pix/docs.gif'.'" alt="Docs" class="iconhelp" />'; 00123 $str .= get_string('moodledocslink', 'install').'</a></p>'; 00124 return $str; 00125 } 00126 00139 public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) { 00140 $driverstatus = $this->driver_installed(); 00141 00142 if ($driverstatus !== true) { 00143 throw new dml_exception('dbdriverproblem', $driverstatus); 00144 } 00145 00146 /* 00147 * Log all Errors. 00148 */ 00149 sqlsrv_configure("WarningsReturnAsErrors", FALSE); 00150 sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_ALL); 00151 sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); 00152 00153 $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); 00154 $this->sqlsrv = sqlsrv_connect($this->dbhost, array 00155 ( 00156 'UID' => $this->dbuser, 00157 'PWD' => $this->dbpass, 00158 'Database' => $this->dbname, 00159 'CharacterSet' => 'UTF-8', 00160 'MultipleActiveResultSets' => true, 00161 'ConnectionPooling' => !empty($this->dboptions['dbpersist']), 00162 'ReturnDatesAsStrings' => true, 00163 )); 00164 00165 if ($this->sqlsrv === false) { 00166 $this->sqlsrv = null; 00167 $dberr = $this->get_last_error(); 00168 00169 throw new dml_connection_exception($dberr); 00170 } 00171 00172 // Allow quoted identifiers 00173 $sql = "SET QUOTED_IDENTIFIER ON"; 00174 $this->query_start($sql, null, SQL_QUERY_AUX); 00175 $result = sqlsrv_query($this->sqlsrv, $sql); 00176 $this->query_end($result); 00177 00178 $this->free_result($result); 00179 00180 // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL 00181 // instead of equal(=) and distinct(<>) symbols 00182 $sql = "SET ANSI_NULLS ON"; 00183 $this->query_start($sql, null, SQL_QUERY_AUX); 00184 $result = sqlsrv_query($this->sqlsrv, $sql); 00185 $this->query_end($result); 00186 00187 $this->free_result($result); 00188 00189 // Force ANSI warnings so arithmetic/string overflows will be 00190 // returning error instead of transparently truncating data 00191 $sql = "SET ANSI_WARNINGS ON"; 00192 $this->query_start($sql, null, SQL_QUERY_AUX); 00193 $result = sqlsrv_query($this->sqlsrv, $sql); 00194 $this->query_end($result); 00195 00196 // Concatenating null with anything MUST return NULL 00197 $sql = "SET CONCAT_NULL_YIELDS_NULL ON"; 00198 $this->query_start($sql, null, SQL_QUERY_AUX); 00199 $result = sqlsrv_query($this->sqlsrv, $sql); 00200 $this->query_end($result); 00201 00202 $this->free_result($result); 00203 00204 // Set transactions isolation level to READ_COMMITTED 00205 // prevents dirty reads when using transactions + 00206 // is the default isolation level of sqlsrv 00207 $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"; 00208 $this->query_start($sql, NULL, SQL_QUERY_AUX); 00209 $result = sqlsrv_query($this->sqlsrv, $sql); 00210 $this->query_end($result); 00211 00212 $this->free_result($result); 00213 00214 // Connection established and configured, going to instantiate the temptables controller 00215 $this->temptables = new sqlsrv_native_moodle_temptables($this); 00216 00217 return true; 00218 } 00219 00225 public function dispose() { 00226 parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection 00227 00228 if ($this->sqlsrv) { 00229 sqlsrv_close($this->sqlsrv); 00230 $this->sqlsrv = null; 00231 } 00232 } 00233 00242 protected function query_start($sql, array $params = null, $type, $extrainfo = null) { 00243 parent::query_start($sql, $params, $type, $extrainfo); 00244 } 00245 00251 protected function query_end($result) { 00252 parent::query_end($result); 00253 } 00254 00259 public function get_server_info() { 00260 static $info; 00261 00262 if (!$info) { 00263 $server_info = sqlsrv_server_info($this->sqlsrv); 00264 00265 if ($server_info) { 00266 $info['description'] = $server_info['SQLServerName']; 00267 $info['version'] = $server_info['SQLServerVersion']; 00268 $info['database'] = $server_info['CurrentDatabase']; 00269 } 00270 } 00271 return $info; 00272 } 00273 00280 protected function is_min_version($version) { 00281 $server = $this->get_server_info(); 00282 $server = $server['version']; 00283 return version_compare($server, $version, '>='); 00284 } 00285 00293 protected function fix_table_names($sql) { 00294 if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/i', $sql, $matches)) { 00295 foreach ($matches[0] as $key => $match) { 00296 $name = $matches[1][$key]; 00297 00298 if ($this->temptables->is_temptable($name)) { 00299 $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql); 00300 } else { 00301 $sql = str_replace($match, $this->prefix.$name, $sql); 00302 } 00303 } 00304 } 00305 return $sql; 00306 } 00307 00312 protected function allowed_param_types() { 00313 return SQL_PARAMS_QM; // sqlsrv 1.1 can bind 00314 } 00315 00320 public function get_last_error() { 00321 $retErrors = sqlsrv_errors(SQLSRV_ERR_ALL); 00322 $errorMessage = 'No errors found'; 00323 00324 if ($retErrors != null) { 00325 $errorMessage = ''; 00326 00327 foreach ($retErrors as $arrError) { 00328 $errorMessage .= "SQLState: ".$arrError['SQLSTATE']."<br>\n"; 00329 $errorMessage .= "Error Code: ".$arrError['code']."<br>\n"; 00330 $errorMessage .= "Message: ".$arrError['message']."<br>\n"; 00331 } 00332 } 00333 00334 return $errorMessage; 00335 } 00336 00337 /*** 00338 * Bound variables *are* supported. Until I can get it to work, emulate the bindings 00339 * The challenge/problem/bug is that although they work, doing a SELECT SCOPE_IDENTITY() 00340 * doesn't return a value (no result set) 00341 */ 00342 00352 private function do_query($sql, $params, $sql_query_type, $free_result = true, $scrollable = false) { 00353 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 00354 00355 $sql = $this->emulate_bound_params($sql, $params); 00356 $this->query_start($sql, $params, $sql_query_type); 00357 if (!$scrollable) { // Only supporting next row 00358 $result = sqlsrv_query($this->sqlsrv, $sql); 00359 } else { // Suporting absolute/relative rows 00360 $result = sqlsrv_query($this->sqlsrv, $sql, array(), array('Scrollable' => SQLSRV_CURSOR_STATIC)); 00361 } 00362 00363 if ($result === false) { 00364 // TODO do something with error or just use if DEV or DEBUG? 00365 $dberr = $this->get_last_error(); 00366 } 00367 00368 $this->query_end($result); 00369 00370 if ($free_result) { 00371 $this->free_result($result); 00372 return true; 00373 } 00374 return $result; 00375 } 00376 00381 public function get_tables($usecache = true) { 00382 if ($usecache and count($this->tables) > 0) { 00383 return $this->tables; 00384 } 00385 $this->tables = array (); 00386 $prefix = str_replace('_', '\\_', $this->prefix); 00387 $sql = "SELECT table_name 00388 FROM information_schema.tables 00389 WHERE table_name LIKE '$prefix%' ESCAPE '\\' AND table_type = 'BASE TABLE'"; 00390 00391 $this->query_start($sql, null, SQL_QUERY_AUX); 00392 $result = sqlsrv_query($this->sqlsrv, $sql); 00393 $this->query_end($result); 00394 00395 if ($result) { 00396 while ($row = sqlsrv_fetch_array($result)) { 00397 $tablename = reset($row); 00398 if (strpos($tablename, $this->prefix) !== 0) { 00399 continue; 00400 } 00401 $tablename = substr($tablename, strlen($this->prefix)); 00402 $this->tables[$tablename] = $tablename; 00403 } 00404 $this->free_result($result); 00405 } 00406 00407 // Add the currently available temptables 00408 $this->tables = array_merge($this->tables, $this->temptables->get_temptables()); 00409 return $this->tables; 00410 } 00411 00416 public function get_indexes($table) { 00417 $indexes = array (); 00418 $tablename = $this->prefix.$table; 00419 00420 // Indexes aren't covered by information_schema metatables, so we need to 00421 // go to sys ones. Skipping primary key indexes on purpose. 00422 $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name 00423 FROM sys.indexes i 00424 JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id 00425 JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id 00426 JOIN sys.tables t ON i.object_id = t.object_id 00427 WHERE t.name = '$tablename' AND i.is_primary_key = 0 00428 ORDER BY i.name, i.index_id, ic.index_column_id"; 00429 00430 $this->query_start($sql, null, SQL_QUERY_AUX); 00431 $result = sqlsrv_query($this->sqlsrv, $sql); 00432 $this->query_end($result); 00433 00434 if ($result) { 00435 $lastindex = ''; 00436 $unique = false; 00437 $columns = array (); 00438 00439 while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { 00440 if ($lastindex and $lastindex != $row['index_name']) 00441 { // Save lastindex to $indexes and reset info 00442 $indexes[$lastindex] = array 00443 ( 00444 'unique' => $unique, 00445 'columns' => $columns 00446 ); 00447 00448 $unique = false; 00449 $columns = array (); 00450 } 00451 $lastindex = $row['index_name']; 00452 $unique = empty($row['is_unique']) ? false : true; 00453 $columns[] = $row['column_name']; 00454 } 00455 00456 if ($lastindex) { // Add the last one if exists 00457 $indexes[$lastindex] = array 00458 ( 00459 'unique' => $unique, 00460 'columns' => $columns 00461 ); 00462 } 00463 00464 $this->free_result($result); 00465 } 00466 return $indexes; 00467 } 00468 00475 public function get_columns($table, $usecache = true) { 00476 if ($usecache and isset($this->columns[$table])) { 00477 return $this->columns[$table]; 00478 } 00479 00480 $this->columns[$table] = array (); 00481 00482 if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema 00483 $sql = "SELECT column_name AS name, 00484 data_type AS type, 00485 numeric_precision AS max_length, 00486 character_maximum_length AS char_max_length, 00487 numeric_scale AS scale, 00488 is_nullable AS is_nullable, 00489 columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, 00490 column_default AS default_value 00491 FROM information_schema.columns 00492 WHERE table_name = '{".$table."}' 00493 ORDER BY ordinal_position"; 00494 } else { // temp table, get metadata from tempdb schema 00495 $sql = "SELECT column_name AS name, 00496 data_type AS type, 00497 numeric_precision AS max_length, 00498 character_maximum_length AS char_max_length, 00499 numeric_scale AS scale, 00500 is_nullable AS is_nullable, 00501 columnproperty(object_id(quotename(table_schema) + '.' + quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, 00502 column_default AS default_value 00503 FROM tempdb.information_schema.columns ". 00504 // check this statement 00505 // JOIN tempdb..sysobjects ON name = table_name 00506 // WHERE id = object_id('tempdb..{".$table."}') 00507 "WHERE table_name LIKE '{".$table."}__________%' 00508 ORDER BY ordinal_position"; 00509 } 00510 00511 list($sql, $params, $type) = $this->fix_sql_params($sql, null); 00512 00513 $this->query_start($sql, null, SQL_QUERY_AUX); 00514 $result = sqlsrv_query($this->sqlsrv, $sql); 00515 $this->query_end($result); 00516 00517 if (!$result) { 00518 return array (); 00519 } 00520 00521 while ($rawcolumn = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { 00522 00523 $rawcolumn = (object)$rawcolumn; 00524 00525 $info = new stdClass(); 00526 $info->name = $rawcolumn->name; 00527 $info->type = $rawcolumn->type; 00528 $info->meta_type = $this->sqlsrvtype2moodletype($info->type); 00529 00530 // Prepare auto_increment info 00531 $info->auto_increment = $rawcolumn->auto_increment ? true : false; 00532 00533 // Define type for auto_increment columns 00534 $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type; 00535 00536 // id columns being auto_incremnt are PK by definition 00537 $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment); 00538 00539 // Put correct length for character and LOB types 00540 $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length; 00541 $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length; 00542 00543 // Scale 00544 $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false; 00545 00546 // Prepare not_null info 00547 $info->not_null = $rawcolumn->is_nullable == 'NO' ? true : false; 00548 00549 // Process defaults 00550 $info->has_default = !empty($rawcolumn->default_value); 00551 if ($rawcolumn->default_value === NULL) { 00552 $info->default_value = NULL; 00553 } else { 00554 $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value); 00555 } 00556 00557 // Process binary 00558 $info->binary = $info->meta_type == 'B' ? true : false; 00559 00560 $this->columns[$table][$info->name] = new database_column_info($info); 00561 } 00562 $this->free_result($result); 00563 00564 return $this->columns[$table]; 00565 } 00566 00574 protected function normalise_value($column, $value) { 00575 if (is_bool($value)) { 00576 $value = (int)$value; 00577 } // And continue processing because text columns with numeric info need special handling below 00578 00579 if ($column->meta_type == 'B') 00580 { // BLOBs need to be properly "packed", but can be inserted directly if so. 00581 if (!is_null($value)) { // If value not null, unpack it to unquoted hexadecimal byte-string format 00582 $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it 00583 } // easily and "bind" the param ok. 00584 00585 } else if ($column->meta_type == 'X') { // sqlsrv doesn't cast from int to text, so if text column 00586 if (is_numeric($value)) { // and is numeric value then cast to string 00587 $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how 00588 } // to "bind" the param ok, avoiding reverse conversion to number 00589 } else if ($value === '') { 00590 00591 if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') { 00592 $value = 0; // prevent '' problems in numeric fields 00593 } 00594 } 00595 return $value; 00596 } 00597 00603 private function free_result($resource) { 00604 if (!is_bool($resource)) { // true/false resources cannot be freed 00605 return sqlsrv_free_stmt($resource); 00606 } 00607 } 00608 00615 private function sqlsrvtype2moodletype($sqlsrv_type) { 00616 $type = null; 00617 00618 switch (strtoupper($sqlsrv_type)) { 00619 case 'BIT': 00620 $type = 'L'; 00621 break; 00622 00623 case 'INT': 00624 case 'SMALLINT': 00625 case 'INTEGER': 00626 case 'BIGINT': 00627 $type = 'I'; 00628 break; 00629 00630 case 'DECIMAL': 00631 case 'REAL': 00632 case 'FLOAT': 00633 $type = 'N'; 00634 break; 00635 00636 case 'VARCHAR': 00637 case 'NVARCHAR': 00638 $type = 'C'; 00639 break; 00640 00641 case 'TEXT': 00642 case 'NTEXT': 00643 case 'VARCHAR(MAX)': 00644 case 'NVARCHAR(MAX)': 00645 $type = 'X'; 00646 break; 00647 00648 case 'IMAGE': 00649 case 'VARBINARY(MAX)': 00650 $type = 'B'; 00651 break; 00652 00653 case 'DATETIME': 00654 $type = 'D'; 00655 break; 00656 } 00657 00658 if (!$type) { 00659 throw new dml_exception('invalidsqlsrvnativetype', $sqlsrv_type); 00660 } 00661 return $type; 00662 } 00663 00670 public function change_database_structure($sql) { 00671 $this->reset_caches(); 00672 00673 $this->query_start($sql, null, SQL_QUERY_STRUCTURE); 00674 $result = sqlsrv_query($this->sqlsrv, $sql); 00675 $this->query_end($result); 00676 00677 return true; 00678 } 00679 00683 protected function build_native_bound_params(array $params = null) { 00684 00685 return null; 00686 } 00687 00688 00693 protected function emulate_bound_params($sql, array $params = null) { 00694 00695 if (empty($params)) { 00696 return $sql; 00697 } 00699 $parts = explode('?', $sql); 00700 $return = array_shift($parts); 00701 foreach ($params as $param) { 00702 if (is_bool($param)) { 00703 $return .= (int)$param; 00704 } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially 00705 $return .= '0x'.$param['hex']; 00706 } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not* 00707 $return .= "N'{$param['numstr']}'"; // be converted back to number params, but bound as strings 00708 } else if (is_null($param)) { 00709 $return .= 'NULL'; 00710 00711 } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646 00712 $return .= "'$param'"; // this is a hack for MDL-23997, we intentionally use string because it is compatible with both nvarchar and int types 00713 } else if (is_float($param)) { 00714 $return .= $param; 00715 } else { 00716 $param = str_replace("'", "''", $param); 00717 $return .= "N'$param'"; 00718 } 00719 00720 $return .= array_shift($parts); 00721 } 00722 return $return; 00723 } 00724 00733 public function execute($sql, array $params = null) { 00734 if (strpos($sql, ';') !== false) { 00735 throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!'); 00736 } 00737 $this->do_query($sql, $params, SQL_QUERY_UPDATE); 00738 return true; 00739 } 00740 00757 public function get_recordset_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) { 00758 $limitfrom = (int)$limitfrom; 00759 $limitnum = (int)$limitnum; 00760 $limitfrom = max(0, $limitfrom); 00761 $limitnum = max(0, $limitnum); 00762 00763 if ($limitfrom or $limitnum) { 00764 if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later) 00765 $fetch = $limitfrom + $limitnum; 00766 if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow 00767 $fetch = PHP_INT_MAX; 00768 } 00769 $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i', 00770 "\\1SELECT\\2 TOP $fetch", $sql); 00771 } 00772 } 00773 $result = $this->do_query($sql, $params, SQL_QUERY_SELECT, false, (bool)$limitfrom); 00774 00775 if ($limitfrom) { // Skip $limitfrom records 00776 sqlsrv_fetch($result, SQLSRV_SCROLL_ABSOLUTE, $limitfrom - 1); 00777 } 00778 return $this->create_recordset($result); 00779 } 00780 00787 protected function create_recordset($result) { 00788 return new sqlsrv_native_moodle_recordset($result); 00789 } 00790 00805 public function get_records_sql($sql, array $params = null, $limitfrom = 0, $limitnum = 0) { 00806 00807 $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum); 00808 00809 $results = array(); 00810 00811 foreach ($rs as $row) { 00812 $id = reset($row); 00813 00814 if (isset($results[$id])) { 00815 $colname = key($row); 00816 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); 00817 } 00818 $results[$id] = (object)$row; 00819 } 00820 $rs->close(); 00821 00822 return $results; 00823 } 00824 00833 public function get_fieldset_sql($sql, array $params = null) { 00834 00835 $rs = $this->get_recordset_sql($sql, $params); 00836 00837 $results = array (); 00838 00839 foreach ($rs as $row) { 00840 $results[] = reset($row); 00841 } 00842 $rs->close(); 00843 00844 return $results; 00845 } 00846 00857 public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) { 00858 if (!is_array($params)) { 00859 $params = (array)$params; 00860 } 00861 if ($customsequence) { 00862 if (!isset($params['id'])) { 00863 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.'); 00864 } 00865 $returnid = false; 00866 // Disable IDENTITY column before inserting record with id 00867 $sql = 'SET IDENTITY_INSERT {'.$table.'} ON'; // Yes, it' ON!! 00868 $this->do_query($sql, null, SQL_QUERY_AUX); 00869 00870 } else { 00871 unset($params['id']); 00872 } 00873 00874 if (empty($params)) { 00875 throw new coding_exception('moodle_database::insert_record_raw() no fields found.'); 00876 } 00877 $fields = implode(',', array_keys($params)); 00878 $qms = array_fill(0, count($params), '?'); 00879 $qms = implode(',', $qms); 00880 $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms)"; 00881 $query_id = $this->do_query($sql, $params, SQL_QUERY_INSERT); 00882 00883 if ($customsequence) { 00884 // Enable IDENTITY column after inserting record with id 00885 $sql = 'SET IDENTITY_INSERT {'.$table.'} OFF'; // Yes, it' OFF!! 00886 $this->do_query($sql, null, SQL_QUERY_AUX); 00887 } 00888 00889 if ($returnid) { 00890 $id = $this->sqlsrv_fetch_id(); 00891 return $id; 00892 } else { 00893 return true; 00894 } 00895 } 00896 00902 private function sqlsrv_fetch_id() { 00903 $query_id = sqlsrv_query($this->sqlsrv, 'SELECT SCOPE_IDENTITY()'); 00904 if ($query_id === false) { 00905 $dberr = $this->get_last_error(); 00906 return false; 00907 } 00908 $row = $this->sqlsrv_fetchrow($query_id); 00909 return (int)$row[0]; 00910 } 00911 00917 private function sqlsrv_fetchrow($query_id) { 00918 $row = sqlsrv_fetch_array($query_id, SQLSRV_FETCH_NUMERIC); 00919 if ($row === false) { 00920 $dberr = $this->get_last_error(); 00921 return false; 00922 } 00923 00924 foreach ($row as $key => $value) { 00925 $row[$key] = ($value === ' ' || $value === NULL) ? '' : $value; 00926 } 00927 return $row; 00928 } 00929 00942 public function insert_record($table, $dataobject, $returnid = true, $bulk = false) { 00943 $dataobject = (array)$dataobject; 00944 00945 $columns = $this->get_columns($table); 00946 $cleaned = array (); 00947 00948 foreach ($dataobject as $field => $value) { 00949 if ($field === 'id') { 00950 continue; 00951 } 00952 if (!isset($columns[$field])) { 00953 continue; 00954 } 00955 $column = $columns[$field]; 00956 $cleaned[$field] = $this->normalise_value($column, $value); 00957 } 00958 00959 return $this->insert_record_raw($table, $cleaned, $returnid, $bulk); 00960 } 00961 00971 public function import_record($table, $dataobject) { 00972 if (!is_object($dataobject)) { 00973 $dataobject = (object)$dataobject; 00974 } 00975 00976 $columns = $this->get_columns($table); 00977 $cleaned = array (); 00978 00979 foreach ($dataobject as $field => $value) { 00980 if (!isset($columns[$field])) { 00981 continue; 00982 } 00983 $column = $columns[$field]; 00984 $cleaned[$field] = $this->normalise_value($column, $value); 00985 } 00986 00987 $this->insert_record_raw($table, $cleaned, false, false, true); 00988 00989 return true; 00990 } 00991 01000 public function update_record_raw($table, $params, $bulk = false) { 01001 $params = (array)$params; 01002 01003 if (!isset($params['id'])) { 01004 throw new coding_exception('moodle_database::update_record_raw() id field must be specified.'); 01005 } 01006 $id = $params['id']; 01007 unset($params['id']); 01008 01009 if (empty($params)) { 01010 throw new coding_exception('moodle_database::update_record_raw() no fields found.'); 01011 } 01012 01013 $sets = array (); 01014 01015 foreach ($params as $field => $value) { 01016 $sets[] = "$field = ?"; 01017 } 01018 01019 $params[] = $id; // last ? in WHERE condition 01020 01021 $sets = implode(',', $sets); 01022 $sql = "UPDATE {".$table."} SET $sets WHERE id = ?"; 01023 01024 $this->do_query($sql, $params, SQL_QUERY_UPDATE); 01025 01026 return true; 01027 } 01028 01042 public function update_record($table, $dataobject, $bulk = false) { 01043 $dataobject = (array)$dataobject; 01044 01045 $columns = $this->get_columns($table); 01046 $cleaned = array (); 01047 01048 foreach ($dataobject as $field => $value) { 01049 if (!isset($columns[$field])) { 01050 continue; 01051 } 01052 $column = $columns[$field]; 01053 $cleaned[$field] = $this->normalise_value($column, $value); 01054 } 01055 01056 return $this->update_record_raw($table, $cleaned, $bulk); 01057 } 01058 01070 public function set_field_select($table, $newfield, $newvalue, $select, array $params = null) { 01071 if ($select) { 01072 $select = "WHERE $select"; 01073 } 01074 01075 if (is_null($params)) { 01076 $params = array (); 01077 } 01078 01079 // convert params to ? types 01080 list($select, $params, $type) = $this->fix_sql_params($select, $params); 01081 01083 $columns = $this->get_columns($table); 01084 $column = $columns[$newfield]; 01085 01086 $newvalue = $this->normalise_value($column, $newvalue); 01087 01088 if (is_null($newvalue)) { 01089 $newfield = "$newfield = NULL"; 01090 } else { 01091 $newfield = "$newfield = ?"; 01092 array_unshift($params, $newvalue); 01093 } 01094 $sql = "UPDATE {".$table."} SET $newfield $select"; 01095 01096 $this->do_query($sql, $params, SQL_QUERY_UPDATE); 01097 01098 return true; 01099 } 01100 01110 public function delete_records_select($table, $select, array $params = null) { 01111 if ($select) { 01112 $select = "WHERE $select"; 01113 } 01114 01115 $sql = "DELETE FROM {".$table."} $select"; 01116 01117 // we use SQL_QUERY_UPDATE because we do not know what is in general SQL, delete constant would not be accurate 01118 $this->do_query($sql, $params, SQL_QUERY_UPDATE); 01119 01120 return true; 01121 } 01122 01123 01125 01126 public function sql_cast_char2int($fieldname, $text = false) { 01127 if (!$text) { 01128 return ' CAST(' . $fieldname . ' AS INT) '; 01129 } else { 01130 return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) '; 01131 } 01132 } 01133 01134 public function sql_cast_char2real($fieldname, $text=false) { 01135 if (!$text) { 01136 return ' CAST(' . $fieldname . ' AS REAL) '; 01137 } else { 01138 return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) '; 01139 } 01140 } 01141 01142 public function sql_ceil($fieldname) { 01143 return ' CEILING('.$fieldname.')'; 01144 } 01145 01146 protected function get_collation() { 01147 if (isset($this->collation)) { 01148 return $this->collation; 01149 } 01150 if (!empty($this->dboptions['dbcollation'])) { 01151 // perf speedup 01152 $this->collation = $this->dboptions['dbcollation']; 01153 return $this->collation; 01154 } 01155 01156 // make some default 01157 $this->collation = 'Latin1_General_CI_AI'; 01158 01159 $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation"; 01160 $this->query_start($sql, null, SQL_QUERY_AUX); 01161 $result = sqlsrv_query($this->sqlsrv, $sql); 01162 $this->query_end($result); 01163 01164 if ($result) { 01165 if ($rawcolumn = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) { 01166 $this->collation = reset($rawcolumn); 01167 } 01168 $this->free_result($result); 01169 } 01170 01171 return $this->collation; 01172 } 01173 01185 public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') { 01186 if (strpos($param, '%') !== false) { 01187 debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)'); 01188 } 01189 01190 $collation = $this->get_collation(); 01191 $LIKE = $notlike ? 'NOT LIKE' : 'LIKE'; 01192 01193 if ($casesensitive) { 01194 $collation = str_replace('_CI', '_CS', $collation); 01195 } else { 01196 $collation = str_replace('_CS', '_CI', $collation); 01197 } 01198 if ($accentsensitive) { 01199 $collation = str_replace('_AI', '_AS', $collation); 01200 } else { 01201 $collation = str_replace('_AS', '_AI', $collation); 01202 } 01203 01204 return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'"; 01205 } 01206 01207 public function sql_concat() { 01208 $arr = func_get_args(); 01209 01210 foreach ($arr as $key => $ele) { 01211 $arr[$key] = ' CAST('.$ele.' AS VARCHAR(255)) '; 01212 } 01213 $s = implode(' + ', $arr); 01214 01215 if ($s === '') { 01216 return " '' "; 01217 } 01218 return " $s "; 01219 } 01220 01221 public function sql_concat_join($separator = "' '", $elements = array ()) { 01222 for ($n = count($elements) - 1; $n > 0; $n--) { 01223 array_splice($elements, $n, 0, $separator); 01224 } 01225 $s = implode(' + ', $elements); 01226 01227 if ($s === '') { 01228 return " '' "; 01229 } 01230 return " $s "; 01231 } 01232 01233 public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) { 01234 if ($textfield) { 01235 return ' ('.$this->sql_compare_text($fieldname)." = '') "; 01236 } else { 01237 return " ($fieldname = '') "; 01238 } 01239 } 01240 01246 public function sql_length($fieldname) { 01247 return ' LEN('.$fieldname.')'; 01248 } 01249 01250 public function sql_order_by_text($fieldname, $numchars = 32) { 01251 return ' CONVERT(varchar, '.$fieldname.', '.$numchars.')'; 01252 } 01253 01257 public function sql_position($needle, $haystack) { 01258 return "CHARINDEX(($needle), ($haystack))"; 01259 } 01260 01270 public function sql_substr($expr, $start, $length = false) { 01271 if (count(func_get_args()) < 2) { 01272 throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 01273 'Originally this function was only returning name of SQL substring function, it now requires all parameters.'); 01274 } 01275 01276 if ($length === false) { 01277 return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))"; 01278 } else { 01279 return "SUBSTRING($expr, $start, $length)"; 01280 } 01281 } 01282 01284 01285 public function session_lock_supported() { 01286 return true; 01287 } 01288 01295 public function get_session_lock($rowid, $timeout) { 01296 if (!$this->session_lock_supported()) { 01297 return; 01298 } 01299 parent::get_session_lock($rowid, $timeout); 01300 01301 $timeoutmilli = $timeout * 1000; 01302 01303 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 01304 // While this may work using proper {call sp_...} calls + binding + 01305 // executing + consuming recordsets, the solution used for the mssql 01306 // driver is working perfectly, so 100% mimic-ing that code. 01307 // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', $timeoutmilli"; 01308 $sql = "BEGIN 01309 DECLARE @result INT 01310 EXECUTE @result = sp_getapplock @Resource='$fullname', 01311 @LockMode='Exclusive', 01312 @LockOwner='Session', 01313 @LockTimeout='$timeoutmilli' 01314 SELECT @result 01315 END"; 01316 $this->query_start($sql, null, SQL_QUERY_AUX); 01317 $result = sqlsrv_query($this->sqlsrv, $sql); 01318 $this->query_end($result); 01319 01320 if ($result) { 01321 $row = sqlsrv_fetch_array($result); 01322 if ($row[0] < 0) { 01323 throw new dml_sessionwait_exception(); 01324 } 01325 } 01326 01327 $this->free_result($result); 01328 } 01329 01330 public function release_session_lock($rowid) { 01331 if (!$this->session_lock_supported()) { 01332 return; 01333 } 01334 parent::release_session_lock($rowid); 01335 01336 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 01337 $sql = "sp_releaseapplock '$fullname', 'Session'"; 01338 $this->query_start($sql, null, SQL_QUERY_AUX); 01339 $result = sqlsrv_query($this->sqlsrv, $sql); 01340 $this->query_end($result); 01341 $this->free_result($result); 01342 } 01343 01344 01346 01347 // NOTE: 01348 // TODO -- should these be wrapped in query start/end? They arn't a query 01349 // but information and error capture is nice. msk 01350 01351 01357 protected function begin_transaction() { 01358 $this->query_start('native sqlsrv_begin_transaction', NULL, SQL_QUERY_AUX); 01359 $result = sqlsrv_begin_transaction($this->sqlsrv); 01360 $this->query_end($result); 01361 } 01362 01368 protected function commit_transaction() { 01369 $this->query_start('native sqlsrv_commit', NULL, SQL_QUERY_AUX); 01370 $result = sqlsrv_commit($this->sqlsrv); 01371 $this->query_end($result); 01372 } 01373 01379 protected function rollback_transaction() { 01380 $this->query_start('native sqlsrv_rollback', NULL, SQL_QUERY_AUX); 01381 $result = sqlsrv_rollback($this->sqlsrv); 01382 $this->query_end($result); 01383 } 01384 }