|
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 3 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 00018 00028 defined('MOODLE_INTERNAL') || die(); 00029 00030 require_once($CFG->libdir.'/dml/moodle_database.php'); 00031 require_once($CFG->libdir.'/dml/mssql_native_moodle_recordset.php'); 00032 require_once($CFG->libdir.'/dml/mssql_native_moodle_temptables.php'); 00033 00037 class mssql_native_moodle_database extends moodle_database { 00038 00039 protected $mssql = null; 00040 protected $last_error_reporting; // To handle mssql driver default verbosity 00041 protected $collation; // current DB collation cache 00042 00048 public function driver_installed() { 00049 if (!function_exists('mssql_connect')) { 00050 return get_string('mssqlextensionisnotpresentinphp', 'install'); 00051 } 00052 return true; 00053 } 00054 00060 public function get_dbfamily() { 00061 return 'mssql'; 00062 } 00063 00069 protected function get_dbtype() { 00070 return 'mssql'; 00071 } 00072 00078 protected function get_dblibrary() { 00079 return 'native'; 00080 } 00081 00087 public function get_name() { 00088 return get_string('nativemssql', 'install'); 00089 } 00090 00096 public function get_configuration_help() { 00097 return get_string('nativemssqlhelp', 'install'); 00098 } 00099 00105 public function get_configuration_hints() { 00106 $str = get_string('databasesettingssub_mssql', 'install'); 00107 $str .= "<p style='text-align:right'><a href=\"javascript:void(0)\" "; 00108 $str .= "onclick=\"return window.open('http://docs.moodle.org/en/Installing_MSSQL_for_PHP')\""; 00109 $str .= ">"; 00110 $str .= '<img src="pix/docs.gif' . '" alt="Docs" class="iconhelp" />'; 00111 $str .= get_string('moodledocslink', 'install') . '</a></p>'; 00112 return $str; 00113 } 00114 00127 public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) { 00128 if ($prefix == '' and !$this->external) { 00129 //Enforce prefixes for everybody but mysql 00130 throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily()); 00131 } 00132 00133 $driverstatus = $this->driver_installed(); 00134 00135 if ($driverstatus !== true) { 00136 throw new dml_exception('dbdriverproblem', $driverstatus); 00137 } 00138 00139 $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions); 00140 00141 $dbhost = $this->dbhost; 00142 if (isset($dboptions['dbport'])) { 00143 if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) { 00144 $dbhost .= ','.$dboptions['dbport']; 00145 } else { 00146 $dbhost .= ':'.$dboptions['dbport']; 00147 } 00148 } 00149 ob_start(); 00150 if (!empty($this->dboptions['dbpersist'])) { // persistent connection 00151 $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true); 00152 } else { 00153 $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true); 00154 } 00155 $dberr = ob_get_contents(); 00156 ob_end_clean(); 00157 00158 if ($this->mssql === false) { 00159 $this->mssql = null; 00160 throw new dml_connection_exception($dberr); 00161 } 00162 00163 // already connected, select database and set some env. variables 00164 $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX); 00165 $result = mssql_select_db($this->dbname, $this->mssql); 00166 $this->query_end($result); 00167 00168 // No need to set charset. It's UTF8, with transparent conversions 00169 // back and forth performed both by FreeTDS or ODBTP 00170 00171 // Allow quoted identifiers 00172 $sql = "SET QUOTED_IDENTIFIER ON"; 00173 $this->query_start($sql, null, SQL_QUERY_AUX); 00174 $result = mssql_query($sql, $this->mssql); 00175 $this->query_end($result); 00176 00177 $this->free_result($result); 00178 00179 // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL 00180 // instead of equal(=) and distinct(<>) symbols 00181 $sql = "SET ANSI_NULLS ON"; 00182 $this->query_start($sql, null, SQL_QUERY_AUX); 00183 $result = mssql_query($sql, $this->mssql); 00184 $this->query_end($result); 00185 00186 $this->free_result($result); 00187 00188 // Force ANSI warnings so arithmetic/string overflows will be 00189 // returning error instead of transparently truncating data 00190 $sql = "SET ANSI_WARNINGS ON"; 00191 $this->query_start($sql, null, SQL_QUERY_AUX); 00192 $result = mssql_query($sql, $this->mssql); 00193 $this->query_end($result); 00194 00195 // Concatenating null with anything MUST return NULL 00196 $sql = "SET CONCAT_NULL_YIELDS_NULL ON"; 00197 $this->query_start($sql, null, SQL_QUERY_AUX); 00198 $result = mssql_query($sql, $this->mssql); 00199 $this->query_end($result); 00200 00201 $this->free_result($result); 00202 00203 // Set transactions isolation level to READ_COMMITTED 00204 // prevents dirty reads when using transactions + 00205 // is the default isolation level of MSSQL 00206 // Requires database to run with READ_COMMITTED_SNAPSHOT ON 00207 $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED"; 00208 $this->query_start($sql, NULL, SQL_QUERY_AUX); 00209 $result = mssql_query($sql, $this->mssql); 00210 $this->query_end($result); 00211 00212 $this->free_result($result); 00213 00214 // Connection stabilised and configured, going to instantiate the temptables controller 00215 $this->temptables = new mssql_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 if ($this->mssql) { 00228 mssql_close($this->mssql); 00229 $this->mssql = null; 00230 } 00231 } 00232 00241 protected function query_start($sql, array $params=null, $type, $extrainfo=null) { 00242 parent::query_start($sql, $params, $type, $extrainfo); 00243 // mssql driver tends to send debug to output, we do not need that ;-) 00244 $this->last_error_reporting = error_reporting(0); 00245 } 00246 00252 protected function query_end($result) { 00253 // reset original debug level 00254 error_reporting($this->last_error_reporting); 00255 parent::query_end($result); 00256 } 00257 00262 public function get_server_info() { 00263 static $info; 00264 if (!$info) { 00265 $info = array(); 00266 $sql = 'sp_server_info 2'; 00267 $this->query_start($sql, null, SQL_QUERY_AUX); 00268 $result = mssql_query($sql, $this->mssql); 00269 $this->query_end($result); 00270 $row = mssql_fetch_row($result); 00271 $info['description'] = $row[2]; 00272 $this->free_result($result); 00273 00274 $sql = 'sp_server_info 500'; 00275 $this->query_start($sql, null, SQL_QUERY_AUX); 00276 $result = mssql_query($sql, $this->mssql); 00277 $this->query_end($result); 00278 $row = mssql_fetch_row($result); 00279 $info['version'] = $row[2]; 00280 $this->free_result($result); 00281 } 00282 return $info; 00283 } 00284 00285 protected function is_min_version($version) { 00286 $server = $this->get_server_info(); 00287 $server = $server['version']; 00288 return version_compare($server, $version, '>='); 00289 } 00290 00298 protected function fix_table_names($sql) { 00299 if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) { 00300 foreach($matches[0] as $key=>$match) { 00301 $name = $matches[1][$key]; 00302 if ($this->temptables->is_temptable($name)) { 00303 $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql); 00304 } else { 00305 $sql = str_replace($match, $this->prefix.$name, $sql); 00306 } 00307 } 00308 } 00309 return $sql; 00310 } 00311 00316 protected function allowed_param_types() { 00317 return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params() 00318 } 00319 00324 public function get_last_error() { 00325 return mssql_get_last_message(); 00326 } 00327 00332 public function get_tables($usecache=true) { 00333 if ($usecache and $this->tables !== null) { 00334 return $this->tables; 00335 } 00336 $this->tables = array(); 00337 $sql = "SELECT table_name 00338 FROM information_schema.tables 00339 WHERE table_name LIKE '$this->prefix%' 00340 AND table_type = 'BASE TABLE'"; 00341 $this->query_start($sql, null, SQL_QUERY_AUX); 00342 $result = mssql_query($sql, $this->mssql); 00343 $this->query_end($result); 00344 00345 if ($result) { 00346 while ($row = mssql_fetch_row($result)) { 00347 $tablename = reset($row); 00348 if (strpos($tablename, $this->prefix) !== 0) { 00349 continue; 00350 } 00351 $tablename = substr($tablename, strlen($this->prefix)); 00352 $this->tables[$tablename] = $tablename; 00353 } 00354 $this->free_result($result); 00355 } 00356 00357 // Add the currently available temptables 00358 $this->tables = array_merge($this->tables, $this->temptables->get_temptables()); 00359 return $this->tables; 00360 } 00361 00366 public function get_indexes($table) { 00367 $indexes = array(); 00368 $tablename = $this->prefix.$table; 00369 00370 // Indexes aren't covered by information_schema metatables, so we need to 00371 // go to sys ones. Skipping primary key indexes on purpose. 00372 $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name 00373 FROM sys.indexes i 00374 JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id 00375 JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id 00376 JOIN sys.tables t ON i.object_id = t.object_id 00377 WHERE t.name = '$tablename' 00378 AND i.is_primary_key = 0 00379 ORDER BY i.name, i.index_id, ic.index_column_id"; 00380 00381 $this->query_start($sql, null, SQL_QUERY_AUX); 00382 $result = mssql_query($sql, $this->mssql); 00383 $this->query_end($result); 00384 00385 if ($result) { 00386 $lastindex = ''; 00387 $unique = false; 00388 $columns = array(); 00389 while ($row = mssql_fetch_assoc($result)) { 00390 if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info 00391 $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns); 00392 $unique = false; 00393 $columns = array(); 00394 } 00395 $lastindex = $row['index_name']; 00396 $unique = empty($row['is_unique']) ? false : true; 00397 $columns[] = $row['column_name']; 00398 } 00399 if ($lastindex ) { // Add the last one if exists 00400 $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns); 00401 } 00402 $this->free_result($result); 00403 } 00404 return $indexes; 00405 } 00406 00413 public function get_columns($table, $usecache=true) { 00414 if ($usecache and isset($this->columns[$table])) { 00415 return $this->columns[$table]; 00416 } 00417 00418 $this->columns[$table] = array(); 00419 00420 if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema 00421 $sql = "SELECT column_name AS name, 00422 data_type AS type, 00423 numeric_precision AS max_length, 00424 character_maximum_length AS char_max_length, 00425 numeric_scale AS scale, 00426 is_nullable AS is_nullable, 00427 columnproperty(object_id(quotename(table_schema) + '.' + 00428 quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, 00429 column_default AS default_value 00430 FROM information_schema.columns 00431 WHERE table_name = '{" . $table . "}' 00432 ORDER BY ordinal_position"; 00433 } else { // temp table, get metadata from tempdb schema 00434 $sql = "SELECT column_name AS name, 00435 data_type AS type, 00436 numeric_precision AS max_length, 00437 character_maximum_length AS char_max_length, 00438 numeric_scale AS scale, 00439 is_nullable AS is_nullable, 00440 columnproperty(object_id(quotename(table_schema) + '.' + 00441 quotename(table_name)), column_name, 'IsIdentity') AS auto_increment, 00442 column_default AS default_value 00443 FROM tempdb.information_schema.columns 00444 JOIN tempdb..sysobjects ON name = table_name 00445 WHERE id = object_id('tempdb..{" . $table . "}') 00446 ORDER BY ordinal_position"; 00447 } 00448 00449 list($sql, $params, $type) = $this->fix_sql_params($sql, null); 00450 00451 $this->query_start($sql, null, SQL_QUERY_AUX); 00452 $result = mssql_query($sql, $this->mssql); 00453 $this->query_end($result); 00454 00455 if (!$result) { 00456 return array(); 00457 } 00458 00459 while ($rawcolumn = mssql_fetch_assoc($result)) { 00460 00461 $rawcolumn = (object)$rawcolumn; 00462 00463 $info = new stdClass(); 00464 $info->name = $rawcolumn->name; 00465 $info->type = $rawcolumn->type; 00466 $info->meta_type = $this->mssqltype2moodletype($info->type); 00467 00468 // Prepare auto_increment info 00469 $info->auto_increment = $rawcolumn->auto_increment ? true : false; 00470 00471 // Define type for auto_increment columns 00472 $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type; 00473 00474 // id columns being auto_incremnt are PK by definition 00475 $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment); 00476 00477 // Put correct length for character and LOB types 00478 $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length; 00479 $info->max_length = ($info->meta_type == 'X' || $info->meta_type == 'B') ? -1 : $info->max_length; 00480 00481 // Scale 00482 $info->scale = $rawcolumn->scale ? $rawcolumn->scale : false; 00483 00484 // Prepare not_null info 00485 $info->not_null = $rawcolumn->is_nullable == 'NO' ? true : false; 00486 00487 // Process defaults 00488 $info->has_default = !empty($rawcolumn->default_value); 00489 if ($rawcolumn->default_value === NULL) { 00490 $info->default_value = NULL; 00491 } else { 00492 $info->default_value = preg_replace("/^[\(N]+[']?(.*?)[']?[\)]+$/", '\\1', $rawcolumn->default_value); 00493 } 00494 00495 // Process binary 00496 $info->binary = $info->meta_type == 'B' ? true : false; 00497 00498 $this->columns[$table][$info->name] = new database_column_info($info); 00499 } 00500 $this->free_result($result); 00501 00502 return $this->columns[$table]; 00503 } 00504 00512 protected function normalise_value($column, $value) { 00513 if (is_bool($value)) { 00514 $value = (int)$value; 00515 } // And continue processing because text columns with numeric info need special handling below 00516 00517 if ($column->meta_type == 'B') { // BLOBs need to be properly "packed", but can be inserted directly if so. 00518 if (!is_null($value)) { // If value not null, unpack it to unquoted hexadecimal byte-string format 00519 $value = unpack('H*hex', $value); // we leave it as array, so emulate_bound_params() can detect it 00520 } // easily and "bind" the param ok. 00521 00522 } else if ($column->meta_type == 'X') { // MSSQL doesn't cast from int to text, so if text column 00523 if (is_numeric($value)) { // and is numeric value then cast to string 00524 $value = array('numstr' => (string)$value); // and put into array, so emulate_bound_params() will know how 00525 } // to "bind" the param ok, avoiding reverse conversion to number 00526 00527 } else if ($value === '') { 00528 if ($column->meta_type == 'I' or $column->meta_type == 'F' or $column->meta_type == 'N') { 00529 $value = 0; // prevent '' problems in numeric fields 00530 } 00531 } 00532 return $value; 00533 } 00534 00540 private function free_result($resource) { 00541 if (!is_bool($resource)) { // true/false resources cannot be freed 00542 mssql_free_result($resource); 00543 } 00544 } 00545 00552 private function mssqltype2moodletype($mssql_type) { 00553 $type = null; 00554 switch (strtoupper($mssql_type)) { 00555 case 'BIT': 00556 $type = 'L'; 00557 break; 00558 case 'INT': 00559 case 'SMALLINT': 00560 case 'INTEGER': 00561 case 'BIGINT': 00562 $type = 'I'; 00563 break; 00564 case 'DECIMAL': 00565 case 'REAL': 00566 case 'FLOAT': 00567 $type = 'N'; 00568 break; 00569 case 'VARCHAR': 00570 case 'NVARCHAR': 00571 $type = 'C'; 00572 break; 00573 case 'TEXT': 00574 case 'NTEXT': 00575 case 'VARCHAR(MAX)': 00576 case 'NVARCHAR(MAX)': 00577 $type = 'X'; 00578 break; 00579 case 'IMAGE': 00580 case 'VARBINARY(MAX)': 00581 $type = 'B'; 00582 break; 00583 case 'DATETIME': 00584 $type = 'D'; 00585 break; 00586 } 00587 if (!$type) { 00588 throw new dml_exception('invalidmssqlnativetype', $mssql_type); 00589 } 00590 return $type; 00591 } 00592 00599 public function change_database_structure($sql) { 00600 $this->reset_caches(); 00601 00602 $this->query_start($sql, null, SQL_QUERY_STRUCTURE); 00603 $result = mssql_query($sql, $this->mssql); 00604 $this->query_end($result); 00605 00606 return true; 00607 } 00608 00613 protected function emulate_bound_params($sql, array $params=null) { 00614 if (empty($params)) { 00615 return $sql; 00616 } 00618 $parts = explode('?', $sql); 00619 $return = array_shift($parts); 00620 foreach ($params as $param) { 00621 if (is_bool($param)) { 00622 $return .= (int)$param; 00623 00624 } else if (is_array($param) && isset($param['hex'])) { // detect hex binary, bind it specially 00625 $return .= '0x' . $param['hex']; 00626 00627 } else if (is_array($param) && isset($param['numstr'])) { // detect numerical strings that *must not* 00628 $return .= "N'{$param['numstr']}'"; // be converted back to number params, but bound as strings 00629 00630 } else if (is_null($param)) { 00631 $return .= 'NULL'; 00632 00633 } else if (is_number($param)) { // we can not use is_numeric() because it eats leading zeros from strings like 0045646 00634 $return .= "'".$param."'"; //fix for MDL-24863 to prevent auto-cast to int. 00635 00636 } else if (is_float($param)) { 00637 $return .= $param; 00638 00639 } else { 00640 $param = str_replace("'", "''", $param); 00641 $return .= "N'$param'"; 00642 } 00643 00644 $return .= array_shift($parts); 00645 } 00646 return $return; 00647 } 00648 00657 public function execute($sql, array $params=null) { 00658 00659 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 00660 $rawsql = $this->emulate_bound_params($sql, $params); 00661 00662 if (strpos($sql, ';') !== false) { 00663 throw new coding_exception('moodle_database::execute() Multiple sql statements found or bound parameters not used properly in query!'); 00664 } 00665 00666 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 00667 $result = mssql_query($rawsql, $this->mssql); 00668 $this->query_end($result); 00669 $this->free_result($result); 00670 00671 return true; 00672 } 00673 00690 public function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { 00691 $limitfrom = (int)$limitfrom; 00692 $limitnum = (int)$limitnum; 00693 $limitfrom = ($limitfrom < 0) ? 0 : $limitfrom; 00694 $limitnum = ($limitnum < 0) ? 0 : $limitnum; 00695 if ($limitfrom or $limitnum) { 00696 if ($limitnum >= 1) { // Only apply TOP clause if we have any limitnum (limitfrom offset is handled later) 00697 $fetch = $limitfrom + $limitnum; 00698 if (PHP_INT_MAX - $limitnum < $limitfrom) { // Check PHP_INT_MAX overflow 00699 $fetch = PHP_INT_MAX; 00700 } 00701 $sql = preg_replace('/^([\s(])*SELECT([\s]+(DISTINCT|ALL))?(?!\s*TOP\s*\()/i', 00702 "\\1SELECT\\2 TOP $fetch", $sql); 00703 } 00704 } 00705 00706 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 00707 $rawsql = $this->emulate_bound_params($sql, $params); 00708 00709 $this->query_start($sql, $params, SQL_QUERY_SELECT); 00710 $result = mssql_query($rawsql, $this->mssql); 00711 $this->query_end($result); 00712 00713 if ($limitfrom) { // Skip $limitfrom records 00714 mssql_data_seek($result, $limitfrom); 00715 } 00716 00717 return $this->create_recordset($result); 00718 } 00719 00720 protected function create_recordset($result) { 00721 return new mssql_native_moodle_recordset($result); 00722 } 00723 00738 public function get_records_sql($sql, array $params=null, $limitfrom=0, $limitnum=0) { 00739 00740 $rs = $this->get_recordset_sql($sql, $params, $limitfrom, $limitnum); 00741 00742 $results = array(); 00743 00744 foreach ($rs as $row) { 00745 $id = reset($row); 00746 if (isset($results[$id])) { 00747 $colname = key($row); 00748 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); 00749 } 00750 $results[$id] = $row; 00751 } 00752 $rs->close(); 00753 00754 return $results; 00755 } 00756 00765 public function get_fieldset_sql($sql, array $params=null) { 00766 00767 $rs = $this->get_recordset_sql($sql, $params); 00768 00769 $results = array(); 00770 00771 foreach ($rs as $row) { 00772 $results[] = reset($row); 00773 } 00774 $rs->close(); 00775 00776 return $results; 00777 } 00778 00789 public function insert_record_raw($table, $params, $returnid=true, $bulk=false, $customsequence=false) { 00790 if (!is_array($params)) { 00791 $params = (array)$params; 00792 } 00793 00794 $returning = ""; 00795 00796 if ($customsequence) { 00797 if (!isset($params['id'])) { 00798 throw new coding_exception('moodle_database::insert_record_raw() id field must be specified if custom sequences used.'); 00799 } 00800 $returnid = false; 00801 00802 // Disable IDENTITY column before inserting record with id 00803 $sql = 'SET IDENTITY_INSERT {' . $table . '} ON'; // Yes, it' ON!! 00804 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null); 00805 $this->query_start($sql, null, SQL_QUERY_AUX); 00806 $result = mssql_query($sql, $this->mssql); 00807 $this->query_end($result); 00808 $this->free_result($result); 00809 00810 } else { 00811 unset($params['id']); 00812 if ($returnid) { 00813 $returning = "; SELECT SCOPE_IDENTITY()"; 00814 } 00815 } 00816 00817 if (empty($params)) { 00818 throw new coding_exception('moodle_database::insert_record_raw() no fields found.'); 00819 } 00820 00821 $fields = implode(',', array_keys($params)); 00822 $qms = array_fill(0, count($params), '?'); 00823 $qms = implode(',', $qms); 00824 00825 $sql = "INSERT INTO {" . $table . "} ($fields) VALUES($qms) $returning"; 00826 00827 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 00828 $rawsql = $this->emulate_bound_params($sql, $params); 00829 00830 $this->query_start($sql, $params, SQL_QUERY_INSERT); 00831 $result = mssql_query($rawsql, $this->mssql); 00832 $this->query_end($result); 00833 00834 if ($returning !== "") { 00835 $row = mssql_fetch_assoc($result); 00836 $params['id'] = reset($row); 00837 } 00838 $this->free_result($result); 00839 00840 if ($customsequence) { 00841 // Enable IDENTITY column after inserting record with id 00842 $sql = 'SET IDENTITY_INSERT {' . $table . '} OFF'; // Yes, it' OFF!! 00843 list($sql, $xparams, $xtype) = $this->fix_sql_params($sql, null); 00844 $this->query_start($sql, null, SQL_QUERY_AUX); 00845 $result = mssql_query($sql, $this->mssql); 00846 $this->query_end($result); 00847 $this->free_result($result); 00848 } 00849 00850 if (!$returnid) { 00851 return true; 00852 } 00853 00854 return (int)$params['id']; 00855 } 00856 00869 public function insert_record($table, $dataobject, $returnid=true, $bulk=false) { 00870 $dataobject = (array)$dataobject; 00871 00872 $columns = $this->get_columns($table); 00873 $cleaned = array(); 00874 00875 foreach ($dataobject as $field => $value) { 00876 if ($field === 'id') { 00877 continue; 00878 } 00879 if (!isset($columns[$field])) { 00880 continue; 00881 } 00882 $column = $columns[$field]; 00883 $cleaned[$field] = $this->normalise_value($column, $value); 00884 } 00885 00886 return $this->insert_record_raw($table, $cleaned, $returnid, $bulk); 00887 } 00888 00898 public function import_record($table, $dataobject) { 00899 $dataobject = (array)$dataobject; 00900 00901 $columns = $this->get_columns($table); 00902 $cleaned = array(); 00903 00904 foreach ($dataobject as $field => $value) { 00905 if (!isset($columns[$field])) { 00906 continue; 00907 } 00908 $column = $columns[$field]; 00909 $cleaned[$field] = $this->normalise_value($column, $value); 00910 } 00911 00912 $this->insert_record_raw($table, $cleaned, false, false, true); 00913 00914 return true; 00915 } 00916 00925 public function update_record_raw($table, $params, $bulk=false) { 00926 $params = (array)$params; 00927 00928 if (!isset($params['id'])) { 00929 throw new coding_exception('moodle_database::update_record_raw() id field must be specified.'); 00930 } 00931 $id = $params['id']; 00932 unset($params['id']); 00933 00934 if (empty($params)) { 00935 throw new coding_exception('moodle_database::update_record_raw() no fields found.'); 00936 } 00937 00938 $sets = array(); 00939 foreach ($params as $field=>$value) { 00940 $sets[] = "$field = ?"; 00941 } 00942 00943 $params[] = $id; // last ? in WHERE condition 00944 00945 $sets = implode(',', $sets); 00946 $sql = "UPDATE {" . $table . "} SET $sets WHERE id = ?"; 00947 00948 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 00949 $rawsql = $this->emulate_bound_params($sql, $params); 00950 00951 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 00952 $result = mssql_query($rawsql, $this->mssql); 00953 $this->query_end($result); 00954 00955 $this->free_result($result); 00956 return true; 00957 } 00958 00972 public function update_record($table, $dataobject, $bulk=false) { 00973 $dataobject = (array)$dataobject; 00974 00975 $columns = $this->get_columns($table); 00976 $cleaned = array(); 00977 00978 foreach ($dataobject as $field => $value) { 00979 if (!isset($columns[$field])) { 00980 continue; 00981 } 00982 $column = $columns[$field]; 00983 $cleaned[$field] = $this->normalise_value($column, $value); 00984 } 00985 00986 return $this->update_record_raw($table, $cleaned, $bulk); 00987 } 00988 01000 public function set_field_select($table, $newfield, $newvalue, $select, array $params=null) { 01001 01002 if ($select) { 01003 $select = "WHERE $select"; 01004 } 01005 if (is_null($params)) { 01006 $params = array(); 01007 } 01008 01009 // convert params to ? types 01010 list($select, $params, $type) = $this->fix_sql_params($select, $params); 01011 01013 $columns = $this->get_columns($table); 01014 $column = $columns[$newfield]; 01015 01016 $newvalue = $this->normalise_value($column, $newvalue); 01017 01018 if (is_null($newvalue)) { 01019 $newfield = "$newfield = NULL"; 01020 } else { 01021 $newfield = "$newfield = ?"; 01022 array_unshift($params, $newvalue); 01023 } 01024 $sql = "UPDATE {" . $table . "} SET $newfield $select"; 01025 01026 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 01027 $rawsql = $this->emulate_bound_params($sql, $params); 01028 01029 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 01030 $result = mssql_query($rawsql, $this->mssql); 01031 $this->query_end($result); 01032 01033 $this->free_result($result); 01034 01035 return true; 01036 } 01037 01047 public function delete_records_select($table, $select, array $params=null) { 01048 01049 if ($select) { 01050 $select = "WHERE $select"; 01051 } 01052 01053 $sql = "DELETE FROM {" . $table . "} $select"; 01054 01055 list($sql, $params, $type) = $this->fix_sql_params($sql, $params); 01056 $rawsql = $this->emulate_bound_params($sql, $params); 01057 01058 $this->query_start($sql, $params, SQL_QUERY_UPDATE); 01059 $result = mssql_query($rawsql, $this->mssql); 01060 $this->query_end($result); 01061 01062 $this->free_result($result); 01063 01064 return true; 01065 } 01066 01068 01069 public function sql_cast_char2int($fieldname, $text=false) { 01070 if (!$text) { 01071 return ' CAST(' . $fieldname . ' AS INT) '; 01072 } else { 01073 return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS INT) '; 01074 } 01075 } 01076 01077 public function sql_cast_char2real($fieldname, $text=false) { 01078 if (!$text) { 01079 return ' CAST(' . $fieldname . ' AS REAL) '; 01080 } else { 01081 return ' CAST(' . $this->sql_compare_text($fieldname) . ' AS REAL) '; 01082 } 01083 } 01084 01085 public function sql_ceil($fieldname) { 01086 return ' CEILING(' . $fieldname . ')'; 01087 } 01088 01089 01090 protected function get_collation() { 01091 if (isset($this->collation)) { 01092 return $this->collation; 01093 } 01094 if (!empty($this->dboptions['dbcollation'])) { 01095 // perf speedup 01096 $this->collation = $this->dboptions['dbcollation']; 01097 return $this->collation; 01098 } 01099 01100 // make some default 01101 $this->collation = 'Latin1_General_CI_AI'; 01102 01103 $sql = "SELECT CAST(DATABASEPROPERTYEX('$this->dbname', 'Collation') AS varchar(255)) AS SQLCollation"; 01104 $this->query_start($sql, null, SQL_QUERY_AUX); 01105 $result = mssql_query($sql, $this->mssql); 01106 $this->query_end($result); 01107 01108 if ($result) { 01109 if ($rawcolumn = mssql_fetch_assoc($result)) { 01110 $this->collation = reset($rawcolumn); 01111 } 01112 $this->free_result($result); 01113 } 01114 01115 return $this->collation; 01116 } 01117 01129 public function sql_like($fieldname, $param, $casesensitive = true, $accentsensitive = true, $notlike = false, $escapechar = '\\') { 01130 if (strpos($param, '%') !== false) { 01131 debugging('Potential SQL injection detected, sql_ilike() expects bound parameters (? or :named)'); 01132 } 01133 01134 $collation = $this->get_collation(); 01135 01136 if ($casesensitive) { 01137 $collation = str_replace('_CI', '_CS', $collation); 01138 } else { 01139 $collation = str_replace('_CS', '_CI', $collation); 01140 } 01141 if ($accentsensitive) { 01142 $collation = str_replace('_AI', '_AS', $collation); 01143 } else { 01144 $collation = str_replace('_AS', '_AI', $collation); 01145 } 01146 01147 $LIKE = $notlike ? 'NOT LIKE' : 'LIKE'; 01148 01149 return "$fieldname COLLATE $collation $LIKE $param ESCAPE '$escapechar'"; 01150 } 01151 01152 public function sql_concat() { 01153 $arr = func_get_args(); 01154 foreach ($arr as $key => $ele) { 01155 $arr[$key] = ' CAST(' . $ele . ' AS VARCHAR(255)) '; 01156 } 01157 $s = implode(' + ', $arr); 01158 if ($s === '') { 01159 return " '' "; 01160 } 01161 return " $s "; 01162 } 01163 01164 public function sql_concat_join($separator="' '", $elements=array()) { 01165 for ($n=count($elements)-1; $n > 0 ; $n--) { 01166 array_splice($elements, $n, 0, $separator); 01167 } 01168 $s = implode(' + ', $elements); 01169 if ($s === '') { 01170 return " '' "; 01171 } 01172 return " $s "; 01173 } 01174 01175 public function sql_isempty($tablename, $fieldname, $nullablefield, $textfield) { 01176 if ($textfield) { 01177 return ' (' . $this->sql_compare_text($fieldname) . " = '') "; 01178 } else { 01179 return " ($fieldname = '') "; 01180 } 01181 } 01182 01188 public function sql_length($fieldname) { 01189 return ' LEN(' . $fieldname . ')'; 01190 } 01191 01192 public function sql_order_by_text($fieldname, $numchars=32) { 01193 return ' CONVERT(varchar, ' . $fieldname . ', ' . $numchars . ')'; 01194 } 01195 01199 public function sql_position($needle, $haystack) { 01200 return "CHARINDEX(($needle), ($haystack))"; 01201 } 01202 01212 public function sql_substr($expr, $start, $length=false) { 01213 if (count(func_get_args()) < 2) { 01214 throw new coding_exception('moodle_database::sql_substr() requires at least two parameters', 'Originaly this function wa 01215 s only returning name of SQL substring function, it now requires all parameters.'); 01216 } 01217 if ($length === false) { 01218 return "SUBSTRING($expr, $start, (LEN($expr) - $start + 1))"; 01219 } else { 01220 return "SUBSTRING($expr, $start, $length)"; 01221 } 01222 } 01223 01225 01226 public function session_lock_supported() { 01227 return true; 01228 } 01229 01236 public function get_session_lock($rowid, $timeout) { 01237 if (!$this->session_lock_supported()) { 01238 return; 01239 } 01240 parent::get_session_lock($rowid, $timeout); 01241 01242 $timeoutmilli = $timeout * 1000; 01243 01244 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 01245 // There is one bug in PHP/freetds (both reproducible with mssql_query() 01246 // and its mssql_init()/mssql_bind()/mssql_execute() alternative) for 01247 // stored procedures, causing scalar results of the execution 01248 // to be cast to boolean (true/fals). Here there is one 01249 // workaround that forces the return of one recordset resource. 01250 // $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', $timeoutmilli"; 01251 $sql = "BEGIN 01252 DECLARE @result INT 01253 EXECUTE @result = sp_getapplock @Resource='$fullname', 01254 @LockMode='Exclusive', 01255 @LockOwner='Session', 01256 @LockTimeout='$timeoutmilli' 01257 SELECT @result 01258 END"; 01259 $this->query_start($sql, null, SQL_QUERY_AUX); 01260 $result = mssql_query($sql, $this->mssql); 01261 $this->query_end($result); 01262 01263 if ($result) { 01264 $row = mssql_fetch_row($result); 01265 if ($row[0] < 0) { 01266 throw new dml_sessionwait_exception(); 01267 } 01268 } 01269 01270 $this->free_result($result); 01271 } 01272 01273 public function release_session_lock($rowid) { 01274 if (!$this->session_lock_supported()) { 01275 return; 01276 } 01277 parent::release_session_lock($rowid); 01278 01279 $fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid; 01280 $sql = "sp_releaseapplock '$fullname', 'Session'"; 01281 $this->query_start($sql, null, SQL_QUERY_AUX); 01282 $result = mssql_query($sql, $this->mssql); 01283 $this->query_end($result); 01284 01285 $this->free_result($result); 01286 } 01287 01289 01295 protected function begin_transaction() { 01296 // requires database to run with READ_COMMITTED_SNAPSHOT ON 01297 $sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation 01298 $this->query_start($sql, NULL, SQL_QUERY_AUX); 01299 $result = mssql_query($sql, $this->mssql); 01300 $this->query_end($result); 01301 01302 $this->free_result($result); 01303 } 01304 01310 protected function commit_transaction() { 01311 $sql = "COMMIT TRANSACTION"; 01312 $this->query_start($sql, NULL, SQL_QUERY_AUX); 01313 $result = mssql_query($sql, $this->mssql); 01314 $this->query_end($result); 01315 01316 $this->free_result($result); 01317 } 01318 01324 protected function rollback_transaction() { 01325 $sql = "ROLLBACK TRANSACTION"; 01326 $this->query_start($sql, NULL, SQL_QUERY_AUX); 01327 $result = mssql_query($sql, $this->mssql); 01328 $this->query_end($result); 01329 01330 $this->free_result($result); 01331 } 01332 }