diff --git a/Tests/DriverSqlsrvTest.php b/Tests/DriverSqlsrvTest.php index 4cc5beed..183eb4c6 100644 --- a/Tests/DriverSqlsrvTest.php +++ b/Tests/DriverSqlsrvTest.php @@ -484,6 +484,33 @@ public function testExecute() $this->assertNotEquals(self::$driver->execute(), false, __LINE__); } + /** + * Test the execute method with a prepared statement + * + * @return void + * + * @since 1.0 + */ + public function testExecutePreparedStatement() + { + $title = 'testTitle'; + $startDate = '2013-04-01 00:00:00.000'; + $description = 'description'; + + /** @var \Joomla\Database\Sqlsrv\SqlsrvQuery $query */ + $query = self::$driver->getQuery(true); + $query->insert('jos_dbtest') + ->columns('title,start_date,description') + ->values('?, ?, ?'); + $query->bind(1, $title); + $query->bind(2, $startDate); + $query->bind(3, $description); + + self::$driver->setQuery($query); + + $this->assertNotEquals(self::$driver->execute(), false, __LINE__); + } + /** * Tests the renameTable method * diff --git a/src/Sqlsrv/SqlsrvDriver.php b/src/Sqlsrv/SqlsrvDriver.php index 5f18432c..568aefdb 100644 --- a/src/Sqlsrv/SqlsrvDriver.php +++ b/src/Sqlsrv/SqlsrvDriver.php @@ -8,9 +8,12 @@ namespace Joomla\Database\Sqlsrv; +use Joomla\Database\DatabaseQuery; use Joomla\Database\Exception\ConnectionFailureException; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\Exception\UnsupportedAdapterException; +use Joomla\Database\Query\LimitableInterface; +use Joomla\Database\Query\PreparableInterface; use Psr\Log; use Joomla\Database\DatabaseDriver; @@ -604,18 +607,33 @@ public function execute() $this->errorNum = 0; $this->errorMsg = ''; + $options = array(); + // SQLSrv_num_rows requires a static or keyset cursor. if (strncmp(ltrim(strtoupper($sql)), 'SELECT', strlen('SELECT')) == 0) { - $array = array('Scrollable' => SQLSRV_CURSOR_KEYSET); + $options = array('Scrollable' => SQLSRV_CURSOR_KEYSET); } - else + + $params = array(); + + // Bind the variables: + if ($this->sql instanceof PreparableInterface) { - $array = array(); + $bounded =& $this->sql->getBounded(); + + if (count($bounded)) + { + foreach ($bounded as $key => $obj) + { + // And add the value as an additional param + $params[] = $obj->value; + } + } } // Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost. - $this->cursor = @sqlsrv_query($this->connection, $sql, array(), $array); + $this->cursor = @sqlsrv_query($this->connection, $sql, $params, $options); // If an error occurred handle it. if (!$this->cursor) @@ -803,6 +821,38 @@ public function select($database) return true; } + /** + * Sets the SQL statement string for later execution. + * + * @param DatabaseQuery|string $query The SQL statement to set either as a DatabaseQuery object or a string. + * @param integer $offset The affected row offset to set. + * @param integer $limit The maximum affected rows to set. + * + * @return SqlsrvDriver This object to support method chaining. + * + * @since __DEPLOY_VERSION__ + */ + public function setQuery($query, $offset = null, $limit = null) + { + $this->connect(); + + $this->freeResult(); + + if (is_string($query)) + { + // Allows taking advantage of bound variables in a direct query: + $query = $this->getQuery(true)->setQuery($query); + } + + if ($query instanceof LimitableInterface && !is_null($offset) && !is_null($limit)) + { + $query->setLimit($limit, $offset); + } + + // Store reference to the DatabaseQuery instance + return parent::setQuery($query, $offset, $limit); + } + /** * Set the connection to use UTF-8 character encoding. * @@ -962,7 +1012,12 @@ protected function fetchObject($cursor = null, $class = 'stdClass') */ protected function freeResult($cursor = null) { - sqlsrv_free_stmt($cursor ? $cursor : $this->cursor); + $useCursor = $cursor ?: $this->cursor; + + if (is_resource($useCursor)) + { + sqlsrv_free_stmt($useCursor); + } } /** diff --git a/src/Sqlsrv/SqlsrvQuery.php b/src/Sqlsrv/SqlsrvQuery.php index c67ddaa8..80f49983 100644 --- a/src/Sqlsrv/SqlsrvQuery.php +++ b/src/Sqlsrv/SqlsrvQuery.php @@ -10,6 +10,7 @@ use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseQuery; +use Joomla\Database\Query\PreparableInterface; use Joomla\Database\Query\QueryElement; /** @@ -17,7 +18,7 @@ * * @since 1.0 */ -class SqlsrvQuery extends DatabaseQuery +class SqlsrvQuery extends DatabaseQuery implements PreparableInterface { /** * The character(s) used to quote SQL statement names such as table names or field names, @@ -39,6 +40,14 @@ class SqlsrvQuery extends DatabaseQuery */ protected $null_date = '1900-01-01 00:00:00'; + /** + * Holds key / value pair of bound objects. + * + * @var mixed + * @since __DEPLOY_VERSION__ + */ + protected $bounded = array(); + /** * Magic function to convert the query to a string. * @@ -95,6 +104,96 @@ public function __toString() return $query; } + /** + * Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also + * removes a variable that has been bounded from the internal bounded array when the passed in value is null. + * + * @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of + * the form ':key', but can also be an integer. + * @param mixed &$value The value that will be bound. The value is passed by reference to support output + * parameters such as those possible with stored procedures. + * @param string $dataType The corresponding bind type. (Unused) + * @param integer $length The length of the variable. Usually required for OUTPUT parameters. (Unused) + * @param array $driverOptions Optional driver options to be used. (Unused) + * + * @return SqlsrvQuery + * + * @since __DEPLOY_VERSION__ + */ + public function bind($key = null, &$value = null, $dataType = 's', $length = 0, $driverOptions = array()) + { + // Case 1: Empty Key (reset $bounded array) + if (empty($key)) + { + $this->bounded = array(); + + return $this; + } + + // Case 2: Key Provided, null value (unset key from $bounded array) + if (is_null($value)) + { + if (isset($this->bounded[$key])) + { + unset($this->bounded[$key]); + } + + return $this; + } + + $obj = new \stdClass; + $obj->value = &$value; + + // Case 3: Simply add the Key/Value into the bounded array + $this->bounded[$key] = $obj; + + return $this; + } + + /** + * Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is + * returned. + * + * @param mixed $key The bounded variable key to retrieve. + * + * @return mixed + * + * @since __DEPLOY_VERSION__ + */ + public function &getBounded($key = null) + { + if (empty($key)) + { + return $this->bounded; + } + + if (isset($this->bounded[$key])) + { + return $this->bounded[$key]; + } + } + + /** + * Clear data from the query or a specific clause of the query. + * + * @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query. + * + * @return SqlsrvQuery Returns this object to allow chaining. + * + * @since __DEPLOY_VERSION__ + */ + public function clear($clause = null) + { + switch ($clause) + { + case null: + $this->bounded = array(); + break; + } + + return parent::clear($clause); + } + /** * Casts a value to a char. *