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