From 692065497fbdbc6768ae245241d587dac51325f3 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 07:05:10 -0400 Subject: [PATCH 1/9] feat: Add migration files for `tree` and `multiple_tree` tables with support for various database drivers. --- migrations/m250707_103609_tree.php | 45 +++++++++++++ migrations/m250707_104009_multiple_tree.php | 45 +++++++++++++ tests/TestCase.php | 70 ++++++++------------ tests/support/stub/EchoMigrateController.php | 15 +++++ 4 files changed, 134 insertions(+), 41 deletions(-) create mode 100644 migrations/m250707_103609_tree.php create mode 100644 migrations/m250707_104009_multiple_tree.php create mode 100644 tests/support/stub/EchoMigrateController.php diff --git a/migrations/m250707_103609_tree.php b/migrations/m250707_103609_tree.php new file mode 100644 index 0000000..49826bd --- /dev/null +++ b/migrations/m250707_103609_tree.php @@ -0,0 +1,45 @@ +db->driverName === 'oci' + ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' + : $this->primaryKey(); + + $nameColumn = $this->db->driverName === 'oci' + ? $this->string(4000)->notNull() + : $this->text()->notNull(); + + $this->createTable('{{%tree}}', [ + 'id' => $primaryKey, + 'name' => $nameColumn, + 'lft' => $this->integer()->notNull(), + 'rgt' => $this->integer()->notNull(), + 'depth' => $this->integer()->notNull(), + ]); + + $this->createIndex('idx_tree_lft', '{{%tree}}', 'lft'); + $this->createIndex('idx_tree_rgt', '{{%tree}}', 'rgt'); + $this->createIndex('idx_tree_depth', '{{%tree}}', 'depth'); + $this->createIndex('idx_tree_lft_rgt', '{{%tree}}', ['lft', 'rgt']); + + if ($this->db->driverName !== 'sqlite') { + $this->addCommentOnTable('{{%tree}}', 'Nested sets tree structure for hierarchical data'); + $this->addCommentOnColumn('{{%tree}}', 'id', 'Primary key of the tree node'); + $this->addCommentOnColumn('{{%tree}}', 'name', 'Name of the tree node'); + $this->addCommentOnColumn('{{%tree}}', 'lft', 'Left boundary of nested set'); + $this->addCommentOnColumn('{{%tree}}', 'rgt', 'Right boundary of nested set'); + $this->addCommentOnColumn('{{%tree}}', 'depth', 'Node depth in the tree hierarchy'); + } + } + + public function safeDown() + { + $this->dropTable('{{%tree}}'); + } + +} diff --git a/migrations/m250707_104009_multiple_tree.php b/migrations/m250707_104009_multiple_tree.php new file mode 100644 index 0000000..5e299df --- /dev/null +++ b/migrations/m250707_104009_multiple_tree.php @@ -0,0 +1,45 @@ +db->driverName === 'oci' + ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' + : $this->primaryKey(); + + $nameColumn = $this->db->driverName === 'oci' + ? $this->string(4000)->notNull() + : $this->text()->notNull(); + + $this->createTable('{{%multiple_tree}}', [ + 'id' => $primaryKey, + 'tree' => $this->integer()->null(), + 'name' => $nameColumn, + 'lft' => $this->integer()->notNull(), + 'rgt' => $this->integer()->notNull(), + 'depth' => $this->integer()->notNull(), + ]); + + $this->createIndex('idx_multiple_tree_tree', '{{%multiple_tree}}', 'tree'); + $this->createIndex('idx_multiple_tree_lft', '{{%multiple_tree}}', 'lft'); + $this->createIndex('idx_multiple_tree_rgt', '{{%multiple_tree}}', 'rgt'); + $this->createIndex('idx_multiple_tree_depth', '{{%multiple_tree}}', 'depth'); + $this->createIndex('idx_multiple_tree_tree_lft_rgt', '{{%multiple_tree}}', ['tree', 'lft', 'rgt']); + + if ($this->db->driverName !== 'sqlite') { + $this->addCommentOnTable('{{%multiple_tree}}', 'Multiple nested sets tree structure for hierarchical data'); + $this->addCommentOnColumn('{{%multiple_tree}}', 'tree', 'Tree identifier for multiple trees support'); + $this->addCommentOnColumn('{{%multiple_tree}}', 'lft', 'Left boundary of nested set'); + $this->addCommentOnColumn('{{%multiple_tree}}', 'rgt', 'Right boundary of nested set'); + $this->addCommentOnColumn('{{%multiple_tree}}', 'depth', 'Node depth in the tree hierarchy'); + } + } + + public function safeDown() + { + $this->dropTable('{{%multiple_tree}}'); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 521ee5d..b69c761 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,8 +9,9 @@ use Yii; use yii\base\InvalidArgumentException; use yii\console\Application; -use yii\db\{ActiveQuery, ActiveRecord, Connection, SchemaBuilderTrait}; +use yii\db\{ActiveQuery, ActiveRecord, Connection, Query, SchemaBuilderTrait}; use yii2\extensions\nestedsets\tests\support\model\{MultipleTree, Tree}; +use yii2\extensions\nestedsets\tests\support\stub\EchoMigrateController; use function array_merge; use function array_values; @@ -168,45 +169,8 @@ protected function buildFlatXMLDataSet(array $dataSet): string protected function createDatabase(): void { - $command = $this->getDb()->createCommand(); - - if ($this->getDb()->getTableSchema('tree', true) !== null) { - $command->dropTable('tree')->execute(); - } - - if ($this->getDb()->getTableSchema('multiple_tree', true) !== null) { - $command->dropTable('multiple_tree')->execute(); - } - - $primaryKey = $this->driverName === 'oci' - ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' - : $this->primaryKey()->notNull(); - $name = $this->driverName === 'oci' - ? $this->string()->notNull() - : $this->text()->notNull(); - - $command->createTable( - 'tree', - [ - 'id' => $primaryKey, - 'name' => $name, - 'lft' => $this->integer()->notNull(), - 'rgt' => $this->integer()->notNull(), - 'depth' => $this->integer()->notNull(), - ], - )->execute(); - - $command->createTable( - 'multiple_tree', - [ - 'id' => $primaryKey, - 'tree' => $this->integer(), - 'name' => $name, - 'lft' => $this->integer()->notNull(), - 'rgt' => $this->integer()->notNull(), - 'depth' => $this->integer()->notNull(), - ], - )->execute(); + $this->runMigrate('down'); + $this->runMigrate('up'); } /** @@ -261,7 +225,7 @@ protected function createTreeStructure( protected function generateFixtureTree(): void { - $this->createDatabase(); + $this->runMigrate('up'); $command = $this->getDb()->createCommand(); @@ -334,6 +298,30 @@ protected function getDataSetMultipleTree(): array return array_values($dataSetMultipleTree); } + /** + * @phpstan-param array $params + */ + protected function runMigrate(string $action, array $params = []): mixed + { + $migrate = new EchoMigrateController( + 'migrate', + Yii::$app, + [ + 'migrationPath' => dirname(__DIR__) . '/migrations', + 'interactive' => false, + ], + ); + + ob_start(); + ob_implicit_flush(false); + + $result = $migrate->run($action, $params); + + ob_get_clean(); + + return $result; + } + protected function loadFixtureXML(string $fileName): SimpleXMLElement { $filePath = "{$this->fixtureDirectory}/{$fileName}"; diff --git a/tests/support/stub/EchoMigrateController.php b/tests/support/stub/EchoMigrateController.php new file mode 100644 index 0000000..316d9c8 --- /dev/null +++ b/tests/support/stub/EchoMigrateController.php @@ -0,0 +1,15 @@ + Date: Mon, 7 Jul 2025 11:05:44 +0000 Subject: [PATCH 2/9] Apply fixes from StyleCI --- migrations/m250707_103609_tree.php | 3 ++- migrations/m250707_104009_multiple_tree.php | 2 ++ tests/support/stub/EchoMigrateController.php | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/migrations/m250707_103609_tree.php b/migrations/m250707_103609_tree.php index 49826bd..494cd26 100644 --- a/migrations/m250707_103609_tree.php +++ b/migrations/m250707_103609_tree.php @@ -1,5 +1,7 @@ dropTable('{{%tree}}'); } - } diff --git a/migrations/m250707_104009_multiple_tree.php b/migrations/m250707_104009_multiple_tree.php index 5e299df..e3bb35d 100644 --- a/migrations/m250707_104009_multiple_tree.php +++ b/migrations/m250707_104009_multiple_tree.php @@ -1,5 +1,7 @@ Date: Mon, 7 Jul 2025 07:22:48 -0400 Subject: [PATCH 3/9] fix: update database migration rollback to include all migrations. --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index b69c761..f861646 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -169,7 +169,7 @@ protected function buildFlatXMLDataSet(array $dataSet): string protected function createDatabase(): void { - $this->runMigrate('down'); + $this->runMigrate('down', ['all']); $this->runMigrate('up'); } From dce2ec72071a42b92e711f8606fb73d1e7514543 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 07:35:22 -0400 Subject: [PATCH 4/9] fix: enhance database creation by ensuring migration table is dropped before running migrations. --- migrations/m250707_103609_tree.php | 4 +-- tests/TestCase.php | 56 +++++++++++++++++------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/migrations/m250707_103609_tree.php b/migrations/m250707_103609_tree.php index 494cd26..faec09c 100644 --- a/migrations/m250707_103609_tree.php +++ b/migrations/m250707_103609_tree.php @@ -10,10 +10,10 @@ public function safeUp() { $primaryKey = $this->db->driverName === 'oci' ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' - : $this->primaryKey(); + : $this->primaryKey()->notNull(); $nameColumn = $this->db->driverName === 'oci' - ? $this->string(4000)->notNull() + ? $this->string()->notNull() : $this->text()->notNull(); $this->createTable('{{%tree}}', [ diff --git a/tests/TestCase.php b/tests/TestCase.php index f861646..148bbd8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -169,7 +169,13 @@ protected function buildFlatXMLDataSet(array $dataSet): string protected function createDatabase(): void { - $this->runMigrate('down', ['all']); + $command = $this->getDb()->createCommand(); + + if ($this->getDb()->getTableSchema('migration', true) !== null) { + $command->dropTable('migration')->execute(); + } + + $this->runMigrate('down', ['all' => true]); $this->runMigrate('up'); } @@ -298,30 +304,6 @@ protected function getDataSetMultipleTree(): array return array_values($dataSetMultipleTree); } - /** - * @phpstan-param array $params - */ - protected function runMigrate(string $action, array $params = []): mixed - { - $migrate = new EchoMigrateController( - 'migrate', - Yii::$app, - [ - 'migrationPath' => dirname(__DIR__) . '/migrations', - 'interactive' => false, - ], - ); - - ob_start(); - ob_implicit_flush(false); - - $result = $migrate->run($action, $params); - - ob_get_clean(); - - return $result; - } - protected function loadFixtureXML(string $fileName): SimpleXMLElement { $filePath = "{$this->fixtureDirectory}/{$fileName}"; @@ -393,6 +375,30 @@ protected function replaceQuotes(string $sql): string }; } + /** + * @phpstan-param array $params + */ + protected function runMigrate(string $action, array $params = []): mixed + { + $migrate = new EchoMigrateController( + 'migrate', + Yii::$app, + [ + 'migrationPath' => dirname(__DIR__) . '/migrations', + 'interactive' => false, + ], + ); + + ob_start(); + ob_implicit_flush(false); + + $result = $migrate->run($action, $params); + + ob_get_clean(); + + return $result; + } + /** * Applies database updates to tree nodes. * From 3ac21283b9e627fbc7dac540ee16a9b998d3f71c Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 07:37:51 -0400 Subject: [PATCH 5/9] fix: update migration rollback parameters for consistency and improve PHPStan type annotation for params. --- tests/TestCase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 148bbd8..fd7f122 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -175,7 +175,7 @@ protected function createDatabase(): void $command->dropTable('migration')->execute(); } - $this->runMigrate('down', ['all' => true]); + $this->runMigrate('down', ['all']); $this->runMigrate('up'); } @@ -376,7 +376,7 @@ protected function replaceQuotes(string $sql): string } /** - * @phpstan-param array $params + * @phpstan-param array $params */ protected function runMigrate(string $action, array $params = []): mixed { From 0eaa55615c7612fb9777c91ced26cebdf8c3c8a1 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 07:59:28 -0400 Subject: [PATCH 6/9] fix: ensure migration tables are dropped before running migrations in tests. --- tests/TestCase.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index fd7f122..6625f77 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -175,7 +175,14 @@ protected function createDatabase(): void $command->dropTable('migration')->execute(); } - $this->runMigrate('down', ['all']); + if ($this->getDb()->getTableSchema('tree', true) !== null) { + $command->dropTable('tree')->execute(); + } + + if ($this->getDb()->getTableSchema('multiple_tree', true) !== null) { + $command->dropTable('multiple_tree')->execute(); + } + $this->runMigrate('up'); } From 80be5fc66dad65112a875276cb44deddfdf876fc Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 08:09:06 -0400 Subject: [PATCH 7/9] fix: replace migration command with direct database creation in test setup. --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 6625f77..66657f6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -238,7 +238,7 @@ protected function createTreeStructure( protected function generateFixtureTree(): void { - $this->runMigrate('up'); + $this->createDatabase(); $command = $this->getDb()->createCommand(); From f3b8b3266a1e4115cfd58673265e711074aaedcf Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 08:14:56 -0400 Subject: [PATCH 8/9] fix: streamline database setup by removing redundant table drops in test case. --- tests/TestCase.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 66657f6..2f963fd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -171,18 +171,12 @@ protected function createDatabase(): void { $command = $this->getDb()->createCommand(); + $this->runMigrate('down', ['all']); + if ($this->getDb()->getTableSchema('migration', true) !== null) { $command->dropTable('migration')->execute(); } - if ($this->getDb()->getTableSchema('tree', true) !== null) { - $command->dropTable('tree')->execute(); - } - - if ($this->getDb()->getTableSchema('multiple_tree', true) !== null) { - $command->dropTable('multiple_tree')->execute(); - } - $this->runMigrate('up'); } From 6de7b05834ea75450aec5fb37b4ad248f7f98bb7 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Mon, 7 Jul 2025 08:37:07 -0400 Subject: [PATCH 9/9] fix: refactor migration table creation for consistency and improve test database teardown process. --- migrations/m250707_103609_tree.php | 27 +++++++++---------- migrations/m250707_104009_multiple_tree.php | 29 +++++++++------------ tests/TestCase.php | 24 +++++++++++++---- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/migrations/m250707_103609_tree.php b/migrations/m250707_103609_tree.php index faec09c..43e0688 100644 --- a/migrations/m250707_103609_tree.php +++ b/migrations/m250707_103609_tree.php @@ -8,21 +8,18 @@ class m250707_103609_tree extends Migration { public function safeUp() { - $primaryKey = $this->db->driverName === 'oci' - ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' - : $this->primaryKey()->notNull(); - - $nameColumn = $this->db->driverName === 'oci' - ? $this->string()->notNull() - : $this->text()->notNull(); - - $this->createTable('{{%tree}}', [ - 'id' => $primaryKey, - 'name' => $nameColumn, - 'lft' => $this->integer()->notNull(), - 'rgt' => $this->integer()->notNull(), - 'depth' => $this->integer()->notNull(), - ]); + $rawPrimaryKey = 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'; + + $this->createTable( + '{{%tree}}', + [ + 'id' => $this->db->driverName !== 'oci' ? $this->primaryKey()->notNull() : $rawPrimaryKey, + 'name' => $this->db->driverName === 'oci' ? $this->string()->notNull() : $this->text()->notNull(), + 'lft' => $this->integer()->notNull(), + 'rgt' => $this->integer()->notNull(), + 'depth' => $this->integer()->notNull(), + ], + ); $this->createIndex('idx_tree_lft', '{{%tree}}', 'lft'); $this->createIndex('idx_tree_rgt', '{{%tree}}', 'rgt'); diff --git a/migrations/m250707_104009_multiple_tree.php b/migrations/m250707_104009_multiple_tree.php index e3bb35d..cfaf25b 100644 --- a/migrations/m250707_104009_multiple_tree.php +++ b/migrations/m250707_104009_multiple_tree.php @@ -8,22 +8,19 @@ class m250707_104009_multiple_tree extends Migration { public function safeUp() { - $primaryKey = $this->db->driverName === 'oci' - ? 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' - : $this->primaryKey(); - - $nameColumn = $this->db->driverName === 'oci' - ? $this->string(4000)->notNull() - : $this->text()->notNull(); - - $this->createTable('{{%multiple_tree}}', [ - 'id' => $primaryKey, - 'tree' => $this->integer()->null(), - 'name' => $nameColumn, - 'lft' => $this->integer()->notNull(), - 'rgt' => $this->integer()->notNull(), - 'depth' => $this->integer()->notNull(), - ]); + $rawPrimaryKey = 'NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'; + + $this->createTable( + '{{%multiple_tree}}', + [ + 'id' => $this->db->driverName !== 'oci' ? $this->primaryKey()->notNull() : $rawPrimaryKey, + 'tree' => $this->integer()->null(), + 'name' => $this->db->driverName === 'oci' ? $this->string()->notNull() : $this->text()->notNull(), + 'lft' => $this->integer()->notNull(), + 'rgt' => $this->integer()->notNull(), + 'depth' => $this->integer()->notNull(), + ], + ); $this->createIndex('idx_multiple_tree_tree', '{{%multiple_tree}}', 'tree'); $this->createIndex('idx_multiple_tree_lft', '{{%multiple_tree}}', 'lft'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 2f963fd..bac0ea2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -170,11 +170,21 @@ protected function buildFlatXMLDataSet(array $dataSet): string protected function createDatabase(): void { $command = $this->getDb()->createCommand(); + $dropTables = [ + 'migration', + 'multiple_tree', + 'tree', + ]; + + try { + $this->runMigrate('down', ['all']); + } catch (RuntimeException) { + } - $this->runMigrate('down', ['all']); - - if ($this->getDb()->getTableSchema('migration', true) !== null) { - $command->dropTable('migration')->execute(); + foreach ($dropTables as $table) { + if ($this->getDb()->getTableSchema($table, true) !== null) { + $command->dropTable($table)->execute(); + } } $this->runMigrate('up'); @@ -395,7 +405,11 @@ protected function runMigrate(string $action, array $params = []): mixed $result = $migrate->run($action, $params); - ob_get_clean(); + $capture = ob_get_clean(); + + if (is_int($result) && $result !== 0) { + throw new RuntimeException("Migration '{$action}' failed with code {$result}.\nOutput: {$capture}"); + } return $result; }