Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f5dff84476 | ||
|
e399821116 | ||
|
648f25a951 | ||
|
fa73e132e2 | ||
|
1bac33cd33 | ||
|
4e76f12ccf | ||
|
724379700e | ||
|
bb929f2b55 | ||
|
6339dc86a8 | ||
|
768747140a | ||
|
1fde3cd04e | ||
|
d0ce545ff5 | ||
|
09b89ed7d0 | ||
|
86c8382593 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -143,4 +143,5 @@ cython_debug/
|
|||||||
.idea
|
.idea
|
||||||
migrations
|
migrations
|
||||||
aerich.ini
|
aerich.ini
|
||||||
src
|
src
|
||||||
|
.vscode
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -2,13 +2,28 @@
|
|||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
- Fix first version error.
|
||||||
|
- Fix init error. (#61)
|
||||||
|
|
||||||
### 0.3.0
|
### 0.3.0
|
||||||
|
|
||||||
- Refactoring migrate logic, and this version is not compatible with previous version.
|
- 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.
|
- Now there don't need `old_models.py` and it store in database.
|
||||||
- Upgrade steps:
|
- Upgrade steps:
|
||||||
1. Upgrade aerich version.
|
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`.
|
3. Delete `migrations/{app}` folder and rerun `aerich init-db`.
|
||||||
4. Update model and `aerich migrate` normally.
|
4. Update model and `aerich migrate` normally.
|
||||||
|
|
||||||
|
2
Makefile
2
Makefile
@@ -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)
|
||||||
|
24
README.md
24
README.md
@@ -10,6 +10,8 @@
|
|||||||
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.
|
||||||
|
|
||||||
|
**Important: You can only use absolutely import in your `models.py` to make `aerich` work.**
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Just install from pypi:
|
Just install from pypi:
|
||||||
@@ -33,7 +35,7 @@ Options:
|
|||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
downgrade Downgrade to previous version.
|
downgrade Downgrade to specified version.
|
||||||
heads Show current available heads in migrate location.
|
heads Show current available heads in migrate location.
|
||||||
history List all migrate items.
|
history List all migrate items.
|
||||||
init Init config file and generate root migrate location.
|
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
|
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 |
|
||||||
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||||
|
@@ -1 +1 @@
|
|||||||
__version__ = "0.3.0"
|
__version__ = "0.3.3"
|
||||||
|
@@ -28,7 +28,9 @@ def coro(f):
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
ctx = args[0]
|
ctx = args[0]
|
||||||
loop.run_until_complete(f(*args, **kwargs))
|
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
|
return wrapper
|
||||||
|
|
||||||
@@ -36,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(
|
||||||
@@ -64,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
|
||||||
@@ -127,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"]
|
||||||
@@ -191,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"]
|
||||||
@@ -247,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 = {
|
||||||
@@ -260,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()
|
||||||
|
@@ -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,16 +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()
|
||||||
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.app = app
|
||||||
cls.migrate_config = migrate_config
|
|
||||||
cls.migrate_location = os.path.join(location, app)
|
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)
|
connection = get_app_connection(config, app)
|
||||||
cls.dialect = connection.schema_generator.DIALECT
|
cls.dialect = connection.schema_generator.DIALECT
|
||||||
@@ -197,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):
|
||||||
@@ -290,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)
|
||||||
@@ -346,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:
|
||||||
@@ -360,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)
|
||||||
|
@@ -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
597
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "aerich"
|
name = "aerich"
|
||||||
version = "0.3.0"
|
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
63
tests/models_second.py
Normal 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)
|
Reference in New Issue
Block a user