diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7f8a8..d988fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # ChangeLog +## 0.8 + +### 0.8.0 + +- Fix mysql drop unique index raises OperationalError. (#346) + + **Upgrade note:** + 1. Use column name as unique key name for mysql + ## 0.7 -### 0.7.2 +### [0.7.2] - 2023-07-20 - Support virtual fields. - Fix modify multiple times. (#279) diff --git a/README.md b/README.md index 5df1db4..f9e0665 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Commands: ## Usage -You need add `aerich.models` to your `Tortoise-ORM` config first. Example: +You need to add `aerich.models` to your `Tortoise-ORM` config first. Example: ```python TORTOISE_ORM = { diff --git a/aerich/ddl/mysql/__init__.py b/aerich/ddl/mysql/__init__.py index 2eea288..5ecb51d 100644 --- a/aerich/ddl/mysql/__init__.py +++ b/aerich/ddl/mysql/__init__.py @@ -1,7 +1,12 @@ +from typing import TYPE_CHECKING, List, Type + from tortoise.backends.mysql.schema_generator import MySQLSchemaGenerator from aerich.ddl import BaseDDL +if TYPE_CHECKING: + from tortoise import Model # noqa:F401 + class MysqlDDL(BaseDDL): schema_generator_cls = MySQLSchemaGenerator @@ -30,3 +35,29 @@ class MysqlDDL(BaseDDL): ) _MODIFY_COLUMN_TEMPLATE = "ALTER TABLE `{table_name}` MODIFY COLUMN {column}" _RENAME_TABLE_TEMPLATE = "ALTER TABLE `{old_table_name}` RENAME TO `{new_table_name}`" + + def _index_name(self, unique: bool, model: "Type[Model]", field_names: List[str]) -> str: + if unique: + if len(field_names) == 1: + # Example: `email = CharField(max_length=50, unique=True)` + # Generate schema: `"email" VARCHAR(10) NOT NULL UNIQUE` + # Unique index key is the same as field name: `email` + return field_names[0] + index_prefix = "uid" + else: + index_prefix = "idx" + return self.schema_generator._generate_index_name(index_prefix, model, field_names) + + def add_index(self, model: "Type[Model]", field_names: List[str], unique=False) -> str: + return self._ADD_INDEX_TEMPLATE.format( + unique="UNIQUE " if unique else "", + index_name=self._index_name(unique, model, field_names), + table_name=model._meta.db_table, + column_names=", ".join(self.schema_generator.quote(f) for f in field_names), + ) + + def drop_index(self, model: "Type[Model]", field_names: List[str], unique=False) -> str: + return self._DROP_INDEX_TEMPLATE.format( + index_name=self._index_name(unique, model, field_names), + table_name=model._meta.db_table, + ) diff --git a/aerich/migrate.py b/aerich/migrate.py index 32cad7f..d5ef6e6 100644 --- a/aerich/migrate.py +++ b/aerich/migrate.py @@ -477,12 +477,13 @@ def diff_models(cls, old_models: Dict[str, dict], new_models: Dict[str, dict], u _, option, old_new = change if option == "indexed": # change index - unique = new_data_field.get("unique") if old_new[0] is False and old_new[1] is True: + unique = new_data_field.get("unique") cls._add_operator( cls._add_index(model, (field_name,), unique), upgrade, True ) else: + unique = old_data_field.get("unique") cls._add_operator( cls._drop_index(model, (field_name,), unique), upgrade, True ) diff --git a/pyproject.toml b/pyproject.toml index 2cc28cd..57f7545 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aerich" -version = "0.7.2" +version = "0.8.0" description = "A database migrations tool for Tortoise ORM." authors = ["long2ice "] license = "Apache-2.0" diff --git a/tests/models.py b/tests/models.py index 597ee59..5dd7f61 100644 --- a/tests/models.py +++ b/tests/models.py @@ -48,6 +48,7 @@ class Category(Model): slug = fields.CharField(max_length=100) name = fields.CharField(max_length=200, null=True, default=default_name) user = fields.ForeignKeyField("models.User", description="User") + title = fields.CharField(max_length=20, unique=False) created_at = fields.DatetimeField(auto_now_add=True) diff --git a/tests/test_ddl.py b/tests/test_ddl.py index 6bf1cb0..a92de94 100644 --- a/tests/test_ddl.py +++ b/tests/test_ddl.py @@ -14,6 +14,7 @@ def test_create_table(): `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `slug` VARCHAR(100) NOT NULL, `name` VARCHAR(200), + `title` VARCHAR(20) NOT NULL, `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `user_id` INT NOT NULL COMMENT 'User', CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE @@ -27,6 +28,7 @@ def test_create_table(): "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "slug" VARCHAR(100) NOT NULL, "name" VARCHAR(200), + "title" VARCHAR(20) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE /* User */ )""" @@ -39,6 +41,7 @@ def test_create_table(): "id" SERIAL NOT NULL PRIMARY KEY, "slug" VARCHAR(100) NOT NULL, "name" VARCHAR(200), + "title" VARCHAR(20) NOT NULL, "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE ); @@ -151,9 +154,7 @@ def test_add_index(): index_u = Migrate.ddl.add_index(Category, ["name"], True) if isinstance(Migrate.ddl, MysqlDDL): assert index == "ALTER TABLE `category` ADD INDEX `idx_category_name_8b0cb9` (`name`)" - assert ( - index_u == "ALTER TABLE `category` ADD UNIQUE INDEX `uid_category_name_8b0cb9` (`name`)" - ) + assert index_u == "ALTER TABLE `category` ADD UNIQUE INDEX `name` (`name`)" elif isinstance(Migrate.ddl, PostgresDDL): assert index == 'CREATE INDEX "idx_category_name_8b0cb9" ON "category" ("name")' assert index_u == 'CREATE UNIQUE INDEX "uid_category_name_8b0cb9" ON "category" ("name")' @@ -169,7 +170,7 @@ def test_drop_index(): ret_u = Migrate.ddl.drop_index(Category, ["name"], True) if isinstance(Migrate.ddl, MysqlDDL): assert ret == "ALTER TABLE `category` DROP INDEX `idx_category_name_8b0cb9`" - assert ret_u == "ALTER TABLE `category` DROP INDEX `uid_category_name_8b0cb9`" + assert ret_u == "ALTER TABLE `category` DROP INDEX `name`" elif isinstance(Migrate.ddl, PostgresDDL): assert ret == 'DROP INDEX "idx_category_name_8b0cb9"' assert ret_u == 'DROP INDEX "uid_category_name_8b0cb9"' diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 9269376..e428a88 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -103,6 +103,21 @@ "constraints": {"ge": 1, "le": 2147483647}, "db_field_types": {"": "INT"}, }, + { + "name": "title", + "field_type": "CharField", + "db_column": "title", + "python_type": "str", + "generated": False, + "nullable": False, + "unique": True, + "indexed": True, + "default": None, + "description": None, + "docstring": None, + "constraints": {"max_length": 20}, + "db_field_types": {"": "VARCHAR(20)"}, + }, ], "fk_fields": [ { @@ -786,7 +801,8 @@ def test_migrate(mocker: MockerFixture): - drop field: User.avatar - add index: Email.email - add many to many: Email.users - - remove unique: User.username + - remove unique: Category.title + - add unique: User.username - change column: length User.password - add unique_together: (name,type) of Product - alter default: Config.status @@ -808,6 +824,7 @@ def test_migrate(mocker: MockerFixture): expected_upgrade_operators = { "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200)", "ALTER TABLE `category` MODIFY COLUMN `slug` VARCHAR(100) NOT NULL", + "ALTER TABLE `category` DROP INDEX `title`", "ALTER TABLE `config` ADD `user_id` INT NOT NULL COMMENT 'User'", "ALTER TABLE `config` ADD CONSTRAINT `fk_config_user_17daa970` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE", "ALTER TABLE `config` ALTER COLUMN `status` DROP DEFAULT", @@ -830,7 +847,7 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `user` MODIFY COLUMN `is_active` BOOL NOT NULL COMMENT 'Is Active' DEFAULT 1", "ALTER TABLE `user` MODIFY COLUMN `is_superuser` BOOL NOT NULL COMMENT 'Is SuperUser' DEFAULT 0", "ALTER TABLE `user` MODIFY COLUMN `longitude` DECIMAL(10,8) NOT NULL", - "ALTER TABLE `user` ADD UNIQUE INDEX `uid_user_usernam_9987ab` (`username`)", + "ALTER TABLE `user` ADD UNIQUE INDEX `username` (`username`)", "CREATE TABLE `email_user` (\n `email_id` INT NOT NULL REFERENCES `email` (`email_id`) ON DELETE CASCADE,\n `user_id` INT NOT NULL REFERENCES `user` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4", "CREATE TABLE IF NOT EXISTS `newmodel` (\n `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,\n `name` VARCHAR(50) NOT NULL\n) CHARACTER SET utf8mb4", "ALTER TABLE `category` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", @@ -840,6 +857,7 @@ def test_migrate(mocker: MockerFixture): expected_downgrade_operators = { "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200) NOT NULL", "ALTER TABLE `category` MODIFY COLUMN `slug` VARCHAR(200) NOT NULL", + "ALTER TABLE `category` ADD UNIQUE INDEX `title` (`title`)", "ALTER TABLE `config` DROP COLUMN `user_id`", "ALTER TABLE `config` DROP FOREIGN KEY `fk_config_user_17daa970`", "ALTER TABLE `config` ALTER COLUMN `status` SET DEFAULT 1", @@ -853,7 +871,7 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `product` DROP INDEX `uid_product_name_869427`", "ALTER TABLE `product` ALTER COLUMN `view_num` DROP DEFAULT", "ALTER TABLE `user` ADD `avatar` VARCHAR(200) NOT NULL DEFAULT ''", - "ALTER TABLE `user` DROP INDEX `idx_user_usernam_9987ab`", + "ALTER TABLE `user` DROP INDEX `username`", "ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(200) NOT NULL", "DROP TABLE IF EXISTS `email_user`", "DROP TABLE IF EXISTS `newmodel`", @@ -877,6 +895,7 @@ def test_migrate(mocker: MockerFixture): elif isinstance(Migrate.ddl, PostgresDDL): expected_upgrade_operators = { + 'DROP INDEX "uid_category_title_f7fc03"', 'ALTER TABLE "category" ALTER COLUMN "name" DROP NOT NULL', 'ALTER TABLE "category" ALTER COLUMN "slug" TYPE VARCHAR(100) USING "slug"::VARCHAR(100)', 'ALTER TABLE "category" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', @@ -909,6 +928,7 @@ def test_migrate(mocker: MockerFixture): 'CREATE UNIQUE INDEX "uid_user_usernam_9987ab" ON "user" ("username")', } expected_downgrade_operators = { + 'CREATE UNIQUE INDEX "uid_category_title_f7fc03" ON "category" ("title")', 'ALTER TABLE "category" ALTER COLUMN "name" SET NOT NULL', 'ALTER TABLE "category" ALTER COLUMN "slug" TYPE VARCHAR(200) USING "slug"::VARCHAR(200)', 'ALTER TABLE "category" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', @@ -935,7 +955,7 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "product" ALTER COLUMN "body" TYPE TEXT USING "body"::TEXT', 'DROP INDEX "idx_product_name_869427"', 'DROP INDEX "idx_email_email_4a1a33"', - 'DROP INDEX "idx_user_usernam_9987ab"', + 'DROP INDEX "uid_user_usernam_9987ab"', 'DROP INDEX "uid_product_name_869427"', 'DROP TABLE IF EXISTS "email_user"', 'DROP TABLE IF EXISTS "newmodel"',