Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: migration with duplicate renaming of columns in some cases #395

Merged
merged 13 commits into from
Dec 27, 2024
54 changes: 37 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,46 @@

## 0.8

### [0.8.1](Unreleased)
### [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)
- KeyError raised when removing or renaming an existing model (#386)
- fix: error when there is `__init__.py` in the migration folder (#272)
- Setting null=false on m2m field causes migration to fail. (#334)
- Fix NonExistentKey when running `aerich init` without `[tool]` section in config file. (#284)
- Fix configuration file reading error when containing Chinese characters. (#286)
- sqlite: failed to create/drop index. (#302)
- PostgreSQL: Cannot drop constraint after deleting or rename FK on a model. (#378)
- Fix create/drop indexes in every migration. (#377)
- Sort m2m fields before comparing them with diff. (#271)
- fix: add o2o field does not create constraint when migrating. ([#396])
- Migration with duplicate renaming of columns in some cases. ([#395])
- 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])
- KeyError raised when removing or renaming an existing model. ([#386])
- fix: error when there is `__init__.py` in the migration folder. ([#272])
- Setting null=false on m2m field causes migration to fail. ([#334])
- Fix NonExistentKey when running `aerich init` without `[tool]` section in config file. ([#284])
- Fix configuration file reading error when containing Chinese characters. ([#286])
- sqlite: failed to create/drop index. ([#302])
- PostgreSQL: Cannot drop constraint after deleting or rename FK on a model. ([#378])
- Fix create/drop indexes in every migration. ([#377])
- Sort m2m fields before comparing them with diff. ([#271])

#### Changed
- Allow run `aerich init-db` with empty migration directories instead of abort with warnings. (#286)
- Add version constraint(>=0.21) for tortoise-orm. (#388)
- Move `tomlkit` to optional and support `pip install aerich[toml]`. (#392)
- Allow run `aerich init-db` with empty migration directories instead of abort with warnings. ([#286])
- Add version constraint(>=0.21) for tortoise-orm. ([#388])
- Move `tomlkit` to optional and support `pip install aerich[toml]`. ([#392])

[#396]: https://github.com/tortoise/aerich/pull/396
[#395]: https://github.com/tortoise/aerich/pull/395
[#394]: https://github.com/tortoise/aerich/pull/394
[#393]: https://github.com/tortoise/aerich/pull/393
[#376]: https://github.com/tortoise/aerich/pull/376
[#386]: https://github.com/tortoise/aerich/pull/386
[#272]: https://github.com/tortoise/aerich/pull/272
[#334]: https://github.com/tortoise/aerich/pull/334
[#284]: https://github.com/tortoise/aerich/pull/284
[#286]: https://github.com/tortoise/aerich/pull/286
[#302]: https://github.com/tortoise/aerich/pull/302
[#378]: https://github.com/tortoise/aerich/pull/378
[#377]: https://github.com/tortoise/aerich/pull/377
[#271]: https://github.com/tortoise/aerich/pull/271
[#286]: https://github.com/tortoise/aerich/pull/286
[#388]: https://github.com/tortoise/aerich/pull/388
[#392]: https://github.com/tortoise/aerich/pull/392

### [0.8.0](../../releases/tag/v0.8.0) - 2024-12-04

Expand All @@ -31,6 +50,7 @@
- Correct the click import. (#360)
- Improve CLI help text and output. (#355)
- Fix mysql drop unique index raises OperationalError. (#346)

**Upgrade note:**
1. Use column name as unique key name for mysql
2. Drop support for Python3.7
Expand Down
66 changes: 46 additions & 20 deletions aerich/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ class Migrate:
_upgrade_m2m: List[str] = []
_downgrade_m2m: List[str] = []
_aerich = Aerich.__name__
_rename_old: List[str] = []
_rename_new: List[str] = []
_rename_fields: Dict[str, Dict[str, str]] = {} # {'model': {'old_field': 'new_field'}}
waketzheng marked this conversation as resolved.
Show resolved Hide resolved

ddl: BaseDDL
ddl_class: Type[BaseDDL]
Expand Down Expand Up @@ -363,6 +362,7 @@ def diff_models(
_aerich = f"{cls.app}.{cls._aerich}"
old_models.pop(_aerich, None)
new_models.pop(_aerich, None)
models_with_rename_field: Set[str] = set() # models that trigger the click.prompt

for new_model_str, new_model_describe in new_models.items():
model = cls._get_model(new_model_describe["name"].split(".")[1])
Expand Down Expand Up @@ -447,33 +447,63 @@ def diff_models(
):
new_data_field = cls.get_field_by_name(new_data_field_name, new_data_fields)
is_rename = False
for old_data_field in old_data_fields:
field_type = new_data_field.get("field_type")
db_column = new_data_field.get("db_column")
new_name = set(new_data_field_name)
for old_data_field in sorted(
old_data_fields,
key=lambda f: (
f.get("field_type") != field_type,
# old field whose name have more same characters with new field's
# should be put in front of the other
len(new_name.symmetric_difference(set(f.get("name", "")))),
),
):
changes = list(diff(old_data_field, new_data_field))
old_data_field_name = cast(str, old_data_field.get("name"))
if len(changes) == 2:
# rename field
name_diff = (old_data_field_name, new_data_field_name)
column_diff = (
old_data_field.get("db_column"),
new_data_field.get("db_column"),
)
column_diff = (old_data_field.get("db_column"), db_column)
if (
changes[0] == ("change", "name", name_diff)
and changes[1] == ("change", "db_column", column_diff)
and old_data_field_name not in new_data_fields_name
):
if upgrade:
if (
rename_fields := cls._rename_fields.get(new_model_str)
) and (
old_data_field_name in rename_fields
or new_data_field_name in rename_fields.values()
):
continue
prefix = f"({new_model_str}) "
if new_model_str not in models_with_rename_field:
if models_with_rename_field:
# When there are multi rename fields with different models,
# print a empty line to warn that is another model
prefix = "\n" + prefix
models_with_rename_field.add(new_model_str)
is_rename = click.prompt(
f"Rename {old_data_field_name} to {new_data_field_name}?",
f"{prefix}Rename {old_data_field_name} to {new_data_field_name}?",
default=True,
type=bool,
show_choices=True,
)
if is_rename:
if rename_fields is None:
rename_fields = cls._rename_fields[new_model_str] = {}
rename_fields[old_data_field_name] = new_data_field_name
else:
is_rename = old_data_field_name in cls._rename_new
is_rename = False
if rename_to := cls._rename_fields.get(new_model_str, {}).get(
new_data_field_name
):
is_rename = True
if rename_to != old_data_field_name:
continue
if is_rename:
cls._rename_new.append(new_data_field_name)
cls._rename_old.append(old_data_field_name)
# only MySQL8+ has rename syntax
if (
cls.dialect == "mysql"
Expand All @@ -492,13 +522,7 @@ def diff_models(
upgrade,
)
if not is_rename:
cls._add_operator(
cls._add_field(
model,
new_data_field,
),
upgrade,
)
cls._add_operator(cls._add_field(model, new_data_field), upgrade)
if (
new_data_field["indexed"]
and new_data_field["db_column"] not in new_o2o_columns
Expand All @@ -511,12 +535,14 @@ def diff_models(
True,
)
# remove fields
rename_fields = cls._rename_fields.get(new_model_str)
for old_data_field_name in set(old_data_fields_name).difference(
set(new_data_fields_name)
):
# don't remove field if is renamed
if (upgrade and old_data_field_name in cls._rename_old) or (
not upgrade and old_data_field_name in cls._rename_new
if rename_fields and (
(upgrade and old_data_field_name in rename_fields)
or (not upgrade and old_data_field_name in rename_fields.values())
):
continue
old_data_field = cls.get_field_by_name(old_data_field_name, old_data_fields)
Expand Down
1 change: 1 addition & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class Product(Model):
pic = fields.CharField(max_length=200)
body = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
is_deleted = fields.BooleanField(default=False)

class Meta:
unique_together = (("name", "type"),)
Expand Down
3 changes: 2 additions & 1 deletion tests/old_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ class Product(Model):
name = fields.CharField(max_length=50)
view_num = fields.IntField(description="View Num")
sort = fields.IntField()
is_reviewed = fields.BooleanField(description="Is Reviewed")
is_review = fields.BooleanField(description="Is Reviewed")
type = fields.IntEnumField(
ProductType, description="Product Type", source_field="type_db_alias"
)
image = fields.CharField(max_length=200)
body = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
is_delete = fields.BooleanField(default=False)


class Config(Model):
Expand Down
Loading
Loading