Browse Source

Merge branch 'master' of github.com:yiisoft/yii2 into 2.1

Conflicts:
	.travis.yml
	framework/classes.php
	tests/framework/ChangeLogTest.php
	tests/framework/filters/auth/CompositeAuthTest.php
tags/3.0.0-alpha1
Paul Klimov 7 years ago
parent
commit
a8d2127ad4
  1. 1
      .gitignore
  2. 2
      docs/guide-es/output-theming.md
  3. 2
      docs/guide-ja/output-theming.md
  4. 2
      docs/guide-pt-BR/output-theming.md
  5. 2
      docs/guide-ru/output-theming.md
  6. 6
      docs/guide/db-query-builder.md
  7. 2
      docs/guide/output-theming.md
  8. 16
      docs/guide/runtime-requests.md
  9. 1
      docs/guide/start-looking-ahead.md
  10. 6
      docs/guide/tutorial-yii-as-micro-framework.md
  11. 27
      framework/CHANGELOG.md
  12. 11
      framework/UPGRADE.md
  13. 2
      framework/behaviors/CacheableWidgetBehavior.php
  14. 2
      framework/console/controllers/MigrateController.php
  15. 37
      framework/db/ArrayExpression.php
  16. 2
      framework/db/BaseActiveRecord.php
  17. 26
      framework/db/Query.php
  18. 16
      framework/db/QueryBuilder.php
  19. 2
      framework/db/Schema.php
  20. 2
      framework/db/cubrid/QueryBuilder.php
  21. 2
      framework/db/mssql/QueryBuilder.php
  22. 63
      framework/db/mysql/ColumnSchema.php
  23. 4
      framework/db/mysql/Schema.php
  24. 2
      framework/db/oci/QueryBuilder.php
  25. 41
      framework/db/pgsql/ColumnSchema.php
  26. 9
      framework/filters/auth/HttpHeaderAuth.php
  27. 1
      framework/helpers/BaseHtml.php
  28. 2
      framework/jquery/assets/yii.activeForm.js
  29. 2
      framework/messages/bs/yii.php
  30. 2
      framework/rbac/BaseManager.php
  31. 4
      framework/validators/EachValidator.php
  32. 12
      framework/validators/ExistValidator.php
  33. 2
      framework/web/Session.php
  34. 1
      tests/data/ar/Document.php
  35. 5
      tests/data/ar/OrderItem.php
  36. 23
      tests/data/ar/Storage.php
  37. 84
      tests/data/base/ArrayAccessObject.php
  38. 2
      tests/data/base/TraversableObject.php
  39. 4
      tests/data/mssql.sql
  40. 7
      tests/data/mysql.sql
  41. 4
      tests/data/oci.sql
  42. 3
      tests/data/postgres.sql
  43. 10
      tests/framework/ChangeLogTest.php
  44. 4
      tests/framework/console/controllers/MigrateControllerTest.php
  45. 6
      tests/framework/db/ActiveRecordTest.php
  46. 45
      tests/framework/db/QueryTest.php
  47. 16
      tests/framework/db/cubrid/QueryBuilderTest.php
  48. 16
      tests/framework/db/mssql/QueryBuilderTest.php
  49. 33
      tests/framework/db/mysql/ActiveRecordTest.php
  50. 22
      tests/framework/db/mysql/QueryBuilderTest.php
  51. 16
      tests/framework/db/oci/QueryBuilderTest.php
  52. 24
      tests/framework/db/pgsql/ActiveRecordTest.php
  53. 22
      tests/framework/db/pgsql/QueryBuilderTest.php
  54. 4
      tests/framework/filters/auth/AuthTest.php
  55. 22
      tests/framework/filters/auth/CompositeAuthTest.php
  56. 14
      tests/framework/helpers/FileHelperTest.php
  57. 10
      tests/framework/helpers/HtmlTest.php
  58. 16
      tests/framework/validators/EachValidatorTest.php
  59. 17
      tests/framework/validators/ExistValidatorTest.php
  60. 2
      tests/framework/web/UserIdentity.php
  61. 18
      tests/framework/web/session/SessionTest.php
  62. 4
      tests/framework/widgets/FragmentCacheTest.php
  63. 3
      tests/framework/widgets/PjaxTest.php
  64. 44
      tests/js/tests/yii.activeForm.test.js

1
.gitignore vendored

@ -38,6 +38,7 @@ phpunit.phar
# ignore dev installed apps and extensions # ignore dev installed apps and extensions
/apps /apps
/extensions /extensions
/packages
# NPM packages # NPM packages
/node_modules /node_modules

2
docs/guide-es/output-theming.md

@ -76,7 +76,7 @@ siguiente configuración para el componente vista, tema:
``` ```
Con la configuración anterior, se puede crear una versión de la vista `@app/widgets/currency/index.php` para que se Con la configuración anterior, se puede crear una versión de la vista `@app/widgets/currency/index.php` para que se
aplique el tema en `@app/themes/basic/widgets/currency/index.php`. aplique el tema en `@app/themes/basic/widgets/currency/views/index.php`.
Uso de Multiples Rutas Uso de Multiples Rutas
---------------------- ----------------------

2
docs/guide-ja/output-theming.md

@ -81,7 +81,7 @@ $file = $theme->getPath('img/logo.gif');
], ],
``` ```
これによって、`@app/widgets/currency/views/index.php` に `@app/themes/basic/widgets/currency/index.php` というテーマを適用することが出来ます。 これによって、`@app/widgets/currency/views/index.php` に `@app/themes/basic/widgets/currency/views/index.php` というテーマを適用することが出来ます。
## テーマの継承 <span id="theme-inheritance"></span> ## テーマの継承 <span id="theme-inheritance"></span>

2
docs/guide-pt-BR/output-theming.md

@ -74,7 +74,7 @@ A fim de configurar temas por widgets, você pode configurar [[yii\base\Theme::p
], ],
``` ```
Isto lhe permitirá tematizar `@app/widgets/currency/views/index.php` com `@app/themes/basic/widgets/currency/index.php`. Isto lhe permitirá tematizar `@app/widgets/currency/views/index.php` com `@app/themes/basic/widgets/currency/views/index.php`.
## Herança de Tema <span id="theme-inheritance"></span> ## Herança de Tema <span id="theme-inheritance"></span>

2
docs/guide-ru/output-theming.md

@ -82,7 +82,7 @@ $file = $theme->getPath('img/logo.gif');
], ],
``` ```
Это позволит вам темизировать `@app/widgets/currency/views/index.php` в `@app/themes/basic/widgets/currency/index.php`. Это позволит вам темизировать `@app/widgets/currency/views/index.php` в `@app/themes/basic/widgets/currency/views/index.php`.
## Наследование тем <span id="theme-inheritance"></span> ## Наследование тем <span id="theme-inheritance"></span>

6
docs/guide/db-query-builder.md

@ -342,7 +342,7 @@ to representative class names:
And so on. And so on.
Using the object format makes it possible to create your own conditions or to change the way default ones are built. Using the object format makes it possible to create your own conditions or to change the way default ones are built.
See [Creating Custom Conditions and Expressions](#creating-custom-conditions-and-expressions) chapter to learn more. See [Adding Custom Conditions and Expressions](#adding-custom-conditions-and-expressions) chapter to learn more.
#### Appending Conditions <span id="appending-conditions"></span> #### Appending Conditions <span id="appending-conditions"></span>
@ -799,7 +799,7 @@ $unbufferedDb->close();
### Adding custom Conditions and Expressions <span id="adding-custom-conditions-and-expressions"></span> ### Adding custom Conditions and Expressions <span id="adding-custom-conditions-and-expressions"></span>
As it was mentioned in [Conditions – Object Format](#object-format) chapter, is is possible to create custom condition As it was mentioned in [Conditions – Object Format](#object-format) chapter, it is possible to create custom condition
classes. For example, let's create a condition that will check that specific columns are less than some value. classes. For example, let's create a condition that will check that specific columns are less than some value.
Using the operator format, it would look like the following: Using the operator format, it would look like the following:
@ -856,7 +856,7 @@ So we can create a condition object:
$conditon = new AllGreaterCondition(['col1', 'col2'], 42); $conditon = new AllGreaterCondition(['col1', 'col2'], 42);
``` ```
But `QueryBuilder` still does not know, to to make an SQL condition out of this object. But `QueryBuilder` still does not know, to make an SQL condition out of this object.
Now we need to create a builder for this condition. It must implement [[yii\db\ExpressionBuilderInterface]] Now we need to create a builder for this condition. It must implement [[yii\db\ExpressionBuilderInterface]]
that requires us to implement a `build()` method. that requires us to implement a `build()` method.

2
docs/guide/output-theming.md

@ -82,7 +82,7 @@ In order to theme widgets, you can configure [[yii\base\Theme::pathMap]] in the
], ],
``` ```
This will allow you to theme `@app/widgets/currency/views/index.php` into `@app/themes/basic/widgets/currency/index.php`. This will allow you to theme `@app/widgets/currency/views/index.php` into `@app/themes/basic/widgets/currency/views/index.php`.
## Theme Inheritance <span id="theme-inheritance"></span> ## Theme Inheritance <span id="theme-inheritance"></span>

16
docs/guide/runtime-requests.md

