From e9716538519c07d46a2dc6cba87fd096758c9471 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Wed, 5 Jun 2024 23:37:30 +0800 Subject: [PATCH 1/4] fix: mysql drop unique index migrate error --- aerich/ddl/mysql/__init__.py | 31 +++++++++++++++++++++++++++++++ aerich/migrate.py | 3 ++- tests/test_ddl.py | 6 ++---- tests/test_migrate.py | 17 +++++++++++------ 4 files changed, 46 insertions(+), 11 deletions(-) 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/tests/test_ddl.py b/tests/test_ddl.py index 6bf1cb0..b04e5d7 100644 --- a/tests/test_ddl.py +++ b/tests/test_ddl.py @@ -151,9 +151,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 +167,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..d51deb0 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -44,8 +44,8 @@ "python_type": "str", "generated": False, "nullable": False, - "unique": False, - "indexed": False, + "unique": True, + "indexed": True, "default": None, "description": None, "docstring": None, @@ -786,7 +786,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.slug + - add unique: User.username - change column: length User.password - add unique_together: (name,type) of Product - alter default: Config.status @@ -806,6 +807,7 @@ def test_migrate(mocker: MockerFixture): Migrate._merge_operators() if isinstance(Migrate.ddl, MysqlDDL): expected_upgrade_operators = { + "ALTER TABLE `category` DROP INDEX `slug`", "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200)", "ALTER TABLE `category` MODIFY COLUMN `slug` VARCHAR(100) NOT NULL", "ALTER TABLE `config` ADD `user_id` INT NOT NULL COMMENT 'User'", @@ -830,7 +832,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)", @@ -838,6 +840,7 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `email` MODIFY COLUMN `is_primary` BOOL NOT NULL DEFAULT 0", } expected_downgrade_operators = { + "ALTER TABLE `category` ADD UNIQUE INDEX `slug` (`slug`)", "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200) NOT NULL", "ALTER TABLE `category` MODIFY COLUMN `slug` VARCHAR(200) NOT NULL", "ALTER TABLE `config` DROP COLUMN `user_id`", @@ -853,7 +856,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 +880,7 @@ def test_migrate(mocker: MockerFixture): elif isinstance(Migrate.ddl, PostgresDDL): expected_upgrade_operators = { + 'DROP INDEX "uid_category_slug_e9bcff"', '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 +913,7 @@ def test_migrate(mocker: MockerFixture): 'CREATE UNIQUE INDEX "uid_user_usernam_9987ab" ON "user" ("username")', } expected_downgrade_operators = { + 'CREATE UNIQUE INDEX "uid_category_slug_e9bcff" ON "category" ("slug")', '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 +940,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"', From 234495d29192f5cf08fd17300439c391d61e4bce Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Thu, 6 Jun 2024 00:07:29 +0800 Subject: [PATCH 2/4] docs: bump up version and update changelog --- CHANGELOG.md | 11 ++++++++++- pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) 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/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" From 84d31d63f6bbc28eec4f99845bc029dfe03179a1 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Thu, 6 Jun 2024 08:42:55 +0800 Subject: [PATCH 3/4] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = { From e764bb56f76cd4def613bdf790dc56f9f2af0ccb Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Thu, 6 Jun 2024 09:07:13 +0800 Subject: [PATCH 4/4] Add new column for unique index remove test --- tests/models.py | 1 + tests/test_ddl.py | 3 +++ tests/test_migrate.py | 29 ++++++++++++++++++++++------- 3 files changed, 26 insertions(+), 7 deletions(-) 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 b04e5d7..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 ); diff --git a/tests/test_migrate.py b/tests/test_migrate.py index d51deb0..e428a88 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -44,8 +44,8 @@ "python_type": "str", "generated": False, "nullable": False, - "unique": True, - "indexed": True, + "unique": False, + "indexed": False, "default": None, "description": None, "docstring": None, @@ -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,7 @@ def test_migrate(mocker: MockerFixture): - drop field: User.avatar - add index: Email.email - add many to many: Email.users - - remove unique: Category.slug + - remove unique: Category.title - add unique: User.username - change column: length User.password - add unique_together: (name,type) of Product @@ -807,9 +822,9 @@ def test_migrate(mocker: MockerFixture): Migrate._merge_operators() if isinstance(Migrate.ddl, MysqlDDL): expected_upgrade_operators = { - "ALTER TABLE `category` DROP INDEX `slug`", "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", @@ -840,9 +855,9 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `email` MODIFY COLUMN `is_primary` BOOL NOT NULL DEFAULT 0", } expected_downgrade_operators = { - "ALTER TABLE `category` ADD UNIQUE INDEX `slug` (`slug`)", "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", @@ -880,7 +895,7 @@ def test_migrate(mocker: MockerFixture): elif isinstance(Migrate.ddl, PostgresDDL): expected_upgrade_operators = { - 'DROP INDEX "uid_category_slug_e9bcff"', + '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', @@ -913,7 +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_slug_e9bcff" ON "category" ("slug")', + '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',