diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e985ce..26d42eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### [0.8.1](Unreleased) #### Fixed +- fix: add o2o field does not create constraint when migrating. (#396) - fix: intermediate table for m2m relation not created. (#394) - Migrate add m2m field with custom through generate duplicated table. (#393) - Migrate drop the wrong m2m field when model have multi m2m fields. (#376) diff --git a/aerich/migrate.py b/aerich/migrate.py index a24b72f..65a48da 100644 --- a/aerich/migrate.py +++ b/aerich/migrate.py @@ -281,6 +281,68 @@ class Migrate: if add: cls._add_operator(cls.drop_m2m(table), upgrade, True) + @classmethod + def _handle_relational( + cls, + key: str, + old_model_describe: Dict, + new_model_describe: Dict, + model: Type[Model], + old_models: Dict, + new_models: Dict, + upgrade=True, + ) -> None: + old_fk_fields = cast(List[dict], old_model_describe.get(key)) + new_fk_fields = cast(List[dict], new_model_describe.get(key)) + + old_fk_fields_name: List[str] = [i.get("name", "") for i in old_fk_fields] + new_fk_fields_name: List[str] = [i.get("name", "") for i in new_fk_fields] + + # add + for new_fk_field_name in set(new_fk_fields_name).difference(set(old_fk_fields_name)): + fk_field = cls.get_field_by_name(new_fk_field_name, new_fk_fields) + if fk_field.get("db_constraint"): + ref_describe = cast(dict, new_models[fk_field["python_type"]]) + sql = cls._add_fk(model, fk_field, ref_describe) + cls._add_operator(sql, upgrade, fk_m2m_index=True) + # drop + for old_fk_field_name in set(old_fk_fields_name).difference(set(new_fk_fields_name)): + old_fk_field = cls.get_field_by_name(old_fk_field_name, cast(List[dict], old_fk_fields)) + if old_fk_field.get("db_constraint"): + ref_describe = cast(dict, old_models[old_fk_field["python_type"]]) + sql = cls._drop_fk(model, old_fk_field, ref_describe) + cls._add_operator(sql, upgrade, fk_m2m_index=True) + + @classmethod + def _handle_fk_fields( + cls, + old_model_describe: Dict, + new_model_describe: Dict, + model: Type[Model], + old_models: Dict, + new_models: Dict, + upgrade=True, + ) -> None: + key = "fk_fields" + cls._handle_relational( + key, old_model_describe, new_model_describe, model, old_models, new_models, upgrade + ) + + @classmethod + def _handle_o2o_fields( + cls, + old_model_describe: Dict, + new_model_describe: Dict, + model: Type[Model], + old_models: Dict, + new_models: Dict, + upgrade=True, + ) -> None: + key = "o2o_fields" + cls._handle_relational( + key, old_model_describe, new_model_describe, model, old_models, new_models, upgrade + ) + @classmethod def diff_models( cls, old_models: Dict[str, dict], new_models: Dict[str, dict], upgrade=True @@ -334,6 +396,13 @@ class Migrate: # current only support rename pk if action == "change" and option == "name": cls._add_operator(cls._rename_field(model, *change), upgrade) + # fk fields + args = (old_model_describe, new_model_describe, model, old_models, new_models) + cls._handle_fk_fields(*args, upgrade=upgrade) + # o2o fields + cls._handle_o2o_fields(*args, upgrade=upgrade) + old_o2o_columns = [i["raw_field"] for i in old_model_describe.get("o2o_fields", [])] + new_o2o_columns = [i["raw_field"] for i in new_model_describe.get("o2o_fields", [])] # m2m fields cls._handle_m2m_fields( old_model_describe, new_model_describe, model, new_models, upgrade @@ -424,7 +493,10 @@ class Migrate: ), upgrade, ) - if new_data_field["indexed"]: + if ( + new_data_field["indexed"] + and new_data_field["db_column"] not in new_o2o_columns + ): cls._add_operator( cls._add_index( model, (new_data_field["db_column"],), new_data_field["unique"] @@ -447,7 +519,10 @@ class Migrate: cls._remove_field(model, db_column), upgrade, ) - if old_data_field["indexed"]: + if ( + old_data_field["indexed"] + and old_data_field["db_column"] not in old_o2o_columns + ): is_unique_field = old_data_field.get("unique") cls._add_operator( cls._drop_index(model, {db_column}, is_unique_field), @@ -455,38 +530,6 @@ class Migrate: True, ) - old_fk_fields = cast(List[dict], old_model_describe.get("fk_fields")) - new_fk_fields = cast(List[dict], new_model_describe.get("fk_fields")) - - old_fk_fields_name: List[str] = [i.get("name", "") for i in old_fk_fields] - new_fk_fields_name: List[str] = [i.get("name", "") for i in new_fk_fields] - - # add fk - for new_fk_field_name in set(new_fk_fields_name).difference( - set(old_fk_fields_name) - ): - fk_field = cls.get_field_by_name(new_fk_field_name, new_fk_fields) - if fk_field.get("db_constraint"): - ref_describe = cast(dict, new_models[fk_field["python_type"]]) - cls._add_operator( - cls._add_fk(model, fk_field, ref_describe), - upgrade, - fk_m2m_index=True, - ) - # drop fk - for old_fk_field_name in set(old_fk_fields_name).difference( - set(new_fk_fields_name) - ): - old_fk_field = cls.get_field_by_name( - old_fk_field_name, cast(List[dict], old_fk_fields) - ) - if old_fk_field.get("db_constraint"): - ref_describe = cast(dict, old_models[old_fk_field["python_type"]]) - cls._add_operator( - cls._drop_fk(model, old_fk_field, ref_describe), - upgrade, - fk_m2m_index=True, - ) # change fields for field_name in set(new_data_fields_name).intersection(set(old_data_fields_name)): old_data_field = cls.get_field_by_name(field_name, old_data_fields) diff --git a/tests/models.py b/tests/models.py index e4ea87c..a9e0a15 100644 --- a/tests/models.py +++ b/tests/models.py @@ -40,6 +40,7 @@ class Email(Model): is_primary = fields.BooleanField(default=False) address = fields.CharField(max_length=200) users: fields.ManyToManyRelation[User] = fields.ManyToManyField("models.User") + config: fields.OneToOneRelation["Config"] = fields.OneToOneField("models.Config") def default_name(): @@ -91,6 +92,8 @@ class Config(Model): "models.User", description="User" ) + email: fields.OneToOneRelation["Email"] + class NewModel(Model): name = fields.CharField(max_length=50) diff --git a/tests/test_migrate.py b/tests/test_migrate.py index cab0fa9..7d324db 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -875,6 +875,7 @@ def test_migrate(mocker: MockerFixture): - drop field: User.avatar - add index: Email.email - add many to many: Email.users + - add one to one: Email.config - remove unique: Category.title - add unique: User.username - change column: length User.password @@ -914,6 +915,8 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `config` ALTER COLUMN `status` DROP DEFAULT", "ALTER TABLE `config` MODIFY COLUMN `value` JSON NOT NULL", "ALTER TABLE `email` ADD `address` VARCHAR(200) NOT NULL", + "ALTER TABLE `email` ADD CONSTRAINT `fk_email_config_76a9dc71` FOREIGN KEY (`config_id`) REFERENCES `config` (`id`) ON DELETE CASCADE", + "ALTER TABLE `email` ADD `config_id` INT NOT NULL UNIQUE", "ALTER TABLE `configs` RENAME TO `config`", "ALTER TABLE `product` DROP COLUMN `uuid`", "ALTER TABLE `product` DROP INDEX `uuid`", @@ -954,6 +957,8 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `config` ALTER COLUMN `status` SET DEFAULT 1", "ALTER TABLE `email` ADD `user_id` INT NOT NULL", "ALTER TABLE `email` DROP COLUMN `address`", + "ALTER TABLE `email` DROP COLUMN `config_id`", + "ALTER TABLE `email` DROP FOREIGN KEY `fk_email_config_76a9dc71`", "ALTER TABLE `config` RENAME TO `configs`", "ALTER TABLE `product` RENAME COLUMN `pic` TO `image`", "ALTER TABLE `email` RENAME COLUMN `email_id` TO `id`", @@ -1007,6 +1012,8 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "email" ADD "address" VARCHAR(200) NOT NULL', 'ALTER TABLE "email" RENAME COLUMN "id" TO "email_id"', 'ALTER TABLE "email" ALTER COLUMN "is_primary" TYPE BOOL USING "is_primary"::BOOL', + 'ALTER TABLE "email" ADD CONSTRAINT "fk_email_config_76a9dc71" FOREIGN KEY ("config_id") REFERENCES "config" ("id") ON DELETE CASCADE', + 'ALTER TABLE "email" ADD "config_id" INT NOT NULL UNIQUE', 'DROP INDEX IF EXISTS "uid_product_uuid_d33c18"', 'ALTER TABLE "product" DROP COLUMN "uuid"', 'ALTER TABLE "product" ALTER COLUMN "view_num" SET DEFAULT 0', @@ -1048,6 +1055,8 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "email" DROP COLUMN "address"', 'ALTER TABLE "email" RENAME COLUMN "email_id" TO "id"', 'ALTER TABLE "email" ALTER COLUMN "is_primary" TYPE BOOL USING "is_primary"::BOOL', + 'ALTER TABLE "email" DROP COLUMN "config_id"', + 'ALTER TABLE "email" DROP CONSTRAINT IF EXISTS "fk_email_config_76a9dc71"', 'ALTER TABLE "product" ADD "uuid" INT NOT NULL UNIQUE', 'CREATE UNIQUE INDEX "uid_product_uuid_d33c18" ON "product" ("uuid")', 'ALTER TABLE "product" ALTER COLUMN "view_num" DROP DEFAULT',