14 Commits

Author SHA1 Message Date
long2ice
f5dff84476 Fix encoding error. (#75) 2020-11-08 23:00:44 +08:00
long2ice
e399821116 update deps 2020-11-05 17:43:41 +08:00
long2ice
648f25a951 Compatible with models file in directory. (#70) 2020-10-30 19:51:46 +08:00
long2ice
fa73e132e2 remove .vscode 2020-10-30 16:45:12 +08:00
long2ice
1bac33cd33 add confirmation_option when downgrade 2020-10-30 16:39:14 +08:00
long2ice
4e76f12ccf update README.md 2020-10-28 17:12:23 +08:00
long2ice
724379700e Support multiple databases. (#68) 2020-10-28 17:02:02 +08:00
long2ice
bb929f2b55 update deps 2020-10-25 17:48:05 +08:00
long2ice
6339dc86a8 Fix migrate to new database error 2020-10-14 20:33:23 +08:00
long2ice
768747140a update changelog 2020-10-12 21:01:53 +08:00
long2ice
1fde3cd04e fix init KeyError (#61) 2020-10-12 20:59:13 +08:00
long2ice
d0ce545ff5 Fix first version error 2020-10-10 15:07:09 +08:00
long2ice
09b89ed7d0 update README 2020-10-09 15:41:37 +08:00
long2ice
86c8382593 update README 2020-10-09 15:34:48 +08:00
12 changed files with 482 additions and 331 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@@ -143,4 +143,5 @@ cython_debug/
.idea
migrations
aerich.ini
src
src
.vscode

View File

@@ -2,13 +2,28 @@
## 0.3
### 0.3.3
- Fix encoding error. (#75)
- Support multiple databases. (#68)
- Compatible with models file in directory. (#70)
### 0.3.2
- Fix migrate to new database error. (#62)
### 0.3.1
- Fix first version error.
- Fix init error. (#61)
### 0.3.0
- Refactoring migrate logic, and this version is not compatible with previous version.
- Now there don't need `old_models.py` and it store in database.
- Upgrade steps:
1. Upgrade aerich version.
2. Drop aerich model in db and recreate with new struct.
2. Drop aerich table in database.
3. Delete `migrations/{app}` folder and rerun `aerich init-db`.
4. Update model and `aerich migrate` normally.

View File

@@ -22,7 +22,7 @@ up:
@poetry update
deps:
@poetry install -E dbdrivers --no-root
@poetry install -E dbdrivers
style: deps
isort -src $(checkfiles)

View File

@@ -10,6 +10,8 @@
Aerich is a database migrations tool for Tortoise-ORM, which like alembic for SQLAlchemy, or Django ORM with it\'s
own migrations solution.
**Important: You can only use absolutely import in your `models.py` to make `aerich` work.**
## Install
Just install from pypi:
@@ -33,7 +35,7 @@ Options:
-h, --help Show this message and exit.
Commands:
downgrade Downgrade to previous version.
downgrade Downgrade to specified version.
heads Show current available heads in migrate location.
history List all migrate items.
init Init config file and generate root migrate location.
@@ -159,10 +161,24 @@ Now your db rollback to specified version.
1_202029051520102929_drop_column.json
```
## Support this project
### Multiple databases
- Just give a star!
- Donation.
```python
tortoise_orm = {
"connections": {
"default": expand_db_url(db_url, True),
"second": expand_db_url(db_url_second, True),
},
"apps": {
"models": {"models": ["tests.models", "aerich.models"], "default_connection": "default"},
"models_second": {"models": ["tests.models_second"], "default_connection": "second",},
},
}
```
You need only specify `aerich.models` in one app, and must specify `--app` when run `aerich migrate` and so on.
## Support this project
| AliPay | WeChatPay | PayPal |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |

View File

@@ -1 +1 @@
__version__ = "0.3.0"
__version__ = "0.3.3"

View File

@@ -28,7 +28,9 @@ def coro(f):
loop = asyncio.get_event_loop()
ctx = args[0]
loop.run_until_complete(f(*args, **kwargs))
Migrate.remove_old_model_file(ctx.obj["app"], ctx.obj["location"])
app = ctx.obj.get("app")
if app:
Migrate.remove_old_model_file(app, ctx.obj["location"])
return wrapper
@@ -36,7 +38,11 @@ def coro(f):
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, "-V", "--version")
@click.option(
"-c", "--config", default="aerich.ini", show_default=True, help="Config file.",
"-c",
"--config",
default="aerich.ini",
show_default=True,
help="Config file.",
)
@click.option("--app", required=False, help="Tortoise-ORM app name.")
@click.option(
@@ -64,8 +70,6 @@ async def cli(ctx: Context, config, app, name):
tortoise_config = get_tortoise_config(ctx, tortoise_orm)
app = app or list(tortoise_config.get("apps").keys())[0]
if "aerich.models" not in tortoise_config.get("apps").get(app).get("models"):
raise UsageError("Check your tortoise config and add aerich.models to it.", ctx=ctx)
ctx.obj["config"] = tortoise_config
ctx.obj["location"] = location
ctx.obj["app"] = app
@@ -127,6 +131,9 @@ async def upgrade(ctx: Context):
help="Specified version, default to last.",
)
@click.pass_context
@click.confirmation_option(
prompt="Downgrade is dangerous, which maybe lose your data, are you sure?",
)
@coro
async def downgrade(ctx: Context, version: int):
app = ctx.obj["app"]
@@ -191,12 +198,17 @@ async def history(ctx: Context):
help="Tortoise-ORM config module dict variable, like settings.TORTOISE_ORM.",
)
@click.option(
"--location", default="./migrations", show_default=True, help="Migrate store location."
"--location",
default="./migrations",
show_default=True,
help="Migrate store location.",
)
@click.pass_context
@coro
async def init(
ctx: Context, tortoise_orm, location,
ctx: Context,
tortoise_orm,
location,
):
config_file = ctx.obj["config_file"]
name = ctx.obj["name"]
@@ -247,7 +259,9 @@ async def init_db(ctx: Context, safe):
version = await Migrate.generate_version()
await Aerich.create(
version=version, app=app, content=Migrate.get_models_content(config, app, location)
version=version,
app=app,
content=Migrate.get_models_content(config, app, location),
)
with open(os.path.join(dirname, version), "w", encoding="utf-8") as f:
content = {
@@ -260,3 +274,7 @@ async def init_db(ctx: Context, safe):
def main():
sys.path.insert(0, ".")
cli()
if __name__ == "__main__":
main()

View File

@@ -1,10 +1,11 @@
import inspect
import json
import os
import re
from datetime import datetime
from importlib import import_module
from io import StringIO
from typing import Dict, List, Tuple, Type
from typing import Dict, List, Optional, Tuple, Type
import click
from tortoise import (
@@ -15,6 +16,7 @@ from tortoise import (
Model,
Tortoise,
)
from tortoise.exceptions import OperationalError
from tortoise.fields import Field
from aerich.ddl import BaseDDL
@@ -53,8 +55,11 @@ class Migrate:
)
@classmethod
async def get_last_version(cls) -> Aerich:
return await Aerich.filter(app=cls.app).first()
async def get_last_version(cls) -> Optional[Aerich]:
try:
return await Aerich.filter(app=cls.app).first()
except OperationalError:
pass
@classmethod
def remove_old_model_file(cls, app: str, location: str):
@@ -67,16 +72,16 @@ class Migrate:
async def init_with_old_models(cls, config: dict, app: str, location: str):
await Tortoise.init(config=config)
last_version = await cls.get_last_version()
content = last_version.content
with open(cls.get_old_model_file(app, location), "w") as f:
f.write(content)
migrate_config = cls._get_migrate_config(config, app, location)
cls.app = app
cls.migrate_config = migrate_config
cls.migrate_location = os.path.join(location, app)
if last_version:
content = last_version.content
with open(cls.get_old_model_file(app, location), "w", encoding="utf-8") as f:
f.write(content)
await Tortoise.init(config=migrate_config)
migrate_config = cls._get_migrate_config(config, app, location)
cls.migrate_config = migrate_config
await Tortoise.init(config=migrate_config)
connection = get_app_connection(config, app)
cls.dialect = connection.schema_generator.DIALECT
@@ -197,7 +202,15 @@ class Migrate:
old_model_files = []
models = config.get("apps").get(app).get("models")
for model in models:
old_model_files.append(import_module(model).__file__)
module = import_module(model)
possible_models = [getattr(module, attr_name) for attr_name in dir(module)]
for attr in filter(
lambda x: inspect.isclass(x) and issubclass(x, Model) and x is not Model,
possible_models,
):
file = inspect.getfile(attr)
if file not in old_model_files:
old_model_files.append(file)
pattern = rf"(\n)?('|\")({app})(.\w+)('|\")"
str_io = StringIO()
for i, model_file in enumerate(old_model_files):
@@ -290,12 +303,15 @@ class Migrate:
is_rename = diff_key in cls._rename_new
if is_rename:
cls._add_operator(
cls._rename_field(new_model, old_field, new_field), upgrade,
cls._rename_field(new_model, old_field, new_field),
upgrade,
)
break
else:
cls._add_operator(
cls._add_field(new_model, new_field), upgrade, cls._is_fk_m2m(new_field),
cls._add_field(new_model, new_field),
upgrade,
cls._is_fk_m2m(new_field),
)
else:
old_field = old_fields_map.get(new_key)
@@ -346,11 +362,15 @@ class Migrate:
if isinstance(new_field, ForeignKeyFieldInstance):
if old_field.db_constraint and not new_field.db_constraint:
cls._add_operator(
cls._drop_fk(new_model, new_field), upgrade, True,
cls._drop_fk(new_model, new_field),
upgrade,
True,
)
if new_field.db_constraint and not old_field.db_constraint:
cls._add_operator(
cls._add_fk(new_model, new_field), upgrade, True,
cls._add_fk(new_model, new_field),
upgrade,
True,
)
for old_key in old_keys:
@@ -360,12 +380,20 @@ class Migrate:
not upgrade and old_key not in cls._rename_new
):
cls._add_operator(
cls._remove_field(old_model, field), upgrade, cls._is_fk_m2m(field),
cls._remove_field(old_model, field),
upgrade,
cls._is_fk_m2m(field),
)
for new_index in new_indexes:
if new_index not in old_indexes:
cls._add_operator(cls._add_index(new_model, new_index,), upgrade)
cls._add_operator(
cls._add_index(
new_model,
new_index,
),
upgrade,
)
for old_index in old_indexes:
if old_index not in new_indexes:
cls._add_operator(cls._remove_index(old_model, old_index), upgrade)

View File

@@ -13,10 +13,15 @@ from aerich.ddl.sqlite import SqliteDDL
from aerich.migrate import Migrate
db_url = os.getenv("TEST_DB", "sqlite://:memory:")
db_url_second = os.getenv("TEST_DB_SECOND", "sqlite://:memory:")
tortoise_orm = {
"connections": {"default": expand_db_url(db_url, True)},
"connections": {
"default": expand_db_url(db_url, True),
"second": expand_db_url(db_url_second, True),
},
"apps": {
"models": {"models": ["tests.models", "aerich.models"], "default_connection": "default"},
"models_second": {"models": ["tests.models_second"], "default_connection": "second"},
},
}

597
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "aerich"
version = "0.3.0"
version = "0.3.3"
description = "A database migrations tool for Tortoise ORM."
authors = ["long2ice <long2ice@gmail.com>"]
license = "Apache-2.0"
@@ -25,7 +25,7 @@ asyncpg = {version = "*", optional = true}
[tool.poetry.dev-dependencies]
flake8 = "*"
isort = "*"
black = "^19.10b0"
black = "*"
pytest = "*"
pytest-xdist = "*"
pytest-asyncio = "*"

63
tests/models_second.py Normal file
View File

@@ -0,0 +1,63 @@
import datetime
from enum import IntEnum
from tortoise import Model, fields
class ProductType(IntEnum):
article = 1
page = 2
class PermissionAction(IntEnum):
create = 1
delete = 2
update = 3
read = 4
class Status(IntEnum):
on = 1
off = 0
class User(Model):
username = fields.CharField(max_length=20, unique=True)
password = fields.CharField(max_length=200)
last_login = fields.DatetimeField(description="Last Login", default=datetime.datetime.now)
is_active = fields.BooleanField(default=True, description="Is Active")
is_superuser = fields.BooleanField(default=False, description="Is SuperUser")
avatar = fields.CharField(max_length=200, default="")
intro = fields.TextField(default="")
class Email(Model):
email = fields.CharField(max_length=200)
is_primary = fields.BooleanField(default=False)
user = fields.ForeignKeyField("models_second.User", db_constraint=False)
class Category(Model):
slug = fields.CharField(max_length=200)
name = fields.CharField(max_length=200)
user = fields.ForeignKeyField("models_second.User", description="User")
created_at = fields.DatetimeField(auto_now_add=True)
class Product(Model):
categories = fields.ManyToManyField("models_second.Category")
name = fields.CharField(max_length=50)
view_num = fields.IntField(description="View Num")
sort = fields.IntField()
is_reviewed = fields.BooleanField(description="Is Reviewed")
type = fields.IntEnumField(ProductType, description="Product Type")
image = fields.CharField(max_length=200)
body = fields.TextField()
created_at = fields.DatetimeField(auto_now_add=True)
class Config(Model):
label = fields.CharField(max_length=200)
key = fields.CharField(max_length=20)
value = fields.JSONField()
status: Status = fields.IntEnumField(Status, default=Status.on)