on-demand release 4.5dev+
[moodle.git] / lib / adodb / drivers / adodb-mssqlnative.inc.php
blobf7e1bcc1672b22377fecca936b80022b9cfd42d9
1 <?php
2 /**
3 * Native MSSQL driver.
5 * Requires mssql client. Works on Windows.
6 * https://docs.microsoft.com/sql/connect/php
8 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
10 * @package ADOdb
11 * @link https://adodb.org Project's web site and documentation
12 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker
14 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause
15 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option,
16 * any later version. This means you can use it in proprietary products.
17 * See the LICENSE.md file distributed with this source code for details.
18 * @license BSD-3-Clause
19 * @license LGPL-2.1-or-later
21 * @copyright 2000-2013 John Lim
22 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
25 // security - hide paths
26 if (!defined('ADODB_DIR')) die();
28 if (!function_exists('sqlsrv_configure')) {
29 die("mssqlnative extension not installed");
32 if (!function_exists('sqlsrv_set_error_handling')) {
33 function sqlsrv_set_error_handling($constant) {
34 sqlsrv_configure("WarningsReturnAsErrors", $constant);
37 if (!function_exists('sqlsrv_log_set_severity')) {
38 function sqlsrv_log_set_severity($constant) {
39 sqlsrv_configure("LogSeverity", $constant);
42 if (!function_exists('sqlsrv_log_set_subsystems')) {
43 function sqlsrv_log_set_subsystems($constant) {
44 sqlsrv_configure("LogSubsystems", $constant);
48 class ADODB_mssqlnative extends ADOConnection {
49 var $databaseType = "mssqlnative";
50 var $dataProvider = "mssqlnative";
51 var $replaceQuote = "''"; // string to use to replace quotes
52 var $fmtDate = "'Y-m-d'";
53 var $fmtTimeStamp = "'Y-m-d\TH:i:s'";
54 /**
55 * Enabling InsertID capability will cause execution of an extra query
56 * {@see $identitySQL} after each INSERT statement. To improve performance
57 * when inserting a large number of records, you should switch this off by
58 * calling {@see enableLastInsertID enableLastInsertID(false)}.
59 * @var bool $hasInsertID
61 var $hasInsertID = true;
62 var $substr = "substring";
63 var $length = 'len';
64 var $hasAffectedRows = true;
65 var $poorAffectedRows = false;
66 var $metaDatabasesSQL = "select name from sys.sysdatabases where name <> 'master'";
67 var $metaTablesSQL="select name,case when type='U' then 'T' else 'V' end from sysobjects where (type='U' or type='V') and (name not in ('sysallocations','syscolumns','syscomments','sysdepends','sysfilegroups','sysfiles','sysfiles1','sysforeignkeys','sysfulltextcatalogs','sysindexes','sysindexkeys','sysmembers','sysobjects','syspermissions','sysprotects','sysreferences','systypes','sysusers','sysalternates','sysconstraints','syssegments','REFERENTIAL_CONSTRAINTS','CHECK_CONSTRAINTS','CONSTRAINT_TABLE_USAGE','CONSTRAINT_COLUMN_USAGE','VIEWS','VIEW_TABLE_USAGE','VIEW_COLUMN_USAGE','SCHEMATA','TABLES','TABLE_CONSTRAINTS','TABLE_PRIVILEGES','COLUMNS','COLUMN_DOMAIN_USAGE','COLUMN_PRIVILEGES','DOMAINS','DOMAIN_CONSTRAINTS','KEY_COLUMN_USAGE','dtproperties'))";
68 var $metaColumnsSQL =
69 "select c.name,
70 t.name as type,
71 c.length,
72 c.xprec as precision,
73 c.xscale as scale,
74 c.isnullable as nullable,
75 c.cdefault as default_value,
76 c.xtype,
77 t.length as type_length,
78 sc.is_identity
79 from syscolumns c
80 join systypes t on t.xusertype=c.xusertype
81 join sysobjects o on o.id=c.id
82 join sys.tables st on st.name=o.name
83 join sys.columns sc on sc.object_id = st.object_id and sc.name=c.name
84 where o.name='%s'";
85 var $hasTop = 'top'; // support mssql SELECT TOP 10 * FROM TABLE
86 var $hasGenID = true;
87 var $sysDate = 'convert(datetime,convert(char,GetDate(),102),102)';
88 var $sysTimeStamp = 'GetDate()';
89 var $maxParameterLen = 4000;
90 var $arrayClass = 'ADORecordSet_array_mssqlnative';
91 var $uniqueSort = true;
92 var $leftOuter = '*=';
93 var $rightOuter = '=*';
94 var $ansiOuter = true; // for mssql7 or later
95 var $identitySQL = 'select SCOPE_IDENTITY()'; // 'select SCOPE_IDENTITY'; # for mssql 2000
96 var $uniqueOrderBy = true;
97 var $_bindInputArray = true;
98 var $_dropSeqSQL = "drop table %s";
100 var $connectionInfo = array('ReturnDatesAsStrings'=>true);
101 var $cachedSchemaFlush = false;
103 var $sequences = false;
104 var $mssql_version = '';
106 function __construct()
108 if ($this->debug) {
109 ADOConnection::outp("<pre>");
110 sqlsrv_set_error_handling( SQLSRV_ERRORS_LOG_ALL );
111 sqlsrv_log_set_severity( SQLSRV_LOG_SEVERITY_ALL );
112 sqlsrv_log_set_subsystems(SQLSRV_LOG_SYSTEM_ALL);
113 sqlsrv_configure('WarningsReturnAsErrors', 0);
114 } else {
115 sqlsrv_set_error_handling(0);
116 sqlsrv_log_set_severity(0);
117 sqlsrv_log_set_subsystems(SQLSRV_LOG_SYSTEM_ALL);
118 sqlsrv_configure('WarningsReturnAsErrors', 0);
123 * Initializes the SQL Server version.
124 * Dies if connected to a non-supported version (2000 and older)
126 function ServerVersion() {
127 $data = $this->ServerInfo();
128 preg_match('/^\d{2}/', $data['version'], $matches);
129 $version = (int)reset($matches);
131 // We only support SQL Server 2005 and up
132 if($version < 9) {
133 die("SQL SERVER VERSION {$data['version']} NOT SUPPORTED IN mssqlnative DRIVER");
136 $this->mssql_version = $version;
139 function ServerInfo() {
140 global $ADODB_FETCH_MODE;
141 static $arr = false;
142 if (is_array($arr))
143 return $arr;
144 if ($this->fetchMode === false) {
145 $savem = $ADODB_FETCH_MODE;
146 $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
147 } elseif ($this->fetchMode >=0 && $this->fetchMode <=2) {
148 $savem = $this->fetchMode;
149 } else
150 $savem = $this->SetFetchMode(ADODB_FETCH_NUM);
152 $arrServerInfo = sqlsrv_server_info($this->_connectionID);
153 $ADODB_FETCH_MODE = $savem;
155 $arr = array();
156 $arr['description'] = $arrServerInfo['SQLServerName'].' connected to '.$arrServerInfo['CurrentDatabase'];
157 $arr['version'] = $arrServerInfo['SQLServerVersion'];//ADOConnection::_findvers($arr['description']);
158 return $arr;
161 function IfNull( $field, $ifNull )
163 return " ISNULL($field, $ifNull) "; // if MS SQL Server
166 public function enableLastInsertID($enable = true) {
167 $this->hasInsertID = $enable;
168 $this->lastInsID = false;
172 * Get the last value inserted into an IDENTITY column.
174 * The value will actually be set in {@see _query()} when executing an
175 * INSERT statement, but only if the connection's $hasInsertId property
176 * is true; this can be set with {@see enableLastInsertId()}.
178 * @inheritDoc
180 protected function _insertID($table = '', $column = '')
182 return $this->lastInsID;
185 function _affectedrows()
187 if ($this->_queryID && is_resource($this->_queryID)) {
188 return sqlsrv_rows_affected($this->_queryID);
190 return false;
193 function GenID($seq='adodbseq',$start=1) {
194 switch($this->mssql_version){
195 case 9:
196 case 10:
197 return $this->GenID2008($seq, $start);
198 break;
199 default:
200 return $this->GenID2012($seq, $start);
201 break;
205 function CreateSequence($seq='adodbseq',$start=1)
207 switch($this->mssql_version){
208 case 9:
209 case 10:
210 return $this->CreateSequence2008($seq, $start);
211 break;
212 default:
213 return $this->CreateSequence2012($seq, $start);
214 break;
219 * For Server 2005,2008, duplicate a sequence with an identity table
221 function CreateSequence2008($seq='adodbseq',$start=1)
223 if($this->debug) ADOConnection::outp("<hr>CreateSequence($seq,$start)");
224 sqlsrv_begin_transaction($this->_connectionID);
225 $start -= 1;
226 $this->Execute("create table $seq (id int)");//was float(53)
227 $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)");
228 if (!$ok) {
229 if($this->debug) ADOConnection::outp("<hr>Error: ROLLBACK");
230 sqlsrv_rollback($this->_connectionID);
231 return false;
233 sqlsrv_commit($this->_connectionID);
234 return true;
238 * Proper Sequences Only available to Server 2012 and up
240 function CreateSequence2012($seq='adodbseq',$start=1){
241 if (!$this->sequences){
242 $sql = "SELECT name FROM sys.sequences";
243 $this->sequences = $this->GetCol($sql);
245 $ok = $this->Execute("CREATE SEQUENCE $seq START WITH $start INCREMENT BY 1");
246 if (!$ok)
247 die("CANNOT CREATE SEQUENCE" . print_r(sqlsrv_errors(),true));
248 $this->sequences[] = $seq;
252 * For Server 2005,2008, duplicate a sequence with an identity table
254 function GenID2008($seq='adodbseq',$start=1)
256 if($this->debug) ADOConnection::outp("<hr>CreateSequence($seq,$start)");
257 sqlsrv_begin_transaction($this->_connectionID);
258 $ok = $this->Execute("update $seq with (tablock,holdlock) set id = id + 1");
259 if (!$ok) {
260 $start -= 1;
261 $this->Execute("create table $seq (id int)");//was float(53)
262 $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)");
263 if (!$ok) {
264 if($this->debug) ADOConnection::outp("<hr>Error: ROLLBACK");
265 sqlsrv_rollback($this->_connectionID);
266 return false;
269 $num = $this->GetOne("select id from $seq");
270 sqlsrv_commit($this->_connectionID);
271 return $num;
274 * Only available to Server 2012 and up
275 * Cannot do this the normal adodb way by trapping an error if the
276 * sequence does not exist because sql server will auto create a
277 * sequence with the starting number of -9223372036854775808
279 function GenID2012($seq='adodbseq',$start=1)
283 * First time in create an array of sequence names that we
284 * can use in later requests to see if the sequence exists
285 * the overhead is creating a list of sequences every time
286 * we need access to at least 1. If we really care about
287 * performance, we could maybe flag a 'nocheck' class variable
289 if (!$this->sequences){
290 $sql = "SELECT name FROM sys.sequences";
291 $this->sequences = $this->GetCol($sql);
293 if (!is_array($this->sequences)
294 || is_array($this->sequences) && !in_array($seq,$this->sequences)){
295 $this->CreateSequence2012($seq, $start);
298 $num = $this->GetOne("SELECT NEXT VALUE FOR $seq");
299 return $num;
302 // Format date column in sql string given an input format that understands Y M D
303 function SQLDate($fmt, $col=false)
305 if (!$col) {
306 $col = $this->sysTimeStamp;
308 $s = '';
310 $ConvertableFmt=array(
311 "m/d/Y"=>101, "m/d/y"=>101 // US
312 ,"Y.m.d"=>102, "y.m.d"=>102 // ANSI
313 ,"d/m/Y"=>103, "d/m/y"=>103 // French /english
314 ,"d.m.Y"=>104, "d.m.y"=>104 // German
315 ,"d-m-Y"=>105, "d-m-y"=>105 // Italian
316 ,"m-d-Y"=>110, "m-d-y"=>110 // US Dash
317 ,"Y/m/d"=>111, "y/m/d"=>111 // Japan
318 ,"Ymd"=>112, "ymd"=>112 // ISO
319 ,"H:i:s"=>108 // Time
321 if (key_exists($fmt,$ConvertableFmt)) {
322 return "convert (varchar ,$col," . $ConvertableFmt[$fmt] . ")";
325 $len = strlen($fmt);
326 for ($i=0; $i < $len; $i++) {
327 if ($s) $s .= '+';
328 $ch = $fmt[$i];
329 switch($ch) {
330 case 'Y':
331 case 'y':
332 $s .= "datename(yyyy,$col)";
333 break;
334 case 'M':
335 $s .= "convert(char(3),$col,0)";
336 break;
337 case 'm':
338 $s .= "replace(str(month($col),2),' ','0')";
339 break;
340 case 'Q':
341 case 'q':
342 $s .= "datename(quarter,$col)";
343 break;
344 case 'D':
345 case 'd':
346 $s .= "replace(str(day($col),2),' ','0')";
347 break;
348 case 'h':
349 $s .= "substring(convert(char(14),$col,0),13,2)";
350 break;
352 case 'H':
353 $s .= "replace(str(datepart(hh,$col),2),' ','0')";
354 break;
356 case 'i':
357 $s .= "replace(str(datepart(mi,$col),2),' ','0')";
358 break;
359 case 's':
360 $s .= "replace(str(datepart(ss,$col),2),' ','0')";
361 break;
362 case 'a':
363 case 'A':
364 $s .= "substring(convert(char(19),$col,0),18,2)";
365 break;
366 case 'l':
367 $s .= "datename(dw,$col)";
368 break;
369 default:
370 if ($ch == '\\') {
371 $i++;
372 $ch = substr($fmt,$i,1);
374 $s .= $this->qstr($ch);
375 break;
378 return $s;
382 function BeginTrans()
384 if ($this->transOff) return true;
385 $this->transCnt += 1;
386 if ($this->debug) ADOConnection::outp('<hr>begin transaction');
387 sqlsrv_begin_transaction($this->_connectionID);
388 return true;
391 function CommitTrans($ok=true)
393 if ($this->transOff) return true;
394 if ($this->debug) ADOConnection::outp('<hr>commit transaction');
395 if (!$ok) return $this->RollbackTrans();
396 if ($this->transCnt) $this->transCnt -= 1;
397 sqlsrv_commit($this->_connectionID);
398 return true;
401 function RollbackTrans()
403 if ($this->transOff) return true;
404 if ($this->debug) ADOConnection::outp('<hr>rollback transaction');
405 if ($this->transCnt) $this->transCnt -= 1;
406 sqlsrv_rollback($this->_connectionID);
407 return true;
410 function SetTransactionMode( $transaction_mode )
412 $this->_transmode = $transaction_mode;
413 if (empty($transaction_mode)) {
414 $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
415 return;
417 if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode;
418 $this->Execute("SET TRANSACTION ".$transaction_mode);
422 Usage:
424 $this->BeginTrans();
425 $this->RowLock('table1,table2','table1.id=33 and table2.id=table1.id'); # lock row 33 for both tables
427 # some operation on both tables table1 and table2
429 $this->CommitTrans();
431 See http://www.swynk.com/friends/achigrik/SQL70Locks.asp
433 function RowLock($tables,$where,$col='1 as adodbignore')
435 if ($col == '1 as adodbignore') $col = 'top 1 null as ignore';
436 if (!$this->transCnt) $this->BeginTrans();
437 return $this->GetOne("select $col from $tables with (ROWLOCK,HOLDLOCK) where $where");
440 function SelectDB($dbName)
442 $this->database = $dbName;
443 if ($this->_connectionID) {
444 $rs = $this->Execute('USE '.$dbName);
445 if($rs) {
446 return true;
447 } else return false;
449 else return false;
452 function ErrorMsg()
454 $retErrors = sqlsrv_errors(SQLSRV_ERR_ALL);
455 if($retErrors != null) {
456 foreach($retErrors as $arrError) {
457 $this->_errorMsg .= "SQLState: ".$arrError[ 'SQLSTATE']."\n";
458 $this->_errorMsg .= "Error Code: ".$arrError[ 'code']."\n";
459 $this->_errorMsg .= "Message: ".$arrError[ 'message']."\n";
462 return $this->_errorMsg;
465 function ErrorNo()
467 $err = sqlsrv_errors(SQLSRV_ERR_ALL);
468 if ($err && $err[0])
469 return $err[0]['code'];
470 else
471 return 0;
474 // returns true or false
475 function _connect($argHostname, $argUsername, $argPassword, $argDatabasename)
477 if (!function_exists('sqlsrv_connect'))
479 if ($this->debug)
480 ADOConnection::outp('Microsoft SQL Server native driver (mssqlnative) not installed');
481 return null;
484 if (!empty($this->port))
486 * Port uses a comma
488 $argHostname .= ",".$this->port;
490 $connectionInfo = $this->connectionInfo;
491 $connectionInfo["Database"] = $argDatabasename;
492 if ((string)$argUsername != '' || (string)$argPassword != '')
495 * If they pass either a userid or password, we assume
496 * SQL Server authentication
498 $connectionInfo["UID"] = $argUsername;
499 $connectionInfo["PWD"] = $argPassword;
501 if ($this->debug)
502 ADOConnection::outp('userid or password supplied, attempting connection with SQL Server Authentication');
505 else
508 * If they don't pass either value, we won't add them to the
509 * connection parameters. This will then force an attempt
510 * to use windows authentication
512 if ($this->debug)
514 ADOConnection::outp('No userid or password supplied, attempting connection with Windows Authentication');
519 * Now merge in the passed connection parameters setting
521 foreach ($this->connectionParameters as $options)
523 foreach($options as $parameter=>$value)
524 $connectionInfo[$parameter] = $value;
527 if ($this->debug) ADOConnection::outp("connecting to host: $argHostname params: ".var_export($connectionInfo,true));
528 if(!($this->_connectionID = @sqlsrv_connect($argHostname,$connectionInfo)))
530 if ($this->debug)
531 ADOConnection::outp( 'Connection Failed: '.print_r( sqlsrv_errors(), true));
532 return false;
535 $this->ServerVersion();
537 return true;
540 // returns true or false
541 function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
543 //return null;//not implemented. NOTE: Persistent connections have no effect if PHP is used as a CGI program. (FastCGI!)
544 return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename);
548 function Prepare($sql)
550 return $sql; // prepare does not work properly with bind parameters as bind parameters are managed by sqlsrv_prepare!
553 // returns concatenated string
554 // MSSQL requires integers to be cast as strings
555 // automatically cast every datatype to VARCHAR(255)
556 // @author David Rogers (introspectshun)
557 function Concat()
559 $s = "";
560 $arr = func_get_args();
562 // Split single record on commas, if possible
563 if (sizeof($arr) == 1) {
564 foreach ($arr as $arg) {
565 $args = explode(',', $arg);
567 $arr = $args;
570 array_walk(
571 $arr,
572 function(&$value, $key) {
573 $value = "CAST(" . $value . " AS VARCHAR(255))";
576 $s = implode('+',$arr);
577 if (sizeof($arr) > 0) return "$s";
579 return '';
583 Unfortunately, it appears that mssql cannot handle varbinary > 255 chars
584 So all your blobs must be of type "image".
586 Remember to set in php.ini the following...
588 ; Valid range 0 - 2147483647. Default = 4096.
589 mssql.textlimit = 0 ; zero to pass through
591 ; Valid range 0 - 2147483647. Default = 4096.
592 mssql.textsize = 0 ; zero to pass through
594 function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB')
597 if (strtoupper($blobtype) == 'CLOB') {
598 $sql = "UPDATE $table SET $column='" . $val . "' WHERE $where";
599 return $this->Execute($sql) != false;
601 $sql = "UPDATE $table SET $column=0x".bin2hex($val)." WHERE $where";
602 return $this->Execute($sql) != false;
606 * Execute a query.
608 * If executing an INSERT statement and $hasInsertId is true, will set
609 * $lastInsId.
611 * @param string $sql
612 * @param array $inputarr
613 * @return resource|false Query Id if successful, otherwise false
615 function _query($sql, $inputarr = false)
617 $this->_errorMsg = false;
619 if (is_array($sql)) {
620 $sql = $sql[1];
623 // Handle native driver flaw for retrieving the last insert ID
624 if ($this->hasInsertID) {
625 // Check if it's an INSERT statement
626 $retrieveLastInsertID = preg_match(
627 '/^\W*insert[\s\w()[\]",.]+values\s*\((?:[^;\']|\'\'|(?:(?:\'\')*\'[^\']+\'(?:\'\')*))*;?$/i',
628 $sql
630 if ($retrieveLastInsertID) {
631 // Append the identity SQL, so it is executed in the same
632 // scope as the insert query.
633 $sql .= '; ' . $this->identitySQL;
635 } else {
636 $retrieveLastInsertID = false;
639 if ($inputarr) {
640 // Ensure that the input array is indexed numerically, as required
641 // by sqlsrv_query(). If param() was used to create portable binds
642 // then the array might be associative.
643 $inputarr = array_values($inputarr);
644 $rez = sqlsrv_query($this->_connectionID, $sql, $inputarr);
645 } else {
646 $rez = sqlsrv_query($this->_connectionID, $sql);
649 $this->lastInsID = false;
650 if (!$rez) {
651 $rez = false;
652 } elseif ($retrieveLastInsertID) {
653 // Get the inserted id from the last result
654 // Note: loop is required as server may return more than one row,
655 // e.g. if triggers are involved (see #41)
656 while (sqlsrv_next_result($rez)) {
657 sqlsrv_fetch($rez);
658 $this->lastInsID = sqlsrv_get_field($rez, 0);
661 return $rez;
665 * Rolls back pending transactions and closes the connection.
667 * @return bool True, unless the connection id is invalid
669 function _close()
671 if ($this->transCnt) {
672 $this->RollbackTrans();
674 if ($this->_connectionID) {
675 return sqlsrv_close($this->_connectionID);
677 $this->_connectionID = false;
678 return true;
682 function MetaIndexes($table,$primary=false, $owner = false)
684 $table = $this->qstr($table);
686 $sql = "SELECT i.name AS ind_name, C.name AS col_name, USER_NAME(O.uid) AS Owner, c.colid, k.Keyno,
687 CASE WHEN I.indid BETWEEN 1 AND 254 AND (I.status & 2048 = 2048 OR I.Status = 16402 AND O.XType = 'V') THEN 1 ELSE 0 END AS IsPK,
688 CASE WHEN I.status & 2 = 2 THEN 1 ELSE 0 END AS IsUnique
689 FROM dbo.sysobjects o INNER JOIN dbo.sysindexes I ON o.id = i.id
690 INNER JOIN dbo.sysindexkeys K ON I.id = K.id AND I.Indid = K.Indid
691 INNER JOIN dbo.syscolumns c ON K.id = C.id AND K.colid = C.Colid
692 WHERE LEFT(i.name, 8) <> '_WA_Sys_' AND o.status >= 0 AND O.Name LIKE $table
693 ORDER BY O.name, I.Name, K.keyno";
695 global $ADODB_FETCH_MODE;
696 $save = $ADODB_FETCH_MODE;
697 $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
698 if ($this->fetchMode !== FALSE) {
699 $savem = $this->SetFetchMode(FALSE);
702 $rs = $this->Execute($sql);
703 if (isset($savem)) {
704 $this->SetFetchMode($savem);
706 $ADODB_FETCH_MODE = $save;
708 if (!is_object($rs)) {
709 return FALSE;
712 $indexes = array();
713 while ($row = $rs->FetchRow()) {
714 if (!$primary && $row[5]) continue;
716 $indexes[$row[0]]['unique'] = $row[6];
717 $indexes[$row[0]]['columns'][] = $row[1];
719 return $indexes;
722 public function metaForeignKeys($table, $owner = '', $upper = false, $associative = false)
724 global $ADODB_FETCH_MODE;
726 $save = $ADODB_FETCH_MODE;
727 $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
728 $table = $this->qstr(strtoupper($table));
730 $sql =
731 "select object_name(constid) as constraint_name,
732 col_name(fkeyid, fkey) as column_name,
733 object_name(rkeyid) as referenced_table_name,
734 col_name(rkeyid, rkey) as referenced_column_name
735 from sysforeignkeys
736 where upper(object_name(fkeyid)) = $table
737 order by constraint_name, referenced_table_name, keyno";
739 $constraints = $this->GetArray($sql);
741 $ADODB_FETCH_MODE = $save;
743 $arr = false;
744 foreach($constraints as $constr) {
745 //print_r($constr);
746 $arr[$constr[0]][$constr[2]][] = $constr[1].'='.$constr[3];
748 if (!$arr) return false;
750 $arr2 = false;
752 foreach($arr as $k => $v) {
753 foreach($v as $a => $b) {
754 if ($upper) $a = strtoupper($a);
755 if (is_array($arr2[$a])) { // a previous foreign key was define for this reference table, we merge the new one
756 $arr2[$a] = array_merge($arr2[$a], $b);
757 } else {
758 $arr2[$a] = $b;
762 return $arr2;
765 //From: Fernando Moreira <FMoreira@imediata.pt>
766 function MetaDatabases()
768 $this->SelectDB("master");
769 $rs = $this->Execute($this->metaDatabasesSQL);
770 $rows = $rs->GetRows();
771 $ret = array();
772 for($i=0;$i<count($rows);$i++) {
773 $ret[] = $rows[$i][0];
775 $this->SelectDB($this->database);
776 if($ret)
777 return $ret;
778 else
779 return false;
782 // "Stein-Aksel Basma" <basma@accelero.no>
783 // tested with MSSQL 2000
784 function MetaPrimaryKeys($table, $owner=false)
786 global $ADODB_FETCH_MODE;
788 $schema = '';
789 $this->_findschema($table,$schema);
790 if (!$schema) $schema = $this->database;
791 if ($schema) $schema = "and k.table_catalog like '$schema%'";
793 $sql = "select distinct k.column_name,ordinal_position from information_schema.key_column_usage k,
794 information_schema.table_constraints tc
795 where tc.constraint_name = k.constraint_name and tc.constraint_type =
796 'PRIMARY KEY' and k.table_name = '$table' $schema order by ordinal_position ";
798 $savem = $ADODB_FETCH_MODE;
799 $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
800 $a = $this->GetCol($sql);
801 $ADODB_FETCH_MODE = $savem;
803 if ($a && sizeof($a)>0) return $a;
804 $false = false;
805 return $false;
809 function MetaTables($ttype=false,$showSchema=false,$mask=false)
811 if ($mask) {
812 $save = $this->metaTablesSQL;
813 $mask = $this->qstr(($mask));
814 $this->metaTablesSQL .= " AND name like $mask";
816 $ret = ADOConnection::MetaTables($ttype,$showSchema);
818 if ($mask) {
819 $this->metaTablesSQL = $save;
821 return $ret;
823 function MetaColumns($table, $upper=true, $schema=false){
826 * A simple caching mechanism, to be replaced in ADOdb V6
828 static $cached_columns = array();
829 if ($this->cachedSchemaFlush)
830 $cached_columns = array();
832 if (array_key_exists($table,$cached_columns)){
833 return $cached_columns[$table];
837 $this->_findschema($table,$schema);
838 if ($schema) {
839 $dbName = $this->database;
840 $this->SelectDB($schema);
842 global $ADODB_FETCH_MODE;
843 $save = $ADODB_FETCH_MODE;
844 $ADODB_FETCH_MODE = ADODB_FETCH_NUM;
846 if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false);
847 $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table));
849 if ($schema) {
850 $this->SelectDB($dbName);
853 if (isset($savem)) $this->SetFetchMode($savem);
854 $ADODB_FETCH_MODE = $save;
855 if (!is_object($rs)) {
856 $false = false;
857 return $false;
860 $retarr = array();
861 while (!$rs->EOF){
863 $fld = new ADOFieldObject();
864 if (array_key_exists(0,$rs->fields)) {
865 $fld->name = $rs->fields[0];
866 $fld->type = $rs->fields[1];
867 $fld->max_length = $rs->fields[2];
868 $fld->precision = $rs->fields[3];
869 $fld->scale = $rs->fields[4];
870 $fld->not_null =!$rs->fields[5];
871 $fld->has_default = $rs->fields[6];
872 $fld->xtype = $rs->fields[7];
873 $fld->type_length = $rs->fields[8];
874 $fld->auto_increment= $rs->fields[9];
875 } else {
876 $fld->name = $rs->fields['name'];
877 $fld->type = $rs->fields['type'];
878 $fld->max_length = $rs->fields['length'];
879 $fld->precision = $rs->fields['precision'];
880 $fld->scale = $rs->fields['scale'];
881 $fld->not_null =!$rs->fields['nullable'];
882 $fld->has_default = $rs->fields['default_value'];
883 $fld->xtype = $rs->fields['xtype'];
884 $fld->type_length = $rs->fields['type_length'];
885 $fld->auto_increment= $rs->fields['is_identity'];
888 if ($save == ADODB_FETCH_NUM)
889 $retarr[] = $fld;
890 else
891 $retarr[strtoupper($fld->name)] = $fld;
893 $rs->MoveNext();
896 $rs->Close();
897 $cached_columns[$table] = $retarr;
899 return $retarr;
903 * Returns a substring of a varchar type field
905 * The SQL server version varies because the length is mandatory, so
906 * we append a reasonable string length
908 * @param string $fld The field to sub-string
909 * @param int $start The start point
910 * @param int $length An optional length
912 * @return The SQL text
914 function substr($fld,$start,$length=0)
916 if ($length == 0)
918 * The length available to varchar is 2GB, but that makes no
919 * sense in a substring, so I'm going to arbitrarily limit
920 * the length to 1K, but you could change it if you want
922 $length = 1024;
924 $text = "SUBSTRING($fld,$start,$length)";
925 return $text;
929 * Returns the maximum size of a MetaType C field. Because of the
930 * database design, SQL Server places no limits on the size of data inserted
931 * Although the actual limit is 2^31-1 bytes.
933 * @return int
935 function charMax()
937 return ADODB_STRINGMAX_NOLIMIT;
941 * Returns the maximum size of a MetaType X field. Because of the
942 * database design, SQL Server places no limits on the size of data inserted
943 * Although the actual limit is 2^31-1 bytes.
945 * @return int
947 function textMax()
949 return ADODB_STRINGMAX_NOLIMIT;
952 * Lists procedures, functions and methods in an array.
954 * @param string $procedureNamePattern (optional)
955 * @param string $catalog (optional)
956 * @param string $schemaPattern (optional)
958 * @return array of stored objects in current database.
961 public function metaProcedures($procedureNamePattern = null, $catalog = null, $schemaPattern = null)
963 $metaProcedures = array();
964 $procedureSQL = '';
965 $catalogSQL = '';
966 $schemaSQL = '';
968 if ($procedureNamePattern)
969 $procedureSQL = "AND ROUTINE_NAME LIKE " . strtoupper($this->qstr($procedureNamePattern));
971 if ($catalog)
972 $catalogSQL = "AND SPECIFIC_SCHEMA=" . strtoupper($this->qstr($catalog));
974 if ($schemaPattern)
975 $schemaSQL = "AND ROUTINE_SCHEMA LIKE {$this->qstr($schemaPattern)}";
977 $fields = " ROUTINE_NAME,ROUTINE_TYPE,ROUTINE_SCHEMA,ROUTINE_CATALOG";
979 $SQL = "SELECT $fields
980 FROM {$this->database}.information_schema.routines
981 WHERE 1=1
982 $procedureSQL
983 $catalogSQL
984 $schemaSQL
985 ORDER BY ROUTINE_NAME
988 $result = $this->execute($SQL);
990 if (!$result)
991 return false;
992 while ($r = $result->fetchRow()){
993 if (!isset($r[0]))
995 * Convert to numeric
997 $r = array_values($r);
999 $procedureName = $r[0];
1000 $schemaName = $r[2];
1001 $routineCatalog= $r[3];
1002 $metaProcedures[$procedureName] = array('type'=> $r[1],
1003 'catalog' => $routineCatalog,
1004 'schema' => $schemaName,
1005 'remarks' => '',
1009 return $metaProcedures;
1013 * An SQL Statement that adds a specific number of
1014 * days or part to local datetime
1016 * @param float $dayFraction
1017 * @param string $date
1019 * @return string
1021 public function offsetDate($dayFraction, $date = false)
1023 if (!$date)
1025 * Use GETDATE() via systTimestamp;
1027 $date = $this->sysTimeStamp;
1030 * seconds, number of seconds, date base
1032 $dateFormat = "DATEADD(s, %s, %s)";
1035 * Adjust the offset back to seconds
1037 $fraction = $dayFraction * 24 * 3600;
1039 return sprintf($dateFormat,$fraction,$date);
1045 /*--------------------------------------------------------------------------------------
1046 Class Name: Recordset
1047 --------------------------------------------------------------------------------------*/
1049 class ADORecordset_mssqlnative extends ADORecordSet {
1051 var $databaseType = "mssqlnative";
1052 var $canSeek = false;
1053 var $fieldOffset = 0;
1054 // _mths works only in non-localised system
1057 * @var bool True if we have retrieved the fields metadata
1059 private $fieldObjectsRetrieved = false;
1062 * Cross-reference the objects by name for easy access
1064 private $fieldObjectsIndex = array();
1067 * Cross references the dateTime objects for faster decoding
1069 private $dateTimeObjects = array();
1072 * flags that we have dateTimeObjects to handle
1074 private $hasDateTimeObjects = false;
1077 * This is cross reference between how the types are stored
1078 * in SQL Server and their english-language description
1079 * -154 is a time field, see #432
1081 private $_typeConversion = array(
1082 -155 => 'datetimeoffset',
1083 -154 => 'char',
1084 -152 => 'xml',
1085 -151 => 'udt',
1086 -11 => 'uniqueidentifier',
1087 -10 => 'ntext',
1088 -9 => 'nvarchar',
1089 -8 => 'nchar',
1090 -7 => 'bit',
1091 -6 => 'tinyint',
1092 -5 => 'bigint',
1093 -4 => 'image',
1094 -3 => 'varbinary',
1095 -2 => 'timestamp',
1096 -1 => 'text',
1097 1 => 'char',
1098 2 => 'numeric',
1099 3 => 'decimal',
1100 4 => 'int',
1101 5 => 'smallint',
1102 6 => 'float',
1103 7 => 'real',
1104 12 => 'varchar',
1105 91 => 'date',
1106 93 => 'datetime'
1112 function __construct($id,$mode=false)
1114 if ($mode === false) {
1115 global $ADODB_FETCH_MODE;
1116 $mode = $ADODB_FETCH_MODE;
1119 $this->fetchMode = $mode;
1120 parent::__construct($id);
1124 function _initrs()
1126 $this->_numOfRows = -1;//not supported
1127 // Cache the metadata right now
1128 $this->_fetchField();
1133 //Contributed by "Sven Axelsson" <sven.axelsson@bokochwebb.se>
1134 // get next resultset - requires PHP 4.0.5 or later
1135 function NextRecordSet()
1137 if (!sqlsrv_next_result($this->_queryID)) return false;
1138 $this->_inited = false;
1139 $this->bind = false;
1140 $this->_currentRow = -1;
1141 $this->Init();
1142 return true;
1145 /* Use associative array to get fields array */
1146 function Fields($colname)
1148 if (!is_array($this->fields))
1150 * Too early
1152 return;
1153 if ($this->fetchMode != ADODB_FETCH_NUM)
1154 return $this->fields[$colname];
1156 if (!$this->bind) {
1157 $this->bind = array();
1158 for ($i=0; $i < $this->_numOfFields; $i++) {
1159 $o = $this->FetchField($i);
1160 $this->bind[strtoupper($o->name)] = $i;
1164 return $this->fields[$this->bind[strtoupper($colname)]];
1168 * Returns: an object containing field information.
1170 * Get column information in the Recordset object. fetchField()
1171 * can be used in order to obtain information about fields in a
1172 * certain query result. If the field offset isn't specified,
1173 * the next field that wasn't yet retrieved by fetchField()
1174 * is retrieved.
1176 * @param int $fieldOffset (optional default=-1 for all
1177 * @return mixed an ADOFieldObject, or array of objects
1179 private function _fetchField($fieldOffset = -1)
1181 if ($this->fieldObjectsRetrieved) {
1182 if ($this->fieldObjectsCache) {
1183 // Already got the information
1184 if ($fieldOffset == -1) {
1185 return $this->fieldObjectsCache;
1186 } else {
1187 return $this->fieldObjectsCache[$fieldOffset];
1189 } else {
1190 // No metadata available
1191 return false;
1195 $this->fieldObjectsRetrieved = true;
1197 * Retrieve all metadata in one go. This is always returned as a
1198 * numeric array.
1200 $fieldMetaData = sqlsrv_field_metadata($this->_queryID);
1202 if (!$fieldMetaData) {
1203 // Not a statement that gives us metaData
1204 return false;
1207 $this->_numOfFields = count($fieldMetaData);
1208 foreach ($fieldMetaData as $key=>$value) {
1209 $fld = new ADOFieldObject;
1210 // Caution - keys are case-sensitive, must respect casing of values
1211 $fld->name = $value['Name'];
1212 $fld->max_length = $value['Size'];
1213 $fld->column_source = $value['Name'];
1214 $fld->type = $this->_typeConversion[$value['Type']];
1216 $this->fieldObjectsCache[$key] = $fld;
1217 $this->fieldObjectsIndex[$fld->name] = $key;
1219 if ($fieldOffset == -1) {
1220 return $this->fieldObjectsCache;
1223 return $this->fieldObjectsCache[$fieldOffset];
1227 * Fetchfield copies the oracle method, it loads the field information
1228 * into the _fieldobjs array once, to save multiple calls to the
1229 * sqlsrv_field_metadata function
1231 * @param int $fieldOffset (optional)
1233 * @return adoFieldObject
1235 * @author KM Newnham
1236 * @date 02/20/2013
1238 function fetchField($fieldOffset = -1)
1240 return $this->fieldObjectsCache[$fieldOffset];
1243 function _seek($row)
1245 return false;//There is no support for cursors in the driver at this time. All data is returned via forward-only streams.
1248 // speedup
1249 function MoveNext()
1251 if ($this->EOF)
1252 return false;
1254 $this->_currentRow++;
1256 if ($this->_fetch())
1257 return true;
1258 $this->EOF = true;
1260 return false;
1263 function _fetch($ignore_fields=false)
1265 if ($this->fetchMode & ADODB_FETCH_ASSOC) {
1266 if ($this->fetchMode & ADODB_FETCH_NUM)
1267 $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_BOTH);
1268 else
1269 $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_ASSOC);
1271 if (is_array($this->fields))
1274 if (ADODB_ASSOC_CASE == ADODB_ASSOC_CASE_LOWER)
1275 $this->fields = array_change_key_case($this->fields,CASE_LOWER);
1276 else if (ADODB_ASSOC_CASE == ADODB_ASSOC_CASE_UPPER)
1277 $this->fields = array_change_key_case($this->fields,CASE_UPPER);
1281 else
1282 $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_NUMERIC);
1284 if (!$this->fields)
1285 return false;
1287 return $this->fields;
1291 * close() only needs to be called if you are worried about using too much
1292 * memory while your script is running. All associated result memory for
1293 * the specified result identifier will automatically be freed.
1295 * @return bool tru if we succeeded in closing down
1297 function _close()
1300 * If we are closing down a failed query, collect any
1301 * error messages. This is a hack fix to the "close too early"
1302 * problem so this might go away later
1304 $this->connection->errorMsg();
1305 if(is_resource($this->_queryID)) {
1306 $rez = sqlsrv_free_stmt($this->_queryID);
1307 $this->_queryID = false;
1308 return $rez;
1311 return true;
1317 class ADORecordSet_array_mssqlnative extends ADORecordSet_array {}
1320 Code Example 1:
1322 select object_name(constid) as constraint_name,
1323 object_name(fkeyid) as table_name,
1324 col_name(fkeyid, fkey) as column_name,
1325 object_name(rkeyid) as referenced_table_name,
1326 col_name(rkeyid, rkey) as referenced_column_name
1327 from sysforeignkeys
1328 where object_name(fkeyid) = x
1329 order by constraint_name, table_name, referenced_table_name, keyno
1331 Code Example 2:
1332 select constraint_name,
1333 column_name,
1334 ordinal_position
1335 from information_schema.key_column_usage
1336 where constraint_catalog = db_name()
1337 and table_name = x
1338 order by constraint_name, ordinal_position
1340 http://www.databasejournal.com/scripts/article.php/1440551