diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc3bdb..72a65ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/aerich/migrate.py b/aerich/migrate.py index 2adbac9..720e51f 100644 --- a/aerich/migrate.py +++ b/aerich/migrate.py @@ -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'}} ddl: BaseDDL ddl_class: Type[BaseDDL] @@ -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]) @@ -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" @@ -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 @@ -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) diff --git a/tests/models.py b/tests/models.py index 1316f84..527af12 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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"),) diff --git a/tests/old_models.py b/tests/old_models.py index 5e73020..92eb7f8 100644 --- a/tests/old_models.py +++ b/tests/old_models.py @@ -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): diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 8e6a8fa..9e85a2e 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -368,7 +368,12 @@ "description": None, "docstring": None, "constraints": {}, - "db_field_types": {"": "BOOL", "sqlite": "INT"}, + "db_field_types": { + "": "BOOL", + "mssql": "BIT", + "oracle": "NUMBER(1)", + "sqlite": "INT", + }, }, { "name": "user_id", @@ -494,9 +499,9 @@ "db_field_types": {"": "INT"}, }, { - "name": "is_reviewed", + "name": "is_review", "field_type": "BooleanField", - "db_column": "is_reviewed", + "db_column": "is_review", "python_type": "bool", "generated": False, "nullable": False, @@ -506,7 +511,12 @@ "description": "Is Reviewed", "docstring": None, "constraints": {}, - "db_field_types": {"": "BOOL", "sqlite": "INT"}, + "db_field_types": { + "": "BOOL", + "mssql": "BIT", + "oracle": "NUMBER(1)", + "sqlite": "INT", + }, }, { "name": "type", @@ -574,6 +584,26 @@ "auto_now_add": True, "auto_now": False, }, + { + "name": "is_delete", + "field_type": "BooleanField", + "db_column": "is_delete", + "python_type": "bool", + "generated": False, + "nullable": False, + "unique": False, + "indexed": False, + "default": False, + "description": None, + "docstring": None, + "constraints": {}, + "db_field_types": { + "": "BOOL", + "mssql": "BIT", + "oracle": "NUMBER(1)", + "sqlite": "INT", + }, + }, ], "fk_fields": [], "backward_fk_fields": [], @@ -691,7 +721,12 @@ "description": "Is Active", "docstring": None, "constraints": {}, - "db_field_types": {"": "BOOL", "sqlite": "INT"}, + "db_field_types": { + "": "BOOL", + "mssql": "BIT", + "oracle": "NUMBER(1)", + "sqlite": "INT", + }, }, { "name": "is_superuser", @@ -706,7 +741,12 @@ "description": "Is SuperUser", "docstring": None, "constraints": {}, - "db_field_types": {"": "BOOL", "sqlite": "INT"}, + "db_field_types": { + "": "BOOL", + "mssql": "BIT", + "oracle": "NUMBER(1)", + "sqlite": "INT", + }, }, { "name": "avatar", @@ -872,8 +912,8 @@ def test_migrate(mocker: MockerFixture): models.py diff with old_models.py - change email pk: id -> email_id - add field: Email.address - - add fk: Config.user - - drop fk: Email.user + - add fk field: Config.user + - drop fk field: Email.user - drop field: User.avatar - add index: Email.email - add many to many: Email.users @@ -886,9 +926,11 @@ def test_migrate(mocker: MockerFixture): - drop unique field: Config.name - alter default: Config.status - rename column: Product.image -> Product.pic + - rename column: Product.is_review -> Product.is_reviewed + - rename column: Product.is_delete -> Product.is_deleted - rename fk column: Category.user -> Category.owner """ - mocker.patch("asyncclick.prompt", side_effect=(True, True)) + mocker.patch("asyncclick.prompt", side_effect=(True, True, True, True)) models_describe = get_models_describe("models") Migrate.app = "models" @@ -910,6 +952,7 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `category` DROP INDEX `title`", "ALTER TABLE `category` RENAME COLUMN `user_id` TO `owner_id`", "ALTER TABLE `category` ADD CONSTRAINT `fk_category_user_110d4c63` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE CASCADE", + "ALTER TABLE `email` DROP COLUMN `user_id`", "ALTER TABLE `config` DROP COLUMN `name`", "ALTER TABLE `config` DROP INDEX `name`", "ALTER TABLE `config` ADD `user_id` INT NOT NULL COMMENT 'User'", @@ -929,20 +972,18 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `product` ADD UNIQUE INDEX `uid_product_name_869427` (`name`, `type_db_alias`)", "ALTER TABLE `product` ALTER COLUMN `view_num` SET DEFAULT 0", "ALTER TABLE `product` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", - "ALTER TABLE `product` MODIFY COLUMN `is_reviewed` BOOL NOT NULL COMMENT 'Is Reviewed'", + "ALTER TABLE `product` RENAME COLUMN `is_delete` TO `is_deleted`", + "ALTER TABLE `product` RENAME COLUMN `is_review` TO `is_reviewed`", "ALTER TABLE `user` DROP COLUMN `avatar`", "ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(100) NOT NULL", "ALTER TABLE `user` MODIFY COLUMN `intro` LONGTEXT NOT NULL", "ALTER TABLE `user` MODIFY COLUMN `last_login` DATETIME(6) NOT NULL COMMENT 'Last Login'", - "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 `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)", "ALTER TABLE `product` MODIFY COLUMN `body` LONGTEXT NOT NULL", - "ALTER TABLE `email` MODIFY COLUMN `is_primary` BOOL NOT NULL DEFAULT 0", "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", "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", "DROP TABLE IF EXISTS `config_category`", @@ -958,6 +999,7 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `config` DROP FOREIGN KEY `fk_config_user_17daa970`", "ALTER TABLE `config` ALTER COLUMN `status` SET DEFAULT 1", "ALTER TABLE `email` ADD `user_id` INT NOT NULL", + "ALTER TABLE `config` DROP COLUMN `user_id`", "ALTER TABLE `email` DROP COLUMN `address`", "ALTER TABLE `email` DROP COLUMN `config_id`", "ALTER TABLE `email` DROP FOREIGN KEY `fk_email_config_76a9dc71`", @@ -970,6 +1012,8 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `email` DROP INDEX `idx_email_email_4a1a33`", "ALTER TABLE `product` DROP INDEX `uid_product_name_869427`", "ALTER TABLE `product` ALTER COLUMN `view_num` DROP DEFAULT", + "ALTER TABLE `product` RENAME COLUMN `is_deleted` TO `is_delete`", + "ALTER TABLE `product` RENAME COLUMN `is_reviewed` TO `is_review`", "ALTER TABLE `user` ADD `avatar` VARCHAR(200) NOT NULL DEFAULT ''", "ALTER TABLE `user` DROP INDEX `username`", "ALTER TABLE `user` MODIFY COLUMN `password` VARCHAR(200) NOT NULL", @@ -980,13 +1024,9 @@ def test_migrate(mocker: MockerFixture): "ALTER TABLE `config` MODIFY COLUMN `value` TEXT NOT NULL", "ALTER TABLE `category` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", "ALTER TABLE `product` MODIFY COLUMN `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)", - "ALTER TABLE `product` MODIFY COLUMN `is_reviewed` BOOL NOT NULL COMMENT 'Is Reviewed'", "ALTER TABLE `user` MODIFY COLUMN `last_login` DATETIME(6) NOT NULL COMMENT 'Last Login'", - "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(12,9) NOT NULL", "ALTER TABLE `product` MODIFY COLUMN `body` LONGTEXT NOT NULL", - "ALTER TABLE `email` MODIFY COLUMN `is_primary` BOOL NOT NULL DEFAULT 0", "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", "DROP TABLE IF EXISTS `config_category_map`", } @@ -1013,22 +1053,21 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "configs" RENAME TO "config"', '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" DROP COLUMN "user_id"', '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', 'ALTER TABLE "product" RENAME COLUMN "image" TO "pic"', - 'ALTER TABLE "product" ALTER COLUMN "is_reviewed" TYPE BOOL USING "is_reviewed"::BOOL', 'ALTER TABLE "product" ALTER COLUMN "body" TYPE TEXT USING "body"::TEXT', 'ALTER TABLE "product" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', + 'ALTER TABLE "product" RENAME COLUMN "is_review" TO "is_reviewed"', + 'ALTER TABLE "product" RENAME COLUMN "is_delete" TO "is_deleted"', 'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(100) USING "password"::VARCHAR(100)', 'ALTER TABLE "user" DROP COLUMN "avatar"', - 'ALTER TABLE "user" ALTER COLUMN "is_superuser" TYPE BOOL USING "is_superuser"::BOOL', 'ALTER TABLE "user" ALTER COLUMN "last_login" TYPE TIMESTAMPTZ USING "last_login"::TIMESTAMPTZ', 'ALTER TABLE "user" ALTER COLUMN "intro" TYPE TEXT USING "intro"::TEXT', - 'ALTER TABLE "user" ALTER COLUMN "is_active" TYPE BOOL USING "is_active"::BOOL', 'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(10,8) USING "longitude"::DECIMAL(10,8)', 'CREATE INDEX "idx_product_name_869427" ON "product" ("name", "type_db_alias")', 'CREATE INDEX "idx_email_email_4a1a33" ON "email" ("email")', @@ -1053,25 +1092,24 @@ def test_migrate(mocker: MockerFixture): 'ALTER TABLE "config" DROP CONSTRAINT IF EXISTS "fk_config_user_17daa970"', 'ALTER TABLE "config" RENAME TO "configs"', 'ALTER TABLE "config" ALTER COLUMN "value" TYPE JSONB USING "value"::JSONB', + 'ALTER TABLE "config" DROP COLUMN "user_id"', 'ALTER TABLE "email" ADD "user_id" INT NOT NULL', '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', 'ALTER TABLE "product" RENAME COLUMN "pic" TO "image"', + 'ALTER TABLE "product" RENAME COLUMN "is_deleted" TO "is_delete"', + 'ALTER TABLE "product" RENAME COLUMN "is_reviewed" TO "is_review"', 'ALTER TABLE "user" ADD "avatar" VARCHAR(200) NOT NULL DEFAULT \'\'', 'ALTER TABLE "user" ALTER COLUMN "password" TYPE VARCHAR(200) USING "password"::VARCHAR(200)', 'ALTER TABLE "user" ALTER COLUMN "last_login" TYPE TIMESTAMPTZ USING "last_login"::TIMESTAMPTZ', - 'ALTER TABLE "user" ALTER COLUMN "is_superuser" TYPE BOOL USING "is_superuser"::BOOL', - 'ALTER TABLE "user" ALTER COLUMN "is_active" TYPE BOOL USING "is_active"::BOOL', 'ALTER TABLE "user" ALTER COLUMN "intro" TYPE TEXT USING "intro"::TEXT', 'ALTER TABLE "user" ALTER COLUMN "longitude" TYPE DECIMAL(12,9) USING "longitude"::DECIMAL(12,9)', 'ALTER TABLE "product" ALTER COLUMN "created_at" TYPE TIMESTAMPTZ USING "created_at"::TIMESTAMPTZ', - 'ALTER TABLE "product" ALTER COLUMN "is_reviewed" TYPE BOOL USING "is_reviewed"::BOOL', 'ALTER TABLE "product" ALTER COLUMN "body" TYPE TEXT USING "body"::TEXT', 'DROP TABLE IF EXISTS "product_user"', 'DROP INDEX IF EXISTS "idx_product_name_869427"',