Skip to content

Commit c0fb439

Browse files
authored
Merge pull request joomla#78 from joomla-framework/sqlsrv-prepared
Prepared statement support for SQL Server
2 parents 82112c9 + 367a088 commit c0fb439

File tree

3 files changed

+187
-6
lines changed

3 files changed

+187
-6
lines changed

Tests/DriverSqlsrvTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,33 @@ public function testExecute()
484484
$this->assertNotEquals(self::$driver->execute(), false, __LINE__);
485485
}
486486

487+
/**
488+
* Test the execute method with a prepared statement
489+
*
490+
* @return void
491+
*
492+
* @since 1.0
493+
*/
494+
public function testExecutePreparedStatement()
495+
{
496+
$title = 'testTitle';
497+
$startDate = '2013-04-01 00:00:00.000';
498+
$description = 'description';
499+
500+
/** @var \Joomla\Database\Sqlsrv\SqlsrvQuery $query */
501+
$query = self::$driver->getQuery(true);
502+
$query->insert('jos_dbtest')
503+
->columns('title,start_date,description')
504+
->values('?, ?, ?');
505+
$query->bind(1, $title);
506+
$query->bind(2, $startDate);
507+
$query->bind(3, $description);
508+
509+
self::$driver->setQuery($query);
510+
511+
$this->assertNotEquals(self::$driver->execute(), false, __LINE__);
512+
}
513+
487514
/**
488515
* Tests the renameTable method
489516
*

src/Sqlsrv/SqlsrvDriver.php

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88

99
namespace Joomla\Database\Sqlsrv;
1010

11+
use Joomla\Database\DatabaseQuery;
1112
use Joomla\Database\Exception\ConnectionFailureException;
1213
use Joomla\Database\Exception\ExecutionFailureException;
1314
use Joomla\Database\Exception\UnsupportedAdapterException;
15+
use Joomla\Database\Query\LimitableInterface;
16+
use Joomla\Database\Query\PreparableInterface;
1417
use Psr\Log;
1518
use Joomla\Database\DatabaseDriver;
1619

@@ -604,18 +607,33 @@ public function execute()
604607
$this->errorNum = 0;
605608
$this->errorMsg = '';
606609

610+
$options = array();
611+
607612
// SQLSrv_num_rows requires a static or keyset cursor.
608613
if (strncmp(ltrim(strtoupper($sql)), 'SELECT', strlen('SELECT')) == 0)
609614
{
610-
$array = array('Scrollable' => SQLSRV_CURSOR_KEYSET);
615+
$options = array('Scrollable' => SQLSRV_CURSOR_KEYSET);
611616
}
612-
else
617+
618+
$params = array();
619+
620+
// Bind the variables:
621+
if ($this->sql instanceof PreparableInterface)
613622
{
614-
$array = array();
623+
$bounded =& $this->sql->getBounded();
624+
625+
if (count($bounded))
626+
{
627+
foreach ($bounded as $key => $obj)
628+
{
629+
// And add the value as an additional param
630+
$params[] = $obj->value;
631+
}
632+
}
615633
}
616634

617635
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
618-
$this->cursor = @sqlsrv_query($this->connection, $sql, array(), $array);
636+
$this->cursor = @sqlsrv_query($this->connection, $sql, $params, $options);
619637

620638
// If an error occurred handle it.
621639
if (!$this->cursor)
@@ -803,6 +821,38 @@ public function select($database)
803821
return true;
804822
}
805823

824+
/**
825+
* Sets the SQL statement string for later execution.
826+
*
827+
* @param DatabaseQuery|string $query The SQL statement to set either as a DatabaseQuery object or a string.
828+
* @param integer $offset The affected row offset to set.
829+
* @param integer $limit The maximum affected rows to set.
830+
*
831+
* @return SqlsrvDriver This object to support method chaining.
832+
*
833+
* @since __DEPLOY_VERSION__
834+
*/
835+
public function setQuery($query, $offset = null, $limit = null)
836+
{
837+
$this->connect();
838+
839+
$this->freeResult();
840+
841+
if (is_string($query))
842+
{
843+
// Allows taking advantage of bound variables in a direct query:
844+
$query = $this->getQuery(true)->setQuery($query);
845+
}
846+
847+
if ($query instanceof LimitableInterface && !is_null($offset) && !is_null($limit))
848+
{
849+
$query->setLimit($limit, $offset);
850+
}
851+
852+
// Store reference to the DatabaseQuery instance
853+
return parent::setQuery($query, $offset, $limit);
854+
}
855+
806856
/**
807857
* Set the connection to use UTF-8 character encoding.
808858
*
@@ -962,7 +1012,12 @@ protected function fetchObject($cursor = null, $class = 'stdClass')
9621012
*/
9631013
protected function freeResult($cursor = null)
9641014
{
965-
sqlsrv_free_stmt($cursor ? $cursor : $this->cursor);
1015+
$useCursor = $cursor ?: $this->cursor;
1016+
1017+
if (is_resource($useCursor))
1018+
{
1019+
sqlsrv_free_stmt($useCursor);
1020+
}
9661021
}
9671022

