diff --git a/docs/en/seeding.rst b/docs/en/seeding.rst new file mode 100644 index 00000000..74c338f7 --- /dev/null +++ b/docs/en/seeding.rst @@ -0,0 +1,229 @@ +Database Seeding +================ + +Seed classes are a great way to easily fill your database with data after +it's created. By default they are stored in the `seeds` directory; however, this +path can be changed in your configuration file. + +.. note:: + + Database seeding is entirely optional, and Migrations does not create a `Seeds` + directory by default. + +Creating a New Seed Class +------------------------- + +Migrations includes a command to easily generate a new seed class: + +.. code-block:: bash + + $ bin/cake bake seed MyNewSeeder + +It is based on a skeleton template: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ],[ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + } + } + +.. note:: + + You must call the ``saveData()`` method to commit your data to the table. + Migrations will buffer data until you do so. + +Truncating Tables +----------------- + +In addition to inserting data Migrations makes it trivial to empty your tables using the +SQL `TRUNCATE` command: + +.. code-block:: php + + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + + // empty the table + $posts->truncate(); + } + } + +.. note:: + + SQLite doesn't natively support the ``TRUNCATE`` command so behind the scenes + ``DELETE FROM`` is used. It is recommended to call the ``VACUUM`` command + after truncating a table. Migrations does not do this automatically. + +Executing Seed Classes +---------------------- + +This is the easy part. To seed your database, simply use the ``migrations seed`` command: + +.. code-block:: bash + + $ bin/cake migrations seed + +By default, Migrations will execute all available seed classes. If you would like to +run a specific class, simply pass in the name of it using the ``--seed`` parameter: + +.. code-block:: bash + + $ bin/cake migrations seed --seed UserSeeder + +You can also run multiple seeders: + +.. code-block:: bash + + $ bin/cake migrations seed --seed UserSeeder --seed PermissionSeeder --seed LogSeeder + +You can also use the `-v` parameter for more output verbosity: + +.. code-block:: bash + + $ bin/cake migrations seed -v + +The Migrations seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database, this is great for development environment +sample data or getting state for demos. diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst new file mode 100644 index 00000000..723c2ab9 --- /dev/null +++ b/docs/en/writing-migrations.rst @@ -0,0 +1,1867 @@ +Writing Migrations +================== + +Migrations are a declarative API that helps you transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Migrations API, but raw SQL is also supported. + +Creating a New Migration +------------------------ +Generating a skeleton migration file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's start by creating a new migration with ``bake``: + +.. code-block:: bash + + $ bin/cake bake migration + +This will create a new migration in the format +``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Bake will automatically creates a skeleton migration file with a single method: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +When executing this migration, Migrations will create the ``user_logins`` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a ``change`` method exists, Migrations will automatically +ignore the ``up`` and ``down`` methods. If you need to use these methods it is +recommended to create a separate migration file. + +.. note:: + + When creating or updating tables inside a ``change()`` method you must use + the Table ``create()`` and ``update()`` methods. Migrations cannot automatically + determine whether a ``save()`` call is creating a new table or modifying an + existing one. + +The following actions are reversible when done through the Table API in +Migrations, and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key + +If a command cannot be reversed then Migrations will throw an +``IrreversibleMigrationException`` when it's migrating down. If you wish to +use a command that cannot be reversed in the change function, you can use an +if statement with ``$this->isMigratingUp()`` to only run things in the +up or down direction. For example: + +.. code-block:: php + + table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } + } + +The Up Method +~~~~~~~~~~~~~ + +The up method is automatically run by Migrations when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +The Down Method +~~~~~~~~~~~~~~~ + +The down method is automatically run by Migrations when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +The Init Method +~~~~~~~~~~~~~~~ + +The ``init()`` method is run by Migrations before the migration methods if it exists. +This can be used for setting common class properties that are then used within +the migration methods. + +The Should Execute Method +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``shouldExecute()`` method is run by Migrations before executing the migration. +This can be used to prevent the migration from being executed at this time. It always +returns true by default. You can override it in your custom ``BaseMigration`` +implementation. + +Executing Queries +----------------- + +Queries can be executed with the ``execute()`` and ``query()`` methods. The +``execute()`` method returns the number of affected rows whereas the +``query()`` method returns the result as a +`CakePHP Statement `_. Both methods +accept an optional second parameter ``$params`` which is an array of elements, +and if used will cause the underlying connection to use a prepared statement. + +.. code-block:: php + + execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + + // using prepared queries + $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); + $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement + $rows = $stmt->fetchAll(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +.. note:: + + These commands run using the PHP Data Objects (PDO) extension which + defines a lightweight, consistent interface for accessing databases + in PHP. Always make sure your queries abide with PDOs before using + the ``execute()`` command. This is especially important when using + DELIMITERs during insertion of stored procedures or triggers which + don't support DELIMITERs. + +.. note:: + + If you wish to execute multiple queries at once, you may not also use the prepared + variant of these functions. When using prepared queries, PDO can only execute + them one at a time. + +.. warning:: + + When using ``execute()`` or ``query()`` with a batch of queries, PDO doesn't + throw an exception if there is an issue with one or more of the queries + in the batch. + + As such, the entire batch is assumed to have passed without issue. + + If Migrations was to iterate any potential result sets, looking to see if one + had an error, then Migrations would be denying access to all the results as there + is no facility in PDO to get a previous result set + `nextRowset() `_ - + but no ``previousSet()``). + + So, as a consequence, due to the design decision in PDO to not throw + an exception for batched queries, Migrations is unable to provide the fullest + support for error handling when batches of queries are supplied. + + Fortunately though, all the features of PDO are available, so multiple batches + can be controlled within the migration by calling upon + `nextRowset() `_ + and examining `errorInfo `_. + +Fetching Rows +------------- + +There are two methods available to fetch rows. The ``fetchRow()`` method will +fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. +Both methods accept raw SQL as their only parameter. + +.. code-block:: php + + fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Inserting Data +-------------- + +Migrations makes it easy to insert data into your tables. Whilst this feature is +intended for the :doc:`seed feature `, you are also free to use the +insert methods in your migrations. + +.. code-block:: php + + table('status'); + + // inserting only one row + $singleRow = [ + 'id' => 1, + 'name' => 'In Progress' + ]; + + $table->insert($singleRow)->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + $table->insert($rows)->saveData(); + } + + /** + * Migrate Down. + */ + public function down() + { + $this->execute('DELETE FROM status'); + } + } + +.. note:: + + You cannot use the insert methods inside a `change()` method. Please use the + `up()` and `down()` methods. + +Working With Tables +------------------- + +The Table Object +~~~~~~~~~~~~~~~~ + +The Table object is one of the most useful APIs provided by Migrations. It allows +you to easily manipulate database tables using PHP code. You can retrieve an +instance of the Table object by calling the ``table()`` method from within +your database migration. + +.. code-block:: php + + table('tableName'); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +You can then manipulate this table using the methods provided by the Table +object. + +Saving Changes +~~~~~~~~~~~~~~ + +When working with the Table object, Migrations stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. To perform this operation, Migrations provides three methods, +``create()``, ``update()``, and ``save()``. ``create()`` will first create +the table and then run the pending changes. ``update()`` will just run the +pending changes, and should be used when the table already exists. ``save()`` +is a helper function that checks first if the table exists and if it does not +will run ``create()``, else it will run ``update()``. + +As stated above, when using the ``change()`` migration method, you should always +use ``create()`` or ``update()``, and never ``save()`` as otherwise migrating +and rolling back may result in different states, due to ``save()`` calling +``create()`` when running migrate and then ``update()`` on rollback. When +using the ``up()``/``down()`` methods, it is safe to use either ``save()`` or +the more explicit methods. + +When in doubt with working with tables, it is always recommended to call +the appropriate function and commit any pending changes to the database. + +Creating a Table +~~~~~~~~~~~~~~~~ + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users. + +.. code-block:: php + + table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } + } + +Columns are added using the ``addColumn()`` method. We create a unique index +for both the username and email columns using the ``addIndex()`` method. +Finally calling ``create()`` commits the changes to the database. + +.. note:: + + Migrations automatically creates an auto-incrementing primary key column called ``id`` for every + table. + +The ``id`` option sets the name of the automatically created identity field, while the ``primary_key`` +option selects the field or fields used for primary key. ``id`` will always override the ``primary_key`` +option unless it's set to false. If you don't need a primary key set ``id`` to false without +specifying a ``primary_key``, and no primary key will be created. + +To specify an alternate primary key, you can specify the ``primary_key`` option +when accessing the Table object. Let's disable the automatic ``id`` column and +create a primary key using two columns instead: + +.. code-block:: php + + table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } + } + +Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. +To simply change the name of the primary key, we need to override the default ``id`` field name: + +.. code-block:: php + + table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + +In addition, the MySQL adapter supports following options: + +========== =========== +Option Description +========== =========== +comment set a text comment on the table +row_format set the table row format +engine define table engine *(defaults to ``InnoDB``)* +collation define table collation *(defaults to ``utf8mb4_unicode_ci``)* +signed whether the primary key is ``signed`` *(defaults to ``false``)* +limit set the maximum length for the primary key +========== =========== + +By default, the primary key is ``unsigned``. +To simply set it to be signed just pass ``signed`` option with a ``true`` value: + +.. code-block:: php + + table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } + } + + +The PostgreSQL adapter supports the following options: + +========= =========== +Option Description +========= =========== +comment set a text comment on the table +========= =========== + +To view available column types and options, see `Valid Column Types`_ for details. + +Determining Whether a Table Exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can determine whether or not a table exists by using the ``hasTable()`` +method. + +.. code-block:: php + + hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Dropping a Table +~~~~~~~~~~~~~~~~ + +Tables can be dropped quite easily using the ``drop()`` method. It is a +good idea to recreate the table again in the ``down()`` method. + +Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } + } + +Renaming a Table +~~~~~~~~~~~~~~~~ + +To rename a table access an instance of the Table object then call the +``rename()`` method. + +.. code-block:: php + + table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } + } + +Changing the Primary Key +~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the primary key on an existing table, use the ``changePrimaryKey()`` method. +Pass in a column name or array of columns names to include in the primary key, or ``null`` to drop the primary key. +Note that the mentioned columns must be added to the table, they will not be added implicitly. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Changing the Table Comment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change the comment on an existing table, use the ``changeComment()`` method. +Pass in a string to set as the new table comment, or ``null`` to drop the existing comment. + +.. code-block:: php + + table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Columns +-------------------- + +.. _valid-column-types: + +Valid Column Types +~~~~~~~~~~~~~~~~~~ + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid + +In addition, the MySQL adapter supports ``enum``, ``set``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob``, ``bit`` and ``json`` column types +(``json`` in MySQL 5.7 and above). When providing a limit value and using ``binary``, ``varbinary`` or ``blob`` and its subtypes, the retained column +type will be based on required length (see `Limit Option and MySQL`_ for details); + +In addition, the Postgres adapter supports ``interval``, ``json``, ``jsonb``, ``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types +(PostgreSQL 9.3 and above). + +Valid Column Options +~~~~~~~~~~~~~~~~~~~~ + +The following are valid column options: + +For any column type: + +======= =========== +Option Description +======= =========== +limit set maximum length for strings, also hints column types in adapters (see note below) +length alias for ``limit`` +default set default value or action +null allow ``NULL`` values, defaults to ``true`` (setting ``identity`` will override default to ``false``) +after specify the column that a new column should be placed after, or use ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` to place the column at the start of the table *(only applies to MySQL)* +comment set a text comment on the column +======= =========== + +For ``decimal`` columns: + +========= =========== +Option Description +========= =========== +precision combine with ``scale`` set to set decimal accuracy +scale combine with ``precision`` to set decimal accuracy +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +========= =========== + +For ``enum`` and ``set`` columns: + +========= =========== +Option Description +========= =========== +values Can be a comma separated list or an array of values +========= =========== + +For ``smallinteger``, ``integer`` and ``biginteger`` columns: + +======== =========== +Option Description +======== =========== +identity enable or disable automatic incrementing (if enabled, will set ``null: false`` if ``null`` option is not set) +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For Postgres, when using ``identity``, it will utilize the ``serial`` type appropriate for the integer size, so that +``smallinteger`` will give you ``smallserial``, ``integer`` gives ``serial``, and ``biginteger`` gives ``bigserial``. + +For ``timestamp`` columns: + +======== =========== +Option Description +======== =========== +default set default value (use with ``CURRENT_TIMESTAMP``) +update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) *(only applies to MySQL)* +timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* +======== =========== + +You can add ``created_at`` and ``updated_at`` timestamps to a table using the ``addTimestamps()`` method. This method accepts +three arguments, where the first two allow setting alternative names for the columns while the third argument allows you to +enable the ``timezone`` option for the columns. The defaults for these arguments are ``created_at``, ``updated_at``, and ``false`` +respectively. For the first and second argument, if you provide ``null``, then the default name will be used, and if you provide +``false``, then that column will not be created. Please note that attempting to set both to ``false`` will throw a +``\RuntimeException``. Additionally, you can use the ``addTimestampsWithTimezone()`` method, which is an alias to +``addTimestamps()`` that will always set the third argument to ``true`` (see examples below). The ``created_at`` column will +have a default set to ``CURRENT_TIMESTAMP``. For MySQL only, ``update_at`` column will have update set to +``CURRENT_TIMESTAMP``. + +.. code-block:: php + + table('users')->addTimestamps()->create(); + // Use defaults (with timezones) + $table = $this->table('users')->addTimestampsWithTimezone()->create(); + + // Override the 'created' column name with 'recorded_at'. + $table = $this->table('books')->addTimestamps('recorded_at')->create(); + + // Override the 'updated' column name with 'amended_at', preserving timezones. + // The two lines below do the same, the second one is simply cleaner. + $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); + $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); + + // Only add the created column to the table + $table = $this->table('books')->addTimestamps(null, false); + // Only add the updated column to the table + $table = $this->table('users')->addTimestamps(false); + // Note, setting both false will throw a \RuntimeError + } + } + +For ``boolean`` columns: + +======== =========== +Option Description +======== =========== +signed enable or disable the ``unsigned`` option *(only applies to MySQL)* +======== =========== + +For ``string`` and ``text`` columns: + +========= =========== +Option Description +========= =========== +collation set collation that differs from table defaults *(only applies to MySQL)* +encoding set character set that differs from table defaults *(only applies to MySQL)* +========= =========== + +For foreign key definitions: + +========== =========== +Option Description +========== =========== +update set an action to be triggered when the row is updated +delete set an action to be triggered when the row is deleted +constraint set a name to be used by foreign key constraint +========== =========== + +You can pass one or more of these options to any column with the optional +third argument array. + +Limit Option and MySQL +~~~~~~~~~~~~~~~~~~~~~~ + +When using the MySQL adapter, there are a couple things to consider when working with limits: + +- When using a ``string`` primary key or index on MySQL 5.7 or below, or the MyISAM storage engine, and the default charset of ``utf8mb4_unicode_ci``, you must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for ``integer``, ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using ``limit`` with one the following options will modify the column type accordingly: + +============ ============== +Limit Column Type +============ ============== +BLOB_TINY TINYBLOB +BLOB_REGULAR BLOB +BLOB_MEDIUM MEDIUMBLOB +BLOB_LONG LONGBLOB +TEXT_TINY TINYTEXT +TEXT_REGULAR TEXT +TEXT_MEDIUM MEDIUMTEXT +TEXT_LONG LONGTEXT +INT_TINY TINYINT +INT_SMALL SMALLINT +INT_MEDIUM MEDIUMINT +INT_REGULAR INT +INT_BIG BIGINT +============ ============== + +For ``binary`` or ``varbinary`` types, if limit is set greater than allowed 255 bytes, the type will be changed to the best matching blob type given the length. + +.. code-block:: php + + table('cart_items'); + $table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); + +Custom Column Types & Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some DBMS systems provide additional column types and default values that are specific to them. +If you don't want to keep your migrations DBMS-agnostic you can use those custom types in your migrations +through the ``\Migrations\Db\Literal::from`` method, which takes a string as its only argument, and returns an +instance of ``\Migrations\Db\Literal``. When Migrations encounters this value as a column's type it knows not to +run any validation on it and to use it exactly as supplied without escaping. This also works for ``default`` +values. + +You can see an example below showing how to add a ``citext`` column as well as a column whose default value +is a function, in PostgreSQL. This method of preventing the built-in escaping is supported in all adapters. + +.. code-block:: php + + table('users') + ->addColumn('username', Literal::from('citext')) + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()') + ]) + ->addColumn('creation', 'timestamp', [ + 'timezone' => true, + 'default' => Literal::from('now()') + ]) + ->create(); + } + } + +Get a column list +~~~~~~~~~~~~~~~~~ + +To retrieve all table columns, simply create a ``table`` object and call ``getColumns()`` +method. This method will return an array of Column classes with basic info. Example below: + +.. code-block:: php + + table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Get a column by name +~~~~~~~~~~~~~~~~~~~~ + +To retrieve one table column, simply create a ``table`` object and call the ``getColumn()`` +method. This method will return a Column class with basic info or NULL when the column doesn't exist. Example below: + +.. code-block:: php + + table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down() + { + ... + } + } + +Checking whether a column exists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can check if a table already has a certain column by using the +``hasColumn()`` method. + +.. code-block:: php + + table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } + } + +Renaming a Column +~~~~~~~~~~~~~~~~~ + +To rename a column, access an instance of the Table object then call the +``renameColumn()`` method. + +.. code-block:: php + + table('users'); + $table->renameColumn('bio', 'biography') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio') + ->save(); + } + } + +Adding a Column After Another Column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When adding a column with the MySQL adapter, you can dictate its position using the ``after`` option, +where its value is the name of the column to position it after. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } + } + +This would create the new column ``city`` and position it after the ``email`` column. The +``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` constant can be used to specify that the new column should be +created as the first column in that table. + +Dropping a Column +~~~~~~~~~~~~~~~~~ + +To drop a column, use the ``removeColumn()`` method. + +.. code-block:: php + + table('users'); + $table->removeColumn('short_name') + ->save(); + } + } + + +Specifying a Column Limit +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can limit the maximum length of a column by using the ``limit`` option. + +.. code-block:: php + + table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } + } + +Changing Column Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To change column type or options on an existing column, use the ``changeColumn()`` method. +See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values. + +.. code-block:: php + + table('users'); + $users->changeColumn('email', 'string', ['limit' => 255]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Working With Indexes +-------------------- + +To add an index to a table you can simply call the ``addIndex()`` method on the +table object. + +.. code-block:: php + + table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +By default Migrations instructs the database adapter to create a normal index. We +can pass an additional parameter ``unique`` to the ``addIndex()`` method to +specify a unique index. We can also explicitly specify a name for the index +using the ``name`` parameter, the index columns sort order can also be specified using +the ``order`` parameter. The order parameter takes an array of column names and sort order key/value pairs. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC']] + ) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +The MySQL adapter also supports ``fulltext`` indexes. If you are using a version before 5.6 you must +ensure the table uses the ``MyISAM`` engine. + +.. code-block:: php + + table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } + } + +In addition, MySQL adapter also supports setting the index length defined by limit option. +When you are using a multi-column index, you are able to define each column index length. +The single column index can define its index length with or without defining column name in limit option. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addColumn('user_guid', 'string', ['limit' => 36]) + ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) + ->addIndex('user_guid', ['limit' => 6]) + ->create(); + } + } + +The SQL Server and PostgreSQL adapters also supports ``include`` (non-key) columns on indexes. + +.. code-block:: php + + table('users'); + $table->addColumn('email', 'string') + ->addColumn('firstname','string') + ->addColumn('lastname','string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); + } + } + +In addition PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes. + +.. code-block:: php + + table('users'); + $table->addColumn('address', 'string') + ->addIndex('address', ['type' => 'gin']) + ->create(); + } + } + +Removing indexes is as easy as calling the ``removeIndex()`` method. You must +call this method for each index. + +.. code-block:: php + + table('users'); + $table->removeIndex(['email']) + ->save(); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + +Working With Foreign Keys +------------------------- + +Migrations has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table: + +.. code-block:: php + + table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey('tag_id', 'tags', 'id', ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION']) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +"On delete" and "On update" actions are defined with a 'delete' and 'update' options array. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and 'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable with the option ``['null' => true]``. +Constraint name can be changed with the 'constraint' option. + +It is also possible to pass ``addForeignKey()`` an array of columns. +This allows us to establish a foreign key relationship to a table which uses a combined key. + +.. code-block:: php + + table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey(['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + ['delete'=> 'NO_ACTION', 'update'=> 'NO_ACTION', 'constraint' => 'user_follower_id']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can add named foreign keys using the ``constraint`` parameter. + +.. code-block:: php + + table('your_table'); + $table->addForeignKey('foreign_id', 'reference_table', ['id'], + ['constraint' => 'your_foreign_key_name']); + ->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +We can also easily check if a foreign key exists: + +.. code-block:: php + + table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + +Finally, to delete a foreign key, use the ``dropForeignKey`` method. + +Note that like other methods in the ``Table`` class, ``dropForeignKey`` also needs ``save()`` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved. + +.. code-block:: php + + table('tag_relationships'); + $table->dropForeignKey('tag_id')->save(); + } + + /** + * Migrate Down. + */ + public function down() + { + + } + } + + + +Using the Query Builder +----------------------- + +It is not uncommon to pair database structure changes with data changes. For example, you may want to +migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, +Migrations provides access to a Query builder object, that you may use to execute complex ``SELECT``, ``UPDATE``, +``INSERT`` or ``DELETE`` statements. + +The Query builder is provided by the `cakephp/database `_ project, and should +be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the +``getQueryBuilder(string $type)`` function. The ``string $type`` options are `'select'`, `'insert'`, `'update'` and `'delete'`: + + +.. code-block:: php + + getQueryBuilder('select'); + $statement = $builder->select('*')->from('users')->execute(); + var_dump($statement->fetchAll()); + } + } + +Selecting Fields +~~~~~~~~~~~~~~~~ + +Adding fields to the SELECT clause: + + +.. code-block:: php + + select(['id', 'title', 'body']); + + // Results in SELECT id AS pk, title AS aliased_title, body ... + $builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + + // Use a closure + $builder->select(function ($builder) { + return ['id', 'title', 'body']; + }); + + +Where Conditions +~~~~~~~~~~~~~~~~ + +Generating conditions: + +.. code-block:: php + + // WHERE id = 1 + $builder->where(['id' => 1]); + + // WHERE id > 1 + $builder->where(['id >' => 1]); + + +As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: + + +.. code-block:: php + + where(['id >' => 1])->andWhere(['title' => 'My Title']); + + // Equivalent to + $builder->where(['id >' => 1, 'title' => 'My title']); + + // WHERE id > 1 OR title = 'My title' + $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); + + +For even more complex conditions you can use closures and expression objects: + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); + + +Which results in: + +.. code-block:: sql + + SELECT * FROM articles + WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 + + +Combining expressions is also possible: + + +.. code-block:: php + + select('*') + ->from('articles') + ->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); + +It generates: + +.. code-block:: sql + + SELECT * + FROM articles + WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 + + +When using the expression objects you can use the following methods to create conditions: + +* ``eq()`` Creates an equality condition. +* ``notEq()`` Create an inequality condition +* ``like()`` Create a condition using the ``LIKE`` operator. +* ``notLike()`` Create a negated ``LIKE`` condition. +* ``in()`` Create a condition using ``IN``. +* ``notIn()`` Create a negated condition using ``IN``. +* ``gt()`` Create a ``>`` condition. +* ``gte()`` Create a ``>=`` condition. +* ``lt()`` Create a ``<`` condition. +* ``lte()`` Create a ``<=`` condition. +* ``isNull()`` Create an ``IS NULL`` condition. +* ``isNotNull()`` Create a negated ``IS NULL`` condition. + + +Aggregates and SQL Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +.. code-block:: php + + select(['count' => $builder->func()->count('*')]); + +A number of commonly used functions can be created with the func() method: + +* ``sum()`` Calculate a sum. The arguments will be treated as literal values. +* ``avg()`` Calculate an average. The arguments will be treated as literal values. +* ``min()`` Calculate the min of a column. The arguments will be treated as literal values. +* ``max()`` Calculate the max of a column. The arguments will be treated as literal values. +* ``count()`` Calculate the count. The arguments will be treated as literal values. +* ``concat()`` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +* ``coalesce()`` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +* ``dateDiff()`` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +* ``now()`` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, +literal arguments and bound parameters. Literal parameters allow you to reference columns or +other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: + + +.. code-block:: php + + func()->concat([ + 'title' => 'literal', + ' NEW' + ]); + $query->select(['title' => $concat]); + + +Getting Results out of a Query +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + + +.. code-block:: php + + execute()->fetchAll('assoc'); + + +Creating an Insert Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating insert queries is also possible: + + +.. code-block:: php + + getQueryBuilder('insert'); + $builder + ->insert(['first_name', 'last_name']) + ->into('users') + ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) + ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) + ->execute(); + + +For increased performance, you can use another builder object as the values for an insert query: + +.. code-block:: php + + getQueryBuilder('select'); + $namesQuery + ->select(['fname', 'lname']) + ->from('users') + ->where(['is_active' => true]); + + $builder = $this->getQueryBuilder('insert'); + $st = $builder + ->insert(['first_name', 'last_name']) + ->into('names') + ->values($namesQuery) + ->execute(); + + var_dump($st->lastInsertId('names', 'id')); + + +The above code will generate: + +.. code-block:: sql + + INSERT INTO names (first_name, last_name) + (SELECT fname, lname FROM USERS where is_active = 1) + + +Creating an update Query +~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating update queries is similar to both inserting and selecting: + +.. code-block:: php + + getQueryBuilder('update'); + $builder + ->update('users') + ->set('fname', 'Snow') + ->where(['fname' => 'Jon']) + ->execute(); + + +Creating a Delete Query +~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, delete queries: + +.. code-block:: php + + getQueryBuilder('delete'); + $builder + ->delete('users') + ->where(['accepted_gdpr' => false]) + ->execute(); diff --git a/phpcs.xml b/phpcs.xml index d180d0a7..7a7a8a87 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,6 +6,7 @@ tests/ */tests/comparisons/* + */tests/TestCase/Util/_files/* */test_app/config/* */TestBlog/config/* */BarPlugin/config/* diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5dae26c..362adcc8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,24 +6,6 @@ parameters: count: 1 path: src/Command/BakeMigrationCommand.php - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parseFields\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parseIndexes\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - - - message: '#^Parameter \#1 \$arguments of method Migrations\\Util\\ColumnParser\:\:parsePrimaryKey\(\) expects array\, array\\|int\<1, max\>, list\\|string\> given\.$#' - identifier: argument.type - count: 1 - path: src/Command/BakeMigrationCommand.php - - message: '#^Call to an undefined method object\:\:loadHelper\(\)\.$#' identifier: method.notFound @@ -60,6 +42,18 @@ parameters: count: 1 path: src/Command/Phinx/CacheClear.php + - + message: '#^Instanceof between Migrations\\CakeManager and Migrations\\CakeManager will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/Command/Phinx/MarkMigrated.php + + - + message: '#^Instanceof between Migrations\\CakeManager and Migrations\\CakeManager will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: src/Command/Phinx/Status.php + - message: '#^Unsafe usage of new static\(\)\.$#' identifier: new.static @@ -115,10 +109,10 @@ parameters: path: src/Db/Adapter/SqlserverAdapter.php - - message: '#^Parameter \#1 \$message of method Cake\\Console\\ConsoleIo\:\:verbose\(\) expects list\\|string, array\ given\.$#' - identifier: argument.type + message: '#^Call to function method_exists\(\) with Migrations\\MigrationInterface and ''useTransactions'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: src/Migration/Manager.php + path: src/Migration/Environment.php - message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, array given\.$#' @@ -138,24 +132,6 @@ parameters: count: 1 path: src/Shim/OutputAdapter.php - - - message: '#^Parameter \#1 \$message of method Cake\\Console\\ConsoleIo\:\:out\(\) expects list\\|string, array\|string given\.$#' - identifier: argument.type - count: 2 - path: src/Shim/OutputAdapter.php - - - - message: '#^Parameter \#2 \$tables of method Cake\\TestSuite\\ConnectionHelper\:\:dropTables\(\) expects list\\|null, non\-empty\-array\ given\.$#' - identifier: argument.type - count: 1 - path: src/TestSuite/Migrator.php - - - - message: '#^Parameter \#2 \$tables of method Cake\\TestSuite\\ConnectionHelper\:\:truncateTables\(\) expects list\\|null, non\-empty\-array\ given\.$#' - identifier: argument.type - count: 2 - path: src/TestSuite/Migrator.php - - message: '#^Offset 0 on non\-empty\-list\ in isset\(\) always exists and is not nullable\.$#' identifier: isset.offset diff --git a/psalm-baseline.xml b/psalm-baseline.xml index c220dbf4..850c9680 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,11 +1,20 @@ - - - - - - + + + + + + + + + + + + + + + @@ -16,6 +25,23 @@ + + + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + addOption('--environment', '-e', InputArgument::OPTIONAL)]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + setManager(new CakeManager($this->getConfig(), $input, $output))]]> + + @@ -33,10 +59,6 @@ - - - - @@ -60,18 +82,6 @@ - - - - - getManager()->maxNameLength]]> - - - - - - - @@ -140,12 +150,6 @@ - {$phpFile}"; - }, - $phpFiles - )]]> @@ -170,11 +174,23 @@ + + + + + + + + + + + + + + + + - - - - io->level()]]> @@ -182,13 +198,6 @@ - - - - - - - regexpParseColumn]]> diff --git a/src/AbstractSeed.php b/src/AbstractSeed.php index fd993807..cfcc9f08 100644 --- a/src/AbstractSeed.php +++ b/src/AbstractSeed.php @@ -23,6 +23,8 @@ * Class AbstractSeed * Extends Phinx base AbstractSeed class in order to extend the features the seed class * offers. + * + * @deprecated 4.5.0 You should use Migrations\BaseSeed for new seeds. */ abstract class AbstractSeed extends BaseAbstractSeed { diff --git a/src/BaseMigration.php b/src/BaseMigration.php new file mode 100644 index 00000000..589ca8eb --- /dev/null +++ b/src/BaseMigration.php @@ -0,0 +1,491 @@ + + */ + protected array $tables = []; + + /** + * Is migrating up prop + * + * @var bool + */ + protected bool $isMigratingUp = true; + + /** + * The version number. + * + * @var int + */ + protected int $version; + + /** + * Whether the tables created in this migration + * should auto-create an `id` field or not + * + * This option is global for all tables created in the migration file. + * If you set it to false, you have to manually add the primary keys for your + * tables using the Migrations\Table::addPrimaryKey() method + * + * @var bool + */ + public bool $autoId = true; + + /** + * Constructor + * + * @param int $version The version this migration is + */ + public function __construct(int $version) + { + $this->validateVersion($version); + $this->version = $version; + } + + /** + * {@inheritDoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Adapter not set.'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return static::class; + } + + /** + * Sets the migration version number. + * + * @param int $version Version + * @return $this + */ + public function setVersion(int $version) + { + $this->validateVersion($version); + $this->version = $version; + + return $this; + } + + /** + * Gets the migration version number. + * + * @return int + */ + public function getVersion(): int + { + return $this->version; + } + + /** + * Sets whether this migration is being applied or reverted + * + * @param bool $isMigratingUp True if the migration is being applied + * @return $this + */ + public function setMigratingUp(bool $isMigratingUp) + { + $this->isMigratingUp = $isMigratingUp; + + return $this; + } + + /** + * Hook method to decide if this migration should use transactions + * + * By default if your driver supports transactions, a transaction will be opened + * before the migration begins, and commit when the migration completes. + * + * @return bool + */ + public function useTransactions(): bool + { + return $this->getAdapter()->hasTransactions(); + } + + /** + * Gets whether this migration is being applied or reverted. + * True means that the migration is being applied. + * + * @return bool + */ + public function isMigratingUp(): bool + { + return $this->isMigratingUp; + } + + /** + * Executes a SQL statement and returns the number of affected rows. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return int + */ + public function execute(string $sql, array $params = []): int + { + return $this->getAdapter()->execute($sql, $params); + } + + /** + * Executes a SQL statement. + * + * The return type depends on the underlying adapter being used. To improve + * IDE auto-completion possibility, you can overwrite the query method + * phpDoc in your (typically custom abstract parent) migration class, where + * you can set the return type by the adapter in your current use. + * + * @param string $sql SQL + * @param array $params parameters to use for prepared query + * @return mixed + */ + public function query(string $sql, array $params = []): mixed + { + return $this->getAdapter()->query($sql, $params); + } + + /** + * Returns a new Query object that can be used to build complex SELECT, UPDATE, INSERT or DELETE + * queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @see https://api.cakephp.org/3.6/class-Cake.Database.Query.html + * @param string $type Query + * @return \Cake\Database\Query + */ + public function getQueryBuilder(string $type): Query + { + return $this->getAdapter()->getQueryBuilder($type); + } + + /** + * Returns a new SelectQuery object that can be used to build complex + * SELECT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\SelectQuery + */ + public function getSelectBuilder(): SelectQuery + { + return $this->getAdapter()->getSelectBuilder(); + } + + /** + * Returns a new InsertQuery object that can be used to build complex + * INSERT queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\InsertQuery + */ + public function getInsertBuilder(): InsertQuery + { + return $this->getAdapter()->getInsertBuilder(); + } + + /** + * Returns a new UpdateQuery object that can be used to build complex + * UPDATE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\UpdateQuery + */ + public function getUpdateBuilder(): UpdateQuery + { + return $this->getAdapter()->getUpdateBuilder(); + } + + /** + * Returns a new DeleteQuery object that can be used to build complex + * DELETE queries and execute them against the current database. + * + * Queries executed through the query builder are always sent to the database, regardless of the + * the dry-run settings. + * + * @return \Cake\Database\Query\DeleteQuery + */ + public function getDeleteBuilder(): DeleteQuery + { + return $this->getAdapter()->getDeleteBuilder(); + } + + /** + * Executes a query and returns only one row as an array. + * + * @param string $sql SQL + * @return array|false + */ + public function fetchRow(string $sql): array|false + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * Executes a query and returns an array of rows. + * + * @param string $sql SQL + * @return array + */ + public function fetchAll(string $sql): array + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * Create a new database. + * + * @param string $name Database Name + * @param array $options Options + * @return void + */ + public function createDatabase(string $name, array $options): void + { + $this->getAdapter()->createDatabase($name, $options); + } + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase(string $name): void + { + $this->getAdapter()->dropDatabase($name); + } + + /** + * Creates schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function createSchema(string $name): void + { + $this->getAdapter()->createSchema($name); + } + + /** + * Drops schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function dropSchema(string $name): void + { + $this->getAdapter()->dropSchema($name); + } + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable(string $tableName): bool + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options = []): Table + { + if ($this->autoId === false) { + $options['id'] = false; + } + + $table = new Table($tableName, $options, $this->getAdapter()); + $this->tables[] = $table; + + return $table; + } + + /** + * Perform checks on the migration, printing a warning + * if there are potential problems. + * + * @return void + */ + public function preFlightCheck(): void + { + if (method_exists($this, MigrationInterface::CHANGE)) { + if ( + method_exists($this, MigrationInterface::UP) || + method_exists($this, MigrationInterface::DOWN) + ) { + $io = $this->getIo(); + if ($io) { + $io->out( + 'warning Migration contains both change() and up()/down() methods.' . + ' Ignoring up() and down().' + ); + } + } + } + } + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @return void + */ + public function postFlightCheck(): void + { + foreach ($this->tables as $table) { + if ($table->hasPendingActions()) { + throw new RuntimeException(sprintf('Migration %s_%s has pending actions after execution!', $this->getVersion(), $this->getName())); + } + } + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return true; + } + + /** + * Makes sure the version int is within range for valid datetime. + * This is required to have a meaningful order in the overview. + * + * @param int $version Version + * @return void + */ + protected function validateVersion(int $version): void + { + $length = strlen((string)$version); + if ($length === 14) { + return; + } + + throw new RuntimeException('Invalid version `' . $version . '`, should be in format `YYYYMMDDHHMMSS` (length of 14).'); + } +} diff --git a/src/BaseSeed.php b/src/BaseSeed.php new file mode 100644 index 00000000..95b60b0b --- /dev/null +++ b/src/BaseSeed.php @@ -0,0 +1,252 @@ +adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Adapter not set.'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return static::class; + } + + /** + * {@inheritDoc} + */ + public function execute(string $sql, array $params = []): int + { + return $this->getAdapter()->execute($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function query(string $sql, array $params = []): mixed + { + return $this->getAdapter()->query($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function fetchRow(string $sql): array|false + { + return $this->getAdapter()->fetchRow($sql); + } + + /** + * {@inheritDoc} + */ + public function fetchAll(string $sql): array + { + return $this->getAdapter()->fetchAll($sql); + } + + /** + * {@inheritDoc} + */ + public function insert(string $tableName, array $data): void + { + // convert to table object + $table = new Table($tableName, [], $this->getAdapter()); + $table->insert($data)->save(); + } + + /** + * {@inheritDoc} + */ + public function hasTable(string $tableName): bool + { + return $this->getAdapter()->hasTable($tableName); + } + + /** + * {@inheritDoc} + */ + public function table(string $tableName, array $options = []): Table + { + return new Table($tableName, $options, $this->getAdapter()); + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return true; + } + + /** + * {@inheritDoc} + */ + public function call(string $seeder, array $options = []): void + { + $io = $this->getIo(); + assert($io !== null, 'Requires ConsoleIo'); + $io->out(''); + $io->out( + ' ====' . + ' ' . $seeder . ':' . + ' seeding' + ); + + $start = microtime(true); + $this->runCall($seeder, $options); + $end = microtime(true); + + $io->out( + ' ====' . + ' ' . $seeder . ':' . + ' seeded' . + ' ' . sprintf('%.4fs', $end - $start) . '' + ); + $io->out(''); + } + + /** + * Calls another seeder from this seeder. + * It will load the Seed class you are calling and run it. + * + * @param string $seeder Name of the seeder to call from the current seed + * @param array $options The CLI options passed to ManagerFactory. + * @return void + */ + protected function runCall(string $seeder, array $options = []): void + { + [$pluginName, $seeder] = pluginSplit($seeder); + $adapter = $this->getAdapter(); + $connection = $adapter->getConnection()->configName(); + + $factory = new ManagerFactory([ + 'plugin' => $options['plugin'] ?? $pluginName ?? null, + 'source' => $options['source'] ?? null, + 'connection' => $options['connection'] ?? $connection, + ]); + $io = $this->getIo(); + assert($io !== null, 'Missing ConsoleIo instance'); + $manager = $factory->createManager($io); + $manager->seed($seeder); + } +} diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 1a1f2e0b..c669bfd4 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -76,12 +76,13 @@ public function templateData(Arguments $arguments): array $pluginPath = $this->plugin . '.'; } - $arguments = $arguments->getArguments(); - unset($arguments[0]); + /** @var array $args */ + $args = $arguments->getArguments(); + unset($args[0]); $columnParser = new ColumnParser(); - $fields = $columnParser->parseFields($arguments); - $indexes = $columnParser->parseIndexes($arguments); - $primaryKey = $columnParser->parsePrimaryKey($arguments); + $fields = $columnParser->parseFields($args); + $indexes = $columnParser->parseIndexes($args); + $primaryKey = $columnParser->parsePrimaryKey($args); $action = $this->detectAction($className); @@ -98,6 +99,7 @@ public function templateData(Arguments $arguments): array 'tables' => [], 'action' => null, 'name' => $className, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } @@ -120,6 +122,7 @@ public function templateData(Arguments $arguments): array 'primaryKey' => $primaryKey, ], 'name' => $className, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index b605732d..3cef914a 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -18,6 +18,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Configure; use Cake\Database\Connection; use Cake\Database\Schema\CollectionInterface; use Cake\Database\Schema\TableSchema; @@ -197,6 +198,7 @@ public function templateData(Arguments $arguments): array 'data' => $this->templateData, 'dumpSchema' => $this->dumpSchema, 'currentSchema' => $this->currentSchema, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } @@ -489,6 +491,7 @@ protected function bakeSnapshot(string $name, Arguments $args, ConsoleIo $io): ? $newArgs = array_merge($newArgs, $this->parseOptions($args)); + // TODO(mark) This nested command call always uses phinx backend. $exitCode = $this->executeCommand(BakeMigrationSnapshotCommand::class, $newArgs, $io); if ($exitCode === 1) { @@ -517,6 +520,7 @@ protected function getDumpSchema(Arguments $args): array $inputArgs['--plugin'] = $args->getOption('plugin'); } + // TODO(mark) This has to change for the built-in backend $className = Dump::class; $definition = (new $className())->getDefinition(); diff --git a/src/Command/BakeMigrationSnapshotCommand.php b/src/Command/BakeMigrationSnapshotCommand.php index ea711069..bc51d3a8 100644 --- a/src/Command/BakeMigrationSnapshotCommand.php +++ b/src/Command/BakeMigrationSnapshotCommand.php @@ -115,6 +115,7 @@ public function templateData(Arguments $arguments): array 'action' => 'create_table', 'name' => $this->_name, 'autoId' => $autoId, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index 6aedaf61..61d2ce72 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -142,6 +142,7 @@ public function templateData(Arguments $arguments): array 'namespace' => $namespace, 'records' => $records, 'table' => $table, + 'backend' => Configure::read('Migrations.backend', 'builtin'), ]; } diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index 7c3f06a8..879ee237 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -21,7 +21,7 @@ use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; use Cake\Utility\Inflector; -use Phinx\Util\Util; +use Migrations\Util\Util; /** * Task class for generating migration snapshot files. diff --git a/src/Command/Phinx/Create.php b/src/Command/Phinx/Create.php index 51d0d441..c434fb73 100644 --- a/src/Command/Phinx/Create.php +++ b/src/Command/Phinx/Create.php @@ -15,13 +15,16 @@ use Cake\Utility\Inflector; use Migrations\ConfigurationTrait; +use Migrations\Util\Util; use Phinx\Console\Command\Create as CreateCommand; -use Phinx\Util\Util; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Create extends CreateCommand { use CommandTrait { diff --git a/src/Command/Phinx/Dump.php b/src/Command/Phinx/Dump.php index 53dc2015..42336b24 100644 --- a/src/Command/Phinx/Dump.php +++ b/src/Command/Phinx/Dump.php @@ -26,6 +26,8 @@ * Dump command class. * A "dump" is a snapshot of a database at a given point in time. It is stored in a * .lock file in the same folder as migrations files. + * + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class Dump extends AbstractCommand { diff --git a/src/Command/Phinx/MarkMigrated.php b/src/Command/Phinx/MarkMigrated.php index 0603a935..1170cbb0 100644 --- a/src/Command/Phinx/MarkMigrated.php +++ b/src/Command/Phinx/MarkMigrated.php @@ -22,7 +22,8 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @method \Migrations\CakeManager|null getManager() + * @method \Migrations\CakeManager getManager() + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class MarkMigrated extends AbstractCommand { diff --git a/src/Command/Phinx/Migrate.php b/src/Command/Phinx/Migrate.php index 01b14057..8c49e7a2 100644 --- a/src/Command/Phinx/Migrate.php +++ b/src/Command/Phinx/Migrate.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Migrate extends MigrateCommand { use CommandTrait { diff --git a/src/Command/Phinx/Rollback.php b/src/Command/Phinx/Rollback.php index 5d779973..c025a2c5 100644 --- a/src/Command/Phinx/Rollback.php +++ b/src/Command/Phinx/Rollback.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Rollback extends RollbackCommand { use CommandTrait { diff --git a/src/Command/Phinx/Seed.php b/src/Command/Phinx/Seed.php index 652585e4..37d886df 100644 --- a/src/Command/Phinx/Seed.php +++ b/src/Command/Phinx/Seed.php @@ -20,6 +20,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. + */ class Seed extends SeedRun { use CommandTrait { diff --git a/src/Command/Phinx/Status.php b/src/Command/Phinx/Status.php index 869e4358..9375a216 100644 --- a/src/Command/Phinx/Status.php +++ b/src/Command/Phinx/Status.php @@ -20,7 +20,8 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * @method \Migrations\CakeManager|null getManager() + * @method \Migrations\CakeManager getManager() + * @deprecated 4.5.0 This command is deprecated alongside phinx compatibility. */ class Status extends StatusCommand { diff --git a/src/Command/SnapshotTrait.php b/src/Command/SnapshotTrait.php index 2c09fea8..67332617 100644 --- a/src/Command/SnapshotTrait.php +++ b/src/Command/SnapshotTrait.php @@ -15,6 +15,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; +use Cake\Core\Configure; /** * Trait needed for all "snapshot" type of bake operations. @@ -40,6 +41,15 @@ protected function createFile(string $path, string $contents, Arguments $args, C return $createFile; } + /** + * @internal + * @return bool Whether or not the builtin backend is active. + */ + protected function useBuiltinBackend(): bool + { + return Configure::read('Migrations.backend', 'builtin') === 'builtin'; + } + /** * Will mark a snapshot created, the snapshot being identified by its * full file path. @@ -62,7 +72,11 @@ protected function markSnapshotApplied(string $path, Arguments $args, ConsoleIo $newArgs = array_merge($newArgs, $this->parseOptions($args)); $io->out('Marking the migration ' . $fileName . ' as migrated...'); - $this->executeCommand(MigrationsMarkMigratedCommand::class, $newArgs, $io); + if ($this->useBuiltinBackend()) { + $this->executeCommand(MarkMigratedCommand::class, $newArgs, $io); + } else { + $this->executeCommand(MigrationsMarkMigratedCommand::class, $newArgs, $io); + } } /** @@ -78,7 +92,11 @@ protected function refreshDump(Arguments $args, ConsoleIo $io): void $newArgs = $this->parseOptions($args); $io->out('Creating a dump of the new database state...'); - $this->executeCommand(MigrationsDumpCommand::class, $newArgs, $io); + if ($this->useBuiltinBackend()) { + $this->executeCommand(DumpCommand::class, $newArgs, $io); + } else { + $this->executeCommand(MigrationsDumpCommand::class, $newArgs, $io); + } } /** diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 96d4f538..fcc67cc7 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -18,7 +18,7 @@ use Migrations\Db\Literal; use Migrations\Db\Table\Column; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Adapter Interface. @@ -138,7 +138,7 @@ public function getColumnForType(string $columnName, string $type, array $option /** * Records a migration being run. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param string $startTime Start Time * @param string $endTime End Time @@ -149,7 +149,7 @@ public function migrated(MigrationInterface $migration, string $direction, strin /** * Toggle a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @return $this */ public function toggleBreakpoint(MigrationInterface $migration); @@ -164,7 +164,7 @@ public function resetAllBreakpoints(): int; /** * Set a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint set + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint set * @return $this */ public function setBreakpoint(MigrationInterface $migration); @@ -172,7 +172,7 @@ public function setBreakpoint(MigrationInterface $migration); /** * Unset a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint unset + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint unset * @return $this */ public function unsetBreakpoint(MigrationInterface $migration); diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php index 42e4f83e..7fe5ba63 100644 --- a/src/Db/Adapter/AdapterWrapper.php +++ b/src/Db/Adapter/AdapterWrapper.php @@ -18,7 +18,7 @@ use Migrations\Db\Literal; use Migrations\Db\Table\Column; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Adapter Wrapper. diff --git a/src/Db/Adapter/PdoAdapter.php b/src/Db/Adapter/PdoAdapter.php index 3d008d87..b1d4452d 100644 --- a/src/Db/Adapter/PdoAdapter.php +++ b/src/Db/Adapter/PdoAdapter.php @@ -17,6 +17,7 @@ use Cake\Database\Query\SelectQuery; use Cake\Database\Query\UpdateQuery; use InvalidArgumentException; +use Migrations\Config\Config; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; @@ -36,10 +37,10 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Migrations\MigrationInterface; use PDO; use PDOException; -use Phinx\Config\Config; -use Phinx\Migration\MigrationInterface; +use Phinx\Util\Literal as PhinxLiteral; use ReflectionMethod; use RuntimeException; use UnexpectedValueException; @@ -315,8 +316,20 @@ public function insert(Table $table, array $row): void $sql .= ' VALUES (' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ');'; $this->io->out($sql); } else { - $sql .= ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; - $this->getConnection()->execute($sql, array_values($row)); + $values = []; + $vals = []; + foreach ($row as $value) { + $placeholder = '?'; + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + $placeholder = (string)$value; + } + $values[] = $placeholder; + if ($placeholder === '?') { + $vals[] = $value; + } + } + $sql .= ' VALUES (' . implode(',', $values) . ')'; + $this->getConnection()->execute($sql, $vals); } } @@ -335,6 +348,9 @@ protected function quoteValue(mixed $value): mixed if ($value === null) { return 'null'; } + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + return (string)$value; + } // TODO remove hacks like this by using cake's database layer better. $driver = $this->getConnection()->getDriver(); $method = new ReflectionMethod($driver, 'getPdo'); @@ -381,22 +397,28 @@ public function bulkinsert(Table $table, array $rows): void $sql .= implode(', ', $values) . ';'; $this->io->out($sql); } else { - $count_keys = count($keys); - $query = '(' . implode(', ', array_fill(0, $count_keys, '?')) . ')'; - $count_vars = count($rows); - $queries = array_fill(0, $count_vars, $query); - $sql .= implode(',', $queries); $vals = []; - + $queries = []; foreach ($rows as $row) { + $values = []; foreach ($row as $v) { - if (is_bool($v)) { - $vals[] = $this->castToBool($v); - } else { - $vals[] = $v; + $placeholder = '?'; + if ($v instanceof Literal || $v instanceof PhinxLiteral) { + $placeholder = (string)$v; + } + $values[] = $placeholder; + if ($placeholder == '?') { + if (is_bool($v)) { + $vals[] = $this->castToBool($v); + } else { + $vals[] = $v; + } } } + $query = '(' . implode(', ', $values) . ')'; + $queries[] = $query; } + $sql .= implode(',', $queries); $this->getConnection()->execute($sql, $vals); } } @@ -547,7 +569,7 @@ public function unsetBreakpoint(MigrationInterface $migration): AdapterInterface /** * Mark a migration breakpoint. * - * @param \Phinx\Migration\MigrationInterface $migration The migration target for the breakpoint + * @param \Migrations\MigrationInterface $migration The migration target for the breakpoint * @param bool $state The required state of the breakpoint * @return \Migrations\Db\Adapter\AdapterInterface */ diff --git a/src/Db/Adapter/PhinxAdapter.php b/src/Db/Adapter/PhinxAdapter.php index f3cf451d..492ca369 100644 --- a/src/Db/Adapter/PhinxAdapter.php +++ b/src/Db/Adapter/PhinxAdapter.php @@ -33,6 +33,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Migrations\Shim\MigrationAdapter; use Phinx\Db\Action\Action as PhinxAction; use Phinx\Db\Action\AddColumn as PhinxAddColumn; use Phinx\Db\Action\AddForeignKey as PhinxAddForeignKey; @@ -52,7 +53,7 @@ use Phinx\Db\Table\ForeignKey as PhinxForeignKey; use Phinx\Db\Table\Index as PhinxIndex; use Phinx\Db\Table\Table as PhinxTable; -use Phinx\Migration\MigrationInterface; +use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; use Phinx\Util\Literal as PhinxLiteral; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -491,9 +492,10 @@ public function getVersionLog(): array /** * @inheritDoc */ - public function migrated(MigrationInterface $migration, string $direction, string $startTime, string $endTime): PhinxAdapterInterface + public function migrated(PhinxMigrationInterface $migration, string $direction, string $startTime, string $endTime): PhinxAdapterInterface { - $this->adapter->migrated($migration, $direction, $startTime, $endTime); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->migrated($wrapped, $direction, $startTime, $endTime); return $this; } @@ -501,9 +503,10 @@ public function migrated(MigrationInterface $migration, string $direction, strin /** * @inheritDoc */ - public function toggleBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function toggleBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->toggleBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->toggleBreakpoint($wrapped); return $this; } @@ -519,9 +522,10 @@ public function resetAllBreakpoints(): int /** * @inheritDoc */ - public function setBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function setBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->setBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->setBreakpoint($wrapped); return $this; } @@ -529,9 +533,10 @@ public function setBreakpoint(MigrationInterface $migration): PhinxAdapterInterf /** * @inheritDoc */ - public function unsetBreakpoint(MigrationInterface $migration): PhinxAdapterInterface + public function unsetBreakpoint(PhinxMigrationInterface $migration): PhinxAdapterInterface { - $this->adapter->unsetBreakpoint($migration); + $wrapped = new MigrationAdapter($migration, $migration->getVersion()); + $this->adapter->unsetBreakpoint($wrapped); return $this; } diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 713c0cc9..a525c219 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -16,6 +16,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; +use Phinx\Util\Literal as PhinxLiteral; class PostgresAdapter extends PdoAdapter { @@ -1561,8 +1562,20 @@ public function insert(Table $table, array $row): void $sql .= ' ' . $override . 'VALUES (' . implode(', ', array_map([$this, 'quoteValue'], $row)) . ');'; $this->io->out($sql); } else { - $sql .= ' ' . $override . 'VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; - $this->getConnection()->execute($sql, array_values($row)); + $values = []; + $vals = []; + foreach ($row as $value) { + $placeholder = '?'; + if ($value instanceof Literal || $value instanceof PhinxLiteral) { + $placeholder = (string)$value; + } + $values[] = $placeholder; + if ($placeholder === '?') { + $vals[] = $value; + } + } + $sql .= ' ' . $override . 'VALUES (' . implode(',', $values) . ')'; + $this->getConnection()->execute($sql, $vals); } } @@ -1593,25 +1606,29 @@ public function bulkinsert(Table $table, array $rows): void $sql .= implode(', ', $values) . ';'; $this->io->out($sql); } else { - $connection = $this->getConnection(); - $count_keys = count($keys); - $query = '(' . implode(', ', array_fill(0, $count_keys, '?')) . ')'; - $count_vars = count($rows); - $queries = array_fill(0, $count_vars, $query); - $sql .= implode(',', $queries); $vals = []; - + $queries = []; foreach ($rows as $row) { + $values = []; foreach ($row as $v) { - if (is_bool($v)) { - $vals[] = $this->castToBool($v); - } else { - $vals[] = $v; + $placeholder = '?'; + if ($v instanceof Literal || $v instanceof PhinxLiteral) { + $placeholder = (string)$v; + } + $values[] = $placeholder; + if ($placeholder == '?') { + if (is_bool($v)) { + $vals[] = $this->castToBool($v); + } else { + $vals[] = $v; + } } } + $query = '(' . implode(', ', $values) . ')'; + $queries[] = $query; } - - $connection->execute($sql, $vals); + $sql .= implode(',', $queries); + $this->getConnection()->execute($sql, $vals); } } diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index e1130c3d..0e3b43c0 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -16,7 +16,7 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\Table; -use Phinx\Migration\MigrationInterface; +use Migrations\MigrationInterface; /** * Migrations SqlServer Adapter. @@ -1238,7 +1238,7 @@ public function getColumnTypes(): array /** * Records a migration being run. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param string $startTime Start Time * @param string $endTime End Time diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php index 300bb80a..52062ab7 100644 --- a/src/Db/Adapter/TimedOutputAdapter.php +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -39,7 +39,7 @@ public function startCommandTimer(): callable return function () use ($started): void { $end = microtime(true); - $this->getIo()?->out(' -> ' . sprintf('%.4fs', $end - $started)); + $this->getIo()?->verbose(' -> ' . sprintf('%.4fs', $end - $started)); }; } diff --git a/src/Db/Table.php b/src/Db/Table.php index 30a62003..a2a63d75 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -8,6 +8,7 @@ namespace Migrations\Db; +use Cake\Collection\Collection; use Cake\Core\Configure; use InvalidArgumentException; use Migrations\Db\Action\AddColumn; @@ -33,11 +34,6 @@ /** * This object is based loosely on: https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html. - * - * TODO(mark) Having both Migrations\Db\Table and Migrations\Db\Table\Table seems redundant. - * The table models should be joined together so that we have a simpler API exposed. - * - * @internal */ class Table { @@ -61,6 +57,15 @@ class Table */ protected array $data = []; + /** + * Primary key for this table. + * Can either be a string or an array in case of composite + * primary key. + * + * @var string|string[] + */ + protected string|array $primaryKey; + /** * @param string $name Table Name * @param array $options Options @@ -289,6 +294,19 @@ public function reset(): void $this->resetData(); } + /** + * Add a primary key to a database table. + * + * @param string|string[] $columns Table Column(s) + * @return $this + */ + public function addPrimaryKey(string|array $columns) + { + $this->primaryKey = $columns; + + return $this; + } + /** * Add a table column. * @@ -538,10 +556,10 @@ public function hasForeignKey(string|array $columns, ?string $constraint = null) * @param bool $withTimezone Whether to set the timezone option on the added columns * @return $this */ - public function addTimestamps(string|false|null $createdAt = 'created_at', string|false|null $updatedAt = 'updated_at', bool $withTimezone = false) + public function addTimestamps(string|false|null $createdAt = 'created', string|false|null $updatedAt = 'updated', bool $withTimezone = false) { - $createdAt = $createdAt ?? 'created_at'; - $updatedAt = $updatedAt ?? 'updated_at'; + $createdAt = $createdAt ?? 'created'; + $updatedAt = $updatedAt ?? 'updated'; if (!$createdAt && !$updatedAt) { throw new RuntimeException('Cannot set both created_at and updated_at columns to false'); @@ -625,11 +643,90 @@ public function insert(array $data) */ public function create(): void { + $options = $this->getTable()->getOptions(); + if ((!isset($options['id']) || $options['id'] === false) && !empty($this->primaryKey)) { + $options['primary_key'] = (array)$this->primaryKey; + $this->filterPrimaryKey($options); + } + + $adapter = $this->getAdapter(); + if ($adapter->getAdapterType() === 'mysql' && empty($options['collation'])) { + // TODO this should be a method on the MySQL adapter. + // It could be a hook method on the adapter? + $encodingRequest = 'SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME + FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :dbname'; + + $connection = $adapter->getConnection(); + $connectionConfig = $connection->config(); + + $statement = $connection->execute($encodingRequest, ['dbname' => $connectionConfig['database']]); + $defaultEncoding = $statement->fetch('assoc'); + if (!empty($defaultEncoding['DEFAULT_COLLATION_NAME'])) { + $options['collation'] = $defaultEncoding['DEFAULT_COLLATION_NAME']; + } + } + + $this->getTable()->setOptions($options); + $this->executeActions(false); $this->saveData(); $this->reset(); // reset pending changes } + /** + * This method is called in case a primary key was defined using the addPrimaryKey() method. + * It currently does something only if using SQLite. + * If a column is an auto-increment key in SQLite, it has to be a primary key and it has to defined + * when defining the column. Phinx takes care of that so we have to make sure columns defined as autoincrement were + * not added with the addPrimaryKey method, otherwise, SQL queries will be wrong. + * + * @return void + */ + protected function filterPrimaryKey(array $options): void + { + if ($this->getAdapter()->getAdapterType() !== 'sqlite' || empty($options['primary_key'])) { + return; + } + + $primaryKey = $options['primary_key']; + if (!is_array($primaryKey)) { + $primaryKey = [$primaryKey]; + } + $primaryKey = array_flip($primaryKey); + + $columnsCollection = (new Collection($this->actions->getActions())) + ->filter(function ($action) { + return $action instanceof AddColumn; + }) + ->map(function ($action) { + /** @var \Phinx\Db\Action\ChangeColumn|\Phinx\Db\Action\RenameColumn|\Phinx\Db\Action\RemoveColumn|\Phinx\Db\Action\AddColumn $action */ + return $action->getColumn(); + }); + $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey) { + return isset($primaryKey[$columnDef->getName()]); + })->toArray(); + + if (empty($primaryKeyColumns)) { + return; + } + + foreach ($primaryKeyColumns as $primaryKeyColumn) { + if ($primaryKeyColumn->isIdentity()) { + unset($primaryKey[$primaryKeyColumn->getName()]); + } + } + + $primaryKey = array_flip($primaryKey); + + if (!empty($primaryKey)) { + $options['primary_key'] = $primaryKey; + } else { + unset($options['primary_key']); + } + + $this->getTable()->setOptions($options); + } + /** * Updates a table from the object instance. * diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index d740e0d0..668d911f 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -9,9 +9,9 @@ namespace Migrations\Db\Table; use Cake\Core\Configure; +use Migrations\Db\Adapter\AdapterInterface; +use Migrations\Db\Adapter\PostgresAdapter; use Migrations\Db\Literal; -use Phinx\Db\Adapter\AdapterInterface; -use Phinx\Db\Adapter\PostgresAdapter; use RuntimeException; /** @@ -765,6 +765,7 @@ protected function getAliasedOptions(): array return [ 'length' => 'limit', 'precision' => 'limit', + 'autoIncrement' => 'identity', ]; } diff --git a/src/Db/Table/Table.php b/src/Db/Table/Table.php index 70f270f6..cf2c16e7 100644 --- a/src/Db/Table/Table.php +++ b/src/Db/Table/Table.php @@ -12,7 +12,7 @@ /** * @internal - * @TODO rename this to `TableMetadata` having two classes with very similar names is confusing for me. + * @TODO rename this to `TableMetadata` having two classes with very similar names is confusing. */ class Table { diff --git a/src/Migration/BackendInterface.php b/src/Migration/BackendInterface.php new file mode 100644 index 00000000..7719494d --- /dev/null +++ b/src/Migration/BackendInterface.php @@ -0,0 +1,81 @@ + $options Options to pass to the command + * Available options are : + * + * - `format` Format to output the response. Can be 'json' + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return array The migrations list and their statuses + */ + public function status(array $options = []): array; + + /** + * Migrates available migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will migrate + * everything it can + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to migrate to + * @return bool Success + */ + public function migrate(array $options = []): bool; + + /** + * Rollbacks migrations + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `target` The version number to migrate to. If not provided, will only migrate + * the last migrations registered in the phinx log + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `date` The date to rollback to + * @return bool Success + */ + public function rollback(array $options = []): bool; + + /** + * Marks a migration as migrated + * + * @param int|string|null $version The version number of the migration to mark as migrated + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * @return bool Success + */ + public function markMigrated(int|string|null $version = null, array $options = []): bool; + + /** + * Seed the database using a seed file + * + * @param array $options Options to pass to the command + * Available options are : + * + * - `connection` The datasource connection to use + * - `source` The folder where migrations are in + * - `plugin` The plugin containing the migrations + * - `seed` The seed file to use + * @return bool Success + */ + public function seed(array $options = []): bool; +} diff --git a/src/Migration/BuiltinBackend.php b/src/Migration/BuiltinBackend.php index 2b806d0e..ee70978c 100644 --- a/src/Migration/BuiltinBackend.php +++ b/src/Migration/BuiltinBackend.php @@ -20,9 +20,6 @@ use DateTime; use InvalidArgumentException; use Migrations\Config\ConfigInterface; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\OutputInterface; /** * The Migrations class is responsible for handling migrations command @@ -30,16 +27,8 @@ * * @internal */ -class BuiltinBackend +class BuiltinBackend implements BackendInterface { - /** - * The OutputInterface. - * Should be a \Symfony\Component\Console\Output\NullOutput instance - * - * @var \Symfony\Component\Console\Output\OutputInterface - */ - protected OutputInterface $output; - /** * Manager instance * @@ -63,14 +52,6 @@ class BuiltinBackend */ 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 * @@ -82,25 +63,13 @@ class BuiltinBackend */ public function __construct(array $default = []) { - $this->output = new NullOutput(); - $this->stubInput = new ArrayInput([]); - if ($default) { $this->default = $default; } } /** - * Returns the status of each migrations based on the options passed - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `format` Format to output the response. Can be 'json' - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return array The migrations list and their statuses + * {@inheritDoc} */ public function status(array $options = []): array { @@ -110,18 +79,7 @@ public function status(array $options = []): array } /** - * Migrates available migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will migrate - * everything it can - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to migrate to - * @return bool Success + * {@inheritDoc} */ public function migrate(array $options = []): bool { @@ -141,18 +99,7 @@ public function migrate(array $options = []): bool } /** - * Rollbacks migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will only migrate - * the last migrations registered in the phinx log - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to rollback to - * @return bool Success + * {@inheritDoc} */ public function rollback(array $options = []): bool { @@ -172,16 +119,7 @@ public function rollback(array $options = []): bool } /** - * Marks a migration as migrated - * - * @param int|string|null $version The version number of the migration to mark as migrated - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return bool Success + * {@inheritDoc} */ public function markMigrated(int|string|null $version = null, array $options = []): bool { @@ -206,16 +144,7 @@ public function markMigrated(int|string|null $version = null, array $options = [ } /** - * Seed the database using a seed file - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `seed` The seed file to use - * @return bool Success + * {@inheritDoc} */ public function seed(array $options = []): bool { diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 1fbb83ed..cd11a46e 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -12,9 +12,9 @@ use Cake\Datasource\ConnectionManager; use Migrations\Db\Adapter\AdapterFactory; use Migrations\Db\Adapter\AdapterInterface; -use Migrations\Db\Adapter\PhinxAdapter; -use Phinx\Migration\MigrationInterface; -use Phinx\Seed\SeedInterface; +use Migrations\MigrationInterface; +use Migrations\SeedInterface; +use Migrations\Shim\MigrationAdapter; use RuntimeException; class Environment @@ -62,7 +62,7 @@ public function __construct(string $name, array $options) /** * Executes the specified migration on this environment. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration * @return void @@ -73,11 +73,11 @@ public function executeMigration(MigrationInterface $migration, string $directio $migration->setMigratingUp($direction === MigrationInterface::UP); $startTime = time(); + // Use an adapter shim to bridge between the new migrations // engine and the Phinx compatible interface $adapter = $this->getAdapter(); - $phinxShim = new PhinxAdapter($adapter); - $migration->setAdapter($phinxShim); + $migration->setAdapter($adapter); $migration->preFlightCheck(); @@ -95,29 +95,32 @@ public function executeMigration(MigrationInterface $migration, string $directio } if (!$fake) { - // Run the migration - if (method_exists($migration, MigrationInterface::CHANGE)) { - if ($direction === MigrationInterface::DOWN) { - // Create an instance of the RecordingAdapter so we can record all - // of the migration commands for reverse playback - - /** @var \Migrations\Db\Adapter\RecordingAdapter $recordAdapter */ - $recordAdapter = AdapterFactory::instance() - ->getWrapper('record', $adapter); - - // Wrap the adapter with a phinx shim to maintain contain - $phinxAdapter = new PhinxAdapter($recordAdapter); - $migration->setAdapter($phinxAdapter); - - $migration->{MigrationInterface::CHANGE}(); - $recordAdapter->executeInvertedCommands(); - - $migration->setAdapter(new PhinxAdapter($this->getAdapter())); + if ($migration instanceof MigrationAdapter) { + $migration->applyDirection($direction); + } else { + // Run the migration + if (method_exists($migration, MigrationInterface::CHANGE)) { + if ($direction === MigrationInterface::DOWN) { + // Create an instance of the RecordingAdapter so we can record all + // of the migration commands for reverse playback + + /** @var \Migrations\Db\Adapter\RecordingAdapter $recordAdapter */ + $recordAdapter = AdapterFactory::instance() + ->getWrapper('record', $adapter); + + // Wrap the adapter with a phinx shim to maintain contain + $migration->setAdapter($adapter); + + $migration->{MigrationInterface::CHANGE}(); + $recordAdapter->executeInvertedCommands(); + + $migration->setAdapter($this->getAdapter()); + } else { + $migration->{MigrationInterface::CHANGE}(); + } } else { - $migration->{MigrationInterface::CHANGE}(); + $migration->{$direction}(); } - } else { - $migration->{$direction}(); } } @@ -135,15 +138,13 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Executes the specified seeder on this environment. * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void { $adapter = $this->getAdapter(); - $phinxAdapter = new PhinxAdapter($adapter); - - $seed->setAdapter($phinxAdapter); + $seed->setAdapter($adapter); if (method_exists($seed, SeedInterface::INIT)) { $seed->{SeedInterface::INIT}(); } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index cb0faf18..dbdb159c 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -14,17 +14,15 @@ use Exception; use InvalidArgumentException; use Migrations\Config\ConfigInterface; -use Migrations\Shim\OutputAdapter; -use Phinx\Migration\AbstractMigration; -use Phinx\Migration\MigrationInterface; -use Phinx\Seed\AbstractSeed; -use Phinx\Seed\SeedInterface; -use Phinx\Util\Util; +use Migrations\MigrationInterface; +use Migrations\SeedInterface; +use Migrations\Shim\MigrationAdapter; +use Migrations\Shim\SeedAdapter; +use Migrations\Util\Util; +use Phinx\Migration\MigrationInterface as PhinxMigrationInterface; +use Phinx\Seed\SeedInterface as PhinxSeedInterface; use Psr\Container\ContainerInterface; use RuntimeException; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Input\InputDefinition; -use Symfony\Component\Console\Input\InputOption; class Manager { @@ -48,12 +46,12 @@ class Manager protected ?Environment $environment; /** - * @var \Phinx\Migration\MigrationInterface[]|null + * @var \Migrations\MigrationInterface[]|null */ protected ?array $migrations = null; /** - * @var \Phinx\Seed\SeedInterface[]|null + * @var \Migrations\SeedInterface[]|null */ protected ?array $seeds = null; @@ -256,14 +254,22 @@ public function markMigrated(int $version, string $path): bool } $migrationFile = $migrationFile[0]; - /** @var class-string<\Phinx\Migration\MigrationInterface> $className */ + /** @var class-string<\Phinx\Migration\MigrationInterface|\Migrations\MigrationInterface> $className */ $className = $this->getMigrationClassName($migrationFile); require_once $migrationFile; - $Migration = new $className('default', $version); + + if (is_subclass_of($className, PhinxMigrationInterface::class)) { + $migration = new MigrationAdapter($className, $version); + } else { + $migration = new $className($version); + } + /** @var \Migrations\MigrationInterface $migration */ + $config = $this->getConfig(); + $migration->setConfig($config); $time = date('Y-m-d H:i:s', time()); - $adapter->migrated($Migration, 'up', $time, $time); + $adapter->migrated($migration, 'up', $time, $time); return true; } @@ -442,7 +448,7 @@ public function migrate(?int $version = null, bool $fake = false): void /** * Execute a migration against the specified environment. * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $direction Direction * @param bool $fake flag that if true, we just record running the migration, but not actually do the migration * @return void @@ -475,7 +481,7 @@ public function executeMigration(MigrationInterface $migration, string $directio /** * Execute a seeder against the specified environment. * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @return void */ public function executeSeed(SeedInterface $seed): void @@ -506,7 +512,7 @@ public function executeSeed(SeedInterface $seed): void /** * Print Migration Status * - * @param \Phinx\Migration\MigrationInterface $migration Migration + * @param \Migrations\MigrationInterface $migration Migration * @param string $status Status of the migration * @param string|null $duration Duration the migration took the be executed * @return void @@ -523,7 +529,7 @@ protected function printMigrationStatus(MigrationInterface $migration, string $s /** * Print Seed Status * - * @param \Phinx\Seed\SeedInterface $seed Seed + * @param \Migrations\SeedInterface $seed Seed * @param string $status Status of the seed * @param string|null $duration Duration the seed took the be executed * @return void @@ -772,7 +778,7 @@ public function setContainer(ContainerInterface $container) /** * Sets the database migrations. * - * @param \Phinx\Migration\AbstractMigration[] $migrations Migrations + * @param \Migrations\MigrationInterface[] $migrations Migrations * @return $this */ public function setMigrations(array $migrations) @@ -787,7 +793,7 @@ public function setMigrations(array $migrations) * order * * @throws \InvalidArgumentException - * @return \Phinx\Migration\MigrationInterface[] + * @return \Migrations\MigrationInterface[] */ public function getMigrations(): array { @@ -807,7 +813,7 @@ function ($phpFile) { // filter the files to only get the ones that match our naming scheme $fileNames = []; - /** @var \Phinx\Migration\AbstractMigration[] $versions */ + /** @var \Migrations\MigrationInterface[] $versions */ $versions = []; $io = $this->getIo(); @@ -851,25 +857,15 @@ function ($phpFile) { } $io->verbose("Constructing $class."); - - $config = $this->getConfig(); - $input = new ArrayInput([ - '--plugin' => $config['plugin'] ?? null, - '--source' => $config['source'] ?? null, - '--connection' => $config->getConnection(), - ]); - $output = new OutputAdapter($io); - - // instantiate it - $migration = new $class('default', $version, $input, $output); - - if (!($migration instanceof AbstractMigration)) { - throw new InvalidArgumentException(sprintf( - 'The class "%s" in file "%s" must extend \Phinx\Migration\AbstractMigration', - $class, - $filePath - )); + if (is_subclass_of($class, PhinxMigrationInterface::class)) { + $migration = new MigrationAdapter($class, $version); + } else { + $migration = new $class($version); } + /** @var \Migrations\MigrationInterface $migration */ + $config = $this->getConfig(); + $migration->setConfig($config); + $migration->setIo($io); $versions[$version] = $migration; } else { @@ -897,7 +893,7 @@ protected function getMigrationFiles(): array /** * Sets the database seeders. * - * @param \Phinx\Seed\SeedInterface[] $seeds Seeders + * @param \Migrations\SeedInterface[] $seeds Seeders * @return $this */ public function setSeeds(array $seeds) @@ -910,8 +906,8 @@ public function setSeeds(array $seeds) /** * Get seed dependencies instances from seed dependency array * - * @param \Phinx\Seed\SeedInterface $seed Seed - * @return \Phinx\Seed\SeedInterface[] + * @param \Migrations\SeedInterface $seed Seed + * @return \Migrations\SeedInterface[] */ protected function getSeedDependenciesInstances(SeedInterface $seed): array { @@ -920,8 +916,9 @@ protected function getSeedDependenciesInstances(SeedInterface $seed): array if (!empty($dependencies) && !empty($this->seeds)) { foreach ($dependencies as $dependency) { foreach ($this->seeds as $seed) { - if (get_class($seed) === $dependency) { - $dependenciesInstances[get_class($seed)] = $seed; + $name = $seed->getName(); + if ($name === $dependency) { + $dependenciesInstances[$name] = $seed; } } } @@ -933,14 +930,15 @@ protected function getSeedDependenciesInstances(SeedInterface $seed): array /** * Order seeds by dependencies * - * @param \Phinx\Seed\SeedInterface[] $seeds Seeds - * @return \Phinx\Seed\SeedInterface[] + * @param \Migrations\SeedInterface[] $seeds Seeds + * @return \Migrations\SeedInterface[] */ protected function orderSeedsByDependencies(array $seeds): array { $orderedSeeds = []; foreach ($seeds as $seed) { - $orderedSeeds[get_class($seed)] = $seed; + $name = $seed->getName(); + $orderedSeeds[$name] = $seed; $dependencies = $this->getSeedDependenciesInstances($seed); if (!empty($dependencies)) { $orderedSeeds = array_merge($this->orderSeedsByDependencies($dependencies), $orderedSeeds); @@ -954,7 +952,7 @@ protected function orderSeedsByDependencies(array $seeds): array * Gets an array of database seeders. * * @throws \InvalidArgumentException - * @return \Phinx\Seed\SeedInterface[] + * @return \Migrations\SeedInterface[] */ public function getSeeds(): array { @@ -963,21 +961,11 @@ public function getSeeds(): array // filter the files to only get the ones that match our naming scheme $fileNames = []; - /** @var \Phinx\Seed\SeedInterface[] $seeds */ + /** @var \Migrations\SeedInterface[] $seeds */ $seeds = []; $config = $this->getConfig(); - $optionDef = new InputDefinition([ - new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), - new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), - new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), - ]); - $input = new ArrayInput([ - '--plugin' => $config['plugin'] ?? null, - '--source' => $config['source'] ?? null, - '--connection' => $config->getConnection(), - ], $optionDef); - $output = new OutputAdapter($this->io); + $io = $this->getIo(); foreach ($phpFiles as $filePath) { if (Util::isValidSeedFileName(basename($filePath))) { @@ -997,23 +985,20 @@ public function getSeeds(): array } // instantiate it - /** @var \Phinx\Seed\AbstractSeed $seed */ + /** @var \Phinx\Seed\AbstractSeed|\Migrations\SeedInterface $seed */ if (isset($this->container)) { $seed = $this->container->get($class); } else { $seed = new $class(); } - $seed->setEnvironment('default'); - $seed->setInput($input); - $seed->setOutput($output); - - if (!($seed instanceof AbstractSeed)) { - throw new InvalidArgumentException(sprintf( - 'The class "%s" in file "%s" must extend \Phinx\Seed\AbstractSeed', - $class, - $filePath - )); + // Shim phinx seeds so that the rest of migrations + // can be isolated from phinx. + if ($seed instanceof PhinxSeedInterface) { + $seed = new SeedAdapter($seed); } + /** @var \Migrations\SeedInterface $seed */ + $seed->setIo($io); + $seed->setConfig($config); $seeds[$class] = $seed; } @@ -1027,12 +1012,6 @@ public function getSeeds(): array return []; } - foreach ($this->seeds as $instance) { - if (isset($input) && $instance instanceof AbstractSeed) { - $instance->setInput($input); - } - } - return $this->seeds; } diff --git a/src/Migration/PhinxBackend.php b/src/Migration/PhinxBackend.php index 4c778f0f..68474a8b 100644 --- a/src/Migration/PhinxBackend.php +++ b/src/Migration/PhinxBackend.php @@ -36,7 +36,7 @@ * * @internal */ -class PhinxBackend +class PhinxBackend implements BackendInterface { use ConfigurationTrait; @@ -134,16 +134,7 @@ public function getCommand(): string } /** - * Returns the status of each migrations based on the options passed - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `format` Format to output the response. Can be 'json' - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return array The migrations list and their statuses + * {@inheritDoc} */ public function status(array $options = []): array { @@ -154,18 +145,7 @@ public function status(array $options = []): array } /** - * Migrates available migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will migrate - * everything it can - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to migrate to - * @return bool Success + * {@inheritDoc} */ public function migrate(array $options = []): bool { @@ -185,18 +165,7 @@ public function migrate(array $options = []): bool } /** - * Rollbacks migrations - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `target` The version number to migrate to. If not provided, will only migrate - * the last migrations registered in the phinx log - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `date` The date to rollback to - * @return bool Success + * {@inheritDoc} */ public function rollback(array $options = []): bool { @@ -216,16 +185,7 @@ public function rollback(array $options = []): bool } /** - * Marks a migration as migrated - * - * @param int|string|null $version The version number of the migration to mark as migrated - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * @return bool Success + * {@inheritDoc} */ public function markMigrated(int|string|null $version = null, array $options = []): bool { @@ -258,16 +218,7 @@ public function markMigrated(int|string|null $version = null, array $options = [ } /** - * Seed the database using a seed file - * - * @param array $options Options to pass to the command - * Available options are : - * - * - `connection` The datasource connection to use - * - `source` The folder where migrations are in - * - `plugin` The plugin containing the migrations - * - `seed` The seed file to use - * @return bool Success + * {@inheritDoc} */ public function seed(array $options = []): bool { @@ -286,13 +237,7 @@ public function seed(array $options = []): bool } /** - * Runs the method needed to execute and return - * - * @param string $method Manager method to call - * @param array $params Manager params to pass - * @param \Symfony\Component\Console\Input\InputInterface $input InputInterface needed for the - * Manager to properly run - * @return mixed The result of the CakeManager::$method() call + * {@inheritDoc} */ protected function run(string $method, array $params, InputInterface $input): mixed { diff --git a/src/MigrationInterface.php b/src/MigrationInterface.php new file mode 100644 index 00000000..8ec7c81f --- /dev/null +++ b/src/MigrationInterface.php @@ -0,0 +1,322 @@ + $options Options + * @return void + */ + public function createDatabase(string $name, array $options): void; + + /** + * Drop a database. + * + * @param string $name Database Name + * @return void + */ + public function dropDatabase(string $name): void; + + /** + * Creates schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function createSchema(string $name): void; + + /** + * Drops schema. + * + * This will thrown an error for adapters that do not support schemas. + * + * @param string $name Schema name + * @return void + * @throws \BadMethodCallException + */ + public function dropSchema(string $name): void; + + /** + * Checks to see if a table exists. + * + * @param string $tableName Table name + * @return bool + */ + public function hasTable(string $tableName): bool; + + /** + * Returns an instance of the \Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options = []): Table; + + /** + * Perform checks on the migration, printing a warning + * if there are potential problems. + * + * @return void + */ + public function preFlightCheck(): void; + + /** + * Perform checks on the migration after completion + * + * Right now, the only check is whether all changes were committed + * + * @return void + */ + public function postFlightCheck(): void; + + /** + * Checks to see if the migration should be executed. + * + * Returns true by default. + * + * You can use this to prevent a migration from executing. + * + * @return bool + */ + public function shouldExecute(): bool; +} diff --git a/src/Migrations.php b/src/Migrations.php index 3f4ba8fe..d089f618 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -17,6 +17,7 @@ use Cake\Database\Connection; use Cake\Datasource\ConnectionManager; use InvalidArgumentException; +use Migrations\Migration\BackendInterface; use Migrations\Migration\BuiltinBackend; use Migrations\Migration\PhinxBackend; use Phinx\Config\ConfigInterface; @@ -28,7 +29,7 @@ /** * The Migrations class is responsible for handling migrations command - * within an none-shell application. + * within an non-shell application. */ class Migrations { @@ -130,9 +131,9 @@ public function getCommand(): string /** * Get the Migrations interface backend based on configuration data. * - * @return \Migrations\Migration\BuiltinBackend|\Migrations\Migration\PhinxBackend + * @return \Migrations\Migration\BackendInterface */ - protected function getBackend(): BuiltinBackend|PhinxBackend + protected function getBackend(): BackendInterface { $backend = (string)(Configure::read('Migrations.backend') ?? 'builtin'); if ($backend === 'builtin') { diff --git a/src/SeedInterface.php b/src/SeedInterface.php new file mode 100644 index 00000000..50ee1285 --- /dev/null +++ b/src/SeedInterface.php @@ -0,0 +1,187 @@ +\Table class. + * + * You can use this class to create and manipulate tables. + * + * @param string $tableName Table name + * @param array $options Options + * @return \Migrations\Db\Table + */ + public function table(string $tableName, array $options = []): Table; + + /** + * Checks to see if the seed should be executed. + * + * Returns true by default. + * + * You can use this to prevent a seed from executing. + * + * @return bool + */ + public function shouldExecute(): bool; + + /** + * Gives the ability to a seeder to call another seeder. + * This is particularly useful if you need to run the seeders of your applications in a specific sequences, + * for instance to respect foreign key constraints. + * + * @param string $seeder Name of the seeder to call from the current seed + * @param array $options The CLI options for the seeder. + * @return void + */ + public function call(string $seeder, array $options = []): void; +} diff --git a/src/Shim/MigrationAdapter.php b/src/Shim/MigrationAdapter.php new file mode 100644 index 00000000..70a3b916 --- /dev/null +++ b/src/Shim/MigrationAdapter.php @@ -0,0 +1,405 @@ +migration = new $migrationClass('default', $version); + } else { + if (!is_subclass_of($migrationClass, PhinxMigrationInterface::class)) { + throw new RuntimeException( + 'The provided $migrationClass must be a ' . + 'subclass of Phinx\Migration\MigrationInterface' + ); + } + $this->migration = $migrationClass; + } + } + + /** + * Because we're a compatibility shim, we implement this hook + * so that it can be conditionally called when it is implemented. + * + * @return void + */ + public function init(): void + { + if (method_exists($this->migration, MigrationInterface::INIT)) { + $this->migration->{MigrationInterface::INIT}(); + } + } + + /** + * Compatibility shim for executing change/up/down + */ + public function applyDirection(string $direction): void + { + $adapter = $this->getAdapter(); + + // Run the migration + if (method_exists($this->migration, MigrationInterface::CHANGE)) { + if ($direction === MigrationInterface::DOWN) { + // Create an instance of the RecordingAdapter so we can record all + // of the migration commands for reverse playback + $adapter = $this->migration->getAdapter(); + assert($adapter !== null, 'Adapter must be set in migration'); + + /** @var \Phinx\Db\Adapter\ProxyAdapter $proxyAdapter */ + $proxyAdapter = PhinxAdapterFactory::instance() + ->getWrapper('proxy', $adapter); + + // Wrap the adapter with a phinx shim to maintain contain + $this->migration->setAdapter($proxyAdapter); + + $this->migration->{MigrationInterface::CHANGE}(); + $proxyAdapter->executeInvertedCommands(); + + $this->migration->setAdapter($adapter); + } else { + $this->migration->{MigrationInterface::CHANGE}(); + } + } else { + $this->migration->{$direction}(); + } + } + + /** + * {@inheritDoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $phinxAdapter = new PhinxAdapter($adapter); + $this->migration->setAdapter($phinxAdapter); + $this->adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Cannot call getAdapter() until after setAdapter().'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + $this->migration->setOutput(new OutputAdapter($io)); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ]); + + $this->migration->setInput($input); + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return $this->migration->getName(); + } + + /** + * {@inheritDoc} + */ + public function setVersion(int $version) + { + $this->migration->setVersion($version); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getVersion(): int + { + return $this->migration->getVersion(); + } + + /** + * {@inheritDoc} + */ + public function useTransactions(): bool + { + if (method_exists($this->migration, 'useTransactions')) { + return $this->migration->useTransactions(); + } + + return $this->migration->getAdapter()->hasTransactions(); + } + + /** + * {@inheritDoc} + */ + public function setMigratingUp(bool $isMigratingUp) + { + $this->migration->setMigratingUp($isMigratingUp); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isMigratingUp(): bool + { + return $this->migration->isMigratingUp(); + } + + /** + * {@inheritDoc} + */ + public function execute(string $sql, array $params = []): int + { + return $this->migration->execute($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function query(string $sql, array $params = []): mixed + { + return $this->migration->query($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function getQueryBuilder(string $type): Query + { + return $this->migration->getQueryBuilder($type); + } + + /** + * {@inheritDoc} + */ + public function getSelectBuilder(): SelectQuery + { + return $this->migration->getSelectBuilder(); + } + + /** + * {@inheritDoc} + */ + public function getInsertBuilder(): InsertQuery + { + return $this->migration->getInsertBuilder(); + } + + /** + * {@inheritDoc} + */ + public function getUpdateBuilder(): UpdateQuery + { + return $this->migration->getUpdateBuilder(); + } + + /** + * {@inheritDoc} + */ + public function getDeleteBuilder(): DeleteQuery + { + return $this->migration->getDeleteBuilder(); + } + + /** + * {@inheritDoc} + */ + public function fetchRow(string $sql): array|false + { + return $this->migration->fetchRow($sql); + } + + /** + * {@inheritDoc} + */ + public function fetchAll(string $sql): array + { + return $this->migration->fetchAll($sql); + } + + /** + * {@inheritDoc} + */ + public function createDatabase(string $name, array $options): void + { + $this->migration->createDatabase($name, $options); + } + + /** + * {@inheritDoc} + */ + public function dropDatabase(string $name): void + { + $this->migration->dropDatabase($name); + } + + /** + * {@inheritDoc} + */ + public function createSchema(string $name): void + { + $this->migration->createSchema($name); + } + + /** + * {@inheritDoc} + */ + public function dropSchema(string $name): void + { + $this->migration->dropSchema($name); + } + + /** + * {@inheritDoc} + */ + public function hasTable(string $tableName): bool + { + return $this->migration->hasTable($tableName); + } + + /** + * {@inheritDoc} + */ + public function table(string $tableName, array $options = []): Table + { + throw new RuntimeException('MigrationAdapter::table is not implemented'); + } + + /** + * {@inheritDoc} + */ + public function preFlightCheck(): void + { + $this->migration->preFlightCheck(); + } + + /** + * {@inheritDoc} + */ + public function postFlightCheck(): void + { + $this->migration->postFlightCheck(); + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return $this->migration->shouldExecute(); + } +} diff --git a/src/Shim/SeedAdapter.php b/src/Shim/SeedAdapter.php new file mode 100644 index 00000000..b1cc65f0 --- /dev/null +++ b/src/Shim/SeedAdapter.php @@ -0,0 +1,252 @@ +seed, PhinxSeedInterface::INIT)) { + $this->seed->{PhinxSeedInterface::INIT}(); + } + } + + /** + * {@inheritDoc} + */ + public function run(): void + { + $this->seed->run(); + } + + /** + * {@inheritDoc} + */ + public function getDependencies(): array + { + return $this->seed->getDependencies(); + } + + /** + * {@inheritDoc} + */ + public function setAdapter(AdapterInterface $adapter) + { + $phinxAdapter = new PhinxAdapter($adapter); + $this->seed->setAdapter($phinxAdapter); + $this->adapter = $adapter; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getAdapter(): AdapterInterface + { + if (!$this->adapter) { + throw new RuntimeException('Cannot call getAdapter() until after setAdapter().'); + } + + return $this->adapter; + } + + /** + * {@inheritDoc} + */ + public function setIo(ConsoleIo $io) + { + $this->io = $io; + $this->seed->setOutput(new OutputAdapter($io)); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getIo(): ?ConsoleIo + { + return $this->io; + } + + /** + * {@inheritDoc} + */ + public function getConfig(): ?ConfigInterface + { + return $this->config; + } + + /** + * {@inheritDoc} + */ + public function setConfig(ConfigInterface $config) + { + $optionDef = new InputDefinition([ + new InputOption('plugin', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('connection', mode: InputOption::VALUE_OPTIONAL, default: ''), + new InputOption('source', mode: InputOption::VALUE_OPTIONAL, default: ''), + ]); + $input = new ArrayInput([ + '--plugin' => $config['plugin'] ?? null, + '--source' => $config['source'] ?? null, + '--connection' => $config->getConnection(), + ], $optionDef); + + $this->seed->setInput($input); + $this->config = $config; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getName(): string + { + return $this->seed->getName(); + } + + /** + * {@inheritDoc} + */ + public function execute(string $sql, array $params = []): int + { + return $this->seed->execute($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function query(string $sql, array $params = []): mixed + { + return $this->seed->query($sql, $params); + } + + /** + * {@inheritDoc} + */ + public function fetchRow(string $sql): array|false + { + return $this->seed->fetchRow($sql); + } + + /** + * {@inheritDoc} + */ + public function fetchAll(string $sql): array + { + return $this->seed->fetchAll($sql); + } + + /** + * {@inheritDoc} + */ + public function insert(string $tableName, array $data): void + { + $this->seed->insert($tableName, $data); + } + + /** + * {@inheritDoc} + */ + public function hasTable(string $tableName): bool + { + return $this->seed->hasTable($tableName); + } + + /** + * {@inheritDoc} + */ + public function table(string $tableName, array $options = []): Table + { + throw new RuntimeException('Not implemented'); + } + + /** + * {@inheritDoc} + */ + public function shouldExecute(): bool + { + return $this->seed->shouldExecute(); + } + + /** + * {@inheritDoc} + */ + public function call(string $seeder, array $options = []): void + { + throw new RuntimeException('Not implemented'); + } +} diff --git a/src/Table.php b/src/Table.php index bfb504b9..5cc3feca 100644 --- a/src/Table.php +++ b/src/Table.php @@ -21,6 +21,8 @@ use Phinx\Util\Literal; /** + * TODO figure out how to update this for built-in backend. + * * @method \Migrations\CakeAdapter getAdapter() */ class Table extends BaseTable diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 6b98ad97..707ebd4e 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -5,7 +5,7 @@ use Cake\Collection\Collection; use Cake\Utility\Hash; -use Phinx\Db\Adapter\AdapterInterface; +use Migrations\Db\Adapter\AdapterInterface; use ReflectionClass; /** @@ -42,7 +42,7 @@ class ColumnParser /** * Parses a list of arguments into an array of fields * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parseFields(array $arguments): array @@ -95,7 +95,7 @@ public function parseFields(array $arguments): array /** * Parses a list of arguments into an array of indexes * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parseIndexes(array $arguments): array @@ -144,7 +144,7 @@ public function parseIndexes(array $arguments): array * Parses a list of arguments into an array of fields composing the primary key * of the table * - * @param array $arguments A list of arguments being parsed + * @param array $arguments A list of arguments being parsed * @return array */ public function parsePrimaryKey(array $arguments): array diff --git a/src/Util/Util.php b/src/Util/Util.php new file mode 100644 index 00000000..f24a8f86 --- /dev/null +++ b/src/Util/Util.php @@ -0,0 +1,285 @@ +format(static::DATE_FORMAT); + } + + /** + * Gets an array of all the existing migration class names. + * + * @param string $path Path + * @return string[] + */ + public static function getExistingMigrationClassNames(string $path): array + { + $classNames = []; + + if (!is_dir($path)) { + return $classNames; + } + + // filter the files to only get the ones that match our naming scheme + $phpFiles = static::getFiles($path); + + foreach ($phpFiles as $filePath) { + $fileName = basename($filePath); + if ($fileName && preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName)) { + $classNames[] = static::mapFileNameToClassName($fileName); + } + } + + return $classNames; + } + + /** + * Get the version from the beginning of a file name. + * + * @param string $fileName File Name + * @return int + */ + public static function getVersionFromFileName(string $fileName): int + { + $matches = []; + preg_match('/^[0-9]+/', basename($fileName), $matches); + $value = (int)($matches[0] ?? null); + if (!$value) { + throw new RuntimeException(sprintf('Cannot get a valid version from filename `%s`', $fileName)); + } + + return $value; + } + + /** + * Turn migration names like 'CreateUserTable' into file names like + * '12345678901234_create_user_table.php' or 'LimitResourceNamesTo30Chars' into + * '12345678901234_limit_resource_names_to_30_chars.php'. + * + * @param string $className Class Name + * @return string + */ + public static function mapClassNameToFileName(string $className): string + { + // TODO it would be nice to replace this with Inflector::underscore + // but it will break compatibility for little end user gain. + $snake = function ($matches) { + return '_' . strtolower($matches[0]); + }; + $fileName = preg_replace_callback('/\d+|[A-Z]/', $snake, $className); + $fileName = static::getCurrentTimestamp() . "$fileName.php"; + + return $fileName; + } + + /** + * Turn file names like '12345678901234_create_user_table.php' into class + * names like 'CreateUserTable'. + * + * @param string $fileName File Name + * @return string + */ + public static function mapFileNameToClassName(string $fileName): string + { + $matches = []; + if (preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName, $matches)) { + $fileName = $matches[1]; + } elseif (preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName)) { + return 'V' . substr($fileName, 0, strlen($fileName) - 4); + } + + return Inflector::camelize($fileName); + } + + /** + * Check if a migration class name is unique regardless of the + * timestamp. + * + * This method takes a class name and a path to a migrations directory. + * + * Migration class names must be in PascalCase format but consecutive + * capitals are allowed. + * e.g: AddIndexToPostsTable or CustomHTMLTitle. + * + * @param string $className Class Name + * @param string $path Path + * @return bool + */ + public static function isUniqueMigrationClassName(string $className, string $path): bool + { + $existingClassNames = static::getExistingMigrationClassNames($path); + + return !in_array($className, $existingClassNames, true); + } + + /** + * Check if a migration file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidMigrationFileName(string $fileName): bool + { + return (bool)preg_match(static::MIGRATION_FILE_NAME_PATTERN, $fileName) + || (bool)preg_match(static::MIGRATION_FILE_NAME_NO_NAME_PATTERN, $fileName); + } + + /** + * Check if a seed file name is valid. + * + * @param string $fileName File Name + * @return bool + */ + public static function isValidSeedFileName(string $fileName): bool + { + return (bool)preg_match(static::SEED_FILE_NAME_PATTERN, $fileName); + } + + /** + * Expands a set of paths with curly braces (if supported by the OS). + * + * @param string[] $paths Paths + * @return array + */ + public static function globAll(array $paths): array + { + $result = []; + + foreach ($paths as $path) { + $result = array_merge($result, static::glob($path)); + } + + return $result; + } + + /** + * Expands a path with curly braces (if supported by the OS). + * + * @param string $path Path + * @return string[] + */ + public static function glob(string $path): array + { + $result = glob($path, defined('GLOB_BRACE') ? GLOB_BRACE : 0); + if ($result) { + return $result; + } + + return []; + } + + /** + * Takes the path to a php file and attempts to include it if readable + * + * @param string $filename Filename + * @param \Symfony\Component\Console\Input\InputInterface|null $input Input + * @param \Symfony\Component\Console\Output\OutputInterface|null $output Output + * @param \Phinx\Console\Command\AbstractCommand|mixed|null $context Context + * @throws \Exception + * @return string + */ + public static function loadPhpFile(string $filename, ?InputInterface $input = null, ?OutputInterface $output = null, mixed $context = null): string + { + $filePath = realpath($filename); + if (!$filePath || !file_exists($filePath)) { + throw new Exception(sprintf("File does not exist: %s \n", $filename)); + } + + /** + * I lifed this from phpunits FileLoader class + * + * @see https://github.com/sebastianbergmann/phpunit/pull/2751 + */ + $isReadable = @fopen($filePath, 'r') !== false; + + if (!$isReadable) { + throw new Exception(sprintf("Cannot open file %s \n", $filename)); + } + + // TODO remove $input, $output, and $context from scope + // prevent this to be propagated to the included file + unset($isReadable); + + include_once $filePath; + + return $filePath; + } + + /** + * Given an array of paths, return all unique PHP files that are in them + * + * @param string|string[] $paths Path or array of paths to get .php files. + * @return string[] + */ + public static function getFiles(string|array $paths): array + { + $files = static::globAll(array_map(function ($path) { + return $path . DIRECTORY_SEPARATOR . '*.php'; + }, (array)$paths)); + // glob() can return the same file multiple times + // This will cause the migration to fail with a + // false assumption of duplicate migrations + // https://php.net/manual/en/function.glob.php#110340 + $files = array_unique($files); + + return $files; + } +} diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index b4d7745c..143d4fbb 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -414,7 +414,7 @@ public function getColumnOption(array $options): array } // TODO this can be cleaned up when we stop using phinx data structures for column definitions - if ($columnOptions['precision'] === null) { + if (!isset($columnOptions['precision']) || $columnOptions['precision'] == null) { unset($columnOptions['precision']); } else { // due to Phinx using different naming for the precision and scale to CakePHP diff --git a/templates/bake/Seed/seed.twig b/templates/bake/Seed/seed.twig index 9ce3b231..d6d691e1 100644 --- a/templates/bake/Seed/seed.twig +++ b/templates/bake/Seed/seed.twig @@ -16,12 +16,21 @@ assertSameAsFile(__FUNCTION__ . $fileSuffix, $result); } + /** + * Test that when the phinx backend is active migrations use + * phinx base classes. + */ + public function testCreatePhinx() + { + Configure::write('Migrations.backend', 'phinx'); + $this->exec('bake migration CreateUsers name --connection test'); + + $file = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*_CreateUsers.php'); + $filePath = current($file); + + $this->assertExitCode(BaseCommand::CODE_SUCCESS); + $result = file_get_contents($filePath); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + /** * Tests that baking a migration with the name as another will throw an exception. */ diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 3d7205d5..a7362740 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -13,6 +13,7 @@ */ namespace Migrations\Test\TestCase\Command; +use Cake\Cache\Cache; use Cake\Console\BaseCommand; use Cake\Core\Configure; use Cake\Core\Plugin; @@ -45,6 +46,7 @@ public function setUp(): void parent::setUp(); $this->generatedFiles = []; + Configure::write('Migrations.backend', 'builtin'); } public function tearDown(): void @@ -55,6 +57,15 @@ public function tearDown(): void unlink($file); } } + 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', 'phinxlog']; + foreach ($tables as $table) { + $connection->execute("DROP TABLE IF EXISTS $table"); + } + Cache::clear('_cake_model_'); + } } /** @@ -206,7 +217,8 @@ protected function runDiffBakingTest(string $scenario): void $destinationDumpPath, ]; - $this->getMigrations("MigrationsDiff$scenario")->migrate(); + $migrations = $this->getMigrations("MigrationsDiff$scenario"); + $migrations->migrate(); unlink($destination); copy($diffDumpPath, $destinationDumpPath); diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php index 2d0c5f04..238e3c37 100644 --- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php @@ -61,6 +61,7 @@ public function setUp(): void $this->migrationPath = ROOT . DS . 'config' . DS . 'Migrations' . DS; $this->generatedFiles = []; + Configure::write('Migrations.backend', 'builtin'); } /** @@ -200,6 +201,7 @@ protected function runSnapshotTest(string $scenario, string $arguments = ''): vo $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php"); $this->generatedFiles = $generatedMigration; $this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock'; + $generatedMigration = basename($generatedMigration[0]); $fileName = pathinfo($generatedMigration, PATHINFO_FILENAME); $this->assertOutputContains('Marking the migration ' . $fileName . ' as migrated...'); diff --git a/tests/TestCase/Command/BakeSeedCommandTest.php b/tests/TestCase/Command/BakeSeedCommandTest.php index 5b3731b7..0ab7be7b 100644 --- a/tests/TestCase/Command/BakeSeedCommandTest.php +++ b/tests/TestCase/Command/BakeSeedCommandTest.php @@ -14,6 +14,7 @@ namespace Migrations\Test\TestCase\Command; use Cake\Console\BaseCommand; +use Cake\Core\Configure; use Cake\Core\Plugin; use Cake\TestSuite\StringCompareTrait; use Migrations\Test\TestCase\TestCase; @@ -51,6 +52,22 @@ public function setUp(): void $this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Seeds' . DS; } + /** + * Test empty migration with phinx base class. + * + * @return void + */ + public function testBasicBakingPhinx() + { + Configure::write('Migrations.backend', 'phinx'); + $this->generatedFile = ROOT . DS . 'config/Seeds/ArticlesSeed.php'; + $this->exec('bake seed Articles --connection test'); + + $this->assertExitCode(BaseCommand::CODE_SUCCESS); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + /** * Test empty migration. * diff --git a/tests/TestCase/Command/MigrateCommandTest.php b/tests/TestCase/Command/MigrateCommandTest.php index 1950d11c..b474a468 100644 --- a/tests/TestCase/Command/MigrateCommandTest.php +++ b/tests/TestCase/Command/MigrateCommandTest.php @@ -108,6 +108,27 @@ public function testMigrateSourceDefault(): void $this->assertFileExists($dumpFile); } + /** + * Integration test for BaseMigration with built-in backend. + */ + public function testMigrateBaseMigration(): void + { + $migrationPath = ROOT . DS . 'config' . DS . 'BaseMigrations'; + $this->exec('migrations migrate -v --source BaseMigrations -c test --no-lock'); + $this->assertExitSuccess(); + + $this->assertOutputContains('using connection test'); + $this->assertOutputContains('using paths ' . $migrationPath); + $this->assertOutputContains('BaseMigrationTables: migrated'); + $this->assertOutputContains('query=121'); + $this->assertOutputContains('fetchRow=122'); + $this->assertOutputContains('hasTable=1'); + $this->assertOutputContains('All Done'); + + $table = $this->fetchTable('Phinxlog'); + $this->assertCount(1, $table->find()->all()->toArray()); + } + /** * Test that running with a no-op migrations is successful */ diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index 8a9bedcc..8bac092e 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -122,6 +122,25 @@ public function testSeederOne(): void $this->assertEquals(1, $query->fetchColumn(0)); } + public function testSeederBaseSeed(): void + { + $this->createTables(); + $this->exec('migrations seed -c test --source BaseSeeds --seed MigrationSeedNumbers'); + $this->assertExitSuccess(); + $this->assertOutputContains('MigrationSeedNumbers: seeding'); + $this->assertOutputContains('AnotherNumbersSeed: seeding'); + $this->assertOutputContains('radix=10'); + $this->assertOutputContains('fetchRow=121'); + $this->assertOutputContains('hasTable=1'); + $this->assertOutputContains('fetchAll=121'); + $this->assertOutputContains('All Done'); + + $connection = ConnectionManager::get('test'); + $query = $connection->execute('SELECT COUNT(*) FROM numbers'); + // Two seeders run == 2 rows + $this->assertEquals(2, $query->fetchColumn(0)); + } + public function testSeederImplictAll(): void { $this->createTables(); diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 99ab98dc..824b0f4d 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -397,7 +397,7 @@ public function testCreateTableAndInheritDefaultCollation() ->save(); $this->assertTrue($adapter->hasTable('table_with_default_collation')); $row = $adapter->fetchRow(sprintf("SHOW TABLE STATUS WHERE Name = '%s'", 'table_with_default_collation')); - $this->assertEquals('utf8mb4_unicode_ci', $row['Collation']); + $this->assertEquals('utf8mb4_0900_ai_ci', $row['Collation']); } public function testCreateTableWithLatin1Collate() @@ -498,13 +498,13 @@ public function testAddTimestampsFeatureFlag() $this->assertCount(3, $columns); $this->assertSame('id', $columns[0]->getName()); - $this->assertEquals('created_at', $columns[1]->getName()); + $this->assertEquals('created', $columns[1]->getName()); $this->assertEquals('datetime', $columns[1]->getType()); $this->assertEquals('', $columns[1]->getUpdate()); $this->assertFalse($columns[1]->isNull()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault()); - $this->assertEquals('updated_at', $columns[2]->getName()); + $this->assertEquals('updated', $columns[2]->getName()); $this->assertEquals('datetime', $columns[2]->getType()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[2]->getUpdate()); $this->assertTrue($columns[2]->isNull()); @@ -2037,6 +2037,37 @@ public function testBulkInsertData() $this->assertEquals('test', $rows[2]['column3']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $data = [ @@ -2072,6 +2103,42 @@ public function testInsertData() $this->assertEquals('foo', $rows[2]['column3']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testDumpCreateTable() { $options = $this->adapter->getOptions(); @@ -2086,7 +2153,7 @@ public function testDumpCreateTable() ->save(); $expectedOutput = <<<'OUTPUT' -CREATE TABLE `table1` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, `column3` VARCHAR(255) NOT NULL DEFAULT 'test', PRIMARY KEY (`id`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE TABLE `table1` (`id` INT(11) unsigned NOT NULL AUTO_INCREMENT, `column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, `column3` VARCHAR(255) NOT NULL DEFAULT 'test', PRIMARY KEY (`id`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; OUTPUT; $actualOutput = join("\n", $this->out->messages()); $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create table query to the output'); @@ -2191,7 +2258,7 @@ public function testDumpCreateTableAndThenInsert() ])->save(); $expectedOutput = <<<'OUTPUT' -CREATE TABLE `table1` (`column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, PRIMARY KEY (`column1`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE TABLE `table1` (`column1` VARCHAR(255) NOT NULL, `column2` INT(11) NULL, PRIMARY KEY (`column1`)) ENGINE = InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; INSERT INTO `table1` (`column1`, `column2`) VALUES ('id1', 1); OUTPUT; $actualOutput = join("\n", $this->out->messages()); diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index a084dbc5..98a9acd8 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -2262,6 +2262,37 @@ public function testBulkInsertBoolean() $this->assertNull($rows[2]['column1']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -2286,6 +2317,42 @@ public function testInsertData() $this->assertEquals(2, $rows[1]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testInsertBoolean() { $table = new Table('table1', [], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index 9b34858c..f1212768 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -1710,6 +1710,37 @@ public function testBulkInsertData() $this->assertNull($rows[3]['column2']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -1751,6 +1782,42 @@ public function testInsertData() $this->assertNull($rows[3]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); + } + public function testBulkInsertDataEnum() { $table = new Table('table1', [], $this->adapter); @@ -1808,7 +1875,7 @@ public function testNullWithoutDefaultValue() public function testDumpCreateTable() { - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string', ['null' => false]) @@ -1835,7 +1902,7 @@ public function testDumpInsert() ->addColumn('int_col', 'integer') ->save(); - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $this->adapter->insert($table->getTable(), [ 'string_col' => 'test data', ]); @@ -1875,7 +1942,7 @@ public function testDumpBulkinsert() ->addColumn('int_col', 'integer') ->save(); - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $this->adapter->bulkinsert($table->getTable(), [ [ 'string_col' => 'test_data1', @@ -1901,7 +1968,7 @@ public function testDumpBulkinsert() public function testDumpCreateTableAndThenInsert() { - $this->adapter->setOptions(['dryrun' => true]); + $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', ['id' => false, 'primary_key' => ['column1']], $this->adapter); $table->addColumn('column1', 'string', ['null' => false]) @@ -2019,8 +2086,8 @@ public function testAlterTableColumnAdd() ['name' => 'string_col', 'type' => 'string', 'default' => '', 'null' => true], ['name' => 'string_col_2', 'type' => 'string', 'default' => null, 'null' => true], ['name' => 'string_col_3', 'type' => 'string', 'default' => null, 'null' => false], - ['name' => 'created_at', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], - ['name' => 'updated_at', 'type' => 'timestamp', 'default' => null, 'null' => true], + ['name' => 'created', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], + ['name' => 'updated', 'type' => 'timestamp', 'default' => null, 'null' => true], ]; $this->assertEquals(count($expected), count($columns)); diff --git a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php index 004a9f49..cc3bbf36 100644 --- a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php @@ -1197,6 +1197,37 @@ public function testBulkInsertData() $this->assertEquals(3, $rows[2]['column2']); } + public function testBulkInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column2' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column2' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertEquals('2024-01-01 00:00:00.000', $rows[1]['column2']); + $this->assertEquals('2025-01-01 00:00:00.000', $rows[2]['column2']); + } + public function testInsertData() { $table = new Table('table1', [], $this->adapter); @@ -1230,6 +1261,42 @@ public function testInsertData() $this->assertEquals(3, $rows[2]['column2']); } + public function testInsertLiteral() + { + $data = [ + [ + 'column1' => 'value1', + 'column3' => Literal::from('CURRENT_TIMESTAMP'), + ], + [ + 'column1' => 'value2', + 'column3' => '2024-01-01 00:00:00', + ], + [ + 'column1' => 'value3', + 'column2' => 'foo', + 'column3' => '2025-01-01 00:00:00', + ], + ]; + $table = new Table('table1', [], $this->adapter); + $table->addColumn('column1', 'string') + ->addColumn('column2', 'string', ['default' => 'test']) + ->addColumn('column3', 'datetime') + ->insert($data) + ->save(); + + $rows = $this->adapter->fetchAll('SELECT * FROM table1'); + $this->assertEquals('value1', $rows[0]['column1']); + $this->assertEquals('value2', $rows[1]['column1']); + $this->assertEquals('value3', $rows[2]['column1']); + $this->assertEquals('test', $rows[0]['column2']); + $this->assertEquals('test', $rows[1]['column2']); + $this->assertEquals('foo', $rows[2]['column2']); + $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertEquals('2024-01-01 00:00:00.000', $rows[1]['column3']); + $this->assertEquals('2025-01-01 00:00:00.000', $rows[2]['column3']); + } + public function testTruncateTable() { $table = new Table('table1', [], $this->adapter); diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 679650c5..4aa2ef26 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -22,7 +22,7 @@ class TableTest extends TestCase { public static function provideAdapters() { - return [[new SqlServerAdapter([])], [new MysqlAdapter([])], [new PostgresAdapter([])], [new SQLiteAdapter([])]]; + return [[new SqlServerAdapter([])], [new MysqlAdapter([])], [new PostgresAdapter([])], [new SQLiteAdapter(['name' => ':memory:'])]]; } public static function provideTimestampColumnNames() diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index e16e6b48..7e79a780 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -8,6 +8,8 @@ use Migrations\Db\Adapter\AdapterWrapper; use Migrations\Db\Adapter\PdoAdapter; use Migrations\Migration\Environment; +use Migrations\Shim\MigrationAdapter; +use Migrations\Shim\SeedAdapter; use Phinx\Migration\AbstractMigration; use Phinx\Migration\MigrationInterface; use Phinx\Seed\AbstractSeed; @@ -129,7 +131,8 @@ public function up(): void } }; - $this->environment->executeMigration($upMigration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($upMigration, $upMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($upMigration->executed); } @@ -154,7 +157,8 @@ public function down(): void } }; - $this->environment->executeMigration($downMigration, MigrationInterface::DOWN); + $migrationWrapper = new MigrationAdapter($downMigration, $downMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::DOWN); $this->assertTrue($downMigration->executed); } @@ -170,7 +174,7 @@ public function testExecutingAMigrationWithTransactions() $adapterStub->expects($this->once()) ->method('commitTransaction'); - $adapterStub->expects($this->exactly(1)) + $adapterStub->expects($this->atLeastOnce()) ->method('hasTransactions') ->willReturn(true); @@ -185,7 +189,8 @@ public function up(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } @@ -201,7 +206,7 @@ public function testExecutingAMigrationWithUseTransactions() $adapterStub->expects($this->never()) ->method('commitTransaction'); - $adapterStub->expects($this->exactly(1)) + $adapterStub->expects($this->atLeastOnce()) ->method('hasTransactions') ->willReturn(true); @@ -222,7 +227,8 @@ public function up(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } @@ -247,7 +253,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($migration->executed); } @@ -272,7 +279,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::DOWN); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::DOWN); $this->assertTrue($migration->executed); } @@ -297,7 +305,8 @@ public function change(): void } }; - $this->environment->executeMigration($migration, MigrationInterface::UP, true); + $migrationWrapper = new MigrationAdapter($migration, $migration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP, true); $this->assertFalse($migration->executed); } @@ -336,8 +345,8 @@ public function up(): void $this->upExecuted = true; } }; - - $this->environment->executeMigration($upMigration, MigrationInterface::UP); + $migrationWrapper = new MigrationAdapter($upMigration, $upMigration->getVersion()); + $this->environment->executeMigration($migrationWrapper, MigrationInterface::UP); $this->assertTrue($upMigration->initExecuted); $this->assertTrue($upMigration->upExecuted); } @@ -351,7 +360,6 @@ public function testExecuteSeedInit() $this->environment->setAdapter($adapterStub); - // up $seed = new class ('mockenv', 20110301080000) extends AbstractSeed { public bool $initExecuted = false; public bool $runExecuted = false; @@ -367,7 +375,9 @@ public function run(): void } }; - $this->environment->executeSeed($seed); + $seedWrapper = new SeedAdapter($seed); + $this->environment->executeSeed($seedWrapper); + $this->assertTrue($seed->initExecuted); $this->assertTrue($seed->runExecuted); } diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index 764b9a5b..58393ef7 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -13,12 +13,10 @@ use Migrations\Db\Adapter\AdapterInterface; use Migrations\Migration\Environment; use Migrations\Migration\Manager; -use Migrations\Shim\OutputAdapter; use Phinx\Console\Command\AbstractCommand; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; -use Symfony\Component\Console\Input\InputInterface; class ManagerTest extends TestCase { @@ -2187,9 +2185,9 @@ public function testExecuteANonExistentSeedWorksAsExpected(): void public function testOrderSeeds(): void { $seeds = array_values($this->manager->getSeeds()); - $this->assertInstanceOf('UserSeeder', $seeds[0]); - $this->assertInstanceOf('GSeeder', $seeds[1]); - $this->assertInstanceOf('PostSeeder', $seeds[2]); + $this->assertEquals('UserSeeder', $seeds[0]->getName()); + $this->assertEquals('GSeeder', $seeds[1]->getName()); + $this->assertEquals('PostSeeder', $seeds[2]->getName()); } public function testSeedWillNotBeExecuted(): void @@ -2205,18 +2203,6 @@ public function testSeedWillNotBeExecuted(): void $this->assertStringContainsString('skipped', $output); } - public function testGettingInputObject(): void - { - $migrations = $this->manager->getMigrations(); - $seeds = $this->manager->getSeeds(); - foreach ($migrations as $migration) { - $this->assertInstanceOf(InputInterface::class, $migration->getInput()); - } - foreach ($seeds as $seed) { - $this->assertInstanceOf(InputInterface::class, $migration->getInput()); - } - } - public function testGettingIo(): void { $migrations = $this->manager->getMigrations(); @@ -2225,10 +2211,10 @@ public function testGettingIo(): void $this->assertInstanceOf(ConsoleIo::class, $io); foreach ($migrations as $migration) { - $this->assertInstanceOf(OutputAdapter::class, $migration->getOutput()); + $this->assertInstanceOf(ConsoleIo::class, $migration->getIo()); } foreach ($seeds as $seed) { - $this->assertInstanceOf(OutputAdapter::class, $seed->getOutput()); + $this->assertInstanceOf(ConsoleIo::class, $seed->getIo()); } } diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index 0d3a8875..9120d182 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -1092,8 +1092,10 @@ protected function runMigrateSnapshots(string $basePath, string $filename, array // change class name to avoid conflict with other classes // to avoid 'Fatal error: Cannot declare class Test...., because the name is already in use' $content = file_get_contents($destination . $copiedFileName); - $pattern = ' extends AbstractMigration'; - $content = str_replace($pattern, 'NewSuffix' . $pattern, $content); + $patterns = [' extends AbstractMigration', ' extends BaseMigration']; + foreach ($patterns as $pattern) { + $content = str_replace($pattern, 'NewSuffix' . $pattern, $content); + } file_put_contents($destination . $copiedFileName, $content); $migrations = new Migrations([ diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php new file mode 100644 index 00000000..a7620401 --- /dev/null +++ b/tests/TestCase/Util/UtilTest.php @@ -0,0 +1,149 @@ +getCorrectedPath(__DIR__ . '/_files/migrations')); + $this->assertCount(count($expectedResults), $existingClassNames); + foreach ($expectedResults as $expectedResult) { + $this->assertContains($expectedResult, $existingClassNames); + } + } + + public function testGetExistingMigrationClassNamesWithFile() + { + $file = $this->getCorrectedPath(__DIR__ . '/_files/migrations/20120111235330_test_migration.php'); + $existingClassNames = Util::getExistingMigrationClassNames($file); + $this->assertCount(0, $existingClassNames); + } + + public function testGetCurrentTimestamp() + { + $dt = new DateTime('now', new DateTimeZone('UTC')); + $expected = $dt->format(Util::DATE_FORMAT); + + $current = Util::getCurrentTimestamp(); + + // Rather than using a strict equals, we use greater/lessthan checks to + // prevent false positives when the test hits the edge of a second. + $this->assertGreaterThanOrEqual($expected, $current); + // We limit the assertion time to 2 seconds, which should never fail. + $this->assertLessThanOrEqual($expected + 2, $current); + } + + public function testGetVersionFromFileName(): void + { + $this->assertSame(20221130101652, Util::getVersionFromFileName('20221130101652_test.php')); + } + + public function testGetVersionFromFileNameErrorNoVersion(): void + { + $this->expectException(RuntimeException::class); + Util::getVersionFromFileName('foo.php'); + } + + public function testGetVersionFromFileNameErrorZeroVersion(): VoidCommand + { + $this->expectException(RuntimeException::class); + Util::getVersionFromFileName('0_foo.php'); + } + + public static function providerMapClassNameToFileName(): array + { + return [ + ['CamelCase87afterSomeBooze', '/^\d{14}_camel_case_87after_some_booze\.php$/'], + ['CreateUserTable', '/^\d{14}_create_user_table\.php$/'], + ['LimitResourceNamesTo30Chars', '/^\d{14}_limit_resource_names_to_30_chars\.php$/'], + ]; + } + + /** + * @dataProvider providerMapClassNameToFileName + */ + public function testMapClassNameToFileName(string $name, string $pattern): void + { + $this->assertMatchesRegularExpression($pattern, Util::mapClassNameToFileName($name)); + } + + public static function providerMapFileName(): array + { + return [ + ['20150902094024_create_user_table.php', 'CreateUserTable'], + ['20150902102548_my_first_migration2.php', 'MyFirstMigration2'], + ['20200412012035_camel_case_87after_some_booze.php', 'CamelCase87afterSomeBooze'], + ['20200412012036_limit_resource_names_to_30_chars.php', 'LimitResourceNamesTo30Chars'], + ['20200412012037_back_compat_names_to30_chars.php', 'BackCompatNamesTo30Chars'], + ['20200412012037.php', 'V20200412012037'], + ]; + } + + /** + * @dataProvider providerMapFileName + */ + public function testMapFileNameToClassName(string $fileName, string $className) + { + $this->assertEquals($className, Util::mapFileNameToClassName($fileName)); + } + + public function testGlobPath() + { + $files = Util::glob(__DIR__ . '/_files/migrations/empty.txt'); + $this->assertCount(1, $files); + $this->assertEquals('empty.txt', basename($files[0])); + + $files = Util::glob(__DIR__ . '/_files/migrations/*.php'); + $this->assertCount(3, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + } + + public function testGlobAll() + { + $files = Util::globAll([ + __DIR__ . '/_files/migrations/*.php', + __DIR__ . '/_files/migrations/subdirectory/*.txt', + ]); + + $this->assertCount(4, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + $this->assertEquals('empty.txt', basename($files[3])); + } + + public function testGetFiles() + { + $files = Util::getFiles([ + __DIR__ . '/_files/migrations', + __DIR__ . '/_files/migrations/subdirectory', + __DIR__ . '/_files/migrations/subdirectory', + ]); + + $this->assertCount(4, $files); + $this->assertEquals('20120111235330_test_migration.php', basename($files[0])); + $this->assertEquals('20120116183504_test_migration_2.php', basename($files[1])); + $this->assertEquals('not_a_migration.php', basename($files[2])); + $this->assertEquals('foobar.php', basename($files[3])); + } +} diff --git a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php new file mode 100644 index 00000000..27b10239 --- /dev/null +++ b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php @@ -0,0 +1,23 @@ +table('users'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->create(); + } +} diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php index bf9471ab..cec07a2c 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKey.php +++ b/tests/comparisons/Migration/testCreatePrimaryKey.php @@ -1,9 +1,9 @@ table('articles'); + $table->insert($data)->save(); + } +} diff --git a/tests/comparisons/Seeds/testPrettifyArray.php b/tests/comparisons/Seeds/testPrettifyArray.php index 58ced7ff..5f48bac7 100644 --- a/tests/comparisons/Seeds/testPrettifyArray.php +++ b/tests/comparisons/Seeds/testPrettifyArray.php @@ -1,12 +1,12 @@ table('base_stores', ['collation' => 'utf8_bin']); + $table + ->addColumn('name', 'string') + ->addTimestamps() + ->addPrimaryKey('id') + ->create(); + $io = $this->getIo(); + + $res = $this->query('SELECT 121 as val'); + $io->out('query=' . $res->fetchColumn(0)); + $io->out('fetchRow=' . $this->fetchRow('SELECT 122 as val')['val']); + $io->out('hasTable=' . $this->hasTable('base_stores')); + + // Run for coverage + $this->getSelectBuilder(); + $this->getInsertBuilder(); + $this->getDeleteBuilder(); + $this->getUpdateBuilder(); + } +} diff --git a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php new file mode 100644 index 00000000..e778b424 --- /dev/null +++ b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php @@ -0,0 +1,45 @@ + '5', + 'radix' => '10', + ], + ]; + + // Call various methods on the seeder for runtime checks + // and generate output to assert behavior with in an integration test. + $this->table('numbers'); + $this->insert('numbers', $data); + + $this->call('AnotherNumbersSeed', ['source' => 'AltSeeds']); + + $io = $this->getIo(); + $query = $this->query('SELECT radix FROM numbers'); + $io->out('radix=' . $query->fetchColumn(0)); + + $row = $this->fetchRow('SELECT 121 as row_val'); + $io->out('fetchRow=' . $row['row_val']); + $io->out('hasTable=' . $this->hasTable('numbers')); + + $rows = $this->fetchAll('SELECT 121 as row_val'); + $io->out('fetchAll=' . $rows[0]['row_val']); + } +}