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