From c17766181feca90a7642ffa4fcd7c52655fc4490 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Mon, 10 Oct 2016 15:35:07 +0300 Subject: [PATCH] Added `QueryInterface::emulateExecution()` Added `QueryInterface::emulateExecution()`, which allows preventing of the actual query execution. This allows to cancel `DataProvider` preventing search query execution in case search model is invalid: ``` php public function search($params) { $query = Item::find(); $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); $this->load($params); if (!$this->validate()) { $query->where('0=1'); $query->emulateExecution(); // No SQL execution will be done return $dataProvider; } ``` This also fix unecessary query in case of `via()` usage. See #12390. fixes #12390 fixes #6373 close #12708 --- framework/CHANGELOG.md | 6 ++- framework/UPGRADE.md | 6 +++ framework/db/ActiveRelationTrait.php | 6 +++ framework/db/Query.php | 29 ++++++++++++++ framework/db/QueryInterface.php | 12 ++++++ framework/db/QueryTrait.php | 22 +++++++++++ tests/framework/db/ActiveRecordTest.php | 66 +++++++++++++++++++++++++++++++ tests/framework/db/QueryTest.php | 70 +++++++++++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 632335a..3c26377 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -28,7 +28,8 @@ Yii Framework 2 Change Log - Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov) - Bug #7727: Fixed truncateHtml leaving extra tags (developeruz) - Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire) -- Enh #6809: Added `\yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) +- Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul) +- Enh #6809: Added `yii\caching\Cache::$defaultDuration` property, allowing to set custom default cache duration (sdkiller) - Enh #7333: Improved error message for `yii\di\Instance::ensure()` when a component does not exist (cebe) - Enh #7420: Attributes for prompt generated with `renderSelectOptions` of `\yii\helpers\Html` helper (arogachev) - Enh #9162: Added support of closures in `value` for attributes in `yii\widgets\DetailView` (arogachev) @@ -37,6 +38,7 @@ Yii Framework 2 Change Log - Enh #11756: Added type mapping for `varbinary` data type in MySQL DBMS (silverfire) - Enh #11929: Changed `type` column type from `int` to `smallInt` in RBAC migrations (silverfire) - Enh #12015: Changed visibility `yii\db\ActiveQueryTrait::createModels()` from private to protected (ArekX, dynasource) +- Enh #12390: Avoid creating queries with false where contdition (`0=1`) when fetching relational data (klimov-paul) - Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006) - Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul) - Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006) @@ -53,7 +55,7 @@ Yii Framework 2 Change Log - Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe) - Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar) - Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333) -- Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) +- Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks) - Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) - Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index d91e00d..7c67fd7 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -50,6 +50,12 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.10 +----------------------- + +* A new method `public function emulateExecution($value = true);` has been added to the `yii\db\QueryInterace`. + This method is implemented in the `yii\db\QueryTrait`, so this only affects your code if you implement QueryInterface + in a class that does not use the trait. Upgrade from Yii 2.0.9 ---------------------- diff --git a/framework/db/ActiveRelationTrait.php b/framework/db/ActiveRelationTrait.php index b8c5241..4d9caf3 100644 --- a/framework/db/ActiveRelationTrait.php +++ b/framework/db/ActiveRelationTrait.php @@ -464,6 +464,9 @@ trait ActiveRelationTrait } } } + if (empty($values)) { + $this->emulateExecution(); + } } else { // composite keys @@ -478,6 +481,9 @@ trait ActiveRelationTrait $v[$attribute] = $model[$link]; } $values[] = $v; + if (empty($v)) { + $this->emulateExecution(); + } } } $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); diff --git a/framework/db/Query.php b/framework/db/Query.php index 6fe9bfc..1bb831d 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -207,6 +207,9 @@ class Query extends Component implements QueryInterface */ public function all($db = null) { + if ($this->emulateExecution) { + return []; + } $rows = $this->createCommand($db)->queryAll(); return $this->populate($rows); } @@ -244,6 +247,9 @@ class Query extends Component implements QueryInterface */ public function one($db = null) { + if ($this->emulateExecution) { + return false; + } return $this->createCommand($db)->queryOne(); } @@ -257,6 +263,9 @@ class Query extends Component implements QueryInterface */ public function scalar($db = null) { + if ($this->emulateExecution) { + return null; + } return $this->createCommand($db)->queryScalar(); } @@ -268,6 +277,10 @@ class Query extends Component implements QueryInterface */ public function column($db = null) { + if ($this->emulateExecution) { + return []; + } + if ($this->indexBy === null) { return $this->createCommand($db)->queryColumn(); } @@ -300,6 +313,9 @@ class Query extends Component implements QueryInterface */ public function count($q = '*', $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("COUNT($q)", $db); } @@ -313,6 +329,9 @@ class Query extends Component implements QueryInterface */ public function sum($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("SUM($q)", $db); } @@ -326,6 +345,9 @@ class Query extends Component implements QueryInterface */ public function average($q, $db = null) { + if ($this->emulateExecution) { + return 0; + } return $this->queryScalar("AVG($q)", $db); } @@ -363,6 +385,9 @@ class Query extends Component implements QueryInterface */ public function exists($db = null) { + if ($this->emulateExecution) { + return false; + } $command = $this->createCommand($db); $params = $command->params; $command->setSql($command->db->getQueryBuilder()->selectExists($command->getSql())); @@ -379,6 +404,10 @@ class Query extends Component implements QueryInterface */ protected function queryScalar($selectExpression, $db) { + if ($this->emulateExecution) { + return null; + } + $select = $this->select; $limit = $this->limit; $offset = $this->offset; diff --git a/framework/db/QueryInterface.php b/framework/db/QueryInterface.php index 28d16c9..7503fa3 100644 --- a/framework/db/QueryInterface.php +++ b/framework/db/QueryInterface.php @@ -252,4 +252,16 @@ interface QueryInterface * @return $this the query object itself */ public function offset($offset); + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true); } diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php index 4a64903..67cd2bf 100644 --- a/framework/db/QueryTrait.php +++ b/framework/db/QueryTrait.php @@ -50,6 +50,12 @@ trait QueryTrait * row data. For more details, see [[indexBy()]]. This property is only used by [[QueryInterface::all()|all()]]. */ public $indexBy; + /** + * @var boolean whether to emulate the actual query execution, returning empty or false results. + * @see emulateExecution() + * @since 2.0.11 + */ + public $emulateExecution = false; /** @@ -388,4 +394,20 @@ trait QueryTrait $this->offset = $offset; return $this; } + + /** + * Sets whether to emulate query execution, preventing any interaction with data storage. + * After this mode is enabled, methods, returning query results like [[one()]], [[all()]], [[exists()]] + * and so on, will return empty or false values. + * You should use this method in case your program logic indicates query should not return any results, like + * in case you set false where condition like `0=1`. + * @param boolean $value whether to prevent query execution. + * @return $this the query object itself. + * @since 2.0.11 + */ + public function emulateExecution($value = true) + { + $this->emulateExecution = $value; + return $this; + } } diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 709757b..8137243 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -1294,4 +1294,70 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals($newTotal, $newOrder->total); } + public function testEmulateExecution() + { + $this->assertGreaterThan(0, Customer::find()->from('customer')->count()); + + $rows = Customer::find() + ->from('customer') + ->emulateExecution() + ->all(); + $this->assertSame([], $rows); + + $row = Customer::find() + ->from('customer') + ->emulateExecution() + ->one(); + $this->assertSame(null, $row); + + $exists = Customer::find() + ->from('customer') + ->emulateExecution() + ->exists(); + $this->assertSame(false, $exists); + + $count = Customer::find() + ->from('customer') + ->emulateExecution() + ->count(); + $this->assertSame(0, $count); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->sum('id'); + $this->assertSame(0, $sum); + + $sum = Customer::find() + ->from('customer') + ->emulateExecution() + ->average('id'); + $this->assertSame(0, $sum); + + $max = Customer::find() + ->from('customer') + ->emulateExecution() + ->max('id'); + $this->assertSame(null, $max); + + $min = Customer::find() + ->from('customer') + ->emulateExecution() + ->min('id'); + $this->assertSame(null, $min); + + $scalar = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar(); + $this->assertSame(null, $scalar); + + $column = Customer::find() + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column(); + $this->assertSame([], $column); + } } diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index a5a68f1..005f9a0 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\db; +use yii\db\Connection; use yii\db\Expression; use yii\db\Query; @@ -317,4 +318,73 @@ abstract class QueryTest extends DatabaseTestCase $count = (new Query)->from('customer')->having(['status' => 2])->count('*', $db); $this->assertEquals(1, $count); } + + public function testEmulateExecution() + { + $db = $this->getConnection(); + + $this->assertGreaterThan(0, (new Query())->from('customer')->count('*', $db)); + + $rows = (new Query()) + ->from('customer') + ->emulateExecution() + ->all($db); + $this->assertSame([], $rows); + + $row = (new Query()) + ->from('customer') + ->emulateExecution() + ->one($db); + $this->assertSame(false, $row); + + $exists = (new Query()) + ->from('customer') + ->emulateExecution() + ->exists($db); + $this->assertSame(false, $exists); + + $count = (new Query()) + ->from('customer') + ->emulateExecution() + ->count('*', $db); + $this->assertSame(0, $count); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->sum('id', $db); + $this->assertSame(0, $sum); + + $sum = (new Query()) + ->from('customer') + ->emulateExecution() + ->average('id', $db); + $this->assertSame(0, $sum); + + $max = (new Query()) + ->from('customer') + ->emulateExecution() + ->max('id', $db); + $this->assertSame(null, $max); + + $min = (new Query()) + ->from('customer') + ->emulateExecution() + ->min('id', $db); + $this->assertSame(null, $min); + + $scalar = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->scalar($db); + $this->assertSame(null, $scalar); + + $column = (new Query()) + ->select(['id']) + ->from('customer') + ->emulateExecution() + ->column($db); + $this->assertSame([], $column); + } }