Skip to content

Commit a624d1b

Browse files
authored
fix: migrate does not recognise attribute changes for string primary key (#428)
* refactor: show warning for unsupported pk field changes * fix: migrate does not recognise attribute changes for string primary key * docs: update changelog * refactor: reduce indents * chore: update docs
1 parent e299f8e commit a624d1b

File tree

5 files changed

+77
-27
lines changed

5 files changed

+77
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### [0.8.2]**(Unreleased)**
66

77
#### Added
8+
- Support changes `max_length` or int type for primary key field. ([#428])
89
- feat: support psycopg. ([#425])
910
- Support run `poetry add aerich` in project that inited by poetry v2. ([#424])
1011
- feat: support command `python -m aerich`. ([#417])

aerich/migrate.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from aerich.coder import load_index
1818
from aerich.ddl import BaseDDL
19+
from aerich.enums import Color
1920
from aerich.models import MAX_VERSION_LENGTH, Aerich
2021
from aerich.utils import (
2122
get_app_connection,
@@ -424,14 +425,8 @@ def diff_models(
424425
)
425426
old_indexes = cls._get_indexes(model, old_model_describe)
426427
new_indexes = cls._get_indexes(model, new_model_describe)
427-
old_pk_field = old_model_describe.get("pk_field")
428-
new_pk_field = new_model_describe.get("pk_field")
429428
# pk field
430-
changes = diff(old_pk_field, new_pk_field)
431-
for action, option, change in changes:
432-
# current only support rename pk
433-
if action == "change" and option == "name":
434-
cls._add_operator(cls._rename_field(model, *change), upgrade)
429+
cls._handle_pk_field_alter(model, old_model_describe, new_model_describe, upgrade)
435430
# fk fields
436431
args = (old_model_describe, new_model_describe, model, old_models, new_models)
437432
cls._handle_fk_fields(*args, upgrade=upgrade)
@@ -605,6 +600,47 @@ def diff_models(
605600
continue
606601
cls._add_operator(cls.drop_model(old_models[old_model]["table"]), upgrade)
607602

603+
@classmethod
604+
def _handle_pk_field_alter(
605+
cls,
606+
model: type[Model],
607+
old_model_describe: dict[str, dict],
608+
new_model_describe: dict[str, dict],
609+
upgrade: bool,
610+
) -> None:
611+
old_pk_field = old_model_describe.get("pk_field", {})
612+
new_pk_field = new_model_describe.get("pk_field", {})
613+
changes = cls._exclude_extra_field_types(diff(old_pk_field, new_pk_field))
614+
sqls: list[str] = []
615+
for action, option, change in changes:
616+
if action != "change":
617+
continue
618+
if option == "db_column":
619+
# rename pk
620+
sql = cls._rename_field(model, *change)
621+
elif option == "constraints.max_length":
622+
sql = cls._modify_field(model, new_pk_field)
623+
elif option == "field_type":
624+
# Only support change field type between int fields, e.g.: IntField -> BigIntField
625+
if not all(field_type.endswith("IntField") for field_type in change):
626+
if upgrade:
627+
model_name = model._meta.full_name.split(".")[-1]
628+
field_name = new_pk_field.get("name", "")
629+
msg = (
630+
f"Does not support change primary_key({model_name}.{field_name}) field type,"
631+
" you may need to do it manually."
632+
)
633+
click.secho(msg, fg=Color.yellow)
634+
return
635+
sql = cls._modify_field(model, new_pk_field)
636+
else:
637+
# Skip option like 'constraints.ge', 'constraints.le', 'db_field_types.'
638+
continue
639+
sqls.append(sql)
640+
for sql in sorted(sqls, key=lambda x: "RENAME" not in x):
641+
# TODO: alter references field in m2m table
642+
cls._add_operator(sql, upgrade)
643+
608644
@classmethod
609645
def _handle_field_changes(
610646
cls,

tests/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Meta:
7979

8080

8181
class Product(Model):
82+
id = fields.BigIntField(primary_key=True)
8283
categories: fields.ManyToManyRelation[Category] = fields.ManyToManyField(
8384
"models.Category", null=False
8485
)
@@ -106,6 +107,7 @@ class Meta:
106107

107108

108109
class Config(Model):
110+
slug = fields.CharField(primary_key=True, max_length=20)
109111
categories: fields.ManyToManyRelation[Category] = fields.ManyToManyField(
110112
"models.Category", through="config_category_map", related_name="category_set"
111113
)

tests/old_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class Product(Model):
7777

7878

7979
class Config(Model):
80+
slug = fields.CharField(primary_key=True, max_length=10)
8081
category: fields.ManyToManyRelation[Category] = fields.ManyToManyField("models.Category")
8182
categories: fields.ManyToManyRelation[Category] = fields.ManyToManyField(
8283
"models.Category", through="config_category_map", related_name="config_set"

tests/test_migrate.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -187,19 +187,19 @@ def describe_index(idx: Index) -> Index | dict:
187187
"unique_together": [],
188188
"indexes": [],
189189
"pk_field": {
190-
"name": "id",
191-
"field_type": "IntField",
192-
"db_column": "id",
193-
"python_type": "int",
194-
"generated": True,
190+
"name": "slug",
191+
"field_type": "CharField",
192+
"db_column": "slug",
193+
"python_type": "str",
194+
"generated": False,
195195
"nullable": False,
196196
"unique": True,
197197
"indexed": True,
198198
"default": None,
199199
"description": None,
200200
"docstring": None,
201-
"constraints": {"ge": MIN_INT, "le": 2147483647},
202-
"db_field_types": {"": "INT"},
201+
"constraints": {"max_length": 10},
202+
"db_field_types": {"": "VARCHAR(10)"},
203203
},
204204
"data_fields": [
205205
{
@@ -939,6 +939,8 @@ def test_migrate(mocker: MockerFixture):
939939
"""
940940
models.py diff with old_models.py
941941
- change email pk: id -> email_id
942+
- change product pk field type: IntField -> BigIntField
943+
- change config pk field attribute: max_length=10 -> max_length=20
942944
- add field: Email.address
943945
- add fk field: Config.user
944946
- drop fk field: Email.user
@@ -991,8 +993,8 @@ def test_migrate(mocker: MockerFixture):
991993
"ALTER TABLE `config` ADD CONSTRAINT `fk_config_user_17daa970` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE",
992994
"ALTER TABLE `config` ALTER COLUMN `status` DROP DEFAULT",
993995
"ALTER TABLE `email` ADD `address` VARCHAR(200) NOT NULL",
994-
"ALTER TABLE `email` ADD CONSTRAINT `fk_email_config_76a9dc71` FOREIGN KEY (`config_id`) REFERENCES `config` (`id`) ON DELETE CASCADE",
995-
"ALTER TABLE `email` ADD `config_id` INT NOT NULL UNIQUE",
996+
"ALTER TABLE `email` ADD CONSTRAINT `fk_email_config_88e28c1b` FOREIGN KEY (`config_id`) REFERENCES `config` (`slug`) ON DELETE CASCADE",
997+
"ALTER TABLE `email` ADD `config_id` VARCHAR(20) NOT NULL UNIQUE",
996998
"ALTER TABLE `email` DROP INDEX `idx_email_company_1c9234`, ADD UNIQUE (`company`)",
997999
"ALTER TABLE `configs` RENAME TO `config`",
9981000
"ALTER TABLE `product` DROP COLUMN `uuid`",
@@ -1008,15 +1010,17 @@ def test_migrate(mocker: MockerFixture):
10081010
"ALTER TABLE `product` ALTER COLUMN `view_num` SET DEFAULT 0",
10091011
"ALTER TABLE `product` RENAME COLUMN `is_delete` TO `is_deleted`",
10101012
"ALTER TABLE `product` RENAME COLUMN `is_review` TO `is_reviewed`",
1013+
"ALTER TABLE `product` MODIFY COLUMN `id` BIGINT NOT NULL",
10111014
"ALTER TABLE `user` DROP COLUMN `avatar`",
10121015
"ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(100) NOT NULL",
10131016
"ALTER TABLE `user` MODIFY COLUMN `longitude` DECIMAL(10,8) NOT NULL",
10141017
"ALTER TABLE `user` ADD UNIQUE INDEX `username` (`username`)",
10151018
"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",
10161019
"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",
1017-
"CREATE TABLE `product_user` (\n `product_id` INT NOT NULL REFERENCES `product` (`id`) ON DELETE CASCADE,\n `user_id` INT NOT NULL REFERENCES `user` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
1018-
"CREATE TABLE `config_category_map` (\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE,\n `config_id` INT NOT NULL REFERENCES `config` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
1020+
"CREATE TABLE `product_user` (\n `product_id` BIGINT NOT NULL REFERENCES `product` (`id`) ON DELETE CASCADE,\n `user_id` INT NOT NULL REFERENCES `user` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
1021+
"CREATE TABLE `config_category_map` (\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE,\n `config_id` VARCHAR(20) NOT NULL REFERENCES `config` (`slug`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
10191022
"DROP TABLE IF EXISTS `config_category`",
1023+
"ALTER TABLE `config` MODIFY COLUMN `slug` VARCHAR(20) NOT NULL",
10201024
}
10211025
upgrade_operators = set(Migrate.upgrade_operators)
10221026
upgrade_more_than_expected = upgrade_operators - expected_upgrade_operators
@@ -1037,11 +1041,12 @@ def test_migrate(mocker: MockerFixture):
10371041
"ALTER TABLE `config` DROP FOREIGN KEY `fk_config_user_17daa970`",
10381042
"ALTER TABLE `config` ALTER COLUMN `status` SET DEFAULT 1",
10391043
"ALTER TABLE `config` DROP COLUMN `user_id`",
1044+
"ALTER TABLE `config` MODIFY COLUMN `slug` VARCHAR(10) NOT NULL",
10401045
"ALTER TABLE `config` RENAME TO `configs`",
10411046
"ALTER TABLE `email` ADD `user_id` INT NOT NULL",
10421047
"ALTER TABLE `email` DROP COLUMN `address`",
10431048
"ALTER TABLE `email` DROP COLUMN `config_id`",
1044-
"ALTER TABLE `email` DROP FOREIGN KEY `fk_email_config_76a9dc71`",
1049+
"ALTER TABLE `email` DROP FOREIGN KEY `fk_email_config_88e28c1b`",
10451050
"ALTER TABLE `email` RENAME COLUMN `email_id` TO `id`",
10461051
"ALTER TABLE `email` DROP INDEX `company`, ADD INDEX (`idx_email_company_1c9234`)",
10471052
"ALTER TABLE `email` DROP INDEX `idx_email_email_4a1a33`",
@@ -1056,14 +1061,15 @@ def test_migrate(mocker: MockerFixture):
10561061
"ALTER TABLE `product` ALTER COLUMN `view_num` DROP DEFAULT",
10571062
"ALTER TABLE `product` RENAME COLUMN `is_deleted` TO `is_delete`",
10581063
"ALTER TABLE `product` RENAME COLUMN `is_reviewed` TO `is_review`",
1064+
"ALTER TABLE `product` MODIFY COLUMN `id` INT NOT NULL",
10591065
"ALTER TABLE `user` ADD `avatar` VARCHAR(200) NOT NULL DEFAULT ''",
10601066
"ALTER TABLE `user` DROP INDEX `username`",
10611067
"ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(200) NOT NULL",
10621068
"DROP TABLE IF EXISTS `email_user`",
10631069
"DROP TABLE IF EXISTS `newmodel`",
10641070
"DROP TABLE IF EXISTS `product_user`",
10651071
"ALTER TABLE `user` MODIFY COLUMN `longitude` DECIMAL(12,9) NOT NULL",
1066-
"CREATE TABLE `config_category` (\n `config_id` INT NOT NULL REFERENCES `config` (`id`) ON DELETE CASCADE,\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
1072+
"CREATE TABLE `config_category` (\n `config_id` VARCHAR(20) NOT NULL REFERENCES `config` (`slug`) ON DELETE CASCADE,\n `category_id` INT NOT NULL REFERENCES `category` (`id`) ON DELETE CASCADE\n) CHARACTER SET utf8mb4",
10671073
"DROP TABLE IF EXISTS `config_category_map`",
10681074
}
10691075
downgrade_operators = set(Migrate.downgrade_operators)
@@ -1081,17 +1087,18 @@ def test_migrate(mocker: MockerFixture):
10811087
'ALTER TABLE "category" ADD CONSTRAINT "fk_category_user_110d4c63" FOREIGN KEY ("owner_id") REFERENCES "user" ("id") ON DELETE CASCADE',
10821088
'CREATE INDEX IF NOT EXISTS "idx_category_slug_e9bcff" ON "category" USING HASH ("slug")',
10831089
'DROP INDEX IF EXISTS "idx_category_slug_e9bcff"',
1090+
'ALTER TABLE "configs" RENAME TO "config"',
10841091
'ALTER TABLE "config" DROP COLUMN "name"',
10851092
'DROP INDEX IF EXISTS "uid_config_name_2c83c8"',
10861093
'ALTER TABLE "config" ADD "user_id" INT NOT NULL',
10871094
'ALTER TABLE "config" ADD CONSTRAINT "fk_config_user_17daa970" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE',
10881095
'ALTER TABLE "config" ALTER COLUMN "status" DROP DEFAULT',
1089-
'ALTER TABLE "configs" RENAME TO "config"',
1096+
'ALTER TABLE "config" ALTER COLUMN "slug" TYPE VARCHAR(20) USING "slug"::VARCHAR(20)',
1097+
'ALTER TABLE "email" ADD "config_id" VARCHAR(20) NOT NULL UNIQUE',
10901098
'ALTER TABLE "email" ADD "address" VARCHAR(200) NOT NULL',
10911099
'ALTER TABLE "email" RENAME COLUMN "id" TO "email_id"',
10921100
'ALTER TABLE "email" DROP COLUMN "user_id"',
1093-
'ALTER TABLE "email" ADD CONSTRAINT "fk_email_config_76a9dc71" FOREIGN KEY ("config_id") REFERENCES "config" ("id") ON DELETE CASCADE',
1094-
'ALTER TABLE "email" ADD "config_id" INT NOT NULL UNIQUE',
1101+
'ALTER TABLE "email" ADD CONSTRAINT "fk_email_config_88e28c1b" FOREIGN KEY ("config_id") REFERENCES "config" ("slug") ON DELETE CASCADE',
10951102
'DROP INDEX IF EXISTS "idx_email_company_1c9234"',
10961103
'CREATE UNIQUE INDEX IF NOT EXISTS "uid_email_company_1c9234" ON "email" ("company")',
10971104
'DROP INDEX IF EXISTS "uid_product_uuid_d33c18"',
@@ -1102,6 +1109,7 @@ def test_migrate(mocker: MockerFixture):
11021109
'ALTER TABLE "product" RENAME COLUMN "is_delete" TO "is_deleted"',
11031110
'ALTER TABLE "product" ADD "price" DOUBLE PRECISION',
11041111
'ALTER TABLE "product" ADD "no" UUID NOT NULL',
1112+
'ALTER TABLE "product" ALTER COLUMN "id" TYPE BIGINT USING "id"::BIGINT',
11051113
'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(100) USING "password"::VARCHAR(100)',
11061114
'ALTER TABLE "user" DROP COLUMN "avatar"',
11071115
'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(10,8) USING "longitude"::DECIMAL(10,8)',
@@ -1112,8 +1120,8 @@ def test_migrate(mocker: MockerFixture):
11121120
'CREATE TABLE IF NOT EXISTS "newmodel" (\n "id" SERIAL NOT NULL PRIMARY KEY,\n "name" VARCHAR(50) NOT NULL\n);\nCOMMENT ON COLUMN "config"."user_id" IS \'User\'',
11131121
'CREATE UNIQUE INDEX IF NOT EXISTS "uid_product_name_869427" ON "product" ("name", "type_db_alias")',
11141122
'CREATE UNIQUE INDEX IF NOT EXISTS "uid_user_usernam_9987ab" ON "user" ("username")',
1115-
'CREATE TABLE "product_user" (\n "product_id" INT NOT NULL REFERENCES "product" ("id") ON DELETE CASCADE,\n "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE\n)',
1116-
'CREATE TABLE "config_category_map" (\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE,\n "config_id" INT NOT NULL REFERENCES "config" ("id") ON DELETE CASCADE\n)',
1123+
'CREATE TABLE "product_user" (\n "product_id" BIGINT NOT NULL REFERENCES "product" ("id") ON DELETE CASCADE,\n "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE\n)',
1124+
'CREATE TABLE "config_category_map" (\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE,\n "config_id" VARCHAR(20) NOT NULL REFERENCES "config" ("slug") ON DELETE CASCADE\n)',
11171125
'DROP TABLE IF EXISTS "config_category"',
11181126
}
11191127
upgrade_operators = set(Migrate.upgrade_operators)
@@ -1136,11 +1144,12 @@ def test_migrate(mocker: MockerFixture):
11361144
'ALTER TABLE "config" DROP CONSTRAINT IF EXISTS "fk_config_user_17daa970"',
11371145
'ALTER TABLE "config" RENAME TO "configs"',
11381146
'ALTER TABLE "config" DROP COLUMN "user_id"',
1147+
'ALTER TABLE "config" ALTER COLUMN "slug" TYPE VARCHAR(10) USING "slug"::VARCHAR(10)',
11391148
'ALTER TABLE "email" ADD "user_id" INT NOT NULL',
11401149
'ALTER TABLE "email" DROP COLUMN "address"',
11411150
'ALTER TABLE "email" RENAME COLUMN "email_id" TO "id"',
11421151
'ALTER TABLE "email" DROP COLUMN "config_id"',
1143-
'ALTER TABLE "email" DROP CONSTRAINT IF EXISTS "fk_email_config_76a9dc71"',
1152+
'ALTER TABLE "email" DROP CONSTRAINT IF EXISTS "fk_email_config_88e28c1b"',
11441153
'CREATE INDEX IF NOT EXISTS "idx_email_company_1c9234" ON "email" ("company")',
11451154
'DROP INDEX IF EXISTS "uid_email_company_1c9234"',
11461155
'ALTER TABLE "product" ADD "uuid" INT NOT NULL UNIQUE',
@@ -1151,6 +1160,7 @@ def test_migrate(mocker: MockerFixture):
11511160
'ALTER TABLE "product" RENAME COLUMN "is_reviewed" TO "is_review"',
11521161
'ALTER TABLE "product" DROP COLUMN "price"',
11531162
'ALTER TABLE "product" DROP COLUMN "no"',
1163+
'ALTER TABLE "product" ALTER COLUMN "id" TYPE INT USING "id"::INT',
11541164
'ALTER TABLE "user" ADD "avatar" VARCHAR(200) NOT NULL DEFAULT \'\'',
11551165
'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(200) USING "password"::VARCHAR(200)',
11561166
'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(12,9) USING "longitude"::DECIMAL(12,9)',
@@ -1162,7 +1172,7 @@ def test_migrate(mocker: MockerFixture):
11621172
'DROP INDEX IF EXISTS "idx_product_no_e4d701"',
11631173
'DROP TABLE IF EXISTS "email_user"',
11641174
'DROP TABLE IF EXISTS "newmodel"',
1165-
'CREATE TABLE "config_category" (\n "config_id" INT NOT NULL REFERENCES "config" ("id") ON DELETE CASCADE,\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE\n)',
1175+
'CREATE TABLE "config_category" (\n "config_id" VARCHAR(20) NOT NULL REFERENCES "config" ("slug") ON DELETE CASCADE,\n "category_id" INT NOT NULL REFERENCES "category" ("id") ON DELETE CASCADE\n)',
11661176
'DROP TABLE IF EXISTS "config_category_map"',
11671177
}
11681178
downgrade_operators = set(Migrate.downgrade_operators)

0 commit comments

Comments
 (0)