9681023
/**

src/Sqlsrv/SqlsrvQuery.php

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010

1111
use Joomla\Database\DatabaseDriver;
1212
use Joomla\Database\DatabaseQuery;
13+
use Joomla\Database\Query\PreparableInterface;
1314
use Joomla\Database\Query\QueryElement;
1415

1516
/**
1617
* SQL Server Query Building Class.
1718
*
1819
* @since 1.0
1920
*/
20-
class SqlsrvQuery extends DatabaseQuery
21+
class SqlsrvQuery extends DatabaseQuery implements PreparableInterface
2122
{
2223
/**
2324
* The character(s) used to quote SQL statement names such as table names or field names,
@@ -39,6 +40,14 @@ class SqlsrvQuery extends DatabaseQuery
3940
*/
4041
protected $null_date = '1900-01-01 00:00:00';
4142

43+
/**
44+
* Holds key / value pair of bound objects.
45+
*
46+
* @var mixed
47+
* @since __DEPLOY_VERSION__
48+
*/
49+
protected $bounded = array();
50+
4251
/**
4352
* Magic function to convert the query to a string.
4453
*
@@ -95,6 +104,96 @@ public function __toString()
95104
return $query;
96105
}
97106

107+
/**
108+
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
109+
* removes a variable that has been bounded from the internal bounded array when the passed in value is null.
110+
*
111+
* @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of
112+
* the form ':key', but can also be an integer.
113+
* @param mixed &$value The value that will be bound. The value is passed by reference to support output
114+
* parameters such as those possible with stored procedures.
115+
* @param string $dataType The corresponding bind type. (Unused)
116+
* @param integer $length The length of the variable. Usually required for OUTPUT parameters. (Unused)
117+
* @param array $driverOptions Optional driver options to be used. (Unused)
118+
*
119+
* @return SqlsrvQuery
120+
*
121+
* @since __DEPLOY_VERSION__
122+
*/
123+
public function bind($key = null, &$value = null, $dataType = 's', $length = 0, $driverOptions = array())
124+
{
125+
// Case 1: Empty Key (reset $bounded array)
126+
if (empty($key))
127+
{
128+
$this->bounded = array();
129+
130+
return $this;
131+
}
132+
133+
// Case 2: Key Provided, null value (unset key from $bounded array)
134+
if (is_null($value))
135+
{
136+
if (isset($this->bounded[$key]))
137+
{
138+
unset($this->bounded[$key]);
139+
}
140+
141+
return $this;
142+
}
143+
144+
$obj = new \stdClass;
145+
$obj->value = &$value;
146+
147+
// Case 3: Simply add the Key/Value into the bounded array
148+
$this->bounded[$key] = $obj;
149+
150+
return $this;
151+
}
152+
153+
/**
154+
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
155+
* returned.
156+
*
157+
* @param mixed $key The bounded variable key to retrieve.
158+
*
159+
* @return mixed
160+
*
161+
* @since __DEPLOY_VERSION__
162+
*/
163+
public function &getBounded($key = null)
164+
{
165+
if (empty($key))
166+
{
167+
return $this->bounded;
168+
}
169+
170+
if (isset($this->bounded[$key]))
171+
{
172+
return $this->bounded[$key];
173+
}
174+
}
175+
176+
/**
177+
* Clear data from the query or a specific clause of the query.
178+
*
179+
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
180+
*
181+
* @return SqlsrvQuery Returns this object to allow chaining.
182+
*
183+
* @since __DEPLOY_VERSION__
184+
*/
185+
public function clear($clause = null)
186+
{
187+
switch ($clause)
188+
{
189+
case null:
190+
$this->bounded = array();
191+
break;
192+
}
193+
194+
return parent::clear($clause);
195+
}
196+
98197
/**
99198
* Casts a value to a char.
100199
*

0 commit comments

Comments
 (0)