From 1d9a7dae2b17d035f0369ae853a7001c7b21cd12 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 29 Jul 2016 00:20:01 -0500 Subject: [PATCH 01/11] Database connection can now pretend to do a query. First step toward doing a prepare. --- system/Database/BaseConnection.php | 41 ++++++++++++++++++++-- tests/system/Database/Live/PretendTest.php | 27 ++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 tests/system/Database/Live/PretendTest.php diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 9e4ca66e7cb4..74819de05277 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -278,6 +278,14 @@ abstract class BaseConnection implements ConnectionInterface */ protected $connectDuration; + /** + * If true, no queries will actually be + * ran against the database. + * + * @var bool + */ + protected $pretend = false; + //-------------------------------------------------------------------- /** @@ -523,8 +531,10 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da $startTime = microtime(true); - // Run the query - if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) + + + // Run the query for real + if (! $this->pretend && false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { $query->setDuration($startTime, $startTime); @@ -545,7 +555,12 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da $this->queries[] = $query; } - return new $resultClass($this->connID, $this->resultID); + // If $pretend is true, then we just want to return + // the actual query object here. There won't be + // any results to return. + return $this->pretend + ? $query + : new $resultClass($this->connID, $this->resultID); } //-------------------------------------------------------------------- @@ -1289,6 +1304,26 @@ public function getFieldData(string $table) //-------------------------------------------------------------------- + /** + * Allows the engine to be set into a mode where queries are not + * actually executed, but they are still generated, timed, etc. + * + * This is primarily used by the prepared query functionality. + * + * @param bool $pretend + * + * @return $this + */ + public function pretend(bool $pretend = true) + { + $this->pretend = $pretend; + + return $this; + } + + //-------------------------------------------------------------------- + + /** * Returns the last error code and message. * diff --git a/tests/system/Database/Live/PretendTest.php b/tests/system/Database/Live/PretendTest.php new file mode 100644 index 000000000000..f872b2fef563 --- /dev/null +++ b/tests/system/Database/Live/PretendTest.php @@ -0,0 +1,27 @@ +db->pretend(false) + ->table('user') + ->get(); + + $this->assertFalse($result instanceof Query); + + $result = $this->db->pretend(true) + ->table('user') + ->get(); + + $this->assertTrue($result instanceof Query); + } + + //-------------------------------------------------------------------- + +} \ No newline at end of file From d518d16992b6c1d134f2428ae7d1bdc6b508bcd9 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 30 Jul 2016 00:06:51 -0500 Subject: [PATCH 02/11] Initial start on prepared queries --- system/Database/BaseConnection.php | 50 ++++++++ system/Database/BasePreparedQuery.php | 140 +++++++++++++++++++++ system/Database/MySQLi/Connection.php | 3 +- system/Database/MySQLi/PreparedQuery.php | 56 +++++++++ system/Database/PreparedQueryInterface.php | 68 ++++++++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 system/Database/BasePreparedQuery.php create mode 100644 system/Database/MySQLi/PreparedQuery.php create mode 100644 system/Database/PreparedQueryInterface.php diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 74819de05277..329a0c4a4e90 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -608,6 +608,46 @@ public function table($tableName) //-------------------------------------------------------------------- + /** + * Creates a prepared statement with the database that can then + * be used to execute multiple statements against. Within the + * closure, you would build the query in any normal way, though + * the Query Builder is the expected manner. + * + * Example: + * $stmt = $db->prepare(function($db) + * { + * return $db->table('users') + * ->where('id', 1) + * ->get(); + * }) + * + * @param \Closure $func + * @param array $options Passed to the prepare() method + * + * @return PreparedQueryInterface|null + */ + public function prepare(\Closure $func, array $options = []) + { + $this->pretend(true); + + $sql = $func($this); + + $this->pretend(false); + + if ($sql instanceof QueryInterface) + { + $sql = $sql->getOriginalQuery(); + } + + $class = str_ireplace('Connection', 'PreparedQuery', get_class($this)); + $class = new $class($this); + + return $class->prepare($sql, $options); + } + + //-------------------------------------------------------------------- + /** * Returns an array containing all of the * @@ -1323,6 +1363,16 @@ public function pretend(bool $pretend = true) //-------------------------------------------------------------------- + public function __get(string $name) + { + if (isset($this->$name)) + { + return $this->$name; + } + } + + //-------------------------------------------------------------------- + /** * Returns the last error code and message. diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php new file mode 100644 index 000000000000..5d60d8d67599 --- /dev/null +++ b/system/Database/BasePreparedQuery.php @@ -0,0 +1,140 @@ +db =& $db; + } + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return ResultInterface + */ + abstract public function execute(array $data); + + //-------------------------------------------------------------------- + + /** + * Takes an array containing multiple rows of data that should + * be inserted, one after the other, using the prepared statement. + * + * @param array $data + * + * @return \CodeIgniter\Database\ResultInterface + */ + public function executeBatch(array $data): ResultInterface + { + + } + + //-------------------------------------------------------------------- + + /** + * Prepares the query against the database, and saves the connection + * info necessary to execute the query later. + * + * NOTE: This version is based on SQL code. Child classes should + * override this method. + * + * @param string $sql + * @param array $options Passed to the connection's prepare statement. + * + * @return mixed + */ + abstract public function prepare(string $sql, array $options = []); + + //-------------------------------------------------------------------- + + /** + * Returns the SQL that has been prepared. + * + * @return string + */ + public function getQueryString(): string + { + return $this->sql; + } + + //-------------------------------------------------------------------- + + /** + * A helper to determine if any error exists. + * + * @return bool + */ + public function hasError() + { + return ! empty($this->errorString); + } + + //-------------------------------------------------------------------- + + + /** + * Returns the error code created while executing this statement. + * + * @return string + */ + public function getErrorCode(): int + { + return $this->errorCode; + } + + //-------------------------------------------------------------------- + + /** + * Returns the error message created while executing this statement. + * + * @return string + */ + public function getErrorMessage(): string + { + return $this->errorString; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 8f20826f1315..c2d272406331 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -89,7 +89,9 @@ class Connection extends BaseConnection implements ConnectionInterface * Connect to the database. * * @param bool $persistent + * * @return mixed + * @throws \CodeIgniter\DatabaseException */ public function connect($persistent = false) { @@ -447,5 +449,4 @@ public function insertID() //-------------------------------------------------------------------- - } diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php new file mode 100644 index 000000000000..21ec595eaa65 --- /dev/null +++ b/system/Database/MySQLi/PreparedQuery.php @@ -0,0 +1,56 @@ +sql = rtrim($sql, ';'); + + // MySQLi also only supports positional placeholders (?) + // so we need to replace our named placeholders (:name) + $this->sql = preg_replace('/:[\S]+/', '?', $this->sql); + + if (! $this->statement = $this->db->mysqli->prepare($this->sql)) + { + $this->errorCode = $this->db->mysqli->errno; + $this->errorString = $this->db->mysqli->error; + } + + return $this; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php new file mode 100644 index 000000000000..be4f454fd199 --- /dev/null +++ b/system/Database/PreparedQueryInterface.php @@ -0,0 +1,68 @@ + Date: Sat, 30 Jul 2016 00:09:36 -0500 Subject: [PATCH 03/11] Removing magic getter since it was causing problems --- system/Database/BaseConnection.php | 11 ----------- system/Database/MySQLi/Connection.php | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 329a0c4a4e90..2ab38b7fffa8 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1363,17 +1363,6 @@ public function pretend(bool $pretend = true) //-------------------------------------------------------------------- - public function __get(string $name) - { - if (isset($this->$name)) - { - return $this->$name; - } - } - - //-------------------------------------------------------------------- - - /** * Returns the last error code and message. * diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index c2d272406331..94d098f53773 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -81,7 +81,7 @@ class Connection extends BaseConnection implements ConnectionInterface * * @var MySQLi */ - protected $mysqli; + public $mysqli; //-------------------------------------------------------------------- From 665aeac5e78be14f2cce16867a7d7430d14c2914 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sat, 30 Jul 2016 22:15:52 -0500 Subject: [PATCH 04/11] Fix overzealous MySQLi placeholder conversion --- system/Database/MySQLi/PreparedQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 21ec595eaa65..17b15be4e0d0 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -41,7 +41,7 @@ public function prepare(string $sql, array $options = []) // MySQLi also only supports positional placeholders (?) // so we need to replace our named placeholders (:name) - $this->sql = preg_replace('/:[\S]+/', '?', $this->sql); + $this->sql = preg_replace('/:[^\s,)]+/', '?', $this->sql); if (! $this->statement = $this->db->mysqli->prepare($this->sql)) { From 366a9f7b172d902438b9e477509f1ff5e81501a1 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 31 Jul 2016 23:38:35 -0500 Subject: [PATCH 05/11] More work on prepare queries... though I can't get it to work just yet. --- system/Database/BasePreparedQuery.php | 32 ++++++------ system/Database/MySQLi/PreparedQuery.php | 57 ++++++++++++++++------ system/Database/PreparedQueryInterface.php | 21 +++----- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 5d60d8d67599..9adb19e8b4a0 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -54,22 +54,7 @@ public function __construct(ConnectionInterface $db) * * @return ResultInterface */ - abstract public function execute(array $data); - - //-------------------------------------------------------------------- - - /** - * Takes an array containing multiple rows of data that should - * be inserted, one after the other, using the prepared statement. - * - * @param array $data - * - * @return \CodeIgniter\Database\ResultInterface - */ - public function executeBatch(array $data): ResultInterface - { - - } + abstract public function execute(...$data); //-------------------------------------------------------------------- @@ -89,6 +74,21 @@ abstract public function prepare(string $sql, array $options = []); //-------------------------------------------------------------------- + /** + * Explicity closes the statement. + */ + public function close() + { + if (! is_object($this->statement)) + { + return; + } + + $this->statement->close(); + } + + //-------------------------------------------------------------------- + /** * Returns the SQL that has been prepared. * diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 17b15be4e0d0..1b696c681159 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -5,21 +5,6 @@ class PreparedQuery extends BasePreparedQuery implements PreparedQueryInterface { - /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - * - * @param array $data - * - * @return ResultInterface|null - */ - public function execute(array $data) - { - - } - - //-------------------------------------------------------------------- - /** * Prepares the query against the database, and saves the connection * info necessary to execute the query later. @@ -53,4 +38,46 @@ public function prepare(string $sql, array $options = []) } //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return ResultInterface + */ + public function execute(...$data) + { + if (is_null($this->statement)) + { + throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + // First off -bind the parameters + $bindTypes = ''; + + foreach ($data as $item) + { + if (is_integer($item)) + { + $bindTypes .= 'i'; + } + elseif (is_numeric($item)) + { + $bindTypes .= 'd'; + } + else + { + $bindTypes .= 's'; + } + } +die(var_dump($data)); + $this->statement->bind_param($bindTypes, ...$data); + + return $this->statement->execute(); + } + + //-------------------------------------------------------------------- + } diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php index be4f454fd199..0417ab1a1959 100644 --- a/system/Database/PreparedQueryInterface.php +++ b/system/Database/PreparedQueryInterface.php @@ -10,19 +10,7 @@ interface PreparedQueryInterface * * @return ResultInterface */ - public function execute(array $data); - - //-------------------------------------------------------------------- - - /** - * Takes an array containing multiple rows of data that should - * be inserted, one after the other, using the prepared statement. - * - * @param array $data - * - * @return \CodeIgniter\Database\ResultInterface - */ - public function executeBatch(array $data): ResultInterface; + public function execute(...$data); //-------------------------------------------------------------------- @@ -39,6 +27,13 @@ public function prepare(string $sql, array $options = []); //-------------------------------------------------------------------- + /** + * Explicity closes the statement. + */ + public function close(); + + //-------------------------------------------------------------------- + /** * Returns the SQL that has been prepared. * From 0f95f25fa15a7fc914db5ac53304a090d3d12131 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Thu, 4 Aug 2016 00:09:26 -0500 Subject: [PATCH 06/11] I think I finally have working MySQLi queries in a flexible manner. Next stop - PostgreSQL --- system/Database/BaseConnection.php | 20 +++++ system/Database/BasePreparedQuery.php | 99 ++++++++++++++++++++---- system/Database/MySQLi/PreparedQuery.php | 16 ++-- system/Database/Query.php | 26 +++++-- 4 files changed, 131 insertions(+), 30 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 2ab38b7fffa8..a10c833de6ea 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -486,6 +486,26 @@ public function saveQueries($save = false) //-------------------------------------------------------------------- + /** + * Stores a new query with the object. This is primarily used by + * the prepared queries. + * + * @param \CodeIgniter\Database\Query $query + * + * @return $this + */ + public function addQuery(Query $query) + { + if ($this->saveQueries) + { + $this->queries[] = $query; + } + + return $this; + } + + //-------------------------------------------------------------------- + /** * Executes the query against the database. * diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 9adb19e8b4a0..d6fd21eca7ff 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -2,13 +2,6 @@ abstract class BasePreparedQuery implements PreparedQueryInterface { - /** - * The SQL that this statement uses. - * - * @var string - */ - protected $sql; - /** * The prepared statement itself. * @@ -30,6 +23,14 @@ abstract class BasePreparedQuery implements PreparedQueryInterface */ protected $errorString; + /** + * Holds the prepared query object + * that is cloned during execute. + * + * @var Query + */ + protected $query; + /** * A reference to the db connection to use. * @@ -46,6 +47,53 @@ public function __construct(ConnectionInterface $db) //-------------------------------------------------------------------- + /** + * Prepares the query against the database, and saves the connection + * info necessary to execute the query later. + * + * NOTE: This version is based on SQL code. Child classes should + * override this method. + * + * @param string $sql + * @param array $options Passed to the connection's prepare statement. + * + * @return mixed + */ + public function prepare(string $sql, array $options = [], $queryClass = 'CodeIgniter\\Database\\Query') + { + // We only supports positional placeholders (?) + // in order to work with the execute method below, so we + // need to replace our named placeholders (:name) + $sql = preg_replace('/:[^\s,)]+/', '?', $sql); + + $query = new $queryClass($this->db); + + $query->setQuery($sql); + + if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) + { + $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre); + } + + $this->query = $query; + + return $this->_prepare($query->getOriginalQuery(), $options); + } + + //-------------------------------------------------------------------- + + /** + * The database-dependent portion of the prepare statement. + * + * @param string $sql + * @param array $options Passed to the connection's prepare statement. + * + * @return mixed + */ + abstract public function _prepare(string $sql, array $options = []); + + //-------------------------------------------------------------------- + /** * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. @@ -54,23 +102,40 @@ public function __construct(ConnectionInterface $db) * * @return ResultInterface */ - abstract public function execute(...$data); + public function execute(...$data) + { + // Execute the Query. + $startTime = microtime(true); + + $this->_execute($data); + + // Update our query object + $query = clone $this->query; + $query->setBinds($data); + + $query->setDuration($startTime); + + // Save it to the connection + $this->db->addQuery($query); + + // Return a result object + $resultClass = str_replace('PreparedQuery', 'Result', get_class($this)); + + $resultID = $this->statement->get_result(); + + return new $resultClass($this->db->connID, $resultID); + } //-------------------------------------------------------------------- /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. + * The database dependant version of the execute method. * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param string $sql - * @param array $options Passed to the connection's prepare statement. + * @param array $data * - * @return mixed + * @return ResultInterface */ - abstract public function prepare(string $sql, array $options = []); + abstract public function _execute($data); //-------------------------------------------------------------------- diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 1b696c681159..5051b6de6287 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -18,16 +18,12 @@ class PreparedQuery extends BasePreparedQuery implements PreparedQueryInterface * * @return mixed */ - public function prepare(string $sql, array $options = []) + public function _prepare(string $sql, array $options = []) { // Mysqli driver doesn't like statements // with terminating semicolons. $this->sql = rtrim($sql, ';'); - // MySQLi also only supports positional placeholders (?) - // so we need to replace our named placeholders (:name) - $this->sql = preg_replace('/:[^\s,)]+/', '?', $this->sql); - if (! $this->statement = $this->db->mysqli->prepare($this->sql)) { $this->errorCode = $this->db->mysqli->errno; @@ -47,7 +43,7 @@ public function prepare(string $sql, array $options = []) * * @return ResultInterface */ - public function execute(...$data) + public function _execute($data) { if (is_null($this->statement)) { @@ -57,6 +53,7 @@ public function execute(...$data) // First off -bind the parameters $bindTypes = ''; + // Determine the type string foreach ($data as $item) { if (is_integer($item)) @@ -72,10 +69,13 @@ public function execute(...$data) $bindTypes .= 's'; } } -die(var_dump($data)); + + // Bind it $this->statement->bind_param($bindTypes, ...$data); - return $this->statement->execute(); + $success = $this->statement->execute(); + + return $success; } //-------------------------------------------------------------------- diff --git a/system/Database/Query.php b/system/Database/Query.php index a6f6982a470c..2c1b1dc2b03c 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -122,10 +122,10 @@ public function __construct(&$db) { $this->db = $db; } - + //-------------------------------------------------------------------- - - + + /** * Sets the raw query string to use for this statement. * @@ -148,6 +148,22 @@ public function setQuery(string $sql, $binds=null) //-------------------------------------------------------------------- + /** + * Will store the variables to bind into the query later. + * + * @param array $binds + * + * @return $this + */ + public function setBinds(array $binds) + { + $this->binds = $binds; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Returns the final, processed query string after binding, etal * has been performed. @@ -230,7 +246,7 @@ public function getDuration(int $decimals = 6) /** * Stores the error description that happened for this query. - * + * * @param int $code * @param string $error */ @@ -451,7 +467,7 @@ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, i /** * Return text representation of the query - * + * * @return type */ public function __toString() From 257db404929cd9a224fa12e6ef7c16499bbdc575 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 7 Aug 2016 23:10:21 -0500 Subject: [PATCH 07/11] Fixing small bug. --- system/Database/BaseConnection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index a10c833de6ea..c97c157db732 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -560,7 +560,7 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da // @todo deal with errors - if ($this->saveQueries) + if ($this->saveQueries && ! $this->pretend) { $this->queries[] = $query; } @@ -570,7 +570,7 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da $query->setDuration($startTime); - if ($this->saveQueries) + if ($this->saveQueries && ! $this->pretend) { $this->queries[] = $query; } From b5d5859d8f24a53ddc6cd48c4eab7b52f522304d Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 7 Aug 2016 23:53:37 -0500 Subject: [PATCH 08/11] [ci skip] Added docs for prepared queries --- user_guide_src/source/database/queries.rst | 109 ++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 8353da6e64b3..0e167c560e12 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -2,6 +2,8 @@ Queries ####### +.. contents:: Table of Contents + ************ Query Basics ************ @@ -121,7 +123,7 @@ this: :: - $search = '20% raise'; + $search = '20% raise'; $sql = "SELECT id FROM table WHERE column LIKE '%" . $db->escapeLikeString($search)."%' ESCAPE '!'"; @@ -187,6 +189,111 @@ example:: } +**************** +Prepared Queries +**************** + +Most database engines support some form of prepared statements, that allow you to prepare a query once, and then run +that query multiple times with new sets of data. This eliminates the possibility of SQL injection since the data is +passed to the database in a different format than the query itself. When you need to run the same query multiple times +it can be quite a bit faster, too. However, to use it for every query can have major performance hits, since you're calling +out to the database twice as often. Since the Query Builder and Database connections already handle escaping the data +for you, the safety aspect is already taken care of for you. There will be times, though, when you need to ability +to optimize the query by running a prepared statement, or prepared query. + +Preparing the Query +------------------- + +This can be easily done with the ``prepare()`` method. This takes a single parameter, which is a Closure that returns +a query object. Query objects are automatically generated by any of the "final" type queries, including **insert**, +**update**, **delete**, **replace**, and **get**. This is handled the easiest by using the Query Builder to +run a query. The query is not actually ran, and the values don't matter since they're never applied, instead acting +as placeholders. This returns a PreparedQuery object:: + + $pQuery = $db->prepare(function($db) + { + return $db->table('user') + ->insert([ + 'name' => 'x', + 'email' => 'y', + 'country' => 'US' + ]); + }); + +If you don't want to use the Query Builder, you can create the Query object manually, using question marks for +value placeholders:: + + $pQuery = $db->prepare(function($db) + { + $sql = "INSERT INTO user (name, email, country) VALUES (?, ?, ?)"; + + return new Query($db)->setQuery($sql); + }); + +If the database requires an array of options passed to it during the prepare statement phase, you can pass that +array through in the second parameter:: + + $pQuery = $db->prepare(function($db) + { + $sql = "INSERT INTO user (name, email, country) VALUES (?, ?, ?)"; + + return new Query($db)->setQuery($sql); + }, $options); + +Executing the Query +------------------- + +Once you have a prepared query, you can use the ``execute()`` method to actually run the query. You can pass in as +many variables as you need in the query parameters. The number of parameters you pass must match the number of +placeholders in the query. They must also be passed in the same order as the placeholders appear in the original +query:: + + // Prepare the Query + $pQuery = $db->prepare(function($db) + { + return $db->table('user') + ->insert([ + 'name' => 'x', + 'email' => 'y', + 'country' => 'US' + ]); + }); + + // Collect the Data + $name = 'John Doe'; + $email' = 'j.doe@example.com'; + $country = 'US'; + + // Run the Query + $results = $pQuery->execute($name, $email, $country); + +This returns a standard :doc:`result set `. + +Other Methods +------------- + +In addition, to these two primary methods, the prepared query object also has the following methods: + +**close()** + +While PHP does a pretty good job of closing all open statements with the database, it's always a good idea to +close out the prepared statement when you're done with it:: + + $pQuery->close(); + +**getQueryString()** + +This returns the prepared query as a string. + +**hasError()** + +Returns boolean true/false if the last execute() call created any errors. + +**getErrorCode()** +**getErrorMessage()** + +If any errors were encountered, these methods can be used to retrieve the error code and string. + ************************** Working with Query Objects ************************** From 62d330bb46a2c0bf28aa817af99524a6c4030494 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 7 Aug 2016 23:56:44 -0500 Subject: [PATCH 09/11] [ci skip] Fixing doc styles --- user_guide_src/source/database/queries.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 0e167c560e12..75743f30371b 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -202,7 +202,7 @@ for you, the safety aspect is already taken care of for you. There will be times to optimize the query by running a prepared statement, or prepared query. Preparing the Query -------------------- +=================== This can be easily done with the ``prepare()`` method. This takes a single parameter, which is a Closure that returns a query object. Query objects are automatically generated by any of the "final" type queries, including **insert**, @@ -241,7 +241,7 @@ array through in the second parameter:: }, $options); Executing the Query -------------------- +=================== Once you have a prepared query, you can use the ``execute()`` method to actually run the query. You can pass in as many variables as you need in the query parameters. The number of parameters you pass must match the number of @@ -270,7 +270,7 @@ query:: This returns a standard :doc:`result set `. Other Methods -------------- +============= In addition, to these two primary methods, the prepared query object also has the following methods: From 0b8fb3182493132c70e989b488af5e9cbf15e3aa Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 8 Aug 2016 00:39:00 -0500 Subject: [PATCH 10/11] Basic tests for prepared queries, and fixing the select test issue. --- .../Database/Live/PreparedQueryTest.php | 55 +++++++++++++++++++ tests/system/Database/Live/SelectTest.php | 11 ++++ 2 files changed, 66 insertions(+) create mode 100644 tests/system/Database/Live/PreparedQueryTest.php diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php new file mode 100644 index 000000000000..fe040f63585e --- /dev/null +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -0,0 +1,55 @@ +db->prepare(function($db){ + return $db->table('user')->insert([ + 'name' => 'a', + 'email' => 'b@example.com' + ]); + }); + + $this->assertTrue($query instanceof BasePreparedQuery); + + $ec = $this->db->escapeChar; + $pre = $this->db->DBPrefix; + + $expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES (?, ?)"; + $this->assertEquals($expected, $query->getQueryString()); + + $query->close(); + } + + //-------------------------------------------------------------------- + + public function testExecuteRunsQueryAndReturnsResultObject() + { + $query = $this->db->prepare(function($db){ + return $db->table('user')->insert([ + 'name' => 'a', + 'email' => 'b@example.com' + ]); + }); + + $query->execute('foo', 'foo@example.com'); + $query->execute('bar', 'bar@example.com'); + + $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'foo', 'email' => 'foo@example.com']); + $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'bar', 'email' => 'bar@example.com']); + + $query->close(); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/system/Database/Live/SelectTest.php b/tests/system/Database/Live/SelectTest.php index 997165cb140c..16a8a3dfb5e0 100644 --- a/tests/system/Database/Live/SelectTest.php +++ b/tests/system/Database/Live/SelectTest.php @@ -11,6 +11,17 @@ class SelectTest extends \CIDatabaseTestCase protected $seed = 'CITestSeeder'; + public function __construct() + { + parent::__construct(); + + $this->db = \Config\Database::connect($this->DBGroup); + $this->db->initialize(); + } + + //-------------------------------------------------------------------- + + public function testSelectAllByDefault() { $row = $this->db->table('job')->get()->getRowArray(); From dfc39cdde2ee38c1e04845ba0c6cb8b7e4962893 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 8 Aug 2016 23:41:25 -0500 Subject: [PATCH 11/11] Implementing Postgre prepared query. --- system/Database/BasePreparedQuery.php | 11 +- system/Database/MySQLi/PreparedQuery.php | 12 ++ system/Database/Postgre/PreparedQuery.php | 112 ++++++++++++++++++ .../Database/Live/PreparedQueryTest.php | 16 ++- 4 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 system/Database/Postgre/PreparedQuery.php diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index d6fd21eca7ff..04427c72cd2d 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -121,7 +121,7 @@ public function execute(...$data) // Return a result object $resultClass = str_replace('PreparedQuery', 'Result', get_class($this)); - $resultID = $this->statement->get_result(); + $resultID = $this->_getResult(); return new $resultClass($this->db->connID, $resultID); } @@ -139,6 +139,15 @@ abstract public function _execute($data); //-------------------------------------------------------------------- + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + abstract public function _getResult(); + + //-------------------------------------------------------------------- + /** * Explicity closes the statement. */ diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 5051b6de6287..88a1e171d8b6 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -80,4 +80,16 @@ public function _execute($data) //-------------------------------------------------------------------- + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->statement->get_result(); + } + + //-------------------------------------------------------------------- + } diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php new file mode 100644 index 000000000000..ee2aa4374f0b --- /dev/null +++ b/system/Database/Postgre/PreparedQuery.php @@ -0,0 +1,112 @@ +name = mt_rand(1, 10000000000000000); + + $this->sql = $this->parameterize($sql); + + if (! $this->statement = pg_prepare($this->db->connID, $this->name, $this->sql)) + { + $this->errorCode = 0; + $this->errorString = pg_last_error($this->db->connID); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return ResultInterface + */ + public function _execute($data) + { + if (is_null($this->statement)) + { + throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + $this->result = pg_execute($this->db->connID, $this->name, $data); + + return (bool)$this->result; + } + + //-------------------------------------------------------------------- + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->result; + } + + //-------------------------------------------------------------------- + + /** + * Replaces the ? placeholders with $1, $2, etc parameters for use + * within the prepared query. + * + * @param string $sql + * + * @return string + */ + public function parameterize(string $sql): string + { + // Track our current value + $count = 0; + + $sql = preg_replace_callback('/\?/', function($matches) use (&$count){ + $count++; + return "\${$count}"; + }, $sql); + + return $sql; + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index fe040f63585e..2d8ae3dcac88 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -24,7 +24,14 @@ public function testPrepareReturnsPreparedQuery() $ec = $this->db->escapeChar; $pre = $this->db->DBPrefix; - $expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES (?, ?)"; + $placeholders = '?, ?'; + + if ($this->db->DBDriver == 'Postgre') + { + $placeholders = '$1, $2'; + } + + $expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES ({$placeholders})"; $this->assertEquals($expected, $query->getQueryString()); $query->close(); @@ -37,12 +44,13 @@ public function testExecuteRunsQueryAndReturnsResultObject() $query = $this->db->prepare(function($db){ return $db->table('user')->insert([ 'name' => 'a', - 'email' => 'b@example.com' + 'email' => 'b@example.com', + 'country' => 'x' ]); }); - $query->execute('foo', 'foo@example.com'); - $query->execute('bar', 'bar@example.com'); + $query->execute('foo', 'foo@example.com', 'US'); + $query->execute('bar', 'bar@example.com', 'GB'); $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'foo', 'email' => 'foo@example.com']); $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'bar', 'email' => 'bar@example.com']);