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. 6
      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. 24
      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. 7
      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. 18
      tests/framework/web/session/SessionTest.php
  61. 4
      tests/framework/widgets/FragmentCacheTest.php
  62. 3
      tests/framework/widgets/PjaxTest.php
  63. 44
      tests/js/tests/yii.activeForm.test.js

1
.gitignore vendored

@ -38,6 +38,7 @@ phpunit.phar
# ignore dev installed apps and extensions
/apps
/extensions
/packages
# NPM packages
/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
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
----------------------

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>

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>

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>

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

@ -342,7 +342,7 @@ to representative class names:
And so on.
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>
@ -799,7 +799,7 @@ $unbufferedDb->close();
### 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.
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);
```
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]]
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>

6
docs/guide/runtime-requests.md

@ -162,7 +162,7 @@ which are located in the `10.0.2.0/24` IP network:
'request' => [
// ...
'trustedHosts' => [
'/^10\.0\.2\.\d+$/',
'10.0.2.0/24',
],
],
```
@ -175,7 +175,7 @@ In case your proxies are using different headers you can use the request configu
'request' => [
// ...
'trustedHosts' => [
'/^10\.0\.2\.\d+$/' => [
'10.0.2.0/24' => [
'X-ProxyUser-Ip',
'Front-End-Https',
],
@ -186,7 +186,7 @@ In case your proxies are using different headers you can use the request configu
'X-Forwarded-Proto',
'X-Proxy-User-Ip',
'Front-End-Https',
];
],
'ipHeaders' => [
'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
- Forum: <http://www.yiiframework.com/forum/>
- 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>
- GitHub: <https://github.com/yiisoft/yii2>
- 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.
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.
Running the following commands will
create a database migration file and apply the migration to the database:
@ -163,12 +163,12 @@ class Post extends ActiveRecord
{
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.
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)
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
------------------------
@ -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 #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 #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 #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)
@ -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 #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 #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 #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)
@ -2148,3 +2168,6 @@ Yii Framework 2 Change Log
- [Smarty View Renderer](https://github.com/yiisoft/yii2-smarty)
- [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.
* 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
return arrays instead of strings after data population and expects arrays to be assigned for further saving them into database.
In case you already implemented such support yourself, please switch to Yii implementation.
* 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
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>
* @since 2.0.14
*/

2
framework/console/controllers/MigrateController.php

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

37
framework/db/ArrayExpression.php

@ -7,6 +7,9 @@
namespace yii\db;
use Traversable;
use yii\base\InvalidConfigException;
/**
* Class ArrayExpression represents an array SQL expression.
*
@ -22,7 +25,7 @@ namespace yii\db;
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @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
@ -33,8 +36,8 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
*/
private $type;
/**
* @var array|QueryInterface|mixed the array content. Either represented as an array of values or a [[Query]] that
* returns these values. A single value will be considered as an array containing one element.
* @var array|QueryInterface the array's content.
* In can be represented as an array of values or a [[Query]] that returns these values.
*/
private $value;
/**
@ -98,7 +101,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* </p>
* <p>
* 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)
{
@ -113,7 +116,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
* @since 2.0.14
*/
public function offsetGet($offset)
{
@ -131,7 +134,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The value to set.
* </p>
* @return void
* @since 5.0.0
* @since 2.0.14
*/
public function offsetSet($offset, $value)
{
@ -146,7 +149,7 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
* @since 2.0.14
*/
public function offsetUnset($offset)
{
@ -161,10 +164,28 @@ class ArrayExpression implements ExpressionInterface, \ArrayAccess, \Countable
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
* @since 2.0.14
*/
public function count()
{
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)
{
if (empty($relation->via)) {
if (empty($relation->via) && $relation->link) {
foreach ($relation->link as $attribute) {
$this->_relationsDependencies[$attribute][$name] = $name;
}

24
framework/db/Query.php

@ -658,22 +658,28 @@ PATTERN;
*/
protected function getUniqueColumns($columns)
{
$columns = array_unique($columns);
$unaliasedColumns = $this->getUnaliasedColumnsFromSelect();
$result = [];
foreach ($columns as $columnAlias => $columnDefinition) {
if ($columnDefinition instanceof Query) {
if (!$columnDefinition instanceof Query) {
if (is_string($columnAlias)) {
$existsInSelect = isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition;
if ($existsInSelect) {
continue;
}
if (
(is_string($columnAlias) && isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition)
|| (is_integer($columnAlias) && in_array($columnDefinition, $unaliasedColumns))
) {
unset($columns[$columnAlias]);
} elseif (is_integer($columnAlias)) {
$existsInSelect = in_array($columnDefinition, $unaliasedColumns, true);
$existsInResultSet = in_array($columnDefinition, $result, true);
if ($existsInSelect || $existsInResultSet) {
continue;
}
}
return $columns;
}
$result[$columnAlias] = $columnDefinition;
}
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).
*
* @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones
* in [[expressionBuilders]] property. This property is write-only.
* @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
* [[expressionBuilders]] property. This property is write-only.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
@ -611,15 +611,15 @@ class QueryBuilder extends \yii\base\BaseObject
$columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
$sets = [];
foreach ($columns as $name => $value) {
$value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
if ($value instanceof ExpressionInterface) {
$sets[] = $this->db->quoteColumnName($name) . '=' . $this->buildExpression($value, $params);
$placeholder = $this->buildExpression($value, $params);
} else {
$phName = $this->bindParam(
isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
$params
);
$sets[] = $this->db->quoteColumnName($name) . '=' . $phName;
$placeholder = $this->bindParam($value, $params);
}
$sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
}
return [$sets, $params];
}

2
framework/db/Schema.php

@ -87,7 +87,7 @@ abstract class Schema extends BaseObject
'SQLSTATE[23' => IntegrityException::class,
];
/**
* @var string column schema class
* @var string|array column schema class or class config
* @since 2.0.11
*/
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);
$mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
. 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS "EXCLUDED" (' . implode(', ', $insertNames) . ') '
. 'ON ' . $on;
. "ON ($on)";
$insertValues = [];
foreach ($insertNames as $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);
$mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
. 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') '
. 'ON ' . $on;
. "ON ($on)";
$insertValues = [];
foreach ($insertNames as $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;
/**
* {@inheritdoc}
*/
public $columnSchemaClass = 'yii\db\mysql\ColumnSchema';
/**
* @var bool whether MySQL used is older than 5.1.
*/
private $_oldMysql;

2
framework/db/oci/QueryBuilder.php

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

41
framework/db/pgsql/ColumnSchema.php

@ -12,7 +12,7 @@ use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
/**
* Class ColumnSchema
* Class ColumnSchema for PostgreSQL database.
*
* @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.
*/
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;
}
if ($this->dimension > 0) {
if (!$this->disableArraySupport && $this->dimension > 0) {
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);
}
@ -48,7 +75,7 @@ class ColumnSchema extends \yii\db\ColumnSchema
*/
public function phpTypecast($value)
{
if ($this->dimension > 0) {
if (!$this->disableArraySupport && $this->dimension > 0) {
if (!is_array($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);
@ -88,7 +117,7 @@ class ColumnSchema extends \yii\db\ColumnSchema
}
return (bool) $value;
case Schema::TYPE_JSON:
return json_decode($value, true);
return $this->disableJsonSupport ? $value : json_decode($value, true);
}
return parent::phpTypecast($value);

7
framework/filters/auth/HttpHeaderAuth.php

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

1
framework/helpers/BaseHtml.php

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

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

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

2
framework/messages/bs/yii.php

@ -35,7 +35,7 @@ return [
'Missing required arguments: {params}' => 'Nedostaju obavezni argumenti: {params}',
'Missing required parameters: {params}' => 'Nedostaju obavezni parametri: {params}',
'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 extensions are allowed: {extensions}.' => 'Samo datoteke sa sljedećim ekstenzijama su dozvoljeni: {extensions}.',
'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
* 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.
*
* @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)
{
$value = $model->$attribute;
if (!is_array($value)) {
if (!is_array($value) && !$value instanceof \ArrayAccess) {
$this->addError($model, $attribute, $this->message, []);
return;
}
@ -172,7 +172,7 @@ class EachValidator extends Validator
*/
protected function validateValue($value)
{
if (!is_array($value)) {
if (!is_array($value) && !$value instanceof \ArrayAccess) {
return [$this->message, []];
}

12
framework/validators/ExistValidator.php

@ -120,12 +120,18 @@ class ExistValidator extends Validator
/** @var ActiveQuery $relationQuery */
$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) {
$exists = $relationQuery->exists();
});
} else {
$relationQuery->exists();
$exists = $relationQuery->exists();
}
@ -247,7 +253,7 @@ class ExistValidator extends Validator
$db = $targetClass::getDb();
$exists = false;
if ($this->forceMasterDb) {
if ($this->forceMasterDb && method_exists($db, 'useMaster')) {
$db->useMaster(function ($db) use ($query, $value, &$exists) {
$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)
{
$this->freeze();
session_name($value);
$this->unfreeze();
}
/**

1
tests/data/ar/Document.php

@ -12,6 +12,7 @@ namespace yiiunit\data\ar;
* @property string $title
* @property string $content
* @property int $version
* @property array $properties
*/
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' ]);
}
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
{
private $data;
protected $data;
private $position = 0;
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);
SET IDENTITY_INSERT [dbo].[department] ON;
INSERT INTO [dbo].[department] (id, title) VALUES (1, 'IT');
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, 2, 'Ann', '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 (2, 2, 1, 'Brilliant 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 */

7
tests/data/mysql.sql

@ -23,6 +23,7 @@ DROP TABLE IF EXISTS `comment` CASCADE;
DROP TABLE IF EXISTS `dossier`;
DROP TABLE IF EXISTS `employee`;
DROP TABLE IF EXISTS `department`;
DROP TABLE IF EXISTS `storage`;
DROP VIEW IF EXISTS `animal_view`;
DROP TABLE IF EXISTS `T_constraints_4` CASCADE;
DROP TABLE IF EXISTS `T_constraints_3` CASCADE;
@ -202,6 +203,12 @@ CREATE TABLE `dossier` (
PRIMARY KEY (`id`)
) 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`;
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 "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 "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 */
@ -213,7 +215,7 @@ CREATE TABLE "employee" (
"department_id" INTEGER NOT NULL,
"first_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";

3
tests/data/postgres.sql

@ -177,7 +177,7 @@ CREATE TABLE "default_pk" (
CREATE TABLE "document" (
id serial primary key,
title varchar(255) not null,
content text not null,
content text,
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;
CREATE TABLE "array_and_json_types" (
id SERIAL NOT NULL PRIMARY KEY,
intarray_col INT[],
textarray2_col TEXT[][],
json_col JSON,

10
tests/framework/ChangeLogTest.php

@ -40,16 +40,20 @@ class ChangeLogTest extends TestCase
*/
public function testContributorLine($line)
{
if ($line === '- no changes in this release.') {
$this->markTestSkipped('Placeholder line');
}
/**
* Each change line is tested for:
* - Starts with "- "
* - 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 ends without a "."
* - Line ends with contributor name between "(" and ")".
*/
//$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?: .*[^.] \(.*\)$/', $line);
$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*( \(CVE\-\d+\-\d+\))*)?: .*[^.] \(.*\)$/', $line);
$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?(\s\(CVE-[\d-]+\))?: .*[^.] \(.*\)$/', $line);
}
}

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

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

6
tests/framework/db/ActiveRecordTest.php

@ -1725,4 +1725,10 @@ abstract class ActiveRecordTest extends DatabaseTestCase
->all();
$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->select('name, name, name as X, name as X');
$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()
@ -676,18 +707,4 @@ abstract class QueryTest extends DatabaseTestCase
$this->assertEquals('user11', $query->cache()->where(['id' => 1])->scalar($db));
}, 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 = [
'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' => [
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' => [
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' => [
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' => [
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' => [
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' => [
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())',
],
'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' => [
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();

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

@ -130,22 +130,22 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
{
$concreteData = [
'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' => [
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' => [
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' => [
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' => [
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' => [
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' => [
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())',
],
'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' => [
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();

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

@ -7,6 +7,8 @@
namespace yiiunit\framework\db\mysql;
use yiiunit\data\ar\Storage;
/**
* @group db
* @group mysql
@ -14,4 +16,35 @@ namespace yiiunit\framework\db\mysql;
class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest
{
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 = [
'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' => [
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' => [
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' => [
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)
SELECT *
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' => [
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)
SELECT *
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' => [
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)
SELECT *
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' => [
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())',
],
'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' => [
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();

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

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

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

@ -10,6 +10,7 @@ namespace yiiunit\framework\filters\auth;
use Yii;
use yii\filters\auth\AuthMethod;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBearerAuth;
use yii\rest\Controller;
use yiiunit\framework\web\UserIdentity;
@ -26,6 +27,8 @@ class TestAuth extends AuthMethod
class TestController extends Controller
{
public $authMethods = [];
public function actionA()
{
return 'success';
@ -66,8 +69,8 @@ class TestController extends Controller
return [
'authenticator' => [
'class' => CompositeAuth::class,
'authMethods' => [
TestAuth::class
'authMethods' => $this->authMethods ?: [
TestAuth::class,
],
],
];
@ -123,4 +126,19 @@ class CompositeAuthTest extends \yiiunit\TestCase
$controller = Yii::$app->createController('test')[0];
$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);
$this->assertEquals($expectedFiles, $foundFiles);
// filter
$expectedFiles = [
$dirName . DIRECTORY_SEPARATOR . 'second_sub_dir'
];
$options = [
'filter' => function ($path) {
return 'second_sub_dir' == basename($path);
return 'second_sub_dir' === basename($path);
},
];
$foundFiles = FileHelper::findDirectories($dirName, $options);
@ -921,5 +922,16 @@ class FileHelperTest extends TestCase
sort($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);
}
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;
use yii\db\ArrayExpression;
use yii\validators\EachValidator;
use yiiunit\data\base\ArrayAccessObject;
use yiiunit\data\base\TraversableObject;
use yiiunit\data\validators\models\FakedValidationModel;
use yiiunit\TestCase;
@ -184,4 +187,17 @@ class EachValidatorTest extends TestCase
$validator->validateAttribute($model, '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

@ -219,6 +219,23 @@ abstract class ExistValidatorTest extends DatabaseTestCase
$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()
{
$connection = $this->getConnectionWithInvalidSlave();

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

@ -70,4 +70,22 @@ class SessionTest extends TestCase
$this->assertNotEquals($oldGcProbability, $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()
{
$this->setOutputCallback(function($output) {
return null;
});
ob_start();
ob_implicit_flush(false);
$view = new View();

3
tests/framework/widgets/PjaxTest.php

@ -45,6 +45,7 @@ class PjaxTest extends TestCase
public function testShouldTriggerInitEvent()
{
$initTriggered = false;
ob_start();
$pjax = new Pjax(
[
'on init' => function () use (&$initTriggered) {
@ -52,7 +53,7 @@ class PjaxTest extends TestCase
}
]
);
ob_end_clean();
$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('afterValidateAttribute', function () {
var afterValidateAttributeSpy;

Loading…
Cancel
Save