diff --git a/docs/en/migrations.rst b/docs/en/migrations.rst index e86030342..d052c6331 100644 --- a/docs/en/migrations.rst +++ b/docs/en/migrations.rst @@ -172,7 +172,9 @@ Executing Queries Queries can be executed with the ``execute()`` and ``query()`` methods. The ``execute()`` method returns the number of affected rows whereas the ``query()`` method returns the result as a -`PDOStatement `_ +`PDOStatement `_. Both methods +accept an optional second parameter ``$params`` which is an array of elements, +and if used will cause the underlying connection to use a prepared statement. .. code-block:: php @@ -193,6 +195,11 @@ Queries can be executed with the ``execute()`` and ``query()`` methods. The // query() $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement $rows = $stmt->fetchAll(); // returns the result as an array + + // using prepared queries + $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); + $stmt = $this->query('SELECT * FROM users WHERE id > 5'); // returns PDOStatement + $rows = $stmt->fetchAll(); } /** @@ -213,6 +220,12 @@ Queries can be executed with the ``execute()`` and ``query()`` methods. The DELIMITERs during insertion of stored procedures or triggers which don't support DELIMITERs. +.. note:: + + If you wish to execute multiple queries at once, you may not also use the prepared + variant of these functions. When using prepared queries, PDO can only execute + them one at a time. + .. warning:: When using ``execute()`` or ``query()`` with a batch of queries, PDO doesn't diff --git a/src/Phinx/Db/Adapter/AdapterInterface.php b/src/Phinx/Db/Adapter/AdapterInterface.php index b5f735366..d13f03244 100644 --- a/src/Phinx/Db/Adapter/AdapterInterface.php +++ b/src/Phinx/Db/Adapter/AdapterInterface.php @@ -256,9 +256,10 @@ public function rollbackTransaction(); * Executes a SQL statement and returns the number of affected rows. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return int */ - public function execute($sql); + public function execute($sql, array $params = []); /** * Executes a list of migration actions for the given table @@ -282,9 +283,10 @@ public function getQueryBuilder(); * The return type depends on the underlying adapter being used. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return mixed */ - public function query($sql); + public function query($sql, array $params = []); /** * Executes a query and returns only one row as an array. diff --git a/src/Phinx/Db/Adapter/AdapterWrapper.php b/src/Phinx/Db/Adapter/AdapterWrapper.php index 7fae31a9c..8f479fc46 100644 --- a/src/Phinx/Db/Adapter/AdapterWrapper.php +++ b/src/Phinx/Db/Adapter/AdapterWrapper.php @@ -151,17 +151,17 @@ public function disconnect() /** * @inheritDoc */ - public function execute($sql) + public function execute($sql, array $params = []) { - return $this->getAdapter()->execute($sql); + return $this->getAdapter()->execute($sql, $params); } /** * @inheritDoc */ - public function query($sql) + public function query($sql, array $params = []) { - return $this->getAdapter()->query($sql); + return $this->getAdapter()->query($sql, $params); } /** diff --git a/src/Phinx/Db/Adapter/PdoAdapter.php b/src/Phinx/Db/Adapter/PdoAdapter.php index cbbe92c38..d9b2535ba 100644 --- a/src/Phinx/Db/Adapter/PdoAdapter.php +++ b/src/Phinx/Db/Adapter/PdoAdapter.php @@ -180,7 +180,7 @@ public function disconnect() /** * @inheritDoc */ - public function execute($sql) + public function execute($sql, array $params = []) { $sql = rtrim($sql, "; \t\n\r\0\x0B") . ';'; $this->verboseLog($sql); @@ -189,7 +189,14 @@ public function execute($sql) return 0; } - return $this->getConnection()->exec($sql); + if (empty($params)) { + return $this->getConnection()->exec($sql); + } + + $stmt = $this->getConnection()->prepare($sql); + $result = $stmt->execute($params); + + return $result ? $stmt->rowCount() : $result; } /** @@ -212,11 +219,17 @@ public function getQueryBuilder() * Executes a query and returns PDOStatement. * * @param string $sql SQL - * @return \PDOStatement + * @return \PDOStatement|false */ - public function query($sql) + public function query($sql, array $params = []) { - return $this->getConnection()->query($sql); + if (empty($params)) { + return $this->getConnection()->query($sql); + } + $stmt = $this->getConnection()->prepare($sql); + $result = $stmt->execute($params); + + return $result ? $stmt : false; } /** diff --git a/src/Phinx/Migration/AbstractMigration.php b/src/Phinx/Migration/AbstractMigration.php index a366c5b10..2eaa8c0ea 100644 --- a/src/Phinx/Migration/AbstractMigration.php +++ b/src/Phinx/Migration/AbstractMigration.php @@ -193,17 +193,17 @@ public function isMigratingUp() /** * @inheritDoc */ - public function execute($sql) + public function execute($sql, array $params = []) { - return $this->getAdapter()->execute($sql); + return $this->getAdapter()->execute($sql, $params); } /** * @inheritDoc */ - public function query($sql) + public function query($sql, array $params = []) { - return $this->getAdapter()->query($sql); + return $this->getAdapter()->query($sql, $params); } /** diff --git a/src/Phinx/Migration/MigrationInterface.php b/src/Phinx/Migration/MigrationInterface.php index 952d2a179..bc87bc5ef 100644 --- a/src/Phinx/Migration/MigrationInterface.php +++ b/src/Phinx/Migration/MigrationInterface.php @@ -132,9 +132,10 @@ public function isMigratingUp(); * Executes a SQL statement and returns the number of affected rows. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return int */ - public function execute($sql); + public function execute($sql, array $params = []); /** * Executes a SQL statement. @@ -145,9 +146,10 @@ public function execute($sql); * you can set the return type by the adapter in your current use. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return mixed */ - public function query($sql); + public function query($sql, array $params = []); /** * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE diff --git a/src/Phinx/Seed/AbstractSeed.php b/src/Phinx/Seed/AbstractSeed.php index f9ed25737..f7657bcde 100644 --- a/src/Phinx/Seed/AbstractSeed.php +++ b/src/Phinx/Seed/AbstractSeed.php @@ -128,17 +128,17 @@ public function getName() /** * @inheritDoc */ - public function execute($sql) + public function execute($sql, array $params = []) { - return $this->getAdapter()->execute($sql); + return $this->getAdapter()->execute($sql, $params); } /** * @inheritDoc */ - public function query($sql) + public function query($sql, array $params = []) { - return $this->getAdapter()->query($sql); + return $this->getAdapter()->query($sql, $params); } /** diff --git a/src/Phinx/Seed/SeedInterface.php b/src/Phinx/Seed/SeedInterface.php index 5759aedb2..94b492234 100644 --- a/src/Phinx/Seed/SeedInterface.php +++ b/src/Phinx/Seed/SeedInterface.php @@ -91,9 +91,10 @@ public function getName(); * Executes a SQL statement and returns the number of affected rows. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return int */ - public function execute($sql); + public function execute($sql, array $params = []); /** * Executes a SQL statement. @@ -104,9 +105,10 @@ public function execute($sql); * you can set the return type by the adapter in your current use. * * @param string $sql SQL + * @param array $params parameters to use for prepared query * @return mixed */ - public function query($sql); + public function query($sql, array $params = []); /** * Executes a query and returns only one row as an array. diff --git a/tests/Phinx/Db/Adapter/MysqlAdapterTest.php b/tests/Phinx/Db/Adapter/MysqlAdapterTest.php index 2f4644d3b..25e377587 100644 --- a/tests/Phinx/Db/Adapter/MysqlAdapterTest.php +++ b/tests/Phinx/Db/Adapter/MysqlAdapterTest.php @@ -1971,6 +1971,37 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } + public function testQueryWithParams() + { + $table = new \Phinx\Db\Table('table1', [], $this->adapter); + $table->addColumn('string_col', 'string') + ->addColumn('int_col', 'integer') + ->save(); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => 'test data', + 'int_col' => 10, + ]); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => null, + ]); + + $this->adapter->insert($table->getTable(), [ + 'int_col' => 23, + ]); + + $countQuery = $this->adapter->query('SELECT COUNT(*) AS c FROM table1 WHERE int_col > ?', [5]); + $res = $countQuery->fetchAll(); + $this->assertEquals(2, $res[0]['c']); + + $this->adapter->execute('UPDATE table1 SET int_col = ? WHERE int_col IS NULL', [12]); + + $countQuery->execute([1]); + $res = $countQuery->fetchAll(); + $this->assertEquals(3, $res[0]['c']); + } + public function testLiteralSupport() { $createQuery = <<<'INPUT' diff --git a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php index ba68d131d..bc3d01d02 100644 --- a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php +++ b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php @@ -2248,6 +2248,37 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } + public function testQueryWithParams() + { + $table = new \Phinx\Db\Table('table1', [], $this->adapter); + $table->addColumn('string_col', 'string') + ->addColumn('int_col', 'integer') + ->save(); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => 'test data', + 'int_col' => 10, + ]); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => null, + ]); + + $this->adapter->insert($table->getTable(), [ + 'int_col' => 23, + ]); + + $countQuery = $this->adapter->query('SELECT COUNT(*) AS c FROM table1 WHERE int_col > ?', [5]); + $res = $countQuery->fetchAll(); + $this->assertEquals(2, $res[0]['c']); + + $this->adapter->execute('UPDATE table1 SET int_col = ? WHERE int_col IS NULL', [12]); + + $countQuery->execute([1]); + $res = $countQuery->fetchAll(); + $this->assertEquals(3, $res[0]['c']); + } + public function testRenameMixedCaseTableAndColumns() { $table = new \Phinx\Db\Table('OrganizationSettings', [], $this->adapter); diff --git a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php index 92319178f..a438e8397 100644 --- a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php +++ b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php @@ -1187,6 +1187,37 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } + public function testQueryWithParams() + { + $table = new \Phinx\Db\Table('table1', [], $this->adapter); + $table->addColumn('string_col', 'string') + ->addColumn('int_col', 'integer') + ->save(); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => 'test data', + 'int_col' => 10, + ]); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => null, + ]); + + $this->adapter->insert($table->getTable(), [ + 'int_col' => 23, + ]); + + $countQuery = $this->adapter->query('SELECT COUNT(*) AS c FROM table1 WHERE int_col > ?', [5]); + $res = $countQuery->fetchAll(); + $this->assertEquals(2, $res[0]['c']); + + $this->adapter->execute('UPDATE table1 SET int_col = ? WHERE int_col IS NULL', [12]); + + $countQuery->execute([1]); + $res = $countQuery->fetchAll(); + $this->assertEquals(3, $res[0]['c']); + } + /** * Tests adding more than one column to a table * that already exists due to adapters having different add column instructions diff --git a/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php b/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php index bd9e2fd41..fce5563c4 100644 --- a/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php +++ b/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php @@ -1108,6 +1108,37 @@ public function testQueryBuilder() $stm->closeCursor(); } + public function testQueryWithParams() + { + $table = new \Phinx\Db\Table('table1', [], $this->adapter); + $table->addColumn('string_col', 'string') + ->addColumn('int_col', 'integer') + ->save(); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => 'test data', + 'int_col' => 10, + ]); + + $this->adapter->insert($table->getTable(), [ + 'string_col' => null, + ]); + + $this->adapter->insert($table->getTable(), [ + 'int_col' => 23, + ]); + + $countQuery = $this->adapter->query('SELECT COUNT(*) AS c FROM table1 WHERE int_col > ?', [5]); + $res = $countQuery->fetchAll(); + $this->assertEquals(2, $res[0]['c']); + + $this->adapter->execute('UPDATE table1 SET int_col = ? WHERE int_col IS NULL', [12]); + + $countQuery->execute([1]); + $res = $countQuery->fetchAll(); + $this->assertEquals(3, $res[0]['c']); + } + public function testLiteralSupport() { $createQuery = <<<'INPUT' diff --git a/tests/Phinx/Db/Mock/PdoAdapterTestPDOMockWithExecChecks.php b/tests/Phinx/Db/Mock/PdoAdapterTestPDOMockWithExecChecks.php index 10d30888b..08c37806c 100644 --- a/tests/Phinx/Db/Mock/PdoAdapterTestPDOMockWithExecChecks.php +++ b/tests/Phinx/Db/Mock/PdoAdapterTestPDOMockWithExecChecks.php @@ -17,6 +17,13 @@ public function exec($sql) $this->sql = $sql; } + public function prepare($sql, $options = []) + { + $this->sql = $sql; + + return new PdoStatementMock(); + } + public function getExecutedSqlForTest() { return $this->sql; diff --git a/tests/Phinx/Db/Mock/PdoStatementMock.php b/tests/Phinx/Db/Mock/PdoStatementMock.php new file mode 100644 index 000000000..e6051a7b3 --- /dev/null +++ b/tests/Phinx/Db/Mock/PdoStatementMock.php @@ -0,0 +1,16 @@ +