diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml new file mode 100644 index 000000000..5970206c9 --- /dev/null +++ b/.github/workflows/bc.yml @@ -0,0 +1,23 @@ +on: + pull_request: + paths: + - 'src/**' + - '.github/workflows/bc.yml' + - 'composer.json' + push: + branches: ['master'] + paths: + - 'src/**' + - '.github/workflows/bc.yml' + - 'composer.json' + +name: backwards compatibility + +jobs: + roave_bc_check: + uses: yiisoft/actions/.github/workflows/bc.yml@master + with: + os: >- + ['ubuntu-latest'] + php: >- + ['8.1'] diff --git a/.github/workflows/bc.yml_ b/.github/workflows/bc.yml_ deleted file mode 100644 index 35b3a8624..000000000 --- a/.github/workflows/bc.yml_ +++ /dev/null @@ -1,15 +0,0 @@ -on: - - pull_request - - push - -name: backwards compatibility -jobs: - roave_bc_check: - name: Roave BC Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: fetch tags - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Roave BC Check - uses: docker://nyholm/roave-bc-check-ga diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index cc226299f..c9d07e456 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -35,7 +35,6 @@ jobs: - ubuntu-latest php: - - 8.0 - 8.1 steps: @@ -71,6 +70,6 @@ jobs: - name: Run infection. run: | - vendor/bin/roave-infection-static-analysis-plugin -j2 --ignore-msi-with-no-mutations --only-covered + vendor/bin/roave-infection-static-analysis-plugin --threads=2 --ignore-msi-with-no-mutations --only-covered env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 0b1707659..b77787984 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -39,6 +39,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' steps: - name: Checkout. @@ -70,4 +71,9 @@ jobs: run: composer update --no-interaction --no-progress --optimize-autoloader --ansi - name: Static analysis. + if: ${{ matrix.php != '8.0' }} run: vendor/bin/psalm --config=${{ inputs.psalm-config }} --shepherd --stats --output-format=github --php-version=${{ matrix.php }} + + - name: Static analysis. + if: ${{ matrix.php == '8.0' }} + run: vendor/bin/psalm --config=psalm4.xml --shepherd --stats --output-format=github --php-version=${{ matrix.php }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 076a9d45f..30f7185b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,25 @@ # SQLite driver for Yii Database Change Log -## 1.0.2 under development +## 2.0.0 under development + +- Enh #289: Implement `SqlParser` and `ExpressionBuilder` driver classes (@Tigrov) + +## 1.2.0 March 21, 2024 + +- Enh #281: Remove unused code in `Command` class (@vjik) +- Enh #282: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov) +- Enh #283: Remove unnecessary check for array type in `Schema::loadTableIndexes()` (@Tigrov) +- Enh #287: Resolve deprecated methods (@Tigrov) +- Enh #288: Minor refactoring of `DDLQueryBuilder` and `Schema` (@Tigrov) + +## 1.1.0 November 12, 2023 - Enh #263: Support json type (@Tigrov) +- Enh #278: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov) - Bug #268: Fix foreign keys: support multiple foreign keys referencing to one table and possible null columns for reference (@Tigrov) - Enh #273: Implement `ColumnSchemaInterface` classes according to the data type of database table columns for type casting performance. Related with yiisoft/db#752 (@Tigrov) +- Bug #271: Refactor `DMLQueryBuilder`, related with yiisoft/db#746 (@Tigrov) ## 1.0.1 July 24, 2023 diff --git a/LICENSE.md b/LICENSE.md index bc5674fe4..6a920d605 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,17 +1,17 @@ -Copyright © 2008 by Yii Software (https://www.yiiframework.com/) +Copyright © 2008 by Yii Software () All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Yii Software nor the names of its +* Neither the name of Yii Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 8b66401ed..b1342beac 100644 --- a/README.md +++ b/README.md @@ -36,19 +36,30 @@ perform advanced database operations such as joins and aggregates. The package could be installed via composer: -```php +```shell composer require yiisoft/db-sqlite ``` -## Usage +## Documentation -For config connection to SQLite database check [Connecting SQLite](https://github.com/yiisoft/db/blob/master/docs/en/connection/sqlite.md). +English: -[Check the documentation docs](https://github.com/yiisoft/db/blob/master/docs/en/README.md) to learn about usage. +- For config connection to SQLite database check [Connecting SQLite](https://github.com/yiisoft/db/blob/master/docs/en/connection/sqlite.md) +- [Check the yiisoft/db docs](https://github.com/yiisoft/db/blob/master/docs/en/README.md) to learn about usage. -## Testing +Português - Brasil: -[Check the documentation](/docs/en/testing.md) to learn about testing. +- Para configurar a conexão com o SQLite leia [Connecting SQLite](https://github.com/yiisoft/db/blob/master/docs/pt-BR/connection/sqlite.md). +- [Confira a documentação](https://github.com/yiisoft/db/blob/master/docs/pt-BR/README.md) para aprender como usar. + +Testing: + +- [Internals](docs/internals.md) + +## Support + +If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. +You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). ## Support the project diff --git a/composer.json b/composer.json index faaac7637..946228614 100644 --- a/composer.json +++ b/composer.json @@ -18,25 +18,25 @@ "forum": "https://www.yiiframework.com/forum/", "wiki": "https://www.yiiframework.com/wiki/", "chat": "https://t.me/yii3en", - "irc": "irc://irc.freenode.net/yii" + "irc": "ircs://irc.libera.chat:6697/yii" }, "require": { "php": "^8.0", "ext-mbstring": "*", "ext-pdo": "*", - "yiisoft/db": "^1.0", + "yiisoft/db": "^1.2", "yiisoft/json": "^1.0" }, "require-dev": { "ext-json": "*", "maglnet/composer-require-checker": "^4.2", "phpunit/phpunit": "^9.6|^10.0", - "rector/rector": "^0.18", + "rector/rector": "^1.0", "roave/infection-static-analysis-plugin": "^1.16", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.3|^5.6", + "vimeo/psalm": "^4.30|^5.20", "yiisoft/aliases": "^2.0", - "yiisoft/cache-file": "^2.0", + "yiisoft/cache-file": "^3.1", "yiisoft/json": "^1.0", "yiisoft/var-dumper": "^1.5" }, diff --git a/docs/en/testing.md b/docs/internals.md similarity index 51% rename from docs/en/testing.md rename to docs/internals.md index 49d60d0e3..4966a62be 100644 --- a/docs/en/testing.md +++ b/docs/internals.md @@ -1,4 +1,4 @@ -# Testing +# Internals ## Github actions @@ -6,15 +6,42 @@ All our packages have github actions by default, so you can test your [contribut > Note: We recommend pull requesting in draft mode until all tests pass. +## Docker image + +For greater ease it is recommended to use docker containers, for this you can use the [docker-compose.yml](https://docs.docker.com/compose/compose-file/) file that is in the docs folder. + +1. [MySQL 8](../../../docker-compose.yml) +2. [MariaDB 10.11](../../../docker-compose-mariadb.yml) + +For running the docker containers you can use the following command: + +MySQL 8.0. + +```dockerfile +docker compose up -d +``` + +MariaDB 10.11. + +```dockerfile +docker compose -f docker-compose-mariadb.yml up -d +``` + ## Unit testing The package is tested with [PHPUnit](https://phpunit.de/). +The following steps are required to run the tests: + +1. Run the docker container for the dbms. +2. Install the dependencies of the project with composer. +3. Run the tests. + ```shell vendor/bin/phpunit ``` -### Mutation testing +## Mutation testing The package tests are checked with [Infection](https://infection.github.io/) mutation framework with [Infection Static Analysis Plugin](https://github.com/Roave/infection-static-analysis-plugin). To run it: @@ -33,16 +60,17 @@ The code is statically analyzed with [Psalm](https://psalm.dev/). To run static ## Rector -Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or -use either newest or any specific version of PHP: +Use [Rector](https://github.com/rectorphp/rector) to make codebase follow some specific rules or +use either newest or any specific version of PHP: ```shell ./vendor/bin/rector ``` -## Composer require checker +## Dependencies -This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`. +Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker) to detect transitive +[Composer](https://getcomposer.org/) dependencies. To run the checker, execute the following command: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c65978bba..368ef3ecc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + diff --git a/psalm.xml b/psalm.xml index 10d319ae3..906206a6b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -15,5 +15,6 @@ + diff --git a/psalm4.xml b/psalm4.xml new file mode 100644 index 000000000..10d319ae3 --- /dev/null +++ b/psalm4.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/rector.php b/rector.php index 6d4e4d49c..8cbdabbd1 100644 --- a/rector.php +++ b/rector.php @@ -11,7 +11,10 @@ $rectorConfig->paths([ __DIR__ . '/src', - __DIR__ . '/tests', + /** + * Disabled ./tests directory due to different branches with main package when testing + */ + // __DIR__ . '/tests', ]); // register a single rule diff --git a/src/AbstractTokenizer.php b/src/AbstractTokenizer.php index 7febb90bd..effa5a310 100644 --- a/src/AbstractTokenizer.php +++ b/src/AbstractTokenizer.php @@ -101,7 +101,6 @@ public function tokenize(): SqlToken $token[] = (new SqlToken())->type(SqlToken::TYPE_STATEMENT); $this->tokenStack->push($token[0]); - /** @psalm-var SqlToken */ $this->currentToken = $this->tokenStack->top(); $length = 0; diff --git a/src/Builder/ExpressionBuilder.php b/src/Builder/ExpressionBuilder.php new file mode 100644 index 000000000..96ac22ee4 --- /dev/null +++ b/src/Builder/ExpressionBuilder.php @@ -0,0 +1,16 @@ +getValue(); if ($value instanceof QueryInterface) { diff --git a/src/Command.php b/src/Command.php index f73ca1390..6996cd7f6 100644 --- a/src/Command.php +++ b/src/Command.php @@ -4,13 +4,10 @@ namespace Yiisoft\Db\Sqlite; -use PDOException; use Throwable; use Yiisoft\Db\Driver\Pdo\AbstractPdoCommand; -use Yiisoft\Db\Exception\ConvertException; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidArgumentException; -use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; use function array_pop; use function count; @@ -44,7 +41,6 @@ public function insertWithReturningPks(string $table, array $columns): bool|arra continue; } - /** @psalm-var mixed */ $result[$name] = $columns[$name] ?? $tableSchema?->getColumn($name)?->getDefaultValue(); } @@ -60,11 +56,6 @@ public function showDatabases(): array return $this->setSql($sql)->queryColumn(); } - protected function getQueryBuilder(): QueryBuilderInterface - { - return $this->db->getQueryBuilder(); - } - /** * Executes the SQL statement. * @@ -80,7 +71,6 @@ public function execute(): int { $sql = $this->getSql(); - /** @psalm-var array $params */ $params = $this->params; $statements = $this->splitStatements($sql, $params); @@ -91,11 +81,8 @@ public function execute(): int $result = 0; - /** @psalm-var array> $statements */ foreach ($statements as $statement) { [$statementSql, $statementParams] = $statement; - $statementSql = is_string($statementSql) ? $statementSql : ''; - $statementParams = is_array($statementParams) ? $statementParams : []; $this->setSql($statementSql)->bindValues($statementParams); $result = parent::execute(); } @@ -105,43 +92,6 @@ public function execute(): int return $result; } - /** - * @psalm-suppress UnusedClosureParam - * - * @throws Throwable - */ - protected function internalExecute(string|null $rawSql): void - { - $attempt = 0; - - while (true) { - try { - if ( - ++$attempt === 1 - && $this->isolationLevel !== null - && $this->db->getTransaction() === null - ) { - $this->db->transaction( - function () use ($rawSql): void { - $this->internalExecute($rawSql); - }, - $this->isolationLevel, - ); - } else { - $this->pdoStatement?->execute(); - } - break; - } catch (PDOException $e) { - $rawSql = $rawSql ?: $this->getRawSql(); - $e = (new ConvertException($e, $rawSql))->run(); - - if ($this->retryHandler === null || !($this->retryHandler)($e, $attempt)) { - throw $e; - } - } - } - } - /** * Performs the actual DB query of an SQL statement. * @@ -156,7 +106,6 @@ protected function queryInternal(int $queryMode): mixed { $sql = $this->getSql(); - /** @psalm-var array $params */ $params = $this->params; $statements = $this->splitStatements($sql, $params); @@ -167,14 +116,7 @@ protected function queryInternal(int $queryMode): mixed [$lastStatementSql, $lastStatementParams] = array_pop($statements); - /** - * @psalm-var array $statements - */ foreach ($statements as $statement) { - /** - * @psalm-var string $statementSql - * @psalm-var array $statementParams - */ [$statementSql, $statementParams] = $statement; $this->setSql($statementSql)->bindValues($statementParams); parent::execute(); @@ -182,7 +124,6 @@ protected function queryInternal(int $queryMode): mixed $this->setSql($lastStatementSql)->bindValues($lastStatementParams); - /** @psalm-var string $result */ $result = parent::queryInternal($queryMode); $this->setSql($sql)->bindValues($params); @@ -200,8 +141,6 @@ protected function queryInternal(int $queryMode): mixed * * @return array|bool List of SQL statements or `false` if there's a single statement. * - * @psalm-param array $params - * * @psalm-return false|list */ private function splitStatements(string $sql, array $params): bool|array @@ -231,8 +170,6 @@ private function splitStatements(string $sql, array $params): bool|array /** * Returns named bindings used in the specified statement token. - * - * @psalm-param array $params */ private function extractUsedParams(SqlToken $statement, array $params): array { diff --git a/src/DDLQueryBuilder.php b/src/DDLQueryBuilder.php index 84a9c89b8..15b8cd92a 100644 --- a/src/DDLQueryBuilder.php +++ b/src/DDLQueryBuilder.php @@ -105,8 +105,8 @@ public function createIndex( [$schema, $table] = $tableParts; } - return 'CREATE ' . ($indexType ? ($indexType . ' ') : '') . 'INDEX ' - . $this->quoter->quoteTableName(($schema ? $schema . '.' : '') . $name) + return 'CREATE ' . (!empty($indexType) ? $indexType . ' ' : '') . 'INDEX ' + . $this->quoter->quoteTableName((!empty($schema) ? $schema . '.' : '') . $name) . ' ON ' . $this->quoter->quoteTableName($table) . ' (' . $this->queryBuilder->buildColumns($columns) . ')'; diff --git a/src/DMLQueryBuilder.php b/src/DMLQueryBuilder.php index a3ef422ed..e772a5985 100644 --- a/src/DMLQueryBuilder.php +++ b/src/DMLQueryBuilder.php @@ -8,13 +8,10 @@ use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; -use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\AbstractDMLQueryBuilder; use function implode; -use function ltrim; -use function reset; /** * Implements a DML (Data Manipulation Language) SQL statements for SQLite Server. @@ -45,8 +42,8 @@ public function resetSequence(string $table, int|string $value = null): string if ($value !== null) { $value = "'" . ((int) $value - 1) . "'"; } else { - $pk = $tableSchema->getPrimaryKey(); - $key = $this->quoter->quoteColumnName(reset($pk)); + $key = $tableSchema->getPrimaryKey()[0]; + $key = $this->quoter->quoteColumnName($key); $value = '(SELECT MAX(' . $key . ') FROM ' . $tableName . ')'; } @@ -59,14 +56,9 @@ public function upsert( bool|array $updateColumns, array &$params ): string { - /** @psalm-var Constraint[] $constraints */ + /** @var Constraint[] $constraints */ $constraints = []; - /** - * @psalm-var string[] $insertNames - * @psalm-var string[] $updateNames - * @psalm-var array|bool $updateColumns - */ [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns( $table, $insertColumns, @@ -78,20 +70,19 @@ public function upsert( return $this->insert($table, $insertColumns, $params); } - /** @psalm-var string[] $placeholders */ [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); - $insertSql = 'INSERT OR IGNORE INTO ' - . $this->quoter->quoteTableName($table) + $quotedTableName = $this->quoter->quoteTableName($table); + + $insertSql = 'INSERT OR IGNORE INTO ' . $quotedTableName . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '') - . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : "$values"); + . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' ' . $values); if ($updateColumns === false) { return $insertSql; } $updateCondition = ['or']; - $quotedTableName = $this->quoter->quoteTableName($table); foreach ($constraints as $constraint) { $constraintCondition = ['and']; @@ -106,13 +97,9 @@ public function upsert( if ($updateColumns === true) { $updateColumns = []; - foreach ($updateNames as $name) { - $quotedName = $this->quoter->quoteColumnName($name); - - if (strrpos($quotedName, '.') === false) { - $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)"; - } - $updateColumns[$name] = new Expression($quotedName); + /** @psalm-var string[] $updateNames */ + foreach ($updateNames as $quotedName) { + $updateColumns[$quotedName] = new Expression("(SELECT $quotedName FROM `EXCLUDED`)"); } } @@ -120,13 +107,9 @@ public function upsert( return $insertSql; } - /** @psalm-var array $params */ - $updateSql = 'WITH "EXCLUDED" (' - . implode(', ', $insertNames) - . ') AS (' . (!empty($placeholders) - ? 'VALUES (' . implode(', ', $placeholders) . ')' - : ltrim("$values", ' ')) . ') ' . - $this->update($table, $updateColumns, $updateCondition, $params); + $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames) . ') AS (' + . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : $values) + . ') ' . $this->update($table, $updateColumns, $updateCondition, $params); return "$updateSql; $insertSql;"; } diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 753e9504c..7723feb9f 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Sqlite; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; @@ -12,6 +13,7 @@ use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\InCondition; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; +use Yiisoft\Db\Sqlite\Builder\ExpressionBuilder; use Yiisoft\Db\Sqlite\Builder\InConditionBuilder; use Yiisoft\Db\Sqlite\Builder\JsonExpressionBuilder; use Yiisoft\Db\Sqlite\Builder\LikeConditionBuilder; @@ -46,7 +48,6 @@ public function build(QueryInterface $query, array $params = []): array $sql = $this->buildOrderByAndLimit($sql, $orderBy, $query->getLimit(), $query->getOffset()); if (!empty($orderBy)) { - /** @psalm-var array $orderBy */ foreach ($orderBy as $expression) { if ($expression instanceof ExpressionInterface) { $this->buildExpression($expression, $params); @@ -57,7 +58,6 @@ public function build(QueryInterface $query, array $params = []): array $groupBy = $query->getGroupBy(); if (!empty($groupBy)) { - /** @psalm-var array $groupBy */ foreach ($groupBy as $expression) { if ($expression instanceof ExpressionInterface) { $this->buildExpression($expression, $params); @@ -94,7 +94,7 @@ public function buildLimit(ExpressionInterface|int|null $limit, ExpressionInterf /** * Limit isn't optional in SQLite. * - * {@see http://www.sqlite.org/syntaxdiagrams.html#select-stmt} + * {@see https://www.sqlite.org/syntaxdiagrams.html#select-stmt} */ $sql = 'LIMIT 9223372036854775807 OFFSET ' . // 2^63-1 ($offset instanceof ExpressionInterface ? $this->buildExpression($offset) : (string)$offset); @@ -138,6 +138,7 @@ protected function defaultExpressionBuilders(): array LikeCondition::class => LikeConditionBuilder::class, InCondition::class => InConditionBuilder::class, JsonExpression::class => JsonExpressionBuilder::class, + Expression::class => ExpressionBuilder::class, ]); } } diff --git a/src/Dsn.php b/src/Dsn.php index 37f0c1cfa..8806397a8 100644 --- a/src/Dsn.php +++ b/src/Dsn.php @@ -24,7 +24,7 @@ public function __construct(private string $driver, private string|null $databas /** * @return string The Data Source Name, or DSN, has the information required to connect to the database. * - * Please refer to the [PHP manual](http://php.net/manual/en/pdo.construct.php) on the format of the DSN string. + * Please refer to the [PHP manual](https://php.net/manual/en/pdo.construct.php) on the format of the DSN string. * * The `driver` array key is used as the driver prefix of the DSN, all further key-value pairs are rendered as * `key=value` and concatenated by `;`. For example: diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 3aa810b1f..4f867bd21 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -14,9 +14,7 @@ final class QueryBuilder extends AbstractQueryBuilder { /** - * @var array Mapping from abstract column types (keys) to physical column types (values). - * - * @psalm-var string[] $typeMap + * @var string[] Mapping from abstract column types (keys) to physical column types (values). */ protected array $typeMap = [ SchemaInterface::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', diff --git a/src/Schema.php b/src/Schema.php index d335a8bca..9badfad6d 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -21,6 +21,7 @@ use Yiisoft\Db\Schema\TableSchemaInterface; use function array_column; +use function array_map; use function array_merge; use function count; use function explode; @@ -75,11 +76,11 @@ final class Schema extends AbstractPdoSchema { /** - * @var array Mapping from physical column types (keys) to abstract column types (values). + * Mapping from physical column types (keys) to abstract column types (values). * - * @psalm-var array $typeMap + * @var string[] */ - private array $typeMap = [ + private const TYPE_MAP = [ 'tinyint' => self::TYPE_TINYINT, 'bit' => self::TYPE_SMALLINT, 'boolean' => self::TYPE_BOOLEAN, @@ -204,7 +205,7 @@ protected function loadTableForeignKeys(string $tableName): array $foreignKeysList = $this->getPragmaForeignKeyList($tableName); /** @psalm-var ForeignKeyInfo[] $foreignKeysList */ - $foreignKeysList = $this->normalizeRowKeyCase($foreignKeysList, true); + $foreignKeysList = array_map('array_change_key_case', $foreignKeysList); $foreignKeysList = DbArrayHelper::index($foreignKeysList, null, ['table']); DbArrayHelper::multisort($foreignKeysList, 'seq'); @@ -221,7 +222,6 @@ protected function loadTableForeignKeys(string $tableName): array $primaryKey = $this->getTablePrimaryKey($table); if ($primaryKey !== null) { - /** @psalm-var string $primaryKeyColumnName */ foreach ((array) $primaryKey->getColumnNames() as $i => $primaryKeyColumnName) { $foreignKey[$i]['to'] = $primaryKeyColumnName; } @@ -255,13 +255,12 @@ protected function loadTableForeignKeys(string $tableName): array * * @return array Indexes for the given table. * - * @psalm-return array|IndexConstraint[] + * @psalm-return IndexConstraint[] */ protected function loadTableIndexes(string $tableName): array { - $tableIndexes = $this->loadTableConstraints($tableName, self::INDEXES); - - return is_array($tableIndexes) ? $tableIndexes : []; + /** @var IndexConstraint[] */ + return $this->loadTableConstraints($tableName, self::INDEXES); } /** @@ -306,7 +305,6 @@ protected function loadTableChecks(string $tableName): array $sql = ($sql === false || $sql === null) ? '' : (string) $sql; - /** @psalm-var SqlToken[]|SqlToken[][]|SqlToken[][][] $code */ $code = (new SqlTokenizer($sql))->tokenize(); $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize(); $result = []; @@ -366,7 +364,6 @@ protected function loadTableDefaultValues(string $tableName): array */ protected function findColumns(TableSchemaInterface $table): bool { - /** @psalm-var ColumnInfo[] $columns */ $columns = $this->getPragmaTableInfo($table->getName()); $jsonColumns = $this->getJsonColumns($table); @@ -447,7 +444,6 @@ public function findUniqueIndexes(TableSchemaInterface $table): array foreach ($indexList as $index) { $indexName = $index['name']; - /** @psalm-var IndexInfo[] $indexInfo */ $indexInfo = $this->getPragmaIndexInfo($index['name']); if ($index['unique']) { @@ -532,7 +528,7 @@ private function getColumnType(string $dbType, array &$info): string } } - return $this->typeMap[$dbType] ?? self::TYPE_STRING; + return self::TYPE_MAP[$dbType] ?? self::TYPE_STRING; } /** @@ -568,13 +564,16 @@ private function normalizeDefaultValue(string|null $defaultValue, ColumnSchemaIn * @throws Throwable * * @return array The table columns info. + * + * @psalm-return ColumnInfo[] $tableColumns; */ private function loadTableColumnsInfo(string $tableName): array { $tableColumns = $this->getPragmaTableInfo($tableName); /** @psalm-var ColumnInfo[] $tableColumns */ - $tableColumns = $this->normalizeRowKeyCase($tableColumns, true); + $tableColumns = array_map('array_change_key_case', $tableColumns); + /** @psalm-var ColumnInfo[] */ return DbArrayHelper::index($tableColumns, 'cid'); } @@ -594,7 +593,7 @@ private function loadTableConstraints(string $tableName, string $returnType): Co { $indexList = $this->getPragmaIndexList($tableName); /** @psalm-var IndexListInfo[] $indexes */ - $indexes = $this->normalizeRowKeyCase($indexList, true); + $indexes = array_map('array_change_key_case', $indexList); $result = [ self::PRIMARY_KEY => null, self::INDEXES => [], @@ -602,7 +601,6 @@ private function loadTableConstraints(string $tableName, string $returnType): Co ]; foreach ($indexes as $index) { - /** @psalm-var IndexInfo[] $columns */ $columns = $this->getPragmaIndexInfo($index['name']); if ($index['origin'] === 'pk') { @@ -628,8 +626,6 @@ private function loadTableConstraints(string $tableName, string $returnType): Co * Extra check for PK in case of `INTEGER PRIMARY KEY` with ROWID. * * @link https://www.sqlite.org/lang_createtable.html#primkeyconst - * - * @psalm-var ColumnInfo[] $tableColumns */ $tableColumns = $this->loadTableColumnsInfo($tableName); @@ -652,9 +648,12 @@ private function loadTableConstraints(string $tableName, string $returnType): Co * @throws Exception * @throws InvalidConfigException * @throws Throwable + * + * @psalm-return ForeignKeyInfo[] */ private function getPragmaForeignKeyList(string $tableName): array { + /** @psalm-var ForeignKeyInfo[] */ return $this->db->createCommand( 'PRAGMA FOREIGN_KEY_LIST(' . $this->db->getQuoter()->quoteSimpleTableName($tableName) . ')' )->queryAll(); @@ -664,16 +663,18 @@ private function getPragmaForeignKeyList(string $tableName): array * @throws Exception * @throws InvalidConfigException * @throws Throwable + * + * @psalm-return IndexInfo[] */ private function getPragmaIndexInfo(string $name): array { $column = $this->db ->createCommand('PRAGMA INDEX_INFO(' . (string) $this->db->getQuoter()->quoteValue($name) . ')') ->queryAll(); - /** @psalm-var IndexInfo[] $column */ - $column = $this->normalizeRowKeyCase($column, true); + $column = array_map('array_change_key_case', $column); DbArrayHelper::multisort($column, 'seqno'); + /** @psalm-var IndexInfo[] $column */ return $column; } @@ -681,9 +682,12 @@ private function getPragmaIndexInfo(string $name): array * @throws Exception * @throws InvalidConfigException * @throws Throwable + * + * @psalm-return IndexListInfo[] */ private function getPragmaIndexList(string $tableName): array { + /** @psalm-var IndexListInfo[] */ return $this->db ->createCommand('PRAGMA INDEX_LIST(' . (string) $this->db->getQuoter()->quoteValue($tableName) . ')') ->queryAll(); @@ -693,9 +697,12 @@ private function getPragmaIndexList(string $tableName): array * @throws Exception * @throws InvalidConfigException * @throws Throwable + * + * @psalm-return ColumnInfo[] */ private function getPragmaTableInfo(string $tableName): array { + /** @psalm-var ColumnInfo[] */ return $this->db->createCommand( 'PRAGMA TABLE_INFO(' . $this->db->getQuoter()->quoteSimpleTableName($tableName) . ')' )->queryAll(); @@ -708,7 +715,7 @@ private function getPragmaTableInfo(string $tableName): array */ protected function findViewNames(string $schema = ''): array { - /** @psalm-var string[][] $views */ + /** @var string[][] $views */ $views = $this->db->createCommand( <<getExpression(), $matches, PREG_SET_ORDER)) { + if (preg_match_all($regexp, $check->getExpression(), $matches, PREG_SET_ORDER) > 0) { foreach ($matches as $match) { $result[] = $match[1]; } diff --git a/src/SqlParser.php b/src/SqlParser.php new file mode 100644 index 000000000..9698d6d68 --- /dev/null +++ b/src/SqlParser.php @@ -0,0 +1,45 @@ +length - 1; + + while ($this->position < $length) { + $pos = $this->position++; + + match ($this->sql[$pos]) { + ':' => ($word = $this->parseWord()) === '' + ? $this->skipChars(':') + : $result = ':' . $word, + '"', "'", '`' => $this->skipQuotedWithoutEscape($this->sql[$pos]), + '[' => $this->sql[$this->position] === '[' + ? $this->skipToAfterString(']]') + : $this->skipQuotedWithoutEscape(']'), + '-' => $this->sql[$this->position] === '-' + ? ++$this->position && $this->skipToAfterChar("\n") + : null, + '/' => $this->sql[$this->position] === '*' + ? ++$this->position && $this->skipToAfterString('*/') + : null, + default => null, + }; + + if ($result !== null) { + $position = $pos; + + return $result; + } + } + + return null; + } +} diff --git a/src/SqlTokenizer.php b/src/SqlTokenizer.php index 54a4154a7..7db891d25 100644 --- a/src/SqlTokenizer.php +++ b/src/SqlTokenizer.php @@ -12,7 +12,7 @@ * * It's used to obtain `CHECK` constraint information from a `CREATE TABLE` SQL code. * - * @link http://www.sqlite.org/draft/tokenreq.html + * @link https://www.sqlite.org/draft/tokenreq.html * @link https://sqlite.org/lang.html */ final class SqlTokenizer extends AbstractTokenizer diff --git a/src/Transaction.php b/src/Transaction.php index 2256e4732..15731a299 100644 --- a/src/Transaction.php +++ b/src/Transaction.php @@ -29,7 +29,7 @@ final class Transaction extends AbstractPdoTransaction * @throws Throwable When unsupported isolation levels are used. SQLite only supports `SERIALIZABLE` * and `READ UNCOMMITTED`. * - * @link http://www.sqlite.org/pragma.html#pragma_read_uncommitted + * @link https://www.sqlite.org/pragma.html#pragma_read_uncommitted */ protected function setTransactionIsolationLevel(string $level): void { diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 7f3dbd4d5..32486c272 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -465,9 +465,10 @@ public function testUpdate( array $columns, array|string $conditions, array $params, - string $expected + array $expectedValues, + int $expectedCount, ): void { - parent::testUpdate($table, $columns, $conditions, $params, $expected); + parent::testUpdate($table, $columns, $conditions, $params, $expectedValues, $expectedCount); } /** diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 6761ba429..f4d4e5d2e 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -176,6 +176,11 @@ public static function upsert(): array WITH "EXCLUDED" (`email`, `address`, `status`, `profile_id`) AS (VALUES (:qp0, :qp1, :qp2, :qp3)) UPDATE `T_upsert` SET `address`=(SELECT `address` FROM `EXCLUDED`), `status`=(SELECT `status` FROM `EXCLUDED`), `profile_id`=(SELECT `profile_id` FROM `EXCLUDED`) WHERE `T_upsert`.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO `T_upsert` (`email`, `address`, `status`, `profile_id`) VALUES (:qp0, :qp1, :qp2, :qp3); SQL, ], + 'regular values with unique at not the first position' => [ + 3 => << [ 3 => <<assertSame( <<insert('json_table', ['json_col' => new JsonExpression(['a' => 1, 'b' => 2])]), ); } + + /** @dataProvider \Yiisoft\Db\Sqlite\Tests\Provider\QueryBuilderProvider::selectScalar */ + public function testSelectScalar(array|bool|float|int|string $columns, string $expected): void + { + parent::testSelectScalar($columns, $expected); + } } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index e15c3f1ba..2a1711279 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -352,11 +352,11 @@ public function testWorkWithPrimaryKeyConstraint(): void public function testNotConnectionPDO(): void { $db = $this->createMock(ConnectionInterface::class); - $schema = new Schema($db, DbHelper::getSchemaCache(), 'system'); + $schema = new Schema($db, DbHelper::getSchemaCache()); $this->expectException(NotSupportedException::class); $this->expectExceptionMessage('Only PDO connections are supported.'); - $schema->refreshTableSchema('customer'); + $schema->refresh(); } } diff --git a/tests/SqlParserTest.php b/tests/SqlParserTest.php new file mode 100644 index 000000000..512b770d6 --- /dev/null +++ b/tests/SqlParserTest.php @@ -0,0 +1,25 @@ +