@ -162,7 +162,7 @@ which are located in the `10.0.2.0/24` IP network:
'request' => [ 'request' => [
// ... // ...
'trustedHosts' => [ 'trustedHosts' => [
'/^10\.0\.2\.\d+$/', '10.0.2.0/24',
], ],
], ],
``` ```
@ -175,18 +175,18 @@ In case your proxies are using different headers you can use the request configu
'request' => [ 'request' => [
// ... // ...
'trustedHosts' => [ 'trustedHosts' => [
'/^10\.0\.2\.\d+$/' => [ '10.0.2.0/24' => [
'X-ProxyUser-Ip', 'X-ProxyUser-Ip',
'Front-End-Https', 'Front-End-Https',
], ],
], ],
'secureHeaders' => [ 'secureHeaders' => [
'X-Forwarded-For', 'X-Forwarded-For',
'X-Forwarded-Host', 'X-Forwarded-Host',
'X-Forwarded-Proto', 'X-Forwarded-Proto',
'X-Proxy-User-Ip', 'X-Proxy-User-Ip',
'Front-End-Https', 'Front-End-Https',
]; ],
'ipHeaders' => [ 'ipHeaders' => [
'X-Proxy-User-Ip', 'X-Proxy-User-Ip',
], ],

1
docs/guide/start-looking-ahead.md

@ -27,6 +27,7 @@ This section will summarize the Yii resources available to help you be more prod
* Community * Community
- Forum: <http://www.yiiframework.com/forum/> - Forum: <http://www.yiiframework.com/forum/>
- IRC chat: The #yii channel on the freenode network (<irc://irc.freenode.net/yii>) - IRC chat: The #yii channel on the freenode network (<irc://irc.freenode.net/yii>)
- Slack chanel: <https://yii.slack.com>
- Gitter chat: <https://gitter.im/yiisoft/yii2> - Gitter chat: <https://gitter.im/yiisoft/yii2>
- GitHub: <https://github.com/yiisoft/yii2> - GitHub: <https://github.com/yiisoft/yii2>
- Facebook: <https://www.facebook.com/groups/yiitalk/> - Facebook: <https://www.facebook.com/groups/yiitalk/>

6
docs/guide/tutorial-yii-as-micro-framework.md

@ -142,7 +142,7 @@ to the application configuration:
> Info: We use an sqlite database here for simplicity. Please refer to the [Database guide](db-dao.md) for more options. > Info: We use an sqlite database here for simplicity. Please refer to the [Database guide](db-dao.md) for more options.
Next we create a [database migration](db-migrations.md) to create a posts table. Next we create a [database migration](db-migrations.md) to create a post table.
Make sure you have a separate configuration file as explained above, we need it to run the console commands below. Make sure you have a separate configuration file as explained above, we need it to run the console commands below.
Running the following commands will Running the following commands will
create a database migration file and apply the migration to the database: create a database migration file and apply the migration to the database:
@ -163,12 +163,12 @@ class Post extends ActiveRecord
{ {
public static function tableName() public static function tableName()
{ {
return '{{posts}}'; return '{{post}}';
} }
} }
``` ```
> Info: The model created here is an ActiveRecord class, which represents the data from the `posts` table. > Info: The model created here is an ActiveRecord class, which represents the data from the `post` table.
> Please refer to the [active record guide](db-active-record.md) for more information. > Please refer to the [active record guide](db-active-record.md) for more information.
To serve posts on our API, add the `PostController` in `controllers`: To serve posts on our API, add the `PostController` in `controllers`:

27
framework/CHANGELOG.md

@ -43,6 +43,26 @@ Yii Framework 2 Change Log
- Chg #15481: Removed `yii\BaseYii::powered()` method (Kolyunya, samdark) - Chg #15481: Removed `yii\BaseYii::powered()` method (Kolyunya, samdark)
2.0.14.1 February 24, 2018
--------------------------
- Bug #15318: Fixed `session_name(): Cannot change session name when session is active` errors (bscheshirwork, samdark)
- Bug #15678: Fixed `resetForm()` method in `yii.activeForm.js` which used an undefined variable (Izumi-kun)
- Bug #15692: Fix ExistValidator with targetRelation ignores filter (developeruz)
- Bug #15692: Fix `yii\validators\ExistValidator` to respect filter when `targetRelation` is used (developeruz)
- Bug #15693: Fixed `yii\filters\auth\HttpHeaderAuth` to work correctly when pattern is set but was not matched (bboure)
- Bug #15696: Fix magic getter for `yii\db\ActiveRecord` (developeruz)
- Bug #15707: Fixed JSON retrieving from MySQL (silverfire)
- Bug #15708: Fixed `yii\db\Command::upsert()` for Cubrid/MSSQL/Oracle (sergeymakinen)
- Bug #15724: Changed shortcut in `yii\console\controllers\BaseMigrateController` for `comment` option from `-c` to `-C` due to conflict (Izumi-kun)
- Bug #15726: Fix ExistValidator is broken for NOSQL (developeruz)
- Bug #15728, #15731: Fixed BC break in `Query::select()` method (silverfire)
- Bug #15742: Updated `yii\helpers\BaseHtml::setActivePlaceholder()` to be consistent with `activeLabel()` (edwards-sj)
- Enh #15716: Added `disableJsonSupport` to MySQL and PgSQL `ColumnSchema`, `disableArraySupport` and `deserializeArrayColumnToArrayExpression` to PgSQL `ColumnSchema` (silverfire)
- Enh #15716: Implemented `\Traversable` in `yii\db\ArrayExpression` (silverfire)
- Enh #15760: Added `ArrayAccess` support as validated value in `yii\validators\EachValidator` (silverfire)
2.0.14 February 18, 2018 2.0.14 February 18, 2018
------------------------ ------------------------
@ -56,7 +76,7 @@ Yii Framework 2 Change Log
- Bug #14296: Fixed log targets to throw exception in case log can not be properly exported (bizley) - Bug #14296: Fixed log targets to throw exception in case log can not be properly exported (bizley)
- Bug #14484: Fixed `yii\validators\UniqueValidator` for target classes with a default scope (laszlovl, developeruz) - Bug #14484: Fixed `yii\validators\UniqueValidator` for target classes with a default scope (laszlovl, developeruz)
- Bug #14604: Fixed `yii\validators\CompareValidator` `compareAttribute` does not work if `compareAttribute` form ID has been changed (mikk150) - Bug #14604: Fixed `yii\validators\CompareValidator` `compareAttribute` does not work if `compareAttribute` form ID has been changed (mikk150)
- Bug #14711 (CVE-2018-6010): Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark) - Bug #14711: (CVE-2018-6010): Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark)
- Bug #14811: Fixed `yii\filters\HttpCache` to work with PHP 7.2 (samdark) - Bug #14811: Fixed `yii\filters\HttpCache` to work with PHP 7.2 (samdark)
- Bug #14859: Fixed OCI DB `defaultSchema` failure when `masterConfig` is used (lovezhl456) - Bug #14859: Fixed OCI DB `defaultSchema` failure when `masterConfig` is used (lovezhl456)
- Bug #14903: Fixed route with extra dashes is executed controller while it should not (developeruz) - Bug #14903: Fixed route with extra dashes is executed controller while it should not (developeruz)
@ -158,7 +178,7 @@ Yii Framework 2 Change Log
- Enh #15422: Added default roles dynamic definition support via closure for `yii\rbac\BaseManager` (deltacube) - Enh #15422: Added default roles dynamic definition support via closure for `yii\rbac\BaseManager` (deltacube)
- Enh #15426: Added abilitiy to create and drop database views (igravity, vladis84) - Enh #15426: Added abilitiy to create and drop database views (igravity, vladis84)
- Enh #15476: Added `\yii\widgets\ActiveForm::$validationStateOn` to be able to specify where to add class for invalid fields (samdark) - Enh #15476: Added `\yii\widgets\ActiveForm::$validationStateOn` to be able to specify where to add class for invalid fields (samdark)
- Enh #15496 (CVE-2018-6009): CSRF token is now regenerated on changing identity (samdark, rhertogh) - Enh #15496: (CVE-2018-6009): CSRF token is now regenerated on changing identity (samdark, rhertogh)
- Enh #15595: `yii\data\DataFilter` can now handle `lt`,`gt`,`lte` and `gte` on `yii\validators\DateValidator` (mikk150) - Enh #15595: `yii\data\DataFilter` can now handle `lt`,`gt`,`lte` and `gte` on `yii\validators\DateValidator` (mikk150)
- Enh #15661: Added `yii\db\ExpressionInterface` support to `yii\db\Command::batchInsert()` (silverfire) - Enh #15661: Added `yii\db\ExpressionInterface` support to `yii\db\Command::batchInsert()` (silverfire)
- Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire) - Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire)
@ -2148,3 +2168,6 @@ Yii Framework 2 Change Log
- [Smarty View Renderer](https://github.com/yiisoft/yii2-smarty) - [Smarty View Renderer](https://github.com/yiisoft/yii2-smarty)
- [Twig View Renderer](https://github.com/yiisoft/yii2-twig) - [Twig View Renderer](https://github.com/yiisoft/yii2-twig)

11
framework/UPGRADE.md

@ -174,8 +174,15 @@ Upgrade from Yii 2.0.13
introduced methods to retrieve the expression content. introduced methods to retrieve the expression content.
* Added JSON support for PostgreSQL and MySQL as well as Arrays support for PostgreSQL in ActiveRecord layer. * Added JSON support for PostgreSQL and MySQL as well as Arrays support for PostgreSQL in ActiveRecord layer.
In case you already implemented such support yourself, please switch to Yii implementation. Active Record will In case you already implemented such support yourself, please switch to Yii implementation.
return arrays instead of strings after data population and expects arrays to be assigned for further saving them into database. * For MySQL JSON and PgSQL JSON & JSONB columns Active Record will return decoded JSON (that can be either array or scalar) after data population
and expects arrays or scalars to be assigned for further saving them into a database.
* For PgSQL Array columns Active Record will return `yii\db\ArrayExpression` object that acts as an array
(it implements `ArrayAccess`, `Traversable` and `Countable` interfaces) and expects array or `yii\db\ArrayExpression` to be
assigned for further saving it into the database.
In case this change makes the upgrade process to Yii 2.0.14 too hard in your project, you can [switch off the described behavior](https://github.com/yiisoft/yii2/issues/15716#issuecomment-368143206)
Then you can take your time to change your code and then re-enable arrays or JSON support.
* `yii\db\PdoValue` class has been introduced to replace a special syntax that was used to declare PDO parameter type * `yii\db\PdoValue` class has been introduced to replace a special syntax that was used to declare PDO parameter type
when binding parameters to an SQL command, for example: `['value', \PDO::PARAM_STR]`. when binding parameters to an SQL command, for example: `['value', \PDO::PARAM_STR]`.

2
framework/behaviors/CacheableWidgetBehavior.php

@ -41,8 +41,6 @@ use yii\di\Instance;
* } * }
* ``` * ```
* *
* @property Widget $owner
*
* @author Nikolay Oleynikov <oleynikovny@mail.ru> * @author Nikolay Oleynikov <oleynikovny@mail.ru>
* @since 2.0.14 * @since 2.0.14
*/ */

2
framework/console/controllers/MigrateController.php

@ -160,7 +160,7 @@ class MigrateController extends BaseMigrateController
public function optionAliases() public function optionAliases()
{ {
return array_merge(parent::optionAliases(), [ return array_merge(parent::optionAliases(), [
'c' => 'comment', 'C' => 'comment',
'f' => 'fields', 'f' => 'fields',
'p' => 'migrationPath', 'p' => 'migrationPath',
't' => 'migrationTable', 't' => 'migrationTable',

37
framework/db/ArrayExpression.php

@ -7,6 +7,9 @@
namespace yii\db; namespace yii\db;
use Traversable;
use yii\base\InvalidConfigException;
/** /**
* Class ArrayExpression represents an array SQL expression. * Class ArrayExpression represents an array SQL expression.
* *
@ -22,7 +25,7 @@ namespace yii\db;
* @author Dmytro Naumenko <d.naumenko.a@gmail.com> * @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14 * @since 2.0.14
*/ */
class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable, \IteratorAggregate
{ {
/** /**
* @var null|string the type of the array elements. Defaults to `null` which means the type is * @var null|string the type of the array elements. Defaults to `null` which means the type is
@ -33,8 +36,8 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
*/ */
private $type; private $type;
/** /**
* @var array|QueryInterface|mixed the array content. Either represented as an array of values or a [[Query]] that * @var array|QueryInterface the array's content.
* returns these values. A single value will be considered as an array containing one element. * In can be represented as an array of values or a [[Query]] that returns these values.
*/ */
private $value; private $value;
/** /**
@ -98,7 +101,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* </p> * </p>
* <p> * <p>
* The return value will be casted to boolean if non-boolean was returned. * The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0 * @since 2.0.14
*/ */
public function offsetExists($offset) public function offsetExists($offset)
{ {
@ -113,7 +116,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The offset to retrieve. * The offset to retrieve.
* </p> * </p>
* @return mixed Can return all value types. * @return mixed Can return all value types.
* @since 5.0.0 * @since 2.0.14
*/ */
public function offsetGet($offset) public function offsetGet($offset)
{ {
@ -131,7 +134,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The value to set. * The value to set.
* </p> * </p>
* @return void * @return void
* @since 5.0.0 * @since 2.0.14
*/ */
public function offsetSet($offset, $value) public function offsetSet($offset, $value)
{ {
@ -146,7 +149,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The offset to unset. * The offset to unset.
* </p> * </p>
* @return void * @return void
* @since 5.0.0 * @since 2.0.14
*/ */
public function offsetUnset($offset) public function offsetUnset($offset)
{ {
@ -161,10 +164,28 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* </p> * </p>
* <p> * <p>
* The return value is cast to an integer. * The return value is cast to an integer.
* @since 5.1.0 * @since 2.0.14
*/ */
public function count() public function count()
{ {
return count($this->value); return count($this->value);
} }
/**
* Retrieve an external iterator
*
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
* @return Traversable An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
* @since 2.0.14.1
* @throws InvalidConfigException when ArrayExpression contains QueryInterface object
*/
public function getIterator()
{
if ($this->getValue() instanceof QueryInterface) {
throw new InvalidConfigException('The ArrayExpression class can not be iterated when the value is a QueryInterface object');
}
return new \ArrayIterator($this->getValue());
}
} }

2
framework/db/BaseActiveRecord.php

@ -1727,7 +1727,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/ */
private function setRelationDependencies($name, $relation) private function setRelationDependencies($name, $relation)
{ {
if (empty($relation->via)) { if (empty($relation->via) && $relation->link) {
foreach ($relation->link as $attribute) { foreach ($relation->link as $attribute) {
$this->_relationsDependencies[$attribute][$name] = $name; $this->_relationsDependencies[$attribute][$name] = $name;
} }

26
framework/db/Query.php

@ -658,22 +658,28 @@ PATTERN;
*/ */
protected function getUniqueColumns($columns) protected function getUniqueColumns($columns)
{ {
$columns = array_unique($columns);
$unaliasedColumns = $this->getUnaliasedColumnsFromSelect(); $unaliasedColumns = $this->getUnaliasedColumnsFromSelect();
$result = [];
foreach ($columns as $columnAlias => $columnDefinition) { foreach ($columns as $columnAlias => $columnDefinition) {
if ($columnDefinition instanceof Query) { if (!$columnDefinition instanceof Query) {
continue; if (is_string($columnAlias)) {
$existsInSelect = isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition;
if ($existsInSelect) {
continue;
}
} elseif (is_integer($columnAlias)) {
$existsInSelect = in_array($columnDefinition, $unaliasedColumns, true);
$existsInResultSet = in_array($columnDefinition, $result, true);
if ($existsInSelect || $existsInResultSet) {
continue;
}
}
} }
if ( $result[$columnAlias] = $columnDefinition;
(is_string($columnAlias) && isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition)
|| (is_integer($columnAlias) && in_array($columnDefinition, $unaliasedColumns))
) {
unset($columns[$columnAlias]);
}
} }
return $columns; return $result;
} }
/** /**

16
framework/db/QueryBuilder.php

@ -22,8 +22,8 @@ use yii\helpers\StringHelper;
* *
* For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder). * For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder).
* *
* @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
* in [[expressionBuilders]] property. This property is write-only. * [[expressionBuilders]] property. This property is write-only.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -611,15 +611,15 @@ class QueryBuilder extends \yii\base\BaseObject
$columnSchemas = $tableSchema !== null ? $tableSchema->columns : []; $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
$sets = []; $sets = [];
foreach ($columns as $name => $value) { foreach ($columns as $name => $value) {
$value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
if ($value instanceof ExpressionInterface) { if ($value instanceof ExpressionInterface) {
$sets[] = $this->db->quoteColumnName($name) . '=' . $this->buildExpression($value, $params); $placeholder = $this->buildExpression($value, $params);
} else { } else {
$phName = $this->bindParam( $placeholder = $this->bindParam($value, $params);
isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
$params
);
$sets[] = $this->db->quoteColumnName($name) . '=' . $phName;
} }
$sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
} }
return [$sets, $params]; return [$sets, $params];
} }

2
framework/db/Schema.php

@ -87,7 +87,7 @@ abstract class Schema extends BaseObject
'SQLSTATE[23' => IntegrityException::class, 'SQLSTATE[23' => IntegrityException::class,
]; ];
/** /**
* @var string column schema class * @var string|array column schema class or class config
* @since 2.0.11 * @since 2.0.11
*/ */
public $columnSchemaClass = ColumnSchema::class; public $columnSchemaClass = ColumnSchema::class;

2
framework/db/cubrid/QueryBuilder.php

@ -85,7 +85,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
[, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
$mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' ' $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
. 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS "EXCLUDED" (' . implode(', ', $insertNames) . ') ' . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS "EXCLUDED" (' . implode(', ', $insertNames) . ') '
. 'ON ' . $on; . "ON ($on)";
$insertValues = []; $insertValues = [];
foreach ($insertNames as $name) { foreach ($insertNames as $name) {
$quotedName = $this->db->quoteColumnName($name); $quotedName = $this->db->quoteColumnName($name);

2
framework/db/mssql/QueryBuilder.php

@ -382,7 +382,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
[, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
$mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) ' $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
. 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') '
. 'ON ' . $on; . "ON ($on)";
$insertValues = []; $insertValues = [];
foreach ($insertNames as $name) { foreach ($insertNames as $name) {
$quotedName = $this->db->quoteColumnName($name); $quotedName = $this->db->quoteColumnName($name);

63
framework/db/mysql/ColumnSchema.php

@ -0,0 +1,63 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db\mysql;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
/**
* Class ColumnSchema for MySQL database
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14.1
*/
class ColumnSchema extends \yii\db\ColumnSchema
{
/**
* @var bool whether the column schema should OMIT using JSON support feature.
* You can use this property to make upgrade to Yii 2.0.14 easier.
* Default to `false`, meaning JSON support is enabled.
*
* @since 2.0.14.1
* @deprecated Since 2.0.14.1 and will be removed in 2.1.
*/
public $disableJsonSupport = false;
/**
* {@inheritdoc}
*/
public function dbTypecast($value)
{
if ($value instanceof ExpressionInterface) {
return $value;
}
if (!$this->disableJsonSupport && $this->dbType === Schema::TYPE_JSON) {
return new JsonExpression($value, $this->type);
}
return $this->typecast($value);
}
/**
* {@inheritdoc}
*/
public function phpTypecast($value)
{
if ($value === null) {
return null;
}
if (!$this->disableJsonSupport && $this->type === Schema::TYPE_JSON) {
return json_decode($value, true);
}
return parent::phpTypecast($value);
}
}

4
framework/db/mysql/Schema.php

@ -31,6 +31,10 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface
use ConstraintFinderTrait; use ConstraintFinderTrait;
/** /**
* {@inheritdoc}
*/
public $columnSchemaClass = 'yii\db\mysql\ColumnSchema';
/**
* @var bool whether MySQL used is older than 5.1. * @var bool whether MySQL used is older than 5.1.
*/ */
private $_oldMysql; private $_oldMysql;

2
framework/db/oci/QueryBuilder.php

@ -236,7 +236,7 @@ EOD;
} }
$mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' ' $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
. 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" ' . 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" '
. 'ON ' . $on; . "ON ($on)";
$insertValues = []; $insertValues = [];
foreach ($insertNames as $name) { foreach ($insertNames as $name) {
$quotedName = $this->db->quoteColumnName($name); $quotedName = $this->db->quoteColumnName($name);

41
framework/db/pgsql/ColumnSchema.php

@ -12,7 +12,7 @@ use yii\db\ExpressionInterface;
use yii\db\JsonExpression; use yii\db\JsonExpression;
/** /**
* Class ColumnSchema * Class ColumnSchema for PostgreSQL database.
* *
* @author Dmytro Naumenko <d.naumenko.a@gmail.com> * @author Dmytro Naumenko <d.naumenko.a@gmail.com>
*/ */
@ -22,6 +22,33 @@ class ColumnSchema extends \yii\db\ColumnSchema
* @var int the dimension of array. Defaults to 0, means this column is not an array. * @var int the dimension of array. Defaults to 0, means this column is not an array.
*/ */
public $dimension = 0; public $dimension = 0;
/**
* @var bool whether the column schema should OMIT using JSON support feature.
* You can use this property to make upgrade to Yii 2.0.14 easier.
* Default to `false`, meaning JSON support is enabled.
*
* @since 2.0.14.1
* @deprecated Since 2.0.14.1 and will be removed in 2.1.
*/
public $disableJsonSupport = false;
/**
* @var bool whether the column schema should OMIT using PgSQL Arrays support feature.
* You can use this property to make upgrade to Yii 2.0.14 easier.
* Default to `false`, meaning Arrays support is enabled.
*
* @since 2.0.14.1
* @deprecated Since 2.0.14.1 and will be removed in 2.1.
*/
public $disableArraySupport = false;
/**
* @var bool whether the Array column value should be unserialized to an [[ArrayExpression]] object.
* You can use this property to make upgrade to Yii 2.0.14 easier.
* Default to `true`, meaning arrays are unserialized to [[ArrayExpression]] objects.
*
* @since 2.0.14.1
* @deprecated Since 2.0.14.1 and will be removed in 2.1.
*/
public $deserializeArrayColumnToArrayExpression = true;
/** /**
@ -33,10 +60,10 @@ class ColumnSchema extends \yii\db\ColumnSchema
return $value; return $value;
} }
if ($this->dimension > 0) { if (!$this->disableArraySupport && $this->dimension > 0) {
return new ArrayExpression($value, $this->dbType, $this->dimension); return new ArrayExpression($value, $this->dbType, $this->dimension);
} }
if (in_array($this->dbType, [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) { if (!$this->disableJsonSupport && in_array($this->dbType, [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
return new JsonExpression($value, $this->type); return new JsonExpression($value, $this->type);
} }
@ -48,7 +75,7 @@ class ColumnSchema extends \yii\db\ColumnSchema
*/ */
public function phpTypecast($value) public function phpTypecast($value)
{ {
if ($this->dimension > 0) { if (!$this->disableArraySupport && $this->dimension > 0) {
if (!is_array($value)) { if (!is_array($value)) {
$value = $this->getArrayParser()->parse($value); $value = $this->getArrayParser()->parse($value);
} }
@ -58,7 +85,9 @@ class ColumnSchema extends \yii\db\ColumnSchema
}); });
} }
return new ArrayExpression($value, $this->dbType, $this->dimension); return $this->deserializeArrayColumnToArrayExpression
? new ArrayExpression($value, $this->dbType, $this->dimension)
: $value;
} }
return $this->phpTypecastValue($value); return $this->phpTypecastValue($value);
@ -88,7 +117,7 @@ class ColumnSchema extends \yii\db\ColumnSchema
} }
return (bool) $value; return (bool) $value;
case Schema::TYPE_JSON: case Schema::TYPE_JSON:
return json_decode($value, true); return $this->disableJsonSupport ? $value : json_decode($value, true);
} }
return parent::phpTypecast($value); return parent::phpTypecast($value);

9
framework/filters/auth/HttpHeaderAuth.php

@ -51,9 +51,14 @@ class HttpHeaderAuth extends AuthMethod
$authHeader = $request->getHeaderLine($this->header); $authHeader = $request->getHeaderLine($this->header);
if ($authHeader !== null) { if ($authHeader !== null) {
if ($this->pattern !== null && preg_match($this->pattern, $authHeader, $matches)) { if ($this->pattern !== null) {
$authHeader = $matches[1]; if (preg_match($this->pattern, $authHeader, $matches)) {
$authHeader = $matches[1];
} else {
return null;
}
} }
$identity = $user->loginByAccessToken($authHeader, get_class($this)); $identity = $user->loginByAccessToken($authHeader, get_class($this));
if ($identity === null) { if ($identity === null) {
$this->challenge($response); $this->challenge($response);

1
framework/helpers/BaseHtml.php

@ -1386,6 +1386,7 @@ class BaseHtml
protected static function setActivePlaceholder($model, $attribute, &$options = []) protected static function setActivePlaceholder($model, $attribute, &$options = [])
{ {
if (isset($options['placeholder']) && $options['placeholder'] === true) { if (isset($options['placeholder']) && $options['placeholder'] === true) {
$attribute = static::getAttributeName($attribute);
$options['placeholder'] = $model->getAttributeLabel($attribute); $options['placeholder'] = $model->getAttributeLabel($attribute);
} }
} }

2
framework/jquery/assets/yii.activeForm.js

@ -444,7 +444,7 @@
this.value = getValue($form, this); this.value = getValue($form, this);
this.status = 0; this.status = 0;
var $container = $form.find(this.container), var $container = $form.find(this.container),
$input = findInput($form, attribute), $input = findInput($form, this),
$errorElement = data.settings.validationStateOn === 'input' ? $input : $container; $errorElement = data.settings.validationStateOn === 'input' ? $input : $container;
$errorElement.removeClass( $errorElement.removeClass(

2
framework/messages/bs/yii.php

@ -35,7 +35,7 @@ return [
'Missing required arguments: {params}' => 'Nedostaju obavezni argumenti: {params}', 'Missing required arguments: {params}' => 'Nedostaju obavezni argumenti: {params}',
'Missing required parameters: {params}' => 'Nedostaju obavezni parametri: {params}', 'Missing required parameters: {params}' => 'Nedostaju obavezni parametri: {params}',
'No' => 'Ne', 'No' => 'Ne',
'No results found.' => 'Nema rezulatata.', 'No results found.' => 'Nema rezultata.',
'Only files with these MIME types are allowed: {mimeTypes}.' => 'Samo datoteke sa sljedećim MIME tipovima su dozvoljeni: {mimeTypes}.', 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Samo datoteke sa sljedećim MIME tipovima su dozvoljeni: {mimeTypes}.',
'Only files with these extensions are allowed: {extensions}.' => 'Samo datoteke sa sljedećim ekstenzijama su dozvoljeni: {extensions}.', 'Only files with these extensions are allowed: {extensions}.' => 'Samo datoteke sa sljedećim ekstenzijama su dozvoljeni: {extensions}.',
'Page not found.' => 'Stranica nije pronađena.', 'Page not found.' => 'Stranica nije pronađena.',

2
framework/rbac/BaseManager.php

@ -19,7 +19,7 @@ use yii\base\InvalidValueException;
* *
* @property Role[] $defaultRoleInstances Default roles. The array is indexed by the role names. This property * @property Role[] $defaultRoleInstances Default roles. The array is indexed by the role names. This property
* is read-only. * is read-only.
* @property array $defaultRoles Default roles. Note that the type of this property differs in getter and * @property string[] $defaultRoles Default roles. Note that the type of this property differs in getter and
* setter. See [[getDefaultRoles()]] and [[setDefaultRoles()]] for details. * setter. See [[getDefaultRoles()]] and [[setDefaultRoles()]] for details.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>

4
framework/validators/EachValidator.php

@ -129,7 +129,7 @@ class EachValidator extends Validator
public function validateAttribute($model, $attribute) public function validateAttribute($model, $attribute)
{ {
$value = $model->$attribute; $value = $model->$attribute;
if (!is_array($value)) { if (!is_array($value) && !$value instanceof \ArrayAccess) {
$this->addError($model, $attribute, $this->message, []); $this->addError($model, $attribute, $this->message, []);
return; return;
} }
@ -172,7 +172,7 @@ class EachValidator extends Validator
*/ */
protected function validateValue($value) protected function validateValue($value)
{ {
if (!is_array($value)) { if (!is_array($value) && !$value instanceof \ArrayAccess) {
return [$this->message, []]; return [$this->message, []];
} }

12
framework/validators/ExistValidator.php

@ -120,12 +120,18 @@ class ExistValidator extends Validator
/** @var ActiveQuery $relationQuery */ /** @var ActiveQuery $relationQuery */
$relationQuery = $model->{'get' . ucfirst($this->targetRelation)}(); $relationQuery = $model->{'get' . ucfirst($this->targetRelation)}();
if ($this->forceMasterDb) { if ($this->filter instanceof \Closure) {
call_user_func($this->filter, $relationQuery);
} elseif ($this->filter !== null) {
$relationQuery->andWhere($this->filter);
}
if ($this->forceMasterDb && method_exists($model::getDb(), 'useMaster')) {
$model::getDb()->useMaster(function() use ($relationQuery, &$exists) { $model::getDb()->useMaster(function() use ($relationQuery, &$exists) {
$exists = $relationQuery->exists(); $exists = $relationQuery->exists();
}); });
} else { } else {
$relationQuery->exists(); $exists = $relationQuery->exists();
} }
@ -247,7 +253,7 @@ class ExistValidator extends Validator
$db = $targetClass::getDb(); $db = $targetClass::getDb();
$exists = false; $exists = false;
if ($this->forceMasterDb) { if ($this->forceMasterDb && method_exists($db, 'useMaster')) {
$db->useMaster(function ($db) use ($query, $value, &$exists) { $db->useMaster(function ($db) use ($query, $value, &$exists) {
$exists = $this->queryValueExists($query, $value); $exists = $this->queryValueExists($query, $value);
}); });

2
framework/web/Session.php

@ -323,7 +323,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
*/ */
public function setName($value) public function setName($value)
{ {
$this->freeze();
session_name($value); session_name($value);
$this->unfreeze();
} }
/** /**

1
tests/data/ar/Document.php

@ -12,6 +12,7 @@ namespace yiiunit\data\ar;
* @property string $title * @property string $title
* @property string $content * @property string $content
* @property int $version * @property int $version
* @property array $properties
*/ */
class Document extends ActiveRecord class Document extends ActiveRecord
{ {

5
tests/data/ar/OrderItem.php

@ -44,4 +44,9 @@ class OrderItem extends ActiveRecord
{ {
return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ]); return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ]);
} }
public function getCustom()
{
return Order::find();
}
} }

23
tests/data/ar/Storage.php

@ -0,0 +1,23 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\data\ar;
/**
* Class Animal
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @property int $id
* @property array $data
*/
class Storage extends ActiveRecord
{
public static function tableName()
{
return 'storage';
}
}

84
tests/data/base/ArrayAccessObject.php

@ -0,0 +1,84 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\data\base;
/**
* ArrayAccessObject
* Object that extends [[TraversableObject]] and implements `\ArrayAccess`
* Used for testing support for ArrayAccess object instead of arrays.
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14.1
*/
class ArrayAccessObject extends TraversableObject implements \ArrayAccess
{
/**
* Whether a offset exists
*
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 2.0.14.1
*/
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
/**
* Offset to retrieve
*
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 2.0.14.1
*/
public function offsetGet($offset)
{
return $this->data[$offset];
}
/**
* Offset to set
*
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @since 2.0.14.1
*/
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
/**
* Offset to unset
*
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 2.0.14.1
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}

2
tests/data/base/TraversableObject.php

@ -16,7 +16,7 @@ namespace yiiunit\data\base;
*/ */
class TraversableObject implements \Iterator, \Countable class TraversableObject implements \Iterator, \Countable
{ {
private $data; protected $data;
private $position = 0; private $position = 0;
public function __construct(array $array) public function __construct(array $array)

4
tests/data/mssql.sql

@ -231,16 +231,20 @@ INSERT INTO [dbo].[order_item_with_null_fk] ([order_id], [item_id], [quantity],
INSERT INTO [dbo].[document] ([title], [content], [version]) VALUES ('Yii 2.0 guide', 'This is Yii 2.0 guide', 0); INSERT INTO [dbo].[document] ([title], [content], [version]) VALUES ('Yii 2.0 guide', 'This is Yii 2.0 guide', 0);
SET IDENTITY_INSERT [dbo].[department] ON;
INSERT INTO [dbo].[department] (id, title) VALUES (1, 'IT'); INSERT INTO [dbo].[department] (id, title) VALUES (1, 'IT');
INSERT INTO [dbo].[department] (id, title) VALUES (2, 'accounting'); INSERT INTO [dbo].[department] (id, title) VALUES (2, 'accounting');
SET IDENTITY_INSERT [dbo].[department] OFF;
INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (1, 1, 'John', 'Doe'); INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (1, 1, 'John', 'Doe');
INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (1, 2, 'Ann', 'Smith'); INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (1, 2, 'Ann', 'Smith');
INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (2, 2, 'Will', 'Smith'); INSERT INTO [dbo].[employee] (id, department_id, first_name, last_name) VALUES (2, 2, 'Will', 'Smith');
SET IDENTITY_INSERT [dbo].[dossier] ON;
INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (1, 1, 1, 'Excellent employee.'); INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (1, 1, 1, 'Excellent employee.');
INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (2, 2, 1, 'Brilliant employee.'); INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (2, 2, 1, 'Brilliant employee.');
INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (3, 2, 2, 'Good employee.'); INSERT INTO [dbo].[dossier] (id, department_id, employee_id, summary) VALUES (3, 2, 2, 'Good employee.');
SET IDENTITY_INSERT [dbo].[dossier] OFF;
/* bit test, see https://github.com/yiisoft/yii2/issues/9006 */ /* bit test, see https://github.com/yiisoft/yii2/issues/9006 */

7
tests/data/mysql.sql

@ -23,6 +23,7 @@ DROP TABLE IF EXISTS `comment` CASCADE;
DROP TABLE IF EXISTS `dossier`; DROP TABLE IF EXISTS `dossier`;
DROP TABLE IF EXISTS `employee`; DROP TABLE IF EXISTS `employee`;
DROP TABLE IF EXISTS `department`; DROP TABLE IF EXISTS `department`;
DROP TABLE IF EXISTS `storage`;
DROP VIEW IF EXISTS `animal_view`; DROP VIEW IF EXISTS `animal_view`;
DROP TABLE IF EXISTS `T_constraints_4` CASCADE; DROP TABLE IF EXISTS `T_constraints_4` CASCADE;
DROP TABLE IF EXISTS `T_constraints_3` CASCADE; DROP TABLE IF EXISTS `T_constraints_3` CASCADE;
@ -202,6 +203,12 @@ CREATE TABLE `dossier` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `storage` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`data` JSON NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE VIEW `animal_view` AS SELECT * FROM `animal`; CREATE VIEW `animal_view` AS SELECT * FROM `animal`;
INSERT INTO `animal` (`type`) VALUES ('yiiunit\data\ar\Cat'); INSERT INTO `animal` (`type`) VALUES ('yiiunit\data\ar\Cat');

4
tests/data/oci.sql

@ -43,6 +43,8 @@ BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "bool_values_SEQ"'; EXCEPTION WHEN OTHERS
BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "animal_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "animal_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;--
BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "document_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "document_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;--
BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "T_upsert_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;-- BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "T_upsert_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;--
BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "department_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;--
BEGIN EXECUTE IMMEDIATE 'DROP SEQUENCE "employee_SEQ"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -2289 THEN RAISE; END IF; END;--
/* STATEMENTS */ /* STATEMENTS */
@ -213,7 +215,7 @@ CREATE TABLE "employee" (
"department_id" INTEGER NOT NULL, "department_id" INTEGER NOT NULL,
"first_name" varchar2(255) not null, "first_name" varchar2(255) not null,
"last_name" varchar2(255) not null, "last_name" varchar2(255) not null,
CONSTRAINT "employee_PK" PRIMARY KEY ("id") ENABLE CONSTRAINT "employee_PK" PRIMARY KEY ("id", "department_id") ENABLE
); );
CREATE SEQUENCE "employee_SEQ"; CREATE SEQUENCE "employee_SEQ";

3
tests/data/postgres.sql

@ -177,7 +177,7 @@ CREATE TABLE "default_pk" (
CREATE TABLE "document" ( CREATE TABLE "document" (
id serial primary key, id serial primary key,
title varchar(255) not null, title varchar(255) not null,
content text not null, content text,
version integer not null default 0 version integer not null default 0
); );
@ -325,6 +325,7 @@ INSERT INTO "bit_values" (id, val) VALUES (1, '0'), (2, '1');
DROP TABLE IF EXISTS "array_and_json_types" CASCADE; DROP TABLE IF EXISTS "array_and_json_types" CASCADE;
CREATE TABLE "array_and_json_types" ( CREATE TABLE "array_and_json_types" (
id SERIAL NOT NULL PRIMARY KEY,
intarray_col INT[], intarray_col INT[],
textarray2_col TEXT[][], textarray2_col TEXT[][],
json_col JSON, json_col JSON,

10
tests/framework/ChangeLogTest.php

@ -40,16 +40,20 @@ class ChangeLogTest extends TestCase
*/ */
public function testContributorLine($line) public function testContributorLine($line)
{ {
if ($line === '- no changes in this release.') {
$this->markTestSkipped('Placeholder line');
}
/** /**
* Each change line is tested for: * Each change line is tested for:
* - Starts with "- " * - Starts with "- "
* - Has a type: Bug, Enh, Chg, New * - Has a type: Bug, Enh, Chg, New
* - Has a number formatted like #12345 * - Has a number formatted like #12345 one or more times
* - Can contain CVE ID
* - Description starts after ": " * - Description starts after ": "
* - Description ends without a "." * - Description ends without a "."
* - Line ends with contributor name between "(" and ")". * - Line ends with contributor name between "(" and ")".
*/ */
//$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?: .*[^.] \(.*\)$/', $line); $this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?(\s\(CVE-[\d-]+\))?: .*[^.] \(.*\)$/', $line);
$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*( \(CVE\-\d+\-\d+\))*)?: .*[^.] \(.*\)$/', $line);
} }
} }

4
tests/framework/console/controllers/MigrateControllerTest.php

@ -171,6 +171,10 @@ class MigrateControllerTest extends TestCase
public function testCreateLongNamedMigration() public function testCreateLongNamedMigration()
{ {
$this->setOutputCallback(function($output) {
return null;
});
$migrationName = str_repeat('a', 180); $migrationName = str_repeat('a', 180);
$this->expectException('yii\console\Exception'); $this->expectException('yii\console\Exception');

6
tests/framework/db/ActiveRecordTest.php

@ -1725,4 +1725,10 @@ abstract class ActiveRecordTest extends DatabaseTestCase
->all(); ->all();
$this->assertCount(3, $orders); $this->assertCount(3, $orders);
} }
public function testCustomARRelation()
{
$orderItem = OrderItem::findOne(1);
$this->assertInstanceOf(Order::className(), $orderItem->custom);
}
} }

45
tests/framework/db/QueryTest.php

@ -63,6 +63,37 @@ abstract class QueryTest extends DatabaseTestCase
$query = new Query(); $query = new Query();
$query->select('name, name, name as X, name as X'); $query->select('name, name, name as X, name as X');
$this->assertEquals(['name', 'name as X'], array_values($query->select)); $this->assertEquals(['name', 'name as X'], array_values($query->select));
/** @see https://github.com/yiisoft/yii2/issues/15676 */
$query = (new Query())->select('id');
$this->assertSame(['id'], $query->select);
$query->select(['id', 'brand_id']);
$this->assertSame(['id', 'brand_id'], $query->select);
/** @see https://github.com/yiisoft/yii2/issues/15676 */
$query = (new Query())->select(['prefix' => 'LEFT(name, 7)', 'prefix_key' => 'LEFT(name, 7)']);
$this->assertSame(['prefix' => 'LEFT(name, 7)', 'prefix_key' => 'LEFT(name, 7)'], $query->select);
$query->addSelect(['LEFT(name,7) as test']);
$this->assertSame(['prefix' => 'LEFT(name, 7)', 'prefix_key' => 'LEFT(name, 7)', 'LEFT(name,7) as test'], $query->select);
$query->addSelect(['LEFT(name,7) as test']);
$this->assertSame(['prefix' => 'LEFT(name, 7)', 'prefix_key' => 'LEFT(name, 7)', 'LEFT(name,7) as test'], $query->select);
$query->addSelect(['test' => 'LEFT(name,7)']);
$this->assertSame(['prefix' => 'LEFT(name, 7)', 'prefix_key' => 'LEFT(name, 7)', 'LEFT(name,7) as test', 'test' => 'LEFT(name,7)'], $query->select);
/** @see https://github.com/yiisoft/yii2/issues/15731 */
$selectedCols = [
'total_sum' => 'SUM(f.amount)',
'in_sum' => 'SUM(IF(f.type = :type_in, f.amount, 0))',
'out_sum' => 'SUM(IF(f.type = :type_out, f.amount, 0))',
];
$query = (new Query())->select($selectedCols)->addParams([
':type_in' => 'in',
':type_out' => 'out',
':type_partner' => 'partner',
]);
$this->assertSame($selectedCols, $query->select);
$query->select($selectedCols);
$this->assertSame($selectedCols, $query->select);
} }
public function testFrom() public function testFrom()
@ -676,18 +707,4 @@ abstract class QueryTest extends DatabaseTestCase
$this->assertEquals('user11', $query->cache()->where(['id' => 1])->scalar($db)); $this->assertEquals('user11', $query->cache()->where(['id' => 1])->scalar($db));
}, 10); }, 10);
} }
/**
* @see https://github.com/yiisoft/yii2/issues/15676
*/
public function testIssue15676()
{
$query = (new Query())
->select('id')
->from('place');
$this->assertSame(['id'], $query->select);
$query->select(['id', 'brand_id']);
$this->assertSame(['id', 'brand_id'], $query->select);
}
} }

16
tests/framework/db/cubrid/QueryBuilderTest.php

@ -70,22 +70,22 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
{ {
$concreteData = [ $concreteData = [
'regular values' => [ 'regular values' => [
3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'regular values with update part' => [ 'regular values with update part' => [
3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"=:qp4, "status"=:qp5, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp4, "status"=:qp5, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'regular values without update part' => [ 'regular values without update part' => [
3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS "EXCLUDED" ("email", "address", "status", "profile_id") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'query' => [ 'query' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', 3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'query with update part' => [ 'query with update part' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', 3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'query without update part' => [ 'query without update part' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', 3 => 'MERGE INTO "T_upsert" USING (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0 LIMIT 1) AS "EXCLUDED" ("email", "status") ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'values and expressions' => [ 'values and expressions' => [
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
@ -97,10 +97,10 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
], ],
'query, values and expressions with update part' => [ 'query, values and expressions with update part' => [
3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) AS "EXCLUDED" ("email", [[time]]) ON (({{%T_upsert}}."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])', 3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) AS "EXCLUDED" ("email", [[time]]) ON ({{%T_upsert}}."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])',
], ],
'query, values and expressions without update part' => [ 'query, values and expressions without update part' => [
3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) AS "EXCLUDED" ("email", [[time]]) ON (({{%T_upsert}}."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])', 3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) AS "EXCLUDED" ("email", [[time]]) ON ({{%T_upsert}}."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])',
], ],
]; ];
$newData = parent::upsertProvider(); $newData = parent::upsertProvider();

16
tests/framework/db/mssql/QueryBuilderTest.php

@ -130,22 +130,22 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
{ {
$concreteData = [ $concreteData = [
'regular values' => [ 'regular values' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [address]=[EXCLUDED].[address], [status]=[EXCLUDED].[status], [profile_id]=[EXCLUDED].[profile_id] WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [address]=[EXCLUDED].[address], [status]=[EXCLUDED].[status], [profile_id]=[EXCLUDED].[profile_id] WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);',
], ],
'regular values with update part' => [ 'regular values with update part' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [address]=:qp4, [status]=:qp5, [orders]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [address]=:qp4, [status]=:qp5, [orders]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);',
], ],
'regular values without update part' => [ 'regular values without update part' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (VALUES (:qp0, :qp1, :qp2, :qp3)) AS [EXCLUDED] ([email], [address], [status], [profile_id]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN NOT MATCHED THEN INSERT ([email], [address], [status], [profile_id]) VALUES ([EXCLUDED].[email], [EXCLUDED].[address], [EXCLUDED].[status], [EXCLUDED].[profile_id]);',
], ],
'query' => [ 'query' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [status]=[EXCLUDED].[status] WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [status]=[EXCLUDED].[status] WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);',
], ],
'query with update part' => [ 'query with update part' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [address]=:qp1, [status]=:qp2, [orders]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [address]=:qp1, [status]=:qp2, [orders]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);',
], ],
'query without update part' => [ 'query without update part' => [
3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON (([T_upsert].[email]=[EXCLUDED].[email])) WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);', 3 => 'MERGE [T_upsert] WITH (HOLDLOCK) USING (SELECT [email], 2 AS [status] FROM [customer] WHERE [name]=:qp0 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [EXCLUDED] ([email], [status]) ON ([T_upsert].[email]=[EXCLUDED].[email]) WHEN NOT MATCHED THEN INSERT ([email], [status]) VALUES ([EXCLUDED].[email], [EXCLUDED].[status]);',
], ],
'values and expressions' => [ 'values and expressions' => [
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
@ -157,10 +157,10 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
], ],
'query, values and expressions with update part' => [ 'query, values and expressions with update part' => [
3 => 'MERGE {{%T_upsert}} WITH (HOLDLOCK) USING (SELECT :phEmail AS [email], now() AS [[time]]) AS [EXCLUDED] ([email], [[time]]) ON (({{%T_upsert}}.[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [ts]=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [[time]]) VALUES ([EXCLUDED].[email], [EXCLUDED].[[time]]);', 3 => 'MERGE {{%T_upsert}} WITH (HOLDLOCK) USING (SELECT :phEmail AS [email], now() AS [[time]]) AS [EXCLUDED] ([email], [[time]]) ON ({{%T_upsert}}.[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [ts]=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [[time]]) VALUES ([EXCLUDED].[email], [EXCLUDED].[[time]]);',
], ],
'query, values and expressions without update part' => [ 'query, values and expressions without update part' => [
3 => 'MERGE {{%T_upsert}} WITH (HOLDLOCK) USING (SELECT :phEmail AS [email], now() AS [[time]]) AS [EXCLUDED] ([email], [[time]]) ON (({{%T_upsert}}.[email]=[EXCLUDED].[email])) WHEN MATCHED THEN UPDATE SET [ts]=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [[time]]) VALUES ([EXCLUDED].[email], [EXCLUDED].[[time]]);', 3 => 'MERGE {{%T_upsert}} WITH (HOLDLOCK) USING (SELECT :phEmail AS [email], now() AS [[time]]) AS [EXCLUDED] ([email], [[time]]) ON ({{%T_upsert}}.[email]=[EXCLUDED].[email]) WHEN MATCHED THEN UPDATE SET [ts]=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ([email], [[time]]) VALUES ([EXCLUDED].[email], [EXCLUDED].[[time]]);',
], ],
]; ];
$newData = parent::upsertProvider(); $newData = parent::upsertProvider();

33
tests/framework/db/mysql/ActiveRecordTest.php

@ -7,6 +7,8 @@
namespace yiiunit\framework\db\mysql; namespace yiiunit\framework\db\mysql;
use yiiunit\data\ar\Storage;
/** /**
* @group db * @group db
* @group mysql * @group mysql
@ -14,4 +16,35 @@ namespace yiiunit\framework\db\mysql;
class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
{ {
public $driverName = 'mysql'; public $driverName = 'mysql';
public function testJsonColumn()
{
if (version_compare($this->getConnection()->getSchema()->getServerVersion(), '5.7', '<')) {
$this->markTestSkipped('JSON columns are not supported in MySQL < 5.7');
}
if (version_compare(PHP_VERSION, '5.6', '<')) {
$this->markTestSkipped('JSON columns are not supported in PDO for PHP < 5.6');
}
$data = [
'obj' => ['a' => ['b' => ['c' => 2.7418]]],
'array' => [1,2,null,3],
'null_field' => null,
'boolean_field' => true,
'last_update_time' => '2018-02-21',
];
$storage = new Storage(['data' => $data]);
$this->assertTrue($storage->save(), 'Storage can be saved');
$this->assertNotNull($storage->id);
$retrievedStorage = Storage::findOne($storage->id);
$this->assertSame($data, $retrievedStorage->data, 'Properties are restored from JSON to array without changes');
$retrievedStorage->data = ['updatedData' => $data];
$this->assertSame(1, $retrievedStorage->update(), 'Storage can be updated');
$retrievedStorage->refresh();
$this->assertSame(['updatedData' => $data], $retrievedStorage->data, 'Properties have been changed during update');
}
} }

22
tests/framework/db/mysql/QueryBuilderTest.php

@ -210,4 +210,26 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
], ],
]); ]);
} }
public function updateProvider()
{
$items = parent::updateProvider();
$items[] = [
'profile',
[
'description' => new JsonExpression(['abc' => 'def', 123, null]),
],
[
'id' => 1,
],
$this->replaceQuotes('UPDATE [[profile]] SET [[description]]=:qp0 WHERE [[id]]=:qp1'),
[
':qp0' => '{"abc":"def","0":123,"1":null}',
':qp1' => 1,
],
];
return $items;
}
} }

16
tests/framework/db/oci/QueryBuilderTest.php

@ -185,34 +185,34 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
{ {
$concreteData = [ $concreteData = [
'regular values' => [ 'regular values' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'regular values with update part' => [ 'regular values with update part' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"=:qp4, "status"=:qp5, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp4, "status"=:qp5, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'regular values without update part' => [ 'regular values without update part' => [
3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")', 3 => 'MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")',
], ],
'query' => [ 'query' => [
3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0), 3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0),
PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL)
SELECT * SELECT *
FROM PAGINATION FROM PAGINATION
WHERE rownum <= 1) "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "status"="EXCLUDED"."status" WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'query with update part' => [ 'query with update part' => [
3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0), 3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0),
PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL)
SELECT * SELECT *
FROM PAGINATION FROM PAGINATION
WHERE rownum <= 1) "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"=:qp1, "status"=:qp2, "orders"=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'query without update part' => [ 'query without update part' => [
3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0), 3 => 'MERGE INTO "T_upsert" USING (WITH USER_SQL AS (SELECT "email", 2 AS "status" FROM "customer" WHERE "name"=:qp0),
PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL)
SELECT * SELECT *
FROM PAGINATION FROM PAGINATION
WHERE rownum <= 1) "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")', WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NOT MATCHED THEN INSERT ("email", "status") VALUES ("EXCLUDED"."email", "EXCLUDED"."status")',
], ],
'values and expressions' => [ 'values and expressions' => [
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
@ -224,10 +224,10 @@ WHERE rownum <= 1) "EXCLUDED" ON (("T_upsert"."email"="EXCLUDED"."email")) WHEN
3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())',
], ],
'query, values and expressions with update part' => [ 'query, values and expressions with update part' => [
3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) "EXCLUDED" ON (({{%T_upsert}}."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])', 3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) "EXCLUDED" ON ({{%T_upsert}}."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])',
], ],
'query, values and expressions without update part' => [ 'query, values and expressions without update part' => [
3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) "EXCLUDED" ON (({{%T_upsert}}."email"="EXCLUDED"."email")) WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])', 3 => 'MERGE INTO {{%T_upsert}} USING (SELECT :phEmail AS "email", now() AS [[time]]) "EXCLUDED" ON ({{%T_upsert}}."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "ts"=:qp1, [[orders]]=T_upsert.orders + 1 WHEN NOT MATCHED THEN INSERT ("email", [[time]]) VALUES ("EXCLUDED"."email", "EXCLUDED".[[time]])',
], ],
]; ];
$newData = parent::upsertProvider(); $newData = parent::upsertProvider();

24
tests/framework/db/pgsql/ActiveRecordTest.php

@ -195,21 +195,35 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
$value = $type->$attribute; $value = $type->$attribute;
$this->assertEquals($expected, $value, 'In column ' . $attribute); $this->assertEquals($expected, $value, 'In column ' . $attribute);
if ($value instanceof ArrayExpression) { if ($value instanceof ArrayExpression) {
$this->assertInstanceOf('\ArrayAccess', $value); $this->assertInstanceOf('\ArrayAccess', $value);
$this->assertInstanceOf('\Traversable', $value);
foreach ($type->$attribute as $key => $v) { // testing arrayaccess foreach ($type->$attribute as $key => $v) { // testing arrayaccess
$this->assertSame($expected[$key], $value[$key]); $this->assertSame($expected[$key], $value[$key]);
} }
} }
} }
// Testing UPDATE
foreach ($attributes as $attribute => $expected) {
$type->markAttributeDirty($attribute);
}
$this->assertSame(1, $type->update(), 'The record got updated');
} }
public function arrayValuesProvider() public function arrayValuesProvider()
{ {
return [ return [
'simple arrays values' => [[ 'simple arrays values' => [[
'intarray_col' => [new ArrayExpression([1,-2,null,'42'], 'int4', 1)], 'intarray_col' => [
'textarray2_col' => [new ArrayExpression([['text'], [null], [1]], 'text', 2)], new ArrayExpression([1,-2,null,'42'], 'int4', 1),
new ArrayExpression([1,-2,null,42], 'int4', 1),
],
'textarray2_col' => [
new ArrayExpression([['text'], [null], [1]], 'text', 2),
new ArrayExpression([['text'], [null], ['1']], 'text', 2),
],
'json_col' => [['a' => 1, 'b' => null, 'c' => [1,3,5]]], 'json_col' => [['a' => 1, 'b' => null, 'c' => [1,3,5]]],
'jsonb_col' => [[null, 'a', 'b', '\"', '{"af"}']], 'jsonb_col' => [[null, 'a', 'b', '\"', '{"af"}']],
'jsonarray_col' => [new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json')], 'jsonarray_col' => [new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json')],
@ -217,11 +231,11 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
'arrays packed in classes' => [[ 'arrays packed in classes' => [[
'intarray_col' => [ 'intarray_col' => [
new ArrayExpression([1,-2,null,'42'], 'int', 1), new ArrayExpression([1,-2,null,'42'], 'int', 1),
new ArrayExpression([1,-2,null,'42'], 'int4', 1), new ArrayExpression([1,-2,null,42], 'int4', 1),
], ],
'textarray2_col' => [ 'textarray2_col' => [
new ArrayExpression([['text'], [null], [1]], 'text', 2), new ArrayExpression([['text'], [null], [1]], 'text', 2),
new ArrayExpression([['text'], [null], [1]], 'text', 2), new ArrayExpression([['text'], [null], ['1']], 'text', 2),
], ],
'json_col' => [ 'json_col' => [
new JsonExpression(['a' => 1, 'b' => null, 'c' => [1,3,5]]), new JsonExpression(['a' => 1, 'b' => null, 'c' => [1,3,5]]),
@ -234,7 +248,6 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
'jsonarray_col' => [ 'jsonarray_col' => [
new Expression("array['[\",\",\"null\",true,\"false\",\"f\"]'::json]::json[]"), new Expression("array['[\",\",\"null\",true,\"false\",\"f\"]'::json]::json[]"),
new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json'), new ArrayExpression([[',', 'null', true, 'false', 'f']], 'json'),
] ]
]], ]],
'scalars' => [[ 'scalars' => [[
@ -278,6 +291,7 @@ class UserAR extends ActiveRecord
/** /**
* {@inheritdoc} * {@inheritdoc}
* @property array id
* @property array intarray_col * @property array intarray_col
* @property array textarray2_col * @property array textarray2_col
* @property array json_col * @property array json_col

22
tests/framework/db/pgsql/QueryBuilderTest.php

@ -331,4 +331,26 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
} }
return $newData; return $newData;
} }
public function updateProvider()
{
$items = parent::updateProvider();
$items[] = [
'profile',
[
'description' => new JsonExpression(['abc' => 'def', 123, null]),
],
[
'id' => 1,
],
$this->replaceQuotes('UPDATE [[profile]] SET [[description]]=:qp0 WHERE [[id]]=:qp1'),
[
':qp0' => '{"abc":"def","0":123,"1":null}',
':qp1' => 1,
],
];
return $items;
}
} }

4
tests/framework/filters/auth/AuthTest.php

@ -30,6 +30,10 @@ class AuthTest extends \yiiunit\TestCase
{ {
parent::setUp(); parent::setUp();
if (defined('HHVM_VERSION') && getenv('TRAVIS') == 'true') {
$this->markTestSkipped('Can not test reliably with HHVM on travis-ci.');
}
$_SERVER['SCRIPT_FILENAME'] = '/index.php'; $_SERVER['SCRIPT_FILENAME'] = '/index.php';
$_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['SCRIPT_NAME'] = '/index.php';

22
tests/framework/filters/auth/CompositeAuthTest.php

@ -10,6 +10,7 @@ namespace yiiunit\framework\filters\auth;
use Yii; use Yii;
use yii\filters\auth\AuthMethod; use yii\filters\auth\AuthMethod;
use yii\filters\auth\CompositeAuth; use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\rest\Controller; use yii\rest\Controller;
use yiiunit\framework\web\UserIdentity; use yiiunit\framework\web\UserIdentity;
@ -26,6 +27,8 @@ class TestAuth extends AuthMethod
class TestController extends Controller class TestController extends Controller
{ {
public $authMethods = [];
public function actionA() public function actionA()
{ {
return 'success'; return 'success';
@ -66,8 +69,8 @@ class TestController extends Controller
return [ return [
'authenticator' => [ 'authenticator' => [
'class' => CompositeAuth::class, 'class' => CompositeAuth::class,
'authMethods' => [ 'authMethods' => $this->authMethods ?: [
TestAuth::class TestAuth::class,
], ],
], ],
]; ];
@ -123,4 +126,19 @@ class CompositeAuthTest extends \yiiunit\TestCase
$controller = Yii::$app->createController('test')[0]; $controller = Yii::$app->createController('test')[0];
$this->assertEquals('success', $controller->run('c')); $this->assertEquals('success', $controller->run('c'));
} }
public function testCompositeAuth()
{
Yii::$app->request->headers->set('Authorization', base64_encode("foo:bar"));
/** @var TestAuthController $controller */
$controller = Yii::$app->createController('test')[0];
$controller->authMethods = [
HttpBearerAuth::class,
TestAuth::class,
];
try {
$this->assertEquals('success', $controller->run('b'));
} catch (UnauthorizedHttpException $e) {
}
}
} }

14
tests/framework/helpers/FileHelperTest.php

@ -908,12 +908,13 @@ class FileHelperTest extends TestCase
sort($foundFiles); sort($foundFiles);
$this->assertEquals($expectedFiles, $foundFiles); $this->assertEquals($expectedFiles, $foundFiles);
// filter
$expectedFiles = [ $expectedFiles = [
$dirName . DIRECTORY_SEPARATOR . 'second_sub_dir' $dirName . DIRECTORY_SEPARATOR . 'second_sub_dir'
]; ];
$options = [ $options = [
'filter' => function ($path) { 'filter' => function ($path) {
return 'second_sub_dir' == basename($path); return 'second_sub_dir' === basename($path);
}, },
]; ];
$foundFiles = FileHelper::findDirectories($dirName, $options); $foundFiles = FileHelper::findDirectories($dirName, $options);
@ -921,5 +922,16 @@ class FileHelperTest extends TestCase
sort($foundFiles); sort($foundFiles);
$this->assertEquals($expectedFiles, $foundFiles); $this->assertEquals($expectedFiles, $foundFiles);
// except
$expectedFiles = [
$dirName . DIRECTORY_SEPARATOR . 'second_sub_dir'
];
$options = [
'except' => ['test_sub_dir'],
];
$foundFiles = FileHelper::findDirectories($dirName, $options);
sort($expectedFiles);
sort($foundFiles);
$this->assertEquals($expectedFiles, $foundFiles);
} }
} }

10
tests/framework/helpers/HtmlTest.php

@ -1698,6 +1698,16 @@ HTML;
$this->assertContains('placeholder="Custom placeholder"', $html); $this->assertContains('placeholder="Custom placeholder"', $html);
} }
public function testActiveTextInput_placeholderFillFromModelTabular()
{
$model = new HtmlTestModel();
$html = Html::activeTextInput($model, '[0]name', ['placeholder' => true]);
$this->assertContains('placeholder="Name"', $html);
}
} }
/** /**

16
tests/framework/validators/EachValidatorTest.php

@ -7,7 +7,10 @@
namespace yiiunit\framework\validators; namespace yiiunit\framework\validators;
use yii\db\ArrayExpression;
use yii\validators\EachValidator; use yii\validators\EachValidator;
use yiiunit\data\base\ArrayAccessObject;
use yiiunit\data\base\TraversableObject;
use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\data\validators\models\FakedValidationModel;
use yiiunit\TestCase; use yiiunit\TestCase;
@ -184,4 +187,17 @@ class EachValidatorTest extends TestCase
$validator->validateAttribute($model, 'attr_one'); $validator->validateAttribute($model, 'attr_one');
$this->assertCount(2, $model->getErrors('attr_one')); $this->assertCount(2, $model->getErrors('attr_one'));
} }
public function testValidateArrayAccess()
{
$model = FakedValidationModel::createWithAttributes([
'attr_array' => new ArrayAccessObject([1,2,3]),
]);
$validator = new EachValidator(['rule' => ['integer']]);
$validator->validateAttribute($model, 'attr_array');
$this->assertFalse($model->hasErrors('array'));
$this->assertTrue($validator->validate($model->attr_array));
}
} }

17
tests/framework/validators/ExistValidatorTest.php

@ -218,6 +218,23 @@ abstract class ExistValidatorTest extends DatabaseTestCase
$val->validateAttribute($m, 'id'); $val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id')); $this->assertTrue($m->hasErrors('id'));
} }
public function testTargetRelationWithFilter()
{
$val = new ExistValidator(['targetRelation' => 'references', 'filter' => function ($query) {
$query->andWhere(['a_field' => 'ref_to_2']);
}]);
$m = ValidatorTestMainModel::findOne(2);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
$val = new ExistValidator(['targetRelation' => 'references', 'filter' => function ($query) {
$query->andWhere(['a_field' => 'ref_to_3']);
}]);
$m = ValidatorTestMainModel::findOne(2);
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
}
public function testForceMaster() public function testForceMaster()
{ {

2
tests/framework/web/UserIdentity.php

@ -43,4 +43,4 @@ class UserIdentity extends Component implements IdentityInterface
{ {
return $authKey === 'ABCD1234'; return $authKey === 'ABCD1234';
} }
} }

18
tests/framework/web/session/SessionTest.php

@ -70,4 +70,22 @@ class SessionTest extends TestCase
$this->assertNotEquals($oldGcProbability, $newGcProbability); $this->assertNotEquals($oldGcProbability, $newGcProbability);
$this->assertEquals(100, $newGcProbability); $this->assertEquals(100, $newGcProbability);
} }
/**
* Test set name. Also check set name twice and after open
*/
public function testSetName()
{
$session = new Session();
$session->setName('oldName');
$this->assertEquals('oldName', $session->getName());
$session->open();
$session->setName('newName');
$this->assertEquals('newName', $session->getName());
$session->destroy();
}
} }

4
tests/framework/widgets/FragmentCacheTest.php

@ -198,6 +198,10 @@ class FragmentCacheTest extends \yiiunit\TestCase
public function testVariations() public function testVariations()
{ {
$this->setOutputCallback(function($output) {
return null;
});
ob_start(); ob_start();
ob_implicit_flush(false); ob_implicit_flush(false);
$view = new View(); $view = new View();

3
tests/framework/widgets/PjaxTest.php

@ -45,6 +45,7 @@ class PjaxTest extends TestCase
public function testShouldTriggerInitEvent() public function testShouldTriggerInitEvent()
{ {
$initTriggered = false; $initTriggered = false;
ob_start();
$pjax = new Pjax( $pjax = new Pjax(
[ [
'on init' => function () use (&$initTriggered) { 'on init' => function () use (&$initTriggered) {
@ -52,7 +53,7 @@ class PjaxTest extends TestCase
} }
] ]
); );
ob_end_clean();
$this->assertTrue($initTriggered); $this->assertTrue($initTriggered);
} }
} }

44
tests/js/tests/yii.activeForm.test.js

@ -80,6 +80,50 @@ describe('yii.activeForm', function () {
}); });
}); });
describe('resetForm method', function () {
var windowSetTimeoutStub;
beforeEach(function () {
windowSetTimeoutStub = sinon.stub(window, 'setTimeout', function (callback) {
callback();
});
});
afterEach(function () {
windowSetTimeoutStub.restore();
});
it('should remove classes from error element', function () {
var inputId = 'name';
var $input = $('#' + inputId);
var options = {
validatingCssClass: 'validating',
errorCssClass: 'error',
successCssClass: 'success',
validationStateOn: 'input'
};
$activeForm = $('#w0');
$activeForm.yiiActiveForm('destroy');
$activeForm.yiiActiveForm([
{
id: inputId,
input: '#' + inputId
}
], options);
$input.addClass(options.validatingCssClass);
$input.addClass(options.errorCssClass);
$input.addClass(options.successCssClass);
$input.addClass('test');
$activeForm.yiiActiveForm('resetForm');
assert.isFalse($input.hasClass(options.validatingCssClass));
assert.isFalse($input.hasClass(options.errorCssClass));
assert.isFalse($input.hasClass(options.successCssClass));
assert.isTrue($input.hasClass('test'));
});
});
describe('events', function () { describe('events', function () {
describe('afterValidateAttribute', function () { describe('afterValidateAttribute', function () {
var afterValidateAttributeSpy; var afterValidateAttributeSpy;

Loading…
Cancel
Save