diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 28902213..146abc4e 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -238,7 +238,9 @@ public function isIdempotent(): bool public function call(string $seeder, array $options = []): void { $io = $this->getIo(); - assert($io !== null, 'Requires ConsoleIo'); + if ($io === null) { + throw new RuntimeException('ConsoleIo is required for calling other seeders.'); + } $io->out(''); $io->out( ' ====' . @@ -285,7 +287,9 @@ protected function runCall(string $seeder, array $options = []): void 'source' => $options['source'], ]); $io = $this->getIo(); - assert($io !== null, 'Missing ConsoleIo instance'); + if ($io === null) { + throw new RuntimeException('ConsoleIo is required for calling other seeders.'); + } $manager = $factory->createManager($io); $manager->seed($seeder); } diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index cb3ad01e..68bcd71b 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -19,8 +19,15 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Database\Connection; +use Cake\Database\Schema\CachedCollection; +use Cake\Database\Schema\CheckConstraint; use Cake\Database\Schema\CollectionInterface; +use Cake\Database\Schema\Column; +use Cake\Database\Schema\Constraint; +use Cake\Database\Schema\ForeignKey; +use Cake\Database\Schema\Index; use Cake\Database\Schema\TableSchema; +use Cake\Database\Schema\UniqueKey; use Cake\Datasource\ConnectionManager; use Cake\Event\Event; use Cake\Event\EventManager; @@ -485,7 +492,7 @@ protected function checkSync(): bool $lastVersion = $this->migratedItems[0]['version']; $lastFile = end($this->migrationsFiles); - return $lastFile && (bool)strpos($lastFile, (string)$lastVersion); + return $lastFile && str_contains($lastFile, (string)$lastVersion); } return false; @@ -546,7 +553,21 @@ protected function getDumpSchema(Arguments $args): array $this->io->abort($msg); } - return unserialize((string)file_get_contents($path)); + $contents = (string)file_get_contents($path); + + // Use allowed_classes to restrict deserialization to safe CakePHP schema classes + return unserialize($contents, [ + 'allowed_classes' => [ + TableSchema::class, + CachedCollection::class, + Column::class, + Index::class, + Constraint::class, + UniqueKey::class, + ForeignKey::class, + CheckConstraint::class, + ], + ]); } /** diff --git a/src/Command/DumpCommand.php b/src/Command/DumpCommand.php index 0e694b41..53c79026 100644 --- a/src/Command/DumpCommand.php +++ b/src/Command/DumpCommand.php @@ -141,7 +141,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int return self::CODE_SUCCESS; } - $io->error("An error occurred while writing dump file `{$filePath}`"); + $io->err("An error occurred while writing dump file `{$filePath}`"); return self::CODE_ERROR; } diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index eaca68bd..0873581a 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -725,7 +725,9 @@ protected function getRenameColumnInstructions(string $tableName, string $column foreach ($rows as $row) { if (strcasecmp($row['Field'], $columnName) === 0) { $null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL'; - $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : ''; + $comment = isset($row['Comment']) && $row['Comment'] !== '' + ? ' COMMENT ' . $this->getConnection()->getDriver()->schemaValue($row['Comment']) + : ''; // create the extra string by also filtering out the DEFAULT_GENERATED option (MySQL 8 fix) $extras = array_filter( diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index 14602abc..f1f330dc 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -1105,27 +1105,38 @@ private function updateSQLForIdentityInsert(string $tableName, string $sql): str /** * @inheritDoc + * + * Note: Check constraints are not supported for SQL Server adapter. + * This method returns an empty array. Use raw SQL via execute() if you need + * check constraints on SQL Server. */ protected function getCheckConstraints(string $tableName): array { - // TODO: Implement check constraints for SQL Server return []; } /** * @inheritDoc + * @throws \BadMethodCallException Check constraints are not supported for SQL Server. */ protected function getAddCheckConstraintInstructions(TableMetadata $table, CheckConstraint $checkConstraint): AlterInstructions { - throw new BadMethodCallException('Check constraints are not yet implemented for SQL Server adapter'); + throw new BadMethodCallException( + 'Check constraints are not supported for the SQL Server adapter. ' . + 'Use $this->execute() with raw SQL to add check constraints.', + ); } /** * @inheritDoc + * @throws \BadMethodCallException Check constraints are not supported for SQL Server. */ protected function getDropCheckConstraintInstructions(string $tableName, string $constraintName): AlterInstructions { - throw new BadMethodCallException('Check constraints are not yet implemented for SQL Server adapter'); + throw new BadMethodCallException( + 'Check constraints are not supported for the SQL Server adapter. ' . + 'Use $this->execute() with raw SQL to drop check constraints.', + ); } /** diff --git a/src/Db/Table.php b/src/Db/Table.php index 3998627d..aeeb0906 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -955,7 +955,7 @@ public function saveData(): void $c = array_keys($row); foreach ($this->getData() as $row) { $k = array_keys($row); - if ($k != $c) { + if ($k !== $c) { $bulk = false; break; } diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index a16585f9..98f28b31 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -789,6 +789,7 @@ protected function getValidOptions(): array 'update', 'comment', 'signed', + 'unsigned', 'timezone', 'properties', 'values', diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 2ce00a17..862e7627 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -1101,18 +1101,46 @@ protected function getSeedDependenciesInstances(SeedInterface $seed): array * Order seeds by dependencies * * @param \Migrations\SeedInterface[] $seeds Seeds + * @param array $visiting Seeds currently being visited (for cycle detection) + * @param array $visited Seeds that have been fully processed * @return \Migrations\SeedInterface[] + * @throws \RuntimeException When a circular dependency is detected */ - protected function orderSeedsByDependencies(array $seeds): array + protected function orderSeedsByDependencies(array $seeds, array $visiting = [], array &$visited = []): array { $orderedSeeds = []; foreach ($seeds as $seed) { $name = $seed->getName(); - $orderedSeeds[$name] = $seed; + + // Skip if already fully processed + if (isset($visited[$name])) { + continue; + } + + // Check for circular dependency + if (isset($visiting[$name])) { + $cycle = array_keys($visiting); + $cycle[] = $name; + throw new RuntimeException( + 'Circular dependency detected in seeds: ' . implode(' -> ', $cycle), + ); + } + + // Mark as currently visiting + $visiting[$name] = true; + $dependencies = $this->getSeedDependenciesInstances($seed); if ($dependencies) { - $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds); + $orderedSeeds = array_merge( + $this->orderSeedsByDependencies($dependencies, $visiting, $visited), + $orderedSeeds, + ); } + + // Mark as fully visited and add to result + $visited[$name] = true; + unset($visiting[$name]); + $orderedSeeds[$name] = $seed; } return $orderedSeeds; diff --git a/src/Migrations.php b/src/Migrations.php index bba10eee..b61bf07c 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -36,7 +36,7 @@ class Migrations * * @var string */ - protected string $command; + protected string $command = ''; /** * Constructor diff --git a/src/TestSuite/Migrator.php b/src/TestSuite/Migrator.php index a33e9526..a0c30160 100644 --- a/src/TestSuite/Migrator.php +++ b/src/TestSuite/Migrator.php @@ -199,7 +199,7 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo if (!empty($messages['missing'])) { $hasProblems = true; $output[] = 'Applied but missing migrations:'; - $output = array_merge($output, array_map($itemize, $messages['down'])); + $output = array_merge($output, array_map($itemize, $messages['missing'])); $output[] = ''; } if ($output) {