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