Store versions in db

This commit is contained in:
long2ice 2020-05-21 21:22:06 +08:00
parent ea1191bb10
commit aa921355b9
7 changed files with 86 additions and 75 deletions

View File

@ -8,6 +8,7 @@ ChangeLog
----- -----
- Add sqlite and postgres support. - Add sqlite and postgres support.
- Fix dependency import. - Fix dependency import.
- Store versions in db.
0.1.4 0.1.4
----- -----

View File

@ -54,6 +54,19 @@ Quick Start
Usage Usage
===== =====
You need add ``aerich.models`` to your ``Tortoise-ORM`` config first, example:
.. code-block:: python
TORTOISE_ORM = {
"connections": {"default": "mysql://root:123456@127.0.0.1:3306/test"},
"apps": {
"models": {
"models": ["tests.models", "aerich.models"],
"default_connection": "default",
},
},
}
Initialization Initialization
-------------- --------------
@ -100,7 +113,7 @@ Update models and make migrate
Success migrate 1_202029051520102929_drop_column.json Success migrate 1_202029051520102929_drop_column.json
Format of migrate filename is ``{version}_{datetime}_{name|update}.json`` Format of migrate filename is ``{version_num}_{datetime}_{name|update}.json``
Upgrade to latest version Upgrade to latest version
------------------------- -------------------------

View File

@ -13,6 +13,7 @@ from aerich.migrate import Migrate
from aerich.utils import get_app_connection, get_app_connection_name, get_tortoise_config from aerich.utils import get_app_connection, get_app_connection_name, get_tortoise_config
from . import __version__ from . import __version__
from .models import Aerich
class Color(str, Enum): class Color(str, Enum):
@ -83,24 +84,23 @@ async def migrate(ctx: Context, name):
@cli.command(help="Upgrade to latest version.") @cli.command(help="Upgrade to latest version.")
@click.pass_context @click.pass_context
async def upgrade(ctx: Context): async def upgrade(ctx: Context):
app = ctx.obj["app"]
config = ctx.obj["config"] config = ctx.obj["config"]
available_versions = Migrate.get_all_version_files(is_all=False) app = ctx.obj["app"]
if not available_versions: migrated = False
return click.secho("No migrate items", fg=Color.yellow) for version in Migrate.get_all_version_files():
if not await Aerich.exists(version=version, app=app):
async with in_transaction(get_app_connection_name(config, app)) as conn: async with in_transaction(get_app_connection_name(config, app)) as conn:
for file in available_versions: file_path = os.path.join(Migrate.migrate_location, version)
file_path = os.path.join(Migrate.migrate_location, file)
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = json.load(f) content = json.load(f)
upgrade_query_list = content.get("upgrade") upgrade_query_list = content.get("upgrade")
for upgrade_query in upgrade_query_list: for upgrade_query in upgrade_query_list:
await conn.execute_query(upgrade_query) await conn.execute_query(upgrade_query)
await Aerich.create(version=version, app=app)
with open(file_path, "w") as f: click.secho(f"Success upgrade {version}", fg=Color.green)
content["migrate"] = True migrated = True
json.dump(content, f, indent=2, ensure_ascii=False) if not migrated:
click.secho(f"Success upgrade {file}", fg=Color.green) click.secho("No migrate items", fg=Color.yellow)
@cli.command(help="Downgrade to previous version.") @cli.command(help="Downgrade to previous version.")
@ -108,39 +108,43 @@ async def upgrade(ctx: Context):
async def downgrade(ctx: Context): async def downgrade(ctx: Context):
app = ctx.obj["app"] app = ctx.obj["app"]
config = ctx.obj["config"] config = ctx.obj["config"]
available_versions = Migrate.get_all_version_files() last_version = await Migrate.get_last_version()
if not available_versions: if not last_version:
return click.secho("No migrate items", fg=Color.yellow) return click.secho("No last version found", fg=Color.yellow)
file = last_version.version
async with in_transaction(get_app_connection_name(config, app)) as conn: async with in_transaction(get_app_connection_name(config, app)) as conn:
for file in reversed(available_versions):
file_path = os.path.join(Migrate.migrate_location, file) file_path = os.path.join(Migrate.migrate_location, file)
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = json.load(f) content = json.load(f)
if content.get("migrate"):
downgrade_query_list = content.get("downgrade") downgrade_query_list = content.get("downgrade")
for downgrade_query in downgrade_query_list: for downgrade_query in downgrade_query_list:
await conn.execute_query(downgrade_query) await conn.execute_query(downgrade_query)
else: await last_version.delete()
continue
with open(file_path, "w") as f:
content["migrate"] = False
json.dump(content, f, indent=2, ensure_ascii=False)
return click.secho(f"Success downgrade {file}", fg=Color.green) return click.secho(f"Success downgrade {file}", fg=Color.green)
@cli.command(help="Show current available heads in migrate location.") @cli.command(help="Show current available heads in migrate location.")
@click.pass_context @click.pass_context
def heads(ctx: Context): async def heads(ctx: Context):
for version in Migrate.get_all_version_files(is_all=False): app = ctx.obj["app"]
versions = Migrate.get_all_version_files()
is_heads = False
for version in versions:
if not await Aerich.exists(version=version, app=app):
click.secho(version, fg=Color.green) click.secho(version, fg=Color.green)
is_heads = True
if not is_heads:
click.secho("No available heads,try migrate", fg=Color.green)
@cli.command(help="List all migrate items.") @cli.command(help="List all migrate items.")
@click.pass_context @click.pass_context
def history(ctx): def history(ctx):
for version in Migrate.get_all_version_files(): versions = Migrate.get_all_version_files()
for version in versions:
click.secho(version, fg=Color.green) click.secho(version, fg=Color.green)
if not versions:
click.secho("No history,try migrate", fg=Color.green)
@cli.command(help="Init config file and generate root migrate location.") @cli.command(help="Init config file and generate root migrate location.")

