diff --git a/.github/workflows/build-mssql.yml b/.github/workflows/build-mssql.yml new file mode 100644 index 0000000..95c3a02 --- /dev/null +++ b/.github/workflows/build-mssql.yml @@ -0,0 +1,58 @@ +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + +name: build-mssql + +jobs: + mssql: + name: SQL Server tests. + uses: php-forge/actions/.github/workflows/phpunit-database.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + concurrency-group: mssql-${{ github.ref }} + database-env: | + { + "ACCEPT_EULA": "Y", + "SA_PASSWORD": "YourStrong!Passw0rd", + "MSSQL_PID": "Developer" + } + database-health-cmd: "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" + database-health-retries: 5 + database-image: mcr.microsoft.com/mssql/server + database-port: 1433 + database-type: mssql + database-versions: '["2022-latest"]' + enable-concurrency: true + extensions: pdo, pdo_sqlsrv, sqlsrv + os: '["ubuntu-latest"]' + php-version: '["8.4"]' + phpunit-group: mssql + setup-commands: | + # Install Microsoft ODBC Driver for SQL Server + sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 + + # Wait for SQL Server to be fully ready + sleep 15 + + # Create test database + docker exec -i database /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q " + IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'yiitest') + BEGIN + CREATE DATABASE yiitest; + END + " diff --git a/.github/workflows/build-mysql.yml b/.github/workflows/build-mysql.yml new file mode 100644 index 0000000..1e830ac --- /dev/null +++ b/.github/workflows/build-mysql.yml @@ -0,0 +1,43 @@ +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + +name: build-mysql + +jobs: + mysql: + name: MySQL tests. + uses: php-forge/actions/.github/workflows/phpunit-database.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + concurrency-group: mysql-${{ github.ref }} + database-env: | + { + "MYSQL_DATABASE": "yiitest", + "MYSQL_ROOT_PASSWORD": "root", + } + database-health-cmd: "mysqladmin ping" + database-health-retries: 3 + database-image: mysql + database-port: 3306 + database-type: mysql + database-versions: '["8.0", "8.4", "latest"]' + enable-concurrency: true + extensions: pdo, pdo_mysql + os: '["ubuntu-latest"]' + php-version: '["8.4"]' + phpunit-group: mysql diff --git a/.github/workflows/build-pgsql.yml b/.github/workflows/build-pgsql.yml new file mode 100644 index 0000000..45682bd --- /dev/null +++ b/.github/workflows/build-pgsql.yml @@ -0,0 +1,44 @@ +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + +name: build-pgsql + +jobs: + pgsql: + name: PostgreSQL tests. + uses: php-forge/actions/.github/workflows/phpunit-database.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + concurrency-group: pgsql-${{ github.ref }} + database-env: | + { + "POSTGRES_DB": "yiitest", + "POSTGRES_USER": "root", + "POSTGRES_PASSWORD": "root" + } + database-health-cmd: "pg_isready -U postgres" + database-health-retries: 3 + database-image: postgres + database-port: 5432 + database-type: pgsql + database-versions: '["15", "16", "17"]' + enable-concurrency: true + extensions: pdo, pdo_pgsql + os: '["ubuntu-latest"]' + php-version: '["8.4"]' + phpunit-group: pgsql diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 089297a..ce9b83a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,7 @@ jobs: composer require yiisoft/yii2:22.0.x-dev --prefer-dist --no-progress --no-interaction --no-scripts --ansi concurrency-group: phpunit-${{ github.workflow }}-${{ github.ref }} extensions: pdo, pdo_sqlite + phpunit-group: sqlite phpunit-compatibility: uses: php-forge/actions/.github/workflows/phpunit.yml@main secrets: @@ -34,3 +35,4 @@ jobs: with: concurrency-group: compatibility-${{ github.workflow }}-${{ github.ref }} extensions: pdo, pdo_sqlite + phpunit-group: sqlite diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 9c6f1ec..6d35498 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -22,5 +22,6 @@ jobs: uses: php-forge/actions/.github/workflows/infection.yml@main with: phpstan: true + framework-options: --test-framework-options="--group=sqlite" secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/composer.json b/composer.json index 7fe8f94..3d299ea 100644 --- a/composer.json +++ b/composer.json @@ -51,8 +51,8 @@ "scripts": { "check-dependencies": "./vendor/bin/composer-require-checker check", "ecs": "./vendor/bin/ecs --fix", - "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100", - "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan", + "mutation": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100 --test-framework-options=--group=sqlite", + "mutation-static": "./vendor/bin/infection --threads=4 --ignore-msi-with-no-mutations --only-covered --min-msi=100 --min-covered-msi=100 --static-analysis-tool=phpstan --test-framework-options=--group=sqlite", "rector": "./vendor/bin/rector process src", "static": "./vendor/bin/phpstan --memory-limit=512M", "tests": "./vendor/bin/phpunit" diff --git a/src/NestedSetsBehavior.php b/src/NestedSetsBehavior.php index 7becb13..211d7d9 100644 --- a/src/NestedSetsBehavior.php +++ b/src/NestedSetsBehavior.php @@ -1070,7 +1070,10 @@ protected function beforeInsertNode(int $value, int $depth): void */ protected function beforeInsertRootNode(): void { - if ($this->treeAttribute === false && $this->getOwner()::find()->roots()->exists()) { + if ( + $this->treeAttribute === false && + $this->getOwner()::find()->andWhere([$this->leftAttribute => 1])->exists() + ) { throw new Exception('Can not create more than one root when "treeAttribute" is false.'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 0bf1beb..58c9e0e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,7 @@ use function array_values; use function dom_import_simplexml; use function file_get_contents; +use function preg_replace; use function simplexml_load_string; use function str_replace; @@ -38,9 +39,12 @@ class TestCase extends \PHPUnit\Framework\TestCase { use SchemaBuilderTrait; + protected string $driverName = 'sqlite'; protected string|null $dsn = null; protected string $fixtureDirectory = __DIR__ . '/support/data/'; + protected string $password = ''; + protected string $username = ''; protected function setUp(): void { @@ -103,12 +107,12 @@ protected function assertQueryHasOrderBy(ActiveQuery $query, string $methodName) self::assertStringContainsString( 'ORDER BY', - $sql, + $this->replaceQuotes($sql), "'{$methodName}' query should include 'ORDER BY' clause for deterministic results.", ); self::assertStringContainsString( - '`lft`', + $this->replaceQuotes('[[lft]]'), $sql, "'{$methodName}' query should order by 'left' attribute for consistent ordering.", ); @@ -167,11 +171,11 @@ protected function createDatabase(): void $command = $this->getDb()->createCommand(); if ($this->getDb()->getTableSchema('tree', true) !== null) { - $command->dropTable('tree'); + $command->dropTable('tree')->execute(); } if ($this->getDb()->getTableSchema('multiple_tree', true) !== null) { - $command->dropTable('multiple_tree'); + $command->dropTable('multiple_tree')->execute(); } $command->createTable( @@ -291,14 +295,14 @@ protected function generateFixtureTree(): void */ protected function getDataSet(): array { - $dataSetTree = Tree::find()->asArray()->all(); + $dataSetTree = Tree::find()->orderBy(['id' => SORT_ASC])->asArray()->all(); foreach ($dataSetTree as $key => $value) { $dataSetTree[$key]['type'] = 'tree'; $dataSetTree[$key]['tree'] = 0; } - $dataSetMultipleTree = MultipleTree::find()->asArray()->all(); + $dataSetMultipleTree = MultipleTree::find()->orderBy(['id' => SORT_ASC])->asArray()->all(); foreach ($dataSetMultipleTree as $key => $value) { $dataSetMultipleTree[$key]['type'] = 'multiple_tree'; @@ -314,7 +318,7 @@ protected function getDataSet(): array */ protected function getDataSetMultipleTree(): array { - $dataSetMultipleTree = MultipleTree::find()->asArray()->all(); + $dataSetMultipleTree = MultipleTree::find()->orderBy(['id' => SORT_ASC])->asArray()->all(); foreach ($dataSetMultipleTree as $key => $value) { $dataSetMultipleTree[$key]['type'] = 'multiple_tree'; @@ -352,12 +356,48 @@ protected function mockConsoleApplication(): void 'db' => [ 'class' => Connection::class, 'dsn' => $this->dsn !== null ? $this->dsn : 'sqlite::memory:', + 'password' => $this->password, + 'username' => $this->username, ], ], ], ); } + /** + * Adjust dbms specific escaping. + * + * @param string $sql SQL to adjust. + * + * @return string Adjusted SQL. + */ + protected function replaceQuotes(string $sql): string + { + return match ($this->driverName) { + 'mysql', 'sqlite' => str_replace( + ['[[', ']]'], + '`', + $sql, + ), + 'oci' => str_replace( + ['[[', ']]'], + '"', + $sql, + ), + 'pgsql' => str_replace( + ['\\[', '\\]'], + ['[', ']'], + preg_replace('/(\[\[)|((? str_replace( + ['[[', ']]'], + ['[', ']'], + $sql, + ), + default => $sql, + }; + } + /** * Applies database updates to tree nodes. * diff --git a/tests/base/AbstractQueryBehavior.php b/tests/base/AbstractQueryBehavior.php index 53c3e2f..2dfbbdd 100644 --- a/tests/base/AbstractQueryBehavior.php +++ b/tests/base/AbstractQueryBehavior.php @@ -128,7 +128,7 @@ public function testRootsMethodRequiresLeftAttributeOrderingWhenTreeAttributeIsD "'roots()' query should include 'ORDER BY' clause for consistent results.", ); self::assertStringContainsString( - '`lft`', + $this->replaceQuotes('[[lft]]'), $sql, "'roots()' query should order by 'left' attribute for deterministic ordering.", ); diff --git a/tests/mssql/CacheManagementTest.php b/tests/mssql/CacheManagementTest.php new file mode 100644 index 0000000..804feb0 --- /dev/null +++ b/tests/mssql/CacheManagementTest.php @@ -0,0 +1,17 @@ +