diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74212692..194036e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.4'] + php-version: ['8.2', '8.4'] db-type: [mariadb, mysql, pgsql, sqlite] prefer-lowest: [''] - cake_version: [''] include: - - php-version: '8.1' + - php-version: '8.2' db-type: 'sqlite' prefer-lowest: 'prefer-lowest' - php-version: '8.3' @@ -106,13 +105,9 @@ jobs: - name: Composer install run: | if [[ ${{ matrix.php-version }} == '8.2' || ${{ matrix.php-version }} == '8.3' || ${{ matrix.php-version }} == '8.4' ]]; then - composer install --ignore-platform-req=php + composer install elif ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then composer update --prefer-lowest --prefer-stable - elif ${{ matrix.cake_version != '' }}; then - composer require --dev "cakephp/cakephp:${{ matrix.cake_version }}" - composer require --dev --with-all-dependencies "cakephp/bake:dev-3.next as 3.1.0" - composer update else composer update fi @@ -151,11 +146,11 @@ jobs: testsuite-windows: runs-on: windows-2022 - name: Windows - PHP 8.1 & SQL Server + name: Windows - PHP 8.2 & SQL Server env: EXTENSIONS: mbstring, intl, pdo_sqlsrv - PHP_VERSION: '8.1' + PHP_VERSION: '8.2' steps: - uses: actions/checkout@v5 diff --git a/composer.json b/composer.json index e4761a88..96e89334 100644 --- a/composer.json +++ b/composer.json @@ -22,17 +22,15 @@ "source": "https://github.com/cakephp/migrations" }, "require": { - "php": ">=8.1", - "cakephp/cache": "^5.2", - "cakephp/orm": "^5.2", - "symfony/config": "^6.0 || ^7.0", - "symfony/console": "^6.0 || ^7.0" + "php": ">=8.2", + "cakephp/cache": "dev-5.next as 5.3.0", + "cakephp/orm": "dev-5.next as 5.3.0" }, "require-dev": { "cakephp/bake": "^3.3", - "cakephp/cakephp": "^5.2.5", + "cakephp/cakephp": "dev-5.next as 5.3.0", "cakephp/cakephp-codesniffer": "^5.0", - "phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.2.4" + "phpunit/phpunit": "^11.5.3 || ^12.1.3" }, "suggest": { "cakephp/bake": "If you want to generate migrations.", @@ -51,7 +49,9 @@ "Migrations\\Test\\": "tests/", "SimpleSnapshot\\": "tests/test_app/Plugin/SimpleSnapshot/src/", "TestApp\\": "tests/test_app/App/", - "TestBlog\\": "tests/test_app/Plugin/TestBlog/src/" + "TestBlog\\": "tests/test_app/Plugin/TestBlog/src/", + "Blog\\": "tests/test_app/Plugin/Blog/src/", + "Migrator\\": "tests/test_app/Plugin/Migrator/src/" } }, "config": { diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 43fe62b7..45047f20 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -42,11 +42,8 @@ use Migrations\Db\Table\Index; use Migrations\Db\Table\Table as TableMetadata; use Migrations\MigrationInterface; -use Migrations\Shim\OutputAdapter; use PDOException; use RuntimeException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; /** * Base Abstract Database Adapter. @@ -300,38 +297,6 @@ protected function verboseLog(string $message): void $io->out($message); } - /** - * @inheritDoc - */ - public function setInput(InputInterface $input): AdapterInterface - { - throw new RuntimeException('Using setInput() interface is not supported.'); - } - - /** - * @inheritDoc - */ - public function getInput(): ?InputInterface - { - throw new RuntimeException('Using getInput() interface is not supported.'); - } - - /** - * @inheritDoc - */ - public function setOutput(OutputInterface $output): AdapterInterface - { - throw new RuntimeException('Using setInput() method is not supported'); - } - - /** - * @inheritDoc - */ - public function getOutput(): OutputInterface - { - return new OutputAdapter($this->io); - } - /** * Gets the schema table name. * diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 6585e153..7501e795 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -42,6 +42,10 @@ interface AdapterInterface public const PHINX_TYPE_BINARYUUID = TableSchemaInterface::TYPE_BINARY_UUID; public const PHINX_TYPE_BOOLEAN = TableSchemaInterface::TYPE_BOOLEAN; public const PHINX_TYPE_JSON = TableSchemaInterface::TYPE_JSON; + /** + * @deprecated 5.0.0 Use TableSchemaInterface::TYPE_JSON instead. + */ + public const PHINX_TYPE_JSONB = 'jsonb'; public const PHINX_TYPE_UUID = TableSchemaInterface::TYPE_UUID; public const PHINX_TYPE_NATIVEUUID = TableSchemaInterface::TYPE_NATIVE_UUID; diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 327de308..1b6b7b0e 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -35,6 +35,7 @@ class PostgresAdapter extends AbstractAdapter */ protected static array $specificColumnTypes = [ self::PHINX_TYPE_JSON, + self::PHINX_TYPE_JSONB, self::PHINX_TYPE_CIDR, self::PHINX_TYPE_INET, self::PHINX_TYPE_MACADDR, @@ -454,6 +455,8 @@ protected function getChangeColumnInstructions( $columnSql = $dialect->columnDefinitionSql($this->mapColumnData($newColumn->toArray())); // Remove the column name from $columnSql $columnType = preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql); + // Remove generated clause + $columnType = preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType); $sql = sprintf( 'ALTER COLUMN %s TYPE %s', diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index d2279a49..6d9ffa86 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -834,6 +834,7 @@ public function toArray(): array 'length' => $length, 'null' => $this->getNull(), 'default' => $default, + 'generated' => $this->getGenerated(), 'unsigned' => !$this->getSigned(), 'onUpdate' => $this->getUpdate(), 'collate' => $this->getCollation(), diff --git a/src/Migrations.php b/src/Migrations.php index f227e09c..bba10eee 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -15,10 +15,6 @@ use Migrations\Migration\BackendInterface; use Migrations\Migration\BuiltinBackend; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\OutputInterface; /** * The Migrations class is responsible for handling migrations command @@ -26,14 +22,6 @@ */ class Migrations { - /** - * The OutputInterface. - * Should be a \Symfony\Component\Console\Output\NullOutput instance - * - * @var \Symfony\Component\Console\Output\OutputInterface - */ - protected OutputInterface $output; - /** * Default options to use * @@ -50,14 +38,6 @@ class Migrations */ protected string $command; - /** - * Stub input to feed the manager class since we might not have an input ready when we get the Manager using - * the `getManager()` method - * - * @var \Symfony\Component\Console\Input\ArrayInput - */ - protected ArrayInput $stubInput; - /** * Constructor * @@ -69,9 +49,6 @@ class Migrations */ public function __construct(array $default = []) { - $this->output = new NullOutput(); - $this->stubInput = new ArrayInput([]); - if ($default) { $this->default = $default; } @@ -195,51 +172,4 @@ public function seed(array $options = []): bool return $backend->seed($options); } - - /** - * Get the input needed for each commands to be run - * - * TODO(mark) Remove as part of phinx removal - * - * @param string $command Command name for which we need the InputInterface - * @param array $arguments Simple key/values array representing the command arguments - * to pass to the InputInterface - * @param array $options Simple key/values array representing the command options - * to pass to the InputInterface - * @return \Symfony\Component\Console\Input\InputInterface InputInterface needed for the - * Manager to properly run - */ - public function getInput(string $command, array $arguments, array $options): InputInterface - { - $className = 'Migrations\Command\\' . $command; - $options = $arguments + $this->prepareOptions($options); - /** @var \Symfony\Component\Console\Command\Command $command */ - $command = new $className(); - $definition = $command->getDefinition(); - - return new ArrayInput($options, $definition); - } - - /** - * Prepares the option to pass on to the InputInterface - * - * TODO(mark) Remove as part of phinx removal - * - * @param array $options Simple key-values array to pass to the InputInterface - * @return array Prepared $options - */ - protected function prepareOptions(array $options = []): array - { - $options += $this->default; - if (!$options) { - return $options; - } - - foreach ($options as $name => $value) { - $options['--' . $name] = $value; - unset($options[$name]); - } - - return $options; - } } diff --git a/src/Shim/OutputAdapter.php b/src/Shim/OutputAdapter.php deleted file mode 100644 index a2fe3fc2..00000000 --- a/src/Shim/OutputAdapter.php +++ /dev/null @@ -1,145 +0,0 @@ -io->out($messages, $newline ? 1 : 0); - } - - /** - * @inheritDoc - */ - public function writeln(string|iterable $messages, $options = 0): void - { - if ($messages instanceof Traversable) { - $messages = iterator_to_array($messages); - } - $this->io->out($messages, 1); - } - - /** - * Sets the verbosity of the output. - * - * @param self::VERBOSITY_* $level - * @return void - */ - public function setVerbosity(int $level): void - { - // TODO map values - $this->io->level($level); - } - - /** - * Gets the current verbosity of the output. - * - * @see self::VERBOSITY_* - * @return int - */ - public function getVerbosity(): int - { - // TODO map values - return $this->io->level(); - } - - /** - * Returns whether verbosity is quiet (-q). - */ - public function isQuiet(): bool - { - return $this->io->level() === ConsoleIo::QUIET; - } - - /** - * Returns whether verbosity is verbose (-v). - */ - public function isVerbose(): bool - { - return $this->io->level() === ConsoleIo::VERBOSE; - } - - /** - * Returns whether verbosity is very verbose (-vv). - */ - public function isVeryVerbose(): bool - { - return false; - } - - /** - * Returns whether verbosity is debug (-vvv). - */ - public function isDebug(): bool - { - return false; - } - - /** - * Sets the decorated flag. - * - * @return void - */ - public function setDecorated(bool $decorated): void - { - throw new RuntimeException('setDecorated is not implemented'); - } - - /** - * Gets the decorated flag. - */ - public function isDecorated(): bool - { - throw new RuntimeException('isDecorated is not implemented'); - } - - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter): void - { - throw new RuntimeException('setFormatter is not implemented'); - } - - /** - * Returns current output formatter instance. - */ - public function getFormatter(): OutputFormatterInterface - { - throw new RuntimeException('getFormatter is not implemented'); - } -} diff --git a/src/Util/UtilTrait.php b/src/Util/UtilTrait.php index bc464cf4..c44617c8 100644 --- a/src/Util/UtilTrait.php +++ b/src/Util/UtilTrait.php @@ -13,28 +13,13 @@ */ namespace Migrations\Util; -use Cake\Core\Plugin as CorePlugin; use Cake\Utility\Inflector; -use Symfony\Component\Console\Input\InputInterface; /** * Trait gathering useful methods needed in various places of the plugin */ trait UtilTrait { - /** - * Get the plugin name based on the current InputInterface - * - * @param \Symfony\Component\Console\Input\InputInterface $input Input of the current command. - * @return string|null - */ - protected function getPlugin(InputInterface $input): ?string - { - $plugin = $input->getOption('plugin') ?: null; - - return $plugin; - } - /** * Get the phinx table name used to store migrations data * @@ -54,30 +39,4 @@ protected function getPhinxTable(?string $plugin = null): string return $plugin . $table; } - - /** - * Get the migrations or seeds files path based on the current InputInterface - * - * @param \Symfony\Component\Console\Input\InputInterface $input Input of the current command. - * @param string $default Default folder to set if no source option is found in the $input param - * @return string - */ - protected function getOperationsPath(InputInterface $input, string $default = 'Migrations'): string - { - $folder = $input->getOption('source') ?: $default; - - $dir = ROOT . DS . 'config' . DS . $folder; - - if (defined('CONFIG')) { - $dir = CONFIG . $folder; - } - - $plugin = $this->getPlugin($input); - - if ($plugin !== null) { - $dir = CorePlugin::path($plugin) . 'config' . DS . $folder; - } - - return $dir; - } } diff --git a/tests/CommandTester.php b/tests/CommandTester.php deleted file mode 100644 index d019a3b8..00000000 --- a/tests/CommandTester.php +++ /dev/null @@ -1,182 +0,0 @@ - - */ - private $inputs = []; - - /** - * @var int - */ - private $statusCode; - - /** - * Constructor. - * - * @param \Symfony\Component\Console\Command\Command $command A Command instance to test - */ - public function __construct(Command $command) - { - $this->command = $command; - } - - /** - * Executes the command. - * - * Available execution options: - * - * * interactive: Sets the input interactive flag - * * decorated: Sets the output decorated flag - * * verbosity: Sets the output verbosity flag - * - * @param array $input An array of command arguments and options - * @param array $options An array of execution options - * @return int The command exit code - */ - public function execute(array $input, array $options = []) - { - // set the command name automatically if the application requires - // this argument and no command name was passed - $application = $this->command->getApplication(); - if ( - !isset($input['command']) - && ($application !== null) - && $application->getDefinition()->hasArgument('command') - ) { - $input += ['command' => $this->command->getName()]; - } - - $this->input = new ArrayInput($input); - if ($this->inputs) { - $this->input->setStream(self::createStream($this->inputs)); - } - - if (isset($options['interactive'])) { - $this->input->setInteractive($options['interactive']); - } - - // This is where the magic does its magic : we use the output object of the command. - $this->output = $this->command->getManager()->getOutput(); - $this->output->setDecorated($options['decorated'] ?? false); - if (isset($options['verbosity'])) { - $this->output->setVerbosity($options['verbosity']); - } - - return $this->statusCode = $this->command->run($this->input, $this->output); - } - - /** - * Gets the display returned by the last execution of the command. - * - * @param bool $normalize Whether to normalize end of lines to \n or not - * @return string The display - */ - public function getDisplay($normalize = false) - { - rewind($this->output->getStream()); - - $display = stream_get_contents($this->output->getStream()); - - if ($normalize) { - $display = str_replace(PHP_EOL, "\n", $display); - } - - return $display; - } - - /** - * Gets the input instance used by the last execution of the command. - * - * @return \Symfony\Component\Console\Input\InputInterface The current input instance - */ - public function getInput() - { - return $this->input; - } - - /** - * Gets the output instance used by the last execution of the command. - * - * @return \Symfony\Component\Console\Output\OutputInterface The current output instance - */ - public function getOutput() - { - return $this->output; - } - - /** - * Gets the status code returned by the last execution of the application. - * - * @return int The status code - */ - public function getStatusCode() - { - return $this->statusCode; - } - - /** - * Sets the user inputs. - * - * @param array $inputs An array of strings representing each input - * passed to the command input stream. - * @return $this - */ - public function setInputs(array $inputs) - { - $this->inputs = $inputs; - - return $this; - } - - /** - * @param array $inputs - * @return resource|false - */ - private static function createStream(array $inputs) - { - $stream = fopen('php://memory', 'r+', false); - - fwrite($stream, implode(PHP_EOL, $inputs)); - rewind($stream); - - return $stream; - } -} diff --git a/tests/RawBufferedOutput.php b/tests/RawBufferedOutput.php deleted file mode 100644 index c574876d..00000000 --- a/tests/RawBufferedOutput.php +++ /dev/null @@ -1,24 +0,0 @@ -message. - */ -class RawBufferedOutput extends BufferedOutput -{ - /** - * @param iterable|string $messages - * @param int $options - * @return void - */ - public function writeln(string|iterable $messages, int $options = OutputInterface::OUTPUT_NORMAL): void - { - $this->write($messages, true, $options | self::OUTPUT_RAW); - } -} diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 3409e3c8..070c65ec 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -60,7 +60,7 @@ public function tearDown(): void if (env('DB_URL_COMPARE')) { // Clean up the comparison database each time. Table order is important. $connection = ConnectionManager::get('test_comparisons'); - $tables = ['articles', 'categories', 'comments', 'users', 'orphan_table', 'phinxlog', 'tags']; + $tables = ['articles', 'categories', 'comments', 'users', 'orphan_table', 'phinxlog', 'tags', 'test_blog_phinxlog']; foreach ($tables as $table) { $connection->execute("DROP TABLE IF EXISTS $table"); } diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 2937593b..c1a459be 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -637,7 +637,6 @@ public function testAddColumnWithDefaultZero() public function testAddColumnWithAutoIdentity() { - $this->markTestIncomplete('Requires cakephp/database to use identity columns'); if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } @@ -670,14 +669,13 @@ public static function providerAddColumnIdentity(): array return [ [PostgresAdapter::GENERATED_ALWAYS, true], //testAddColumnWithIdentityAlways [PostgresAdapter::GENERATED_BY_DEFAULT, false], //testAddColumnWithIdentityDefault - [null, true], //testAddColumnWithoutIdentity + [PostgresAdapter::GENERATED_BY_DEFAULT, true], ]; } #[DataProvider('providerAddColumnIdentity')] public function testAddColumnIdentity($generated, $addToColumn) { - $this->markTestIncomplete('Requires cakephp/database to use identity columns'); if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } @@ -692,8 +690,8 @@ public function testAddColumnIdentity($generated, $addToColumn) $columns = $this->adapter->getColumns('table1'); foreach ($columns as $column) { if ($column->getName() === 'id') { - $this->assertEquals((bool)$generated, $column->getIdentity()); - $this->assertEquals($generated, $column->getGenerated()); + $this->assertEquals((bool)$generated, $column->getIdentity(), 'identity value does not match'); + $this->assertEquals($generated, $column->getGenerated(), 'generated value does not match'); } } } @@ -922,7 +920,6 @@ public static function providerChangeColumnIdentity(): array #[DataProvider('providerChangeColumnIdentity')] public function testChangeColumnIdentity($generated) { - $this->markTestIncomplete('Requires cakephp/database to use identity columns'); if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } @@ -943,7 +940,6 @@ public function testChangeColumnIdentity($generated) public function testChangeColumnDropIdentity() { - $this->markTestIncomplete('Requires cakephp/database to use identity columns'); if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } @@ -961,7 +957,6 @@ public function testChangeColumnDropIdentity() public function testChangeColumnChangeIdentity() { - $this->markTestIncomplete('Requires cakephp/database to use identity columns'); if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } @@ -2457,9 +2452,10 @@ public function testDumpCreateTable() ->save(); if ($this->usingPostgres10()) { - $expectedOutput = 'CREATE TABLE "public"."table1" ("id" SERIAL NOT NULL, ' . + $expectedOutput = 'CREATE TABLE "public"."table1" ("id" INT NOT NULL GENERATED BY DEFAULT AS IDENTITY, ' . '"column1" VARCHAR DEFAULT NULL, ' . - '"column2" INT DEFAULT NULL, "column3" VARCHAR NOT NULL DEFAULT \'test\', ' . + '"column2" INT DEFAULT NULL, ' . + '"column3" VARCHAR NOT NULL DEFAULT \'test\', ' . 'CONSTRAINT "table1_pkey" PRIMARY KEY ("id"));'; } else { $expectedOutput = 'CREATE TABLE "public"."table1" ("id" SERIAL NOT NULL, ' . @@ -2489,7 +2485,8 @@ public function testDumpCreateTableWithSchema() ->save(); if ($this->usingPostgres10()) { - $expectedOutput = 'CREATE TABLE "schema1"."table1" ("id" SERIAL NOT NULL, "column1" VARCHAR DEFAULT NULL, ' . + $expectedOutput = 'CREATE TABLE "schema1"."table1" ("id" INT NOT NULL GENERATED BY DEFAULT AS IDENTITY, ' . + '"column1" VARCHAR DEFAULT NULL, ' . '"column2" INT DEFAULT NULL, "column3" VARCHAR NOT NULL DEFAULT \'test\', CONSTRAINT ' . '"table1_pkey" PRIMARY KEY ("id"));'; } else { diff --git a/tests/test_app/Plugin/Blog/src/Plugin.php b/tests/test_app/Plugin/Blog/src/Plugin.php new file mode 100644 index 00000000..4108a602 --- /dev/null +++ b/tests/test_app/Plugin/Blog/src/Plugin.php @@ -0,0 +1,14 @@ +