1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Database
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * SQL Server database driver
14 *
15 * @link https://msdn.microsoft.com/en-us/library/cc296152(SQL.90).aspx
16 * @since 12.1
17 */
18 class JDatabaseDriverSqlsrv extends JDatabaseDriver
19 {
20 /**
21 * The name of the database driver.
22 *
23 * @var string
24 * @since 12.1
25 */
26 public $name = 'sqlsrv';
27
28 /**
29 * The type of the database server family supported by this driver.
30 *
31 * @var string
32 * @since CMS 3.5.0
33 */
34 public $serverType = 'mssql';
35
36 /**
37 * The character(s) used to quote SQL statement names such as table names or field names,
38 * etc. The child classes should define this as necessary. If a single character string the
39 * same character is used for both sides of the quoted name, else the first character will be
40 * used for the opening quote and the second for the closing quote.
41 *
42 * @var string
43 * @since 12.1
44 */
45 protected $nameQuote = '[]';
46
47 /**
48 * The null or zero representation of a timestamp for the database driver. This should be
49 * defined in child classes to hold the appropriate value for the engine.
50 *
51 * @var string
52 * @since 12.1
53 */
54 protected $nullDate = '1900-01-01 00:00:00';
55
56 /**
57 * @var string The minimum supported database version.
58 * @since 12.1
59 */
60 protected static $dbMinimum = '10.50.1600.1';
61
62 /**
63 * Test to see if the SQLSRV connector is available.
64 *
65 * @return boolean True on success, false otherwise.
66 *
67 * @since 12.1
68 */
69 public static function isSupported()
70 {
71 return function_exists('sqlsrv_connect');
72 }
73
74 /**
75 * Constructor.
76 *
77 * @param array $options List of options used to configure the connection
78 *
79 * @since 12.1
80 */
81 public function __construct($options)
82 {
83 // Get some basic values from the options.
84 $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost';
85 $options['user'] = (isset($options['user'])) ? $options['user'] : '';
86 $options['password'] = (isset($options['password'])) ? $options['password'] : '';
87 $options['database'] = (isset($options['database'])) ? $options['database'] : '';
88 $options['select'] = (isset($options['select'])) ? (bool) $options['select'] : true;
89
90 // Finalize initialisation
91 parent::__construct($options);
92 }
93
94 /**
95 * Destructor.
96 *
97 * @since 12.1
98 */
99 public function __destruct()
100 {
101 $this->disconnect();
102 }
103
104 /**
105 * Connects to the database if needed.
106 *
107 * @return void Returns void if the database connected successfully.
108 *
109 * @since 12.1
110 * @throws RuntimeException
111 */
112 public function connect()
113 {
114 if ($this->connection)
115 {
116 return;
117 }
118
119 // Build the connection configuration array.
120 $config = array(
121 'Database' => $this->options['database'],
122 'uid' => $this->options['user'],
123 'pwd' => $this->options['password'],
124 'CharacterSet' => 'UTF-8',
125 'ReturnDatesAsStrings' => true,
126 );
127
128 // Make sure the SQLSRV extension for PHP is installed and enabled.
129 if (!self::isSupported())
130 {
131 throw new JDatabaseExceptionUnsupported('PHP extension sqlsrv_connect is not available.');
132 }
133
134 // Attempt to connect to the server.
135 if (!($this->connection = @ sqlsrv_connect($this->options['host'], $config)))
136 {
137 throw new JDatabaseExceptionConnecting('Database sqlsrv_connect failed, ' . print_r(sqlsrv_errors(), true));
138 }
139
140 // Make sure that DB warnings are not returned as errors.
141 sqlsrv_configure('WarningsReturnAsErrors', 0);
142
143 // If auto-select is enabled select the given database.
144 if ($this->options['select'] && !empty($this->options['database']))
145 {
146 $this->select($this->options['database']);
147 }
148
149 // Set charactersets.
150 $this->utf = $this->setUtf();
151
152 // Set QUOTED_IDENTIFIER always ON
153 sqlsrv_query($this->connection, 'SET QUOTED_IDENTIFIER ON');
154 }
155
156 /**
157 * Disconnects the database.
158 *
159 * @return void
160 *
161 * @since 12.1
162 */
163 public function disconnect()
164 {
165 // Close the connection.
166 if (is_resource($this->connection))
167 {
168 foreach ($this->disconnectHandlers as $h)
169 {
170 call_user_func_array($h, array( &$this));
171 }
172
173 sqlsrv_close($this->connection);
174 }
175
176 $this->connection = null;
177 }
178
179 /**
180 * Get table constraints
181 *
182 * @param string $tableName The name of the database table.
183 *
184 * @return array Any constraints available for the table.
185 *
186 * @since 12.1
187 */
188 protected function getTableConstraints($tableName)
189 {
190 $this->connect();
191
192 $query = $this->getQuery(true);
193
194 $this->setQuery(
195 'SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME = ' . $query->quote($tableName)
196 );
197
198 return $this->loadColumn();
199 }
200
201 /**
202 * Rename constraints.
203 *
204 * @param array $constraints Array(strings) of table constraints
205 * @param string $prefix A string
206 * @param string $backup A string
207 *
208 * @return void
209 *
210 * @since 12.1
211 */
212 protected function renameConstraints($constraints = array(), $prefix = null, $backup = null)
213 {
214 $this->connect();
215
216 foreach ($constraints as $constraint)
217 {
218 $this->setQuery('sp_rename ' . $constraint . ',' . str_replace($prefix, $backup, $constraint));
219 $this->execute();
220 }
221 }
222
223 /**
224 * Method to escape a string for usage in an SQL statement.
225 *
226 * The escaping for MSSQL isn't handled in the driver though that would be nice. Because of this we need
227 * to handle the escaping ourselves.
228 *
229 * @param string $text The string to be escaped.
230 * @param boolean $extra Optional parameter to provide extra escaping.
231 *
232 * @return string The escaped string.
233 *
234 * @since 12.1
235 */
236 public function escape($text, $extra = false)
237 {
238 $result = str_replace("'", "''", $text);
239
240 // SQL Server does not accept NULL byte in query string
241 $result = str_replace("\0", "' + CHAR(0) + N'", $result);
242
243 // Fix for SQL Server escape sequence, see https://support.microsoft.com/en-us/kb/164291
244 $result = str_replace(
245 array("\\\n", "\\\r", "\\\\\r\r\n"),
246 array("\\\\\n\n", "\\\\\r\r", "\\\\\r\n\r\n"),
247 $result
248 );
249
250 if ($extra)
251 {
252 // Escape special chars
253 $result = str_replace(
254 array('[', '_', '%'),
255 array('[[]', '[_]', '[%]'),
256 $result
257 );
258 }
259
260 return $result;
261 }
262
263 /**
264 * Quotes and optionally escapes a string to database requirements for use in database queries.
265 *
266 * @param mixed $text A string or an array of strings to quote.
267 * @param boolean $escape True (default) to escape the string, false to leave it unchanged.
268 *
269 * @return string The quoted input string.
270 *
271 * @note Accepting an array of strings was added in 12.3.
272 * @since 11.1
273 */
274 public function quote($text, $escape = true)
275 {
276 if (is_array($text))
277 {
278 return parent::quote($text, $escape);
279 }
280
281 // To support unicode on MSSQL we have to add prefix N
282 return 'N\'' . ($escape ? $this->escape($text) : $text) . '\'';
283 }
284
285 /**
286 * Determines if the connection to the server is active.
287 *
288 * @return boolean True if connected to the database engine.
289 *
290 * @since 12.1
291 */
292 public function connected()
293 {
294 // TODO: Run a blank query here
295 return true;
296 }
297
298 /**
299 * Drops a table from the database.
300 *
301 * @param string $tableName The name of the database table to drop.
302 * @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
303 *
304 * @return JDatabaseDriverSqlsrv Returns this object to support chaining.
305 *
306 * @since 12.1
307 */
308 public function dropTable($tableName, $ifExists = true)
309 {
310 $this->connect();
311
312 $query = $this->getQuery(true);
313
314 if ($ifExists)
315 {
316 $this->setQuery(
317 'IF EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ' . $query->quote($tableName) . ') DROP TABLE ' . $tableName
318 );
319 }
320 else
321 {
322 $this->setQuery('DROP TABLE ' . $tableName);
323 }
324
325 $this->execute();
326
327 return $this;
328 }
329
330 /**
331 * Get the number of affected rows for the previous executed SQL statement.
332 *
333 * @return integer The number of affected rows.
334 *
335 * @since 12.1
336 */
337 public function getAffectedRows()
338 {
339 $this->connect();
340
341 return sqlsrv_rows_affected($this->cursor);
342 }
343
344 /**
345 * Method to get the database collation in use by sampling a text field of a table in the database.
346 *
347 * @return mixed The collation in use by the database or boolean false if not supported.
348 *
349 * @since 12.1
350 */
351 public function getCollation()
352 {
353 // TODO: Not fake this
354 return 'MSSQL UTF-8 (UCS2)';
355 }
356
357 /**
358 * Method to get the database connection collation, as reported by the driver. If the connector doesn't support
359 * reporting this value please return an empty string.
360 *
361 * @return string
362 */
363 public function getConnectionCollation()
364 {
365 // TODO: Not fake this
366 return 'MSSQL UTF-8 (UCS2)';
367 }
368
369 /**
370 * Get the number of returned rows for the previous executed SQL statement.
371 *
372 * @param resource $cursor An optional database cursor resource to extract the row count from.
373 *
374 * @return integer The number of returned rows.
375 *
376 * @since 12.1
377 */
378 public function getNumRows($cursor = null)
379 {
380 $this->connect();
381
382 return sqlsrv_num_rows($cursor ? $cursor : $this->cursor);
383 }
384
385 /**
386 * Retrieves field information about the given tables.
387 *
388 * @param mixed $table A table name
389 * @param boolean $typeOnly True to only return field types.
390 *
391 * @return array An array of fields.
392 *
393 * @since 12.1
394 * @throws RuntimeException
395 */
396 public function getTableColumns($table, $typeOnly = true)
397 {
398 $result = array();
399
400 $table_temp = $this->replacePrefix((string) $table);
401
402 // Set the query to get the table fields statement.
403 $this->setQuery(
404 'SELECT column_name as Field, data_type as Type, is_nullable as \'Null\', column_default as \'Default\'' .
405 ' FROM information_schema.columns WHERE table_name = ' . $this->quote($table_temp)
406 );
407 $fields = $this->loadObjectList();
408
409 // If we only want the type as the value add just that to the list.
410 if ($typeOnly)
411 {
412 foreach ($fields as $field)
413 {
414 $result[$field->Field] = preg_replace('/[(0-9)]/', '', $field->Type);
415 }
416 }
417 // If we want the whole field data object add that to the list.
418 else
419 {
420 foreach ($fields as $field)
421 {
422 $field->Default = preg_replace("/(^(\(\(|\('|\(N'|\()|(('\)|(?<!\()\)\)|\))$))/i", '', $field->Default);
423 $result[$field->Field] = $field;
424 }
425 }
426
427 return $result;
428 }
429
430 /**
431 * Shows the table CREATE statement that creates the given tables.
432 *
433 * This is unsupported by MSSQL.
434 *
435 * @param mixed $tables A table name or a list of table names.
436 *
437 * @return array A list of the create SQL for the tables.
438 *
439 * @since 12.1
440 * @throws RuntimeException
441 */
442 public function getTableCreate($tables)
443 {
444 $this->connect();
445
446 return '';
447 }
448
449 /**
450 * Get the details list of keys for a table.
451 *
452 * @param string $table The name of the table.
453 *
454 * @return array An array of the column specification for the table.
455 *
456 * @since 12.1
457 * @throws RuntimeException
458 */
459 public function getTableKeys($table)
460 {
461 $this->connect();
462
463 // TODO To implement.
464 return array();
465 }
466
467 /**
468 * Method to get an array of all tables in the database.
469 *
470 * @return array An array of all the tables in the database.
471 *
472 * @since 12.1
473 * @throws RuntimeException
474 */
475 public function getTableList()
476 {
477 $this->connect();
478
479 // Set the query to get the tables statement.
480 $this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Tables WHERE type = \'U\';');
481 $tables = $this->loadColumn();
482
483 return $tables;
484 }
485
486 /**
487 * Get the version of the database connector.
488 *
489 * @return string The database connector version.
490 *
491 * @since 12.1
492 */
493 public function getVersion()
494 {
495 $this->connect();
496
497 $version = sqlsrv_server_info($this->connection);
498
499 return $version['SQLServerVersion'];
500 }
501
502 /**
503 * Inserts a row into a table based on an object's properties.
504 *
505 * @param string $table The name of the database table to insert into.
506 * @param object &$object A reference to an object whose public properties match the table fields.
507 * @param string $key The name of the primary key. If provided the object property is updated.
508 *
509 * @return boolean True on success.
510 *
511 * @since 12.1
512 * @throws RuntimeException
513 */
514 public function insertObject($table, &$object, $key = null)
515 {
516 $fields = array();
517 $values = array();
518 $statement = 'INSERT INTO ' . $this->quoteName($table) . ' (%s) VALUES (%s)';
519
520 foreach (get_object_vars($object) as $k => $v)
521 {
522 // Only process non-null scalars.
523 if (is_array($v) or is_object($v) or $v === null)
524 {
525 continue;
526 }
527
528 if (!$this->checkFieldExists($table, $k))
529 {
530 continue;
531 }
532
533 if ($k[0] == '_')
534 {
535 // Internal field
536 continue;
537 }
538
539 if ($k == $key && $key == 0)
540 {
541 continue;
542 }
543
544 $fields[] = $this->quoteName($k);
545 $values[] = $this->Quote($v);
546 }
547 // Set the query and execute the insert.
548 $this->setQuery(sprintf($statement, implode(',', $fields), implode(',', $values)));
549
550 if (!$this->execute())
551 {
552 return false;
553 }
554
555 $id = $this->insertid();
556
557 if ($key && $id)
558 {
559 $object->$key = $id;
560 }
561
562 return true;
563 }
564
565 /**
566 * Method to get the auto-incremented value from the last INSERT statement.
567 *
568 * @return integer The value of the auto-increment field from the last inserted row.
569 *
570 * @since 12.1
571 */
572 public function insertid()
573 {
574 $this->connect();
575
576 // TODO: SELECT IDENTITY
577 $this->setQuery('SELECT @@IDENTITY');
578
579 return (int) $this->loadResult();
580 }
581
582 /**
583 * Execute the SQL statement.
584 *
585 * @return mixed A database cursor resource on success, boolean false on failure.
586 *
587 * @since 12.1
588 * @throws RuntimeException
589 * @throws Exception
590 */
591 public function execute()
592 {
593 $this->connect();
594
595 // Take a local copy so that we don't modify the original query and cause issues later
596 $query = $this->replacePrefix((string) $this->sql);
597
598 if (!($this->sql instanceof JDatabaseQuery) && ($this->limit > 0 || $this->offset > 0))
599 {
600 $query = $this->limit($query, $this->limit, $this->offset);
601 }
602
603 if (!is_resource($this->connection))
604 {
605 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database');
606 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum);
607 }
608
609 // Increment the query counter.
610 $this->count++;
611
612 // Reset the error values.
613 $this->errorNum = 0;
614 $this->errorMsg = '';
615
616 // If debugging is enabled then let's log the query.
617 if ($this->debug)
618 {
619 // Add the query to the object queue.
620 $this->log[] = $query;
621
622 JLog::add($query, JLog::DEBUG, 'databasequery');
623
624 $this->timings[] = microtime(true);
625 }
626
627 // SQLSrv_num_rows requires a static or keyset cursor.
628 if (strncmp(ltrim(strtoupper($query)), 'SELECT', strlen('SELECT')) == 0)
629 {
630 $array = array('Scrollable' => SQLSRV_CURSOR_KEYSET);
631 }
632 else
633 {
634 $array = array();
635 }
636
637 // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
638 $this->cursor = @sqlsrv_query($this->connection, $query, array(), $array);
639
640 if ($this->debug)
641 {
642 $this->timings[] = microtime(true);
643
644 if (defined('DEBUG_BACKTRACE_IGNORE_ARGS'))
645 {
646 $this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
647 }
648 else
649 {
650 $this->callStacks[] = debug_backtrace();
651 }
652 }
653
654 // If an error occurred handle it.
655 if (!$this->cursor)
656 {
657 // Get the error number and message before we execute any more queries.
658 $errorNum = $this->getErrorNumber();
659 $errorMsg = $this->getErrorMessage();
660
661 // Check if the server was disconnected.
662 if (!$this->connected())
663 {
664 try
665 {
666 // Attempt to reconnect.
667 $this->connection = null;
668 $this->connect();
669 }
670 // If connect fails, ignore that exception and throw the normal exception.
671 catch (RuntimeException $e)
672 {
673 // Get the error number and message.
674 $this->errorNum = $this->getErrorNumber();
675 $this->errorMsg = $this->getErrorMessage();
676
677 // Throw the normal query exception.
678 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
679
680 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum, $e);
681 }
682
683 // Since we were able to reconnect, run the query again.
684 return $this->execute();
685 }
686 // The server was not disconnected.
687 else
688 {
689 // Get the error number and message from before we tried to reconnect.
690 $this->errorNum = $errorNum;
691 $this->errorMsg = $errorMsg;
692
693 // Throw the normal query exception.
694 JLog::add(JText::sprintf('JLIB_DATABASE_QUERY_FAILED', $this->errorNum, $this->errorMsg), JLog::ERROR, 'database-error');
695
696 throw new JDatabaseExceptionExecuting($query, $this->errorMsg, $this->errorNum);
697 }
698 }
699
700 return $this->cursor;
701 }
702
703 /**
704 * This function replaces a string identifier <var>$prefix</var> with the string held is the
705 * <var>tablePrefix</var> class variable.
706 *
707 * @param string $query The SQL statement to prepare.
708 * @param string $prefix The common table prefix.
709 *
710 * @return string The processed SQL statement.
711 *
712 * @since 12.1
713 */
714 public function replacePrefix($query, $prefix = '#__')
715 {
716 $query = trim($query);
717
718 if (strpos($query, "'"))
719 {
720 $parts = explode("'", $query);
721
722 for ($nIndex = 0, $size = count($parts); $nIndex < $size; $nIndex = $nIndex + 2)
723 {
724 if (strpos($parts[$nIndex], $prefix) !== false)
725 {
726 $parts[$nIndex] = str_replace($prefix, $this->tablePrefix, $parts[$nIndex]);
727 }
728 }
729
730 $query = implode("'", $parts);
731 }
732 else
733 {
734 $query = str_replace($prefix, $this->tablePrefix, $query);
735 }
736
737 return $query;
738 }
739
740 /**
741 * Select a database for use.
742 *
743 * @param string $database The name of the database to select for use.
744 *
745 * @return boolean True if the database was successfully selected.
746 *
747 * @since 12.1
748 * @throws RuntimeException
749 */
750 public function select($database)
751 {
752 $this->connect();
753
754 if (!$database)
755 {
756 return false;
757 }
758
759 if (!sqlsrv_query($this->connection, 'USE ' . $database, null, array('scrollable' => SQLSRV_CURSOR_STATIC)))
760 {
761 throw new JDatabaseExceptionConnecting('Could not connect to database');
762 }
763
764 return true;
765 }
766
767 /**
768 * Set the connection to use UTF-8 character encoding.
769 *
770 * @return boolean True on success.
771 *
772 * @since 12.1
773 */
774 public function setUtf()
775 {
776 return false;
777 }
778
779 /**
780 * Method to commit a transaction.
781 *
782 * @param boolean $toSavepoint If true, commit to the last savepoint.
783 *
784 * @return void
785 *
786 * @since 12.1
787 * @throws RuntimeException
788 */
789 public function transactionCommit($toSavepoint = false)
790 {
791 $this->connect();
792
793 if (!$toSavepoint || $this->transactionDepth <= 1)
794 {
795 if ($this->setQuery('COMMIT TRANSACTION')->execute())
796 {
797 $this->transactionDepth = 0;
798 }
799
800 return;
801 }
802
803 $this->transactionDepth--;
804 }
805
806 /**
807 * Method to roll back a transaction.
808 *
809 * @param boolean $toSavepoint If true, rollback to the last savepoint.
810 *
811 * @return void
812 *
813 * @since 12.1
814 * @throws RuntimeException
815 */
816 public function transactionRollback($toSavepoint = false)
817 {
818 $this->connect();
819
820 if (!$toSavepoint || $this->transactionDepth <= 1)
821 {
822 if ($this->setQuery('ROLLBACK TRANSACTION')->execute())
823 {
824 $this->transactionDepth = 0;
825 }
826
827 return;
828 }
829
830 $savepoint = 'SP_' . ($this->transactionDepth - 1);
831 $this->setQuery('ROLLBACK TRANSACTION ' . $this->quoteName($savepoint));
832
833 if ($this->execute())
834 {
835 $this->transactionDepth--;
836 }
837 }
838
839 /**
840 * Method to initialize a transaction.
841 *
842 * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
843 *
844 * @return void
845 *
846 * @since 12.1
847 * @throws RuntimeException
848 */
849 public function transactionStart($asSavepoint = false)
850 {
851 $this->connect();
852
853 if (!$asSavepoint || !$this->transactionDepth)
854 {
855 if ($this->setQuery('BEGIN TRANSACTION')->execute())
856 {
857 $this->transactionDepth = 1;
858 }
859
860 return;
861 }
862
863 $savepoint = 'SP_' . $this->transactionDepth;
864 $this->setQuery('BEGIN TRANSACTION ' . $this->quoteName($savepoint));
865
866 if ($this->execute())
867 {
868 $this->transactionDepth++;
869 }
870 }
871
872 /**
873 * Method to fetch a row from the result set cursor as an array.
874 *
875 * @param mixed $cursor The optional result set cursor from which to fetch the row.
876 *
877 * @return mixed Either the next row from the result set or false if there are no more rows.
878 *
879 * @since 12.1
880 */
881 protected function fetchArray($cursor = null)
882 {
883 return sqlsrv_fetch_array($cursor ? $cursor : $this->cursor, SQLSRV_FETCH_NUMERIC);
884 }
885
886 /**
887 * Method to fetch a row from the result set cursor as an associative array.
888 *
889 * @param mixed $cursor The optional result set cursor from which to fetch the row.
890 *
891 * @return mixed Either the next row from the result set or false if there are no more rows.
892 *
893 * @since 12.1
894 */
895 protected function fetchAssoc($cursor = null)
896 {
897 return sqlsrv_fetch_array($cursor ? $cursor : $this->cursor, SQLSRV_FETCH_ASSOC);
898 }
899
900 /**
901 * Method to fetch a row from the result set cursor as an object.
902 *
903 * @param mixed $cursor The optional result set cursor from which to fetch the row.
904 * @param string $class The class name to use for the returned row object.
905 *
906 * @return mixed Either the next row from the result set or false if there are no more rows.
907 *
908 * @since 12.1
909 */
910 protected function fetchObject($cursor = null, $class = 'stdClass')
911 {
912 // Class has to be loaded for sqlsrv on windows platform
913 class_exists($class);
914
915 return sqlsrv_fetch_object($cursor ? $cursor : $this->cursor, $class);
916 }
917
918 /**
919 * Method to free up the memory used for the result set.
920 *
921 * @param mixed $cursor The optional result set cursor from which to fetch the row.
922 *
923 * @return void
924 *
925 * @since 12.1
926 */
927 protected function freeResult($cursor = null)
928 {
929 sqlsrv_free_stmt($cursor ? $cursor : $this->cursor);
930 }
931
932 /**
933 * Method to check and see if a field exists in a table.
934 *
935 * @param string $table The table in which to verify the field.
936 * @param string $field The field to verify.
937 *
938 * @return boolean True if the field exists in the table.
939 *
940 * @since 12.1
941 */
942 protected function checkFieldExists($table, $field)
943 {
944 $this->connect();
945
946 $table = $this->replacePrefix((string) $table);
947 $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field' ORDER BY ORDINAL_POSITION";
948 $this->setQuery($query);
949
950 if ($this->loadResult())
951 {
952 return true;
953 }
954 else
955 {
956 return false;
957 }
958 }
959
960 /**
961 * Method to wrap an SQL statement to provide a LIMIT and OFFSET behavior for scrolling through a result set.
962 *
963 * @param string $query The SQL statement to process.
964 * @param integer $limit The maximum affected rows to set.
965 * @param integer $offset The affected row offset to set.
966 *
967 * @return string The processed SQL statement.
968 *
969 * @since 12.1
970 */
971 protected function limit($query, $limit, $offset)
972 {
973 if ($limit)
974 {
975 $total = $offset + $limit;
976 $query = substr_replace($query, 'SELECT TOP ' . (int) $total, stripos($query, 'SELECT'), 6);
977 }
978
979 if (!$offset)
980 {
981 return $query;
982 }
983
984 return PHP_EOL
985 . 'SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS RowNumber FROM ('
986 . $query
987 . PHP_EOL . ') AS A) AS A WHERE RowNumber > ' . (int) $offset;
988 }
989
990 /**
991 * Renames a table in the database.
992 *
993 * @param string $oldTable The name of the table to be renamed
994 * @param string $newTable The new name for the table.
995 * @param string $backup Table prefix
996 * @param string $prefix For the table - used to rename constraints in non-mysql databases
997 *
998 * @return JDatabaseDriverSqlsrv Returns this object to support chaining.
999 *
1000 * @since 12.1
1001 * @throws RuntimeException
1002 */
1003 public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
1004 {
1005 $constraints = array();
1006
1007 if (!is_null($prefix) && !is_null($backup))
1008 {
1009 $constraints = $this->getTableConstraints($oldTable);
1010 }
1011
1012 if (!empty($constraints))
1013 {
1014 $this->renameConstraints($constraints, $prefix, $backup);
1015 }
1016
1017 $this->setQuery("sp_rename '" . $oldTable . "', '" . $newTable . "'");
1018
1019 return $this->execute();
1020 }
1021
1022 /**
1023 * Locks a table in the database.
1024 *
1025 * @param string $tableName The name of the table to lock.
1026 *
1027 * @return JDatabaseDriverSqlsrv Returns this object to support chaining.
1028 *
1029 * @since 12.1
1030 * @throws RuntimeException
1031 */
1032 public function lockTable($tableName)
1033 {
1034 return $this;
1035 }
1036
1037 /**
1038 * Unlocks tables in the database.
1039 *
1040 * @return JDatabaseDriverSqlsrv Returns this object to support chaining.
1041 *
1042 * @since 12.1
1043 * @throws RuntimeException
1044 */
1045 public function unlockTables()
1046 {
1047 return $this;
1048 }
1049
1050 /**
1051 * Return the actual SQL Error number
1052 *
1053 * @return integer The SQL Error number
1054 *
1055 * @since 3.4.6
1056 */
1057 protected function getErrorNumber()
1058 {
1059 $errors = sqlsrv_errors();
1060
1061 return $errors[0]['code'];
1062 }
1063
1064 /**
1065 * Return the actual SQL Error message
1066 *
1067 * @return string The SQL Error message
1068 *
1069 * @since 3.4.6
1070 */
1071 protected function getErrorMessage()
1072 {
1073 $errors = sqlsrv_errors();
1074 $errorMessage = (string) $errors[0]['message'];
1075
1076 // Replace the Databaseprefix with `#__` if we are not in Debug
1077 if (!$this->debug)
1078 {
1079 $errorMessage = str_replace($this->tablePrefix, '#__', $errorMessage);
1080 }
1081
1082 return $errorMessage;
1083 }
1084
1085 /**
1086 * Get the query strings to alter the character set and collation of a table.
1087 *
1088 * @param string $tableName The name of the table
1089 *
1090 * @return string[] The queries required to alter the table's character set and collation
1091 *
1092 * @since CMS 3.5.0
1093 */
1094 public function getAlterTableCharacterSet($tableName)
1095 {
1096 return array();
1097 }
1098
1099 /**
1100 * Return the query string to create new Database.
1101 * Each database driver, other than MySQL, need to override this member to return correct string.
1102 *
1103 * @param stdClass $options Object used to pass user and database name to database driver.
1104 * This object must have "db_name" and "db_user" set.
1105 * @param boolean $utf True if the database supports the UTF-8 character set.
1106 *
1107 * @return string The query that creates database
1108 *
1109 * @since 12.2
1110 */
1111 protected function getCreateDatabaseQuery($options, $utf)
1112 {
1113 return 'CREATE DATABASE ' . $this->quoteName($options->db_name);
1114 }
1115 }
1116