9 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
12 changed files with 470 additions and 329 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

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

View File

@@ -2,6 +2,16 @@
## 0.3 ## 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 ### 0.3.1
- Fix first version error. - Fix first version error.

View File

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

View File

@@ -10,7 +10,7 @@
Aerich is a database migrations tool for Tortoise-ORM, which like alembic for SQLAlchemy, or Django ORM with it\'s Aerich is a database migrations tool for Tortoise-ORM, which like alembic for SQLAlchemy, or Django ORM with it\'s
own migrations solution. own migrations solution.
**If you upgrade aerich from <= 0.2.5 to >= 0.3.0, see [changelog](https://github.com/tortoise/aerich/blob/dev/CHANGELOG.md) for upgrade steps.** **Important: You can only use absolutely import in your `models.py` to make `aerich` work.**
## Install ## Install
@@ -161,10 +161,24 @@ Now your db rollback to specified version.
1_202029051520102929_drop_column.json 1_202029051520102929_drop_column.json
``` ```
## Support this project ### Multiple databases
- Just give a star! ```python
- Donation. 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 | | AliPay | WeChatPay | PayPal |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |

View File

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

View File

@@ -38,7 +38,11 @@ def coro(f):
@click.group(context_settings={"help_option_names": ["-h", "--help"]}) @click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, "-V", "--version") @click.version_option(__version__, "-V", "--version")
@click.option( @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("--app", required=False, help="Tortoise-ORM app name.")
@click.option( @click.option(
@@ -66,8 +70,6 @@ async def cli(ctx: Context, config, app, name):
tortoise_config = get_tortoise_config(ctx, tortoise_orm) tortoise_config = get_tortoise_config(ctx, tortoise_orm)
app = app or list(tortoise_config.get("apps").keys())[0] 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["config"] = tortoise_config
ctx.obj["location"] = location ctx.obj["location"] = location
ctx.obj["app"] = app ctx.obj["app"] = app
@@ -129,6 +131,9 @@ async def upgrade(ctx: Context):
help="Specified version, default to last.", help="Specified version, default to last.",
) )
@click.pass_context @click.pass_context
@click.confirmation_option(
prompt="Downgrade is dangerous, which maybe lose your data, are you sure?",
)
@coro @coro
async def downgrade(ctx: Context, version: int): async def downgrade(ctx: Context, version: int):
app = ctx.obj["app"] app = ctx.obj["app"]
@@ -193,12 +198,17 @@ async def history(ctx: Context):
help="Tortoise-ORM config module dict variable, like settings.TORTOISE_ORM.", help="Tortoise-ORM config module dict variable, like settings.TORTOISE_ORM.",
) )
@click.option( @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 @click.pass_context
@coro @coro
async def init( async def init(
ctx: Context, tortoise_orm, location, ctx: Context,
tortoise_orm,
location,
): ):
config_file = ctx.obj["config_file"] config_file = ctx.obj["config_file"]
name = ctx.obj["name"] name = ctx.obj["name"]
@@ -249,7 +259,9 @@ async def init_db(ctx: Context, safe):
version = await Migrate.generate_version() version = await Migrate.generate_version()
await Aerich.create( 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: with open(os.path.join(dirname, version), "w", encoding="utf-8") as f:
content = { content = {
@@ -262,3 +274,7 @@ async def init_db(ctx: Context, safe):
def main(): def main():
sys.path.insert(0, ".") sys.path.insert(0, ".")
cli() cli()
if __name__ == "__main__":
main()

View File

@@ -1,10 +1,11 @@
import inspect
import json import json
import os import os
import re import re
from datetime import datetime from datetime import datetime
from importlib import import_module from importlib import import_module
from io import StringIO from io import StringIO
from typing import Dict, List, Tuple, Type from typing import Dict, List, Optional, Tuple, Type
import click import click
from tortoise import ( from tortoise import (
@@ -15,6 +16,7 @@ from tortoise import (
Model, Model,
Tortoise, Tortoise,
) )
from tortoise.exceptions import OperationalError
from tortoise.fields import Field from tortoise.fields import Field
from aerich.ddl import BaseDDL from aerich.ddl import BaseDDL
@@ -53,8 +55,11 @@ class Migrate:
) )
@classmethod @classmethod
async def get_last_version(cls) -> Aerich: async def get_last_version(cls) -> Optional[Aerich]:
return await Aerich.filter(app=cls.app).first() try:
return await Aerich.filter(app=cls.app).first()
except OperationalError:
pass
@classmethod @classmethod
def remove_old_model_file(cls, app: str, location: str): def remove_old_model_file(cls, app: str, location: str):
@@ -67,17 +72,16 @@ class Migrate:
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):
await Tortoise.init(config=config) await Tortoise.init(config=config)
last_version = await cls.get_last_version() last_version = await cls.get_last_version()
cls.app = app
cls.migrate_location = os.path.join(location, app)
if last_version: if last_version:
content = last_version.content content = last_version.content
with open(cls.get_old_model_file(app, location), "w") as f: with open(cls.get_old_model_file(app, location), "w", encoding="utf-8") as f:
f.write(content) f.write(content)
migrate_config = cls._get_migrate_config(config, app, location) migrate_config = cls._get_migrate_config(config, app, location)
cls.app = app cls.migrate_config = migrate_config
cls.migrate_config = migrate_config await Tortoise.init(config=migrate_config)
cls.migrate_location = os.path.join(location, app)
await Tortoise.init(config=migrate_config)
connection = get_app_connection(config, app) connection = get_app_connection(config, app)
cls.dialect = connection.schema_generator.DIALECT cls.dialect = connection.schema_generator.DIALECT
@@ -198,7 +202,15 @@ class Migrate:
old_model_files = [] old_model_files = []
models = config.get("apps").get(app).get("models") models = config.get("apps").get(app).get("models")
for model in 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+)('|\")" pattern = rf"(\n)?('|\")({app})(.\w+)('|\")"
str_io = StringIO() str_io = StringIO()
for i, model_file in enumerate(old_model_files): for i, model_file in enumerate(old_model_files):
@@ -291,12 +303,15 @@ class Migrate:
is_rename = diff_key in cls._rename_new is_rename = diff_key in cls._rename_new
if is_rename: if is_rename:
cls._add_operator( cls._add_operator(
cls._rename_field(new_model, old_field, new_field), upgrade, cls._rename_field(new_model, old_field, new_field),
upgrade,
) )
break break
else: else:
cls._add_operator( 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: else:
old_field = old_fields_map.get(new_key) old_field = old_fields_map.get(new_key)
@@ -347,11 +362,15 @@ class Migrate:
if isinstance(new_field, ForeignKeyFieldInstance): if isinstance(new_field, ForeignKeyFieldInstance):
if old_field.db_constraint and not new_field.db_constraint: if old_field.db_constraint and not new_field.db_constraint:
cls._add_operator( 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: if new_field.db_constraint and not old_field.db_constraint:
cls._add_operator( 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: for old_key in old_keys:
@@ -361,12 +380,20 @@ class Migrate:
not upgrade and old_key not in cls._rename_new not upgrade and old_key not in cls._rename_new
): ):
cls._add_operator( 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: for new_index in new_indexes:
if new_index not in old_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: for old_index in old_indexes:
if old_index not in new_indexes: if old_index not in new_indexes:
cls._add_operator(cls._remove_index(old_model, old_index), upgrade) 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 from aerich.migrate import Migrate
db_url = os.getenv("TEST_DB", "sqlite://:memory:") db_url = os.getenv("TEST_DB", "sqlite://:memory:")
db_url_second = os.getenv("TEST_DB_SECOND", "sqlite://:memory:")
tortoise_orm = { 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": { "apps": {
"models": {"models": ["tests.models", "aerich.models"], "default_connection": "default"}, "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] [tool.poetry]
name = "aerich" name = "aerich"
version = "0.3.1" version = "0.3.3"
description = "A database migrations tool for Tortoise ORM." description = "A database migrations tool for Tortoise ORM."
authors = ["long2ice <long2ice@gmail.com>"] authors = ["long2ice <long2ice@gmail.com>"]
license = "Apache-2.0" license = "Apache-2.0"
@@ -25,7 +25,7 @@ asyncpg = {version = "*", optional = true}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
flake8 = "*" flake8 = "*"
isort = "*" isort = "*"
black = "^19.10b0" black = "*"
pytest = "*" pytest = "*"
pytest-xdist = "*" pytest-xdist = "*"
pytest-asyncio = "*" 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)