diff --git a/.github/workflows/ci-mariadb.yml b/.github/workflows/ci-mariadb.yml new file mode 100644 index 00000000000..d12d0a47c65 --- /dev/null +++ b/.github/workflows/ci-mariadb.yml @@ -0,0 +1,67 @@ +on: + - pull_request + - push + +name: ci-mariadb + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: PHP ${{ matrix.php }}-mariadb-${{ matrix.mariadb }} + env: + extensions: curl, intl, pdo, pdo_mysql + XDEBUG_MODE: coverage, develop + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] + mariadb: + - mariadb:10.4 + - mariadb:latest + + services: + mysql: + image: ${{ matrix.mariadb }} + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: yiitest + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout. + uses: actions/checkout@v4 + + - name: Install PHP with extensions. + uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + extensions: ${{ env.EXTENSIONS }} + ini-values: date.timezone='UTC' + php-version: ${{ matrix.php }} + tools: composer:v2, pecl + + - name: Install dependencies with composer + if: matrix.php != '8.4' + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Install dependencies with PHP 8.4. + if: matrix.php == '8.4' + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ignore-platform-reqs --ansi + + - name: Run MariaDB tests with PHPUnit and generate coverage. + run: vendor/bin/phpunit --group mysql --coverage-clover=coverage.xml --colors=always + + - name: Upload coverage to Codecov. + if: matrix.php == '7.4' + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml diff --git a/.gitignore b/.gitignore index 02326c0cb90..cb1cc49dcf0 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ composer.phar phpunit.phar # local phpunit config /phpunit.xml +.phpunit.result.cache # ignore dev installed apps and extensions /apps diff --git a/framework/BaseYii.php b/framework/BaseYii.php index ff716b4d349..c23bccc226d 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -1,5 +1,4 @@ attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); + if ($value instanceof Behavior) { + $this->attachBehavior($name, $value); + } elseif (isset($value['class']) && is_subclass_of($value['class'], Behavior::class, true)) { + $this->attachBehavior($name, Yii::createObject($value)); + } elseif (is_string($value) && is_subclass_of($value, Behavior::class, true)) { + $this->attachBehavior($name, Yii::createObject($value)); + } else { + throw new InvalidConfigException('Class is not of type ' . Behavior::class . ' or its subclasses'); + } return; } diff --git a/framework/base/Configurable.php b/framework/base/Configurable.php index 3fe41fe88bb..b44a082841e 100644 --- a/framework/base/Configurable.php +++ b/framework/base/Configurable.php @@ -1,5 +1,4 @@ YII2_PATH . '/base/ModelEvent.php', 'yii\base\Module' => YII2_PATH . '/base/Module.php', 'yii\base\NotSupportedException' => YII2_PATH . '/base/NotSupportedException.php', - 'yii\base\Object' => YII2_PATH . '/base/Object.php', 'yii\base\Request' => YII2_PATH . '/base/Request.php', 'yii\base\Response' => YII2_PATH . '/base/Response.php', 'yii\base\Security' => YII2_PATH . '/base/Security.php', @@ -74,6 +72,7 @@ 'yii\caching\ArrayCache' => YII2_PATH . '/caching/ArrayCache.php', 'yii\caching\Cache' => YII2_PATH . '/caching/Cache.php', 'yii\caching\CacheInterface' => YII2_PATH . '/caching/CacheInterface.php', + 'yii\caching\CallbackDependency' => YII2_PATH . '/caching/CallbackDependency.php', 'yii\caching\ChainedDependency' => YII2_PATH . '/caching/ChainedDependency.php', 'yii\caching\DbCache' => YII2_PATH . '/caching/DbCache.php', 'yii\caching\DbDependency' => YII2_PATH . '/caching/DbDependency.php', @@ -81,7 +80,6 @@ 'yii\caching\Dependency' => YII2_PATH . '/caching/Dependency.php', 'yii\caching\DummyCache' => YII2_PATH . '/caching/DummyCache.php', 'yii\caching\ExpressionDependency' => YII2_PATH . '/caching/ExpressionDependency.php', - 'yii\caching\CallbackDependency' => YII2_PATH . '/caching/CallbackDependency.php', 'yii\caching\FileCache' => YII2_PATH . '/caching/FileCache.php', 'yii\caching\FileDependency' => YII2_PATH . '/caching/FileDependency.php', 'yii\caching\MemCache' => YII2_PATH . '/caching/MemCache.php', diff --git a/framework/console/Application.php b/framework/console/Application.php index 7d5a87d7b9f..1ec070a55d2 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -1,5 +1,4 @@ db->getServerVersion(); + // check version MySQL >= 8.0.16 - if (version_compare($this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '8.0.16', '<')) { + if (\stripos($version, 'MariaDb') === false && \version_compare($version, '8.0.16', '<')) { throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.'); } @@ -230,17 +231,9 @@ protected function loadTableChecks($tableName) $tableRows = $this->normalizePdoRowKeyCase($tableRows, true); foreach ($tableRows as $tableRow) { - $matches = []; - $columnName = null; - - if (preg_match('/\(`?([a-zA-Z0-9_]+)`?\s*[><=]/', $tableRow['check_clause'], $matches)) { - $columnName = $matches[1]; - } - $check = new CheckConstraint( [ 'name' => $tableRow['constraint_name'], - 'columnNames' => [$columnName], 'expression' => $tableRow['check_clause'], ] ); @@ -380,10 +373,19 @@ protected function findColumns($table) } throw $e; } + + + $jsonColumns = $this->getJsonColumns($table); + foreach ($columns as $info) { if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) { $info = array_change_key_case($info, CASE_LOWER); } + + if (\in_array($info['field'], $jsonColumns, true)) { + $info['type'] = static::TYPE_JSON; + } + $column = $this->loadColumnSchema($info); $table->columns[$column->name] = $column; if ($column->isPrimaryKey) { @@ -641,4 +643,20 @@ private function loadTableConstraints($tableName, $returnType) return $result[$returnType]; } + + private function getJsonColumns(TableSchema $table): array + { + $sql = $this->getCreateTableSql($table); + $result = []; + + $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi'; + + if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $result[] = $match[1]; + } + } + + return $result; + } } diff --git a/framework/db/oci/ColumnSchemaBuilder.php b/framework/db/oci/ColumnSchemaBuilder.php index b104588c4cd..dcb5ff21ef8 100644 --- a/framework/db/oci/ColumnSchemaBuilder.php +++ b/framework/db/oci/ColumnSchemaBuilder.php @@ -1,5 +1,4 @@ 'application/rtf', 'text/xml' => 'application/xml', diff --git a/framework/helpers/mimeExtensions.php b/framework/helpers/mimeExtensions.php index 8247ff28297..29472bfa88e 100644 --- a/framework/helpers/mimeExtensions.php +++ b/framework/helpers/mimeExtensions.php @@ -1,5 +1,4 @@ 'ez', 'application/applixware' => 'aw', @@ -1043,7 +1041,6 @@ 'model/vnd.dwf' => 'dwf', 'model/vnd.gdl' => 'gdl', 'model/vnd.gtw' => 'gtw', - 'model/vnd.mts' => 'mts', 'model/vnd.vtu' => 'vtu', 'model/vrml' => [ 'wrl', @@ -1167,6 +1164,12 @@ 'mj2', 'mjp2', ], + 'video/mp2t' => [ + 'ts', + 'm2t', + 'm2ts', + 'mts', + ], 'video/mp4' => [ 'mp4', 'mp4v', diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php index c5ddb934900..ad7c9ce80a6 100644 --- a/framework/helpers/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -1,5 +1,4 @@ 'application/vnd.lotus-1-2-3', '3dml' => 'text/vnd.in3d.3dml', @@ -420,6 +418,8 @@ 'm1v' => 'video/mpeg', 'm21' => 'application/mp21', 'm2a' => 'audio/mpeg', + 'm2t' => 'video/mp2t', + 'm2ts' => 'video/mp2t', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'audio/x-mpegurl', @@ -505,7 +505,7 @@ 'msi' => 'application/x-msdownload', 'msl' => 'application/vnd.mobius.msl', 'msty' => 'application/vnd.muvee.style', - 'mts' => 'model/vnd.mts', + 'mts' => 'video/mp2t', 'mus' => 'application/vnd.musician', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', @@ -820,6 +820,7 @@ 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trm' => 'application/x-msterminal', + 'ts' => 'video/mp2t', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'font/collection', diff --git a/framework/i18n/DbMessageSource.php b/framework/i18n/DbMessageSource.php index 155cbc3eccc..0686291d2a7 100644 --- a/framework/i18n/DbMessageSource.php +++ b/framework/i18n/DbMessageSource.php @@ -1,5 +1,4 @@ maxFiles != 1 || $this->minFiles > 1) { - $rawFiles = $model->$attribute; - if (!is_array($rawFiles)) { - $this->addError($model, $attribute, $this->uploadRequired); - - return; - } - - $files = $this->filterFiles($rawFiles); - $model->$attribute = $files; + $files = $this->filterFiles(is_array($model->$attribute) ? $model->$attribute : [$model->$attribute]); + $filesCount = count($files); + if ($filesCount === 0 && $this->minFiles > 0) { + $this->addError($model, $attribute, $this->uploadRequired); - if (empty($files)) { - $this->addError($model, $attribute, $this->uploadRequired); - - return; - } - - $filesCount = count($files); - if ($this->maxFiles && $filesCount > $this->maxFiles) { - $this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); - } + return; + } - if ($this->minFiles && $this->minFiles > $filesCount) { - $this->addError($model, $attribute, $this->tooFew, ['limit' => $this->minFiles]); - } + if ($this->maxFiles > 0 && $filesCount > $this->maxFiles) { + $this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); + } + if ($this->minFiles > 0 && $this->minFiles > $filesCount) { + $this->addError($model, $attribute, $this->tooFew, ['limit' => $this->minFiles]); + } - foreach ($files as $file) { - $result = $this->validateValue($file); - if (!empty($result)) { - $this->addError($model, $attribute, $result[0], $result[1]); - } - } - } else { - $result = $this->validateValue($model->$attribute); + foreach ($files as $file) { + $result = $this->validateValue($file); if (!empty($result)) { $this->addError($model, $attribute, $result[0], $result[1]); } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index 2d59f606841..a08b0b51d29 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -1,5 +1,4 @@ getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { - $isValid = true; + $isValid = true; $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; - if ($isArray) { $params[$name] = (array)$params[$name]; } elseif (is_array($params[$name])) { diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php index 6214c83048c..d249afa6cbd 100644 --- a/framework/web/Cookie.php +++ b/framework/web/Cookie.php @@ -1,5 +1,4 @@ hasMethod($method)) { $reflectionMethod = $reflection->getMethod($method); diff --git a/framework/web/ForbiddenHttpException.php b/framework/web/ForbiddenHttpException.php index e04821d711c..924f09e3259 100644 --- a/framework/web/ForbiddenHttpException.php +++ b/framework/web/ForbiddenHttpException.php @@ -1,5 +1,4 @@ expectException('yii\base\NotSupportedException'); } - if ( - $this->driverName === 'mysql' && - version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && - $type === 'checks' - ) { - $this->expectException('yii\base\NotSupportedException'); - } - $constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName); $this->assertMetadataEquals($expected, $constraints); } @@ -797,14 +789,6 @@ public function testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $e $this->expectException('yii\base\NotSupportedException'); } - if ( - $this->driverName === 'mysql' && - version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && - $type === 'checks' - ) { - $this->expectException('yii\base\NotSupportedException'); - } - $connection = $this->getConnection(false); $connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER); $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); @@ -823,21 +807,13 @@ public function testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $e $this->expectException('yii\base\NotSupportedException'); } - if ( - $this->driverName === 'mysql' && - version_compare($this->getConnection(false)->getServerVersion(), '8.0.16', '<') && - $type === 'checks' - ) { - $this->expectException('yii\base\NotSupportedException'); - } - $connection = $this->getConnection(false); $connection->getSlavePdo(true)->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); $this->assertMetadataEquals($expected, $constraints); } - private function assertMetadataEquals($expected, $actual) + protected function assertMetadataEquals($expected, $actual) { switch (\strtolower(\gettype($expected))) { case 'object': @@ -865,7 +841,7 @@ private function assertMetadataEquals($expected, $actual) $this->assertEquals($expected, $actual); } - private function normalizeArrayKeys(array &$array, $caseSensitive) + protected function normalizeArrayKeys(array &$array, $caseSensitive) { $newArray = []; foreach ($array as $value) { @@ -889,7 +865,7 @@ private function normalizeArrayKeys(array &$array, $caseSensitive) $array = $newArray; } - private function normalizeConstraints(&$expected, &$actual) + protected function normalizeConstraints(&$expected, &$actual) { if (\is_array($expected)) { foreach ($expected as $key => $value) { @@ -904,7 +880,7 @@ private function normalizeConstraints(&$expected, &$actual) } } - private function normalizeConstraintPair(Constraint $expectedConstraint, Constraint $actualConstraint) + protected function normalizeConstraintPair(Constraint $expectedConstraint, Constraint $actualConstraint) { if ($expectedConstraint::className() !== $actualConstraint::className()) { return; diff --git a/tests/framework/db/mysql/CommandTest.php b/tests/framework/db/mysql/CommandTest.php index 60c562c737b..6619a93e636 100644 --- a/tests/framework/db/mysql/CommandTest.php +++ b/tests/framework/db/mysql/CommandTest.php @@ -35,6 +35,7 @@ public function testAddDropCheckSeveral() 'int1' => 'integer', 'int2' => 'integer', 'int3' => 'integer', + 'int4' => 'integer', ])->execute(); $this->assertEmpty($schema->getTableChecks($tableName, true)); @@ -43,14 +44,22 @@ public function testAddDropCheckSeveral() ['name' => 'check_int1_positive', 'expression' => '[[int1]] > 0', 'expected' => '(`int1` > 0)'], ['name' => 'check_int2_nonzero', 'expression' => '[[int2]] <> 0', 'expected' => '(`int2` <> 0)'], ['name' => 'check_int3_less_than_100', 'expression' => '[[int3]] < 100', 'expected' => '(`int3` < 100)'], + ['name' => 'check_int1_less_than_int2', 'expression' => '[[int1]] < [[int2]]', 'expected' => '(`int1` < `int2`)'], ]; + if (\stripos($db->getServerVersion(), 'MariaDb') !== false) { + $constraints[0]['expected'] = '`int1` > 0'; + $constraints[1]['expected'] = '`int2` <> 0'; + $constraints[2]['expected'] = '`int3` < 100'; + $constraints[3]['expected'] = '`int1` < `int2`'; + } + foreach ($constraints as $constraint) { $db->createCommand()->addCheck($constraint['name'], $tableName, $constraint['expression'])->execute(); } $tableChecks = $schema->getTableChecks($tableName, true); - $this->assertCount(3, $tableChecks); + $this->assertCount(4, $tableChecks); foreach ($constraints as $index => $constraint) { $this->assertSame( diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index f0f8cdfb397..7097b8d2b61 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -267,35 +267,35 @@ public function conditionProvider() // json conditions [ ['=', 'jsoncol', new JsonExpression(['lang' => 'uk', 'country' => 'UA'])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"lang":"uk","country":"UA"}'], + '[[jsoncol]] = :qp0', [':qp0' => '{"lang":"uk","country":"UA"}'], ], [ ['=', 'jsoncol', new JsonExpression([false])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[false]'] + '[[jsoncol]] = :qp0', [':qp0' => '[false]'] ], 'object with type. Type is ignored for MySQL' => [ ['=', 'prices', new JsonExpression(['seeds' => 15, 'apples' => 25], 'jsonb')], - '[[prices]] = CAST(:qp0 AS JSON)', [':qp0' => '{"seeds":15,"apples":25}'], + '[[prices]] = :qp0', [':qp0' => '{"seeds":15,"apples":25}'], ], 'nested json' => [ ['=', 'data', new JsonExpression(['user' => ['login' => 'silverfire', 'password' => 'c4ny0ur34d17?'], 'props' => ['mood' => 'good']])], - '[[data]] = CAST(:qp0 AS JSON)', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}'] + '[[data]] = :qp0', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}'] ], 'null value' => [ ['=', 'jsoncol', new JsonExpression(null)], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => 'null'] + '[[jsoncol]] = :qp0', [':qp0' => 'null'] ], 'null as array value' => [ ['=', 'jsoncol', new JsonExpression([null])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[null]'] + '[[jsoncol]] = :qp0', [':qp0' => '[null]'] ], 'null as object value' => [ ['=', 'jsoncol', new JsonExpression(['nil' => null])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"nil":null}'] + '[[jsoncol]] = :qp0', [':qp0' => '{"nil":null}'] ], 'with object as value' => [ ['=', 'jsoncol', new JsonExpression(new DynamicModel(['a' => 1, 'b' => 2]))], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"a":1,"b":2}'] + '[[jsoncol]] = :qp0', [':qp0' => '{"a":1,"b":2}'] ], 'query' => [ ['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]))], @@ -307,7 +307,7 @@ public function conditionProvider() ], 'nested and combined json expression' => [ ['=', 'jsoncol', new JsonExpression(new JsonExpression(['a' => 1, 'b' => 2, 'd' => new JsonExpression(['e' => 3])]))], - "[[jsoncol]] = CAST(:qp0 AS JSON)", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}'] + "[[jsoncol]] = :qp0", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}'] ], 'search by property in JSON column (issue #15838)' => [ ['=', new Expression("(jsoncol->>'$.someKey')"), '42'], @@ -328,7 +328,7 @@ public function updateProvider() [ 'id' => 1, ], - $this->replaceQuotes('UPDATE [[profile]] SET [[description]]=CAST(:qp0 AS JSON) WHERE [[id]]=:qp1'), + $this->replaceQuotes('UPDATE [[profile]] SET [[description]]=:qp0 WHERE [[id]]=:qp1'), [ ':qp0' => '{"abc":"def","0":123,"1":null}', ':qp1' => 1, diff --git a/tests/framework/db/mysql/SchemaTest.php b/tests/framework/db/mysql/SchemaTest.php index ba98f45e385..fc9aa46d652 100644 --- a/tests/framework/db/mysql/SchemaTest.php +++ b/tests/framework/db/mysql/SchemaTest.php @@ -78,8 +78,8 @@ public function constraintsProvider() { $result = parent::constraintsProvider(); - $result['1: check'][2][0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; - + $result['1: check'][2][0]->columnNames = null; + $result['1: check'][2][0]->expression = "`C_check` <> ''"; $result['2: primary key'][2]->name = null; // Work aroung bug in MySQL 5.1 - it creates only this table in lowercase. O_o @@ -88,6 +88,115 @@ public function constraintsProvider() return $result; } + /** + * @dataProvider constraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraints($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider uppercaseConstraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $connection = $this->getConnection(false); + $connection->getSlavePdo(true)->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_UPPER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider lowercaseConstraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $connection = $this->getConnection(false); + $connection->getSlavePdo(true)->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + /** * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3. @@ -146,87 +255,108 @@ public function testAlternativeDisplayOfDefaultCurrentTimestampAsNullInMariaDB() public function getExpectedColumns() { - $version = $this->getConnection()->getSchema()->getServerVersion(); + $version = $this->getConnection(false)->getServerVersion(); $columns = array_merge( parent::getExpectedColumns(), [ 'int_col' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'dbType' => 'int(11)', 'phpType' => 'integer', 'allowNull' => false, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => null, ], 'int_col2' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'dbType' => 'int(11)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => 1, ], 'int_col3' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int unsigned' : 'int(11) unsigned', + 'dbType' => 'int(11) unsigned', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => 1, ], 'tinyint_col' => [ 'type' => 'tinyint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'tinyint' : 'tinyint(3)', + 'dbType' => 'tinyint(3)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 3, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 3, + 'size' => 3, + 'precision' => 3, 'scale' => null, 'defaultValue' => 1, ], 'smallint_col' => [ 'type' => 'smallint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'smallint' : 'smallint(1)', + 'dbType' => 'smallint(1)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 1, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 1, + 'size' => 1, + 'precision' => 1, 'scale' => null, 'defaultValue' => 1, ], 'bigint_col' => [ 'type' => 'bigint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'bigint unsigned' : 'bigint(20) unsigned', + 'dbType' => 'bigint(20) unsigned', 'phpType' => 'string', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 20, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 20, + 'size' => 20, + 'precision' => 20, 'scale' => null, 'defaultValue' => null, ], ] ); - if (version_compare($version, '5.7', '<')) { + if (\version_compare($version, '8.0.17', '>') && \stripos($version, 'MariaDb') === false) { + $columns['int_col']['dbType'] = 'int'; + $columns['int_col']['size'] = null; + $columns['int_col']['precision'] = null; + $columns['int_col2']['dbType'] = 'int'; + $columns['int_col2']['size'] = null; + $columns['int_col2']['precision'] = null; + $columns['int_col3']['dbType'] = 'int unsigned'; + $columns['int_col3']['size'] = null; + $columns['int_col3']['precision'] = null; + $columns['tinyint_col']['dbType'] = 'tinyint'; + $columns['tinyint_col']['size'] = null; + $columns['tinyint_col']['precision'] = null; + $columns['smallint_col']['dbType'] = 'smallint'; + $columns['smallint_col']['size'] = null; + $columns['smallint_col']['precision'] = null; + $columns['bigint_col']['dbType'] = 'bigint unsigned'; + $columns['bigint_col']['size'] = null; + $columns['bigint_col']['precision'] = null; + } + + if (version_compare($version, '5.7', '<') && \stripos($version, 'MariaDb') === false) { $columns['int_col3']['phpType'] = 'string'; $columns['json_col']['type'] = 'text'; $columns['json_col']['dbType'] = 'longtext'; diff --git a/tests/framework/db/mysql/connection/DeadLockTest.php b/tests/framework/db/mysql/connection/DeadLockTest.php index 6e3da047eea..550b2852a69 100644 --- a/tests/framework/db/mysql/connection/DeadLockTest.php +++ b/tests/framework/db/mysql/connection/DeadLockTest.php @@ -31,6 +31,9 @@ class DeadLockTest extends \yiiunit\framework\db\mysql\ConnectionTest */ public function testDeadlockException() { + if (\stripos($this->getConnection(false)->getServerVersion(), 'MariaDB') !== false) { + $this->markTestSkipped('MariaDB does not support this test'); + } if (PHP_VERSION_ID >= 70400 && PHP_VERSION_ID < 70500) { $this->markTestSkipped('Stable failed in PHP 7.4'); } diff --git a/tests/framework/db/mysql/type/JsonTest.php b/tests/framework/db/mysql/type/JsonTest.php new file mode 100644 index 00000000000..b955c7221cd --- /dev/null +++ b/tests/framework/db/mysql/type/JsonTest.php @@ -0,0 +1,85 @@ +getConnection(); + + if ($db->getSchema()->getTableSchema('json') !== null) { + $db->createCommand()->dropTable('json')->execute(); + } + + $command = $db->createCommand(); + $command->createTable('json', ['id' => Schema::TYPE_PK, 'data' => Schema::TYPE_JSON])->execute(); + + $this->assertTrue($db->getTableSchema('json') !== null); + $this->assertSame('data', $db->getTableSchema('json')->getColumn('data')->name); + $this->assertSame('json', $db->getTableSchema('json')->getColumn('data')->type); + } + + public function testInsertAndSelect(): void + { + $db = $this->getConnection(true); + $version = $db->getServerVersion(); + + $command = $db->createCommand(); + $command->insert('storage', ['data' => ['a' => 1, 'b' => 2]])->execute(); + + if (\stripos($version, 'MariaDb') === false) { + $rowExpected = '{"a": 1, "b": 2}'; + } else { + $rowExpected = '{"a":1,"b":2}'; + } + + $this->assertSame( + $rowExpected, + $command->setSql( + <<queryScalar(), + ); + } + + public function testInsertJsonExpressionAndSelect(): void + { + $db = $this->getConnection(true); + $version = $db->getServerVersion(); + + $command = $db->createCommand(); + $command->insert('storage', ['data' => new JsonExpression(['a' => 1, 'b' => 2])])->execute(); + + if (\stripos($version, 'MariaDb') === false) { + $rowExpected = '{"a": 1, "b": 2}'; + } else { + $rowExpected = '{"a":1,"b":2}'; + } + + $this->assertSame( + $rowExpected, + $command->setSql( + <<queryScalar(), + ); + } +} diff --git a/tests/framework/validators/FileValidatorTest.php b/tests/framework/validators/FileValidatorTest.php index c25ed2c8312..0b42b54967e 100644 --- a/tests/framework/validators/FileValidatorTest.php +++ b/tests/framework/validators/FileValidatorTest.php @@ -12,6 +12,7 @@ use yii\validators\FileValidator; use yii\web\UploadedFile; use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\data\validators\models\FakedValidationTypedModel; use yiiunit\TestCase; /** @@ -114,11 +115,11 @@ public function testValidateAttributeMultiple() ]); $m = FakedValidationModel::createWithAttributes(['attr_files' => 'path']); $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); + $this->assertFalse($m->hasErrors('attr_files')); $m = FakedValidationModel::createWithAttributes(['attr_files' => []]); $val->validateAttribute($m, 'attr_files'); - $this->assertTrue($m->hasErrors('attr_files')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); + $this->assertFalse($m->hasErrors('attr_files')); + $m = FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles( @@ -314,6 +315,32 @@ public function testValidateAttribute_minFilesTwoMaxFilesUnlimited_hasError() $this->assertTrue($model->hasErrors('attr_images')); } + /** + * https://github.com/yiisoft/yii2/issues/19855 + */ + public function testValidateArrayAttributeWithMinMaxOneAndOneFile() + { + $validator = new FileValidator(['maxFiles' => 1, 'minFiles' => 0]); + $files = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + ] + )[0]; + $model = FakedValidationModel::createWithAttributes(['attr_images' => [$files]]); + + $validator->validateAttribute($model, 'attr_images'); + $this->assertFalse($model->hasErrors('attr_images')); + } + /** * @param array $params * @return UploadedFile[] @@ -395,8 +422,7 @@ public function testValidateAttribute() $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $val->validateAttribute($m, 'attr_files_empty'); - $this->assertTrue($m->hasErrors('attr_files_empty')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); + $this->assertFalse($m->hasErrors('attr_files_empty')); // single File with skipOnEmpty = false $val = new FileValidator(['skipOnEmpty' => false]); @@ -404,8 +430,7 @@ public function testValidateAttribute() $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $val->validateAttribute($m, 'attr_files_empty'); - $this->assertTrue($m->hasErrors('attr_files_empty')); - $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); + $this->assertFalse($m->hasErrors('attr_files_empty')); $m = $this->createModelForAttributeTest(); // too big @@ -672,4 +697,132 @@ public function mimeTypeCaseInsensitive() { ['image/jxra', 'image/jxrA', true], ]; } + + public function testValidateTypedAttributeNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 0, 'maxFiles' => 2]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeExactMinNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 1]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeExactMaxNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['maxFiles' => 1]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeMinError() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 2]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertTrue($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertTrue($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeMaxError() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['maxFiles' => 1]); + $files = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + // single attribute cannot be checked because maxFiles = 0 === no limits + $model->multiple = $files; + $validator->validateAttribute($model, 'multiple'); + $this->assertTrue($model->hasErrors('multiple')); + } } diff --git a/tests/framework/web/ErrorHandlerTest.php b/tests/framework/web/ErrorHandlerTest.php index 13ed041e91c..17a3c34b04b 100644 --- a/tests/framework/web/ErrorHandlerTest.php +++ b/tests/framework/web/ErrorHandlerTest.php @@ -131,7 +131,7 @@ public function dataHtmlEncode() return [ [ "a \t=<>&\"'\x80`\n", - "a \t=<>&\"'�`\n", + "a \t=<>&"'�`\n", ], [ 'test', @@ -139,11 +139,11 @@ public function dataHtmlEncode() ], [ '"hello"', - '"hello"', + '"hello"', ], [ "'hello world'", - "'hello world'", + "'hello world'", ], [ 'Chip&Dale', @@ -171,7 +171,7 @@ public function testHtmlEncodeWithUnicodeSequence() $handler = Yii::$app->getErrorHandler(); $text = "a \t=<>&\"'\x80\u{20bd}`\u{000a}\u{000c}\u{0000}"; - $expected = "a \t=<>&\"'�₽`\n\u{000c}\u{0000}"; + $expected = "a \t=<>&"'�₽`\n\u{000c}\u{0000}"; $this->assertSame($expected, $handler->htmlEncode($text)); }