View File

@ -3,7 +3,7 @@ import os
import re import re
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from typing import Dict, List, Type from typing import Dict, List, Tuple, Type
from tortoise import ( from tortoise import (
BackwardFKRelation, BackwardFKRelation,
@ -16,7 +16,6 @@ from tortoise import (
from tortoise.fields import Field from tortoise.fields import Field
from aerich.ddl import BaseDDL from aerich.ddl import BaseDDL
from aerich.exceptions import ConfigurationError
from aerich.models import Aerich from aerich.models import Aerich
from aerich.utils import get_app_connection from aerich.utils import get_app_connection
@ -41,26 +40,12 @@ class Migrate:
return cls.old_models + ".py" return cls.old_models + ".py"
@classmethod @classmethod
def _get_all_migrate_files(cls): def get_all_version_files(cls) -> List[str]:
return sorted(filter(lambda x: x.endswith("json"), os.listdir(cls.migrate_location))) return sorted(filter(lambda x: x.endswith("json"), os.listdir(cls.migrate_location)))
@classmethod @classmethod
def _get_latest_version(cls) -> int: async def get_last_version(cls) -> Aerich:
ret = cls._get_all_migrate_files() return await Aerich.filter(app=cls.app).first()
if ret:
return int(ret[-1].split("_")[0])
return 0
@classmethod
def get_all_version_files(cls, is_all=True):
files = cls._get_all_migrate_files()
ret = []
for file in files:
with open(os.path.join(cls.migrate_location, file), "r") as f:
content = json.load(f)
if is_all or not content.get("migrate"):
ret.append(file)
return ret
@classmethod @classmethod
async def init_with_old_models(cls, config: dict, app: str, location: str): async def init_with_old_models(cls, config: dict, app: str, location: str):
@ -88,22 +73,29 @@ class Migrate:
else: else:
raise NotImplementedError("Current only support MySQL") raise NotImplementedError("Current only support MySQL")
@classmethod
async def _get_last_version_num(cls):
last_version = await cls.get_last_version()
if not last_version:
return 0
version = last_version.version
return version.split("_")[0]
@classmethod @classmethod
async def _generate_diff_sql(cls, name): async def _generate_diff_sql(cls, name):
now = datetime.now().strftime("%Y%M%D%H%M%S").replace("/", "") now = datetime.now().strftime("%Y%M%D%H%M%S").replace("/", "")
version = f"{cls._get_latest_version() + 1}_{now}_{name}" last_version_num = await cls._get_last_version_num()
filename = f"{version}.json" version = f"{last_version_num + 1}_{now}_{name}.json"
content = { content = {
"upgrade": cls.upgrade_operators, "upgrade": cls.upgrade_operators,
"downgrade": cls.downgrade_operators, "downgrade": cls.downgrade_operators,
} }
with open(os.path.join(cls.migrate_location, filename), "w") as f: with open(os.path.join(cls.migrate_location, version), "w") as f:
json.dump(content, f, indent=2, ensure_ascii=False) json.dump(content, f, indent=2, ensure_ascii=False)
await Aerich.create(version=version) return version
return filename
@classmethod @classmethod
async def migrate(cls, name): async def migrate(cls, name) -> str:
""" """
diff old models and new models to generate diff content diff old models and new models to generate diff content
:param name: :param name:
@ -119,7 +111,7 @@ class Migrate:
cls._merge_operators() cls._merge_operators()
if not cls.upgrade_operators: if not cls.upgrade_operators:
return False return ""
return await cls._generate_diff_sql(name) return await cls._generate_diff_sql(name)
@ -260,7 +252,7 @@ class Migrate:
): ):
cls._add_operator( cls._add_operator(
cls._remove_index( cls._remove_index(
old_model, [old_field.model_field_name], old_field.unique old_model, (old_field.model_field_name,), old_field.unique
), ),
upgrade, upgrade,
isinstance(old_field, (ForeignKeyFieldInstance, ManyToManyFieldInstance)), isinstance(old_field, (ForeignKeyFieldInstance, ManyToManyFieldInstance)),
@ -269,7 +261,7 @@ class Migrate:
new_field.unique and not old_field.unique new_field.unique and not old_field.unique
): ):
cls._add_operator( cls._add_operator(
cls._add_index(new_model, [new_field.model_field_name], new_field.unique), cls._add_index(new_model, (new_field.model_field_name,), new_field.unique),
upgrade, upgrade,
isinstance(new_field, (ForeignKeyFieldInstance, ManyToManyFieldInstance)), isinstance(new_field, (ForeignKeyFieldInstance, ManyToManyFieldInstance)),
) )
@ -299,7 +291,7 @@ class Migrate:
cls._add_operator(cls._remove_index(old_model, old_unique, unique=True), upgrade) cls._add_operator(cls._remove_index(old_model, old_unique, unique=True), upgrade)
@classmethod @classmethod
def _resolve_fk_fields_name(cls, model: Type[Model], fields_name: List[str]): def _resolve_fk_fields_name(cls, model: Type[Model], fields_name: Tuple[str]):
ret = [] ret = []
for field_name in fields_name: for field_name in fields_name:
if field_name in model._meta.fk_fields: if field_name in model._meta.fk_fields:
@ -309,12 +301,12 @@ class Migrate:
return ret return ret
@classmethod @classmethod
def _remove_index(cls, model: Type[Model], fields_name: List[str], unique=False): def _remove_index(cls, model: Type[Model], fields_name: Tuple[str], unique=False):
fields_name = cls._resolve_fk_fields_name(model, fields_name) fields_name = cls._resolve_fk_fields_name(model, fields_name)
return cls.ddl.drop_index(model, fields_name, unique) return cls.ddl.drop_index(model, fields_name, unique)
@classmethod @classmethod
def _add_index(cls, model: Type[Model], fields_name: List[str], unique=False): def _add_index(cls, model: Type[Model], fields_name: Tuple[str], unique=False):
fields_name = cls._resolve_fk_fields_name(model, fields_name) fields_name = cls._resolve_fk_fields_name(model, fields_name)
return cls.ddl.add_index(model, fields_name, unique) return cls.ddl.add_index(model, fields_name, unique)

View File

@ -3,6 +3,7 @@ from tortoise import Model, fields
class Aerich(Model): class Aerich(Model):
version = fields.CharField(max_length=50) version = fields.CharField(max_length=50)
app = fields.CharField(max_length=20)
class Meta: class Meta:
ordering = ["-id"] ordering = ["-id"]

View File

@ -1,10 +1,10 @@
import importlib import importlib
from asyncclick import BadOptionUsage, Context from asyncclick import BadOptionUsage, Context
from tortoise import Tortoise from tortoise import BaseDBAsyncClient, Tortoise
def get_app_connection_name(config, app): def get_app_connection_name(config, app) -> str:
""" """
get connection name get connection name
:param config: :param config:
@ -14,7 +14,7 @@ def get_app_connection_name(config, app):
return config.get("apps").get(app).get("default_connection") return config.get("apps").get(app).get("default_connection")
def get_app_connection(config, app): def get_app_connection(config, app) -> BaseDBAsyncClient:
""" """
get connection name get connection name
:param config: :param config:

View File

@ -1,6 +1,6 @@
TORTOISE_ORM = { TORTOISE_ORM = {
"connections": {"default": "mysql://root:123456@127.0.0.1:3306/test"}, "connections": {"default": "mysql://root:123456@127.0.0.1:3306/test"},
"apps": { "apps": {
"models": {"models": ["tests.models", "aerich.models"], "default_connection": "default",}, "models": {"models": ["tests.models", "aerich.models"], "default_connection": "default"}
}, },
} }