9 Commits

Author SHA1 Message Date
long2ice
354e861dad add more test 2020-05-24 13:47:10 +08:00
long2ice
3a76486993 migrate raise error 2020-05-24 00:05:45 +08:00
long2ice
4d0a6b4de6 Fix version num str 2020-05-22 15:35:35 +08:00
long2ice
c01d2993e0 Exclude models.Aerich.
Add init record when init-db.
2020-05-22 11:59:03 +08:00
long2ice
bab5ebf2f0 migrate exclude aerich.models 2020-05-22 11:14:16 +08:00
long2ice
7e5cefd7d6 write old models exclude aerich.models 2020-05-22 11:03:52 +08:00
long2ice
0cea28d521 update version 2020-05-21 23:57:13 +08:00
long2ice
b92e6551fd update dependency_links 2020-05-21 23:33:58 +08:00
long2ice
bbabde32a1 update version 2020-05-21 21:24:21 +08:00
16 changed files with 320 additions and 150 deletions

View File

@@ -4,6 +4,16 @@ ChangeLog
0.1 0.1
=== ===
0.1.7
-----
- Exclude models.Aerich.
- Add init record when init-db.
- Fix version num str.
0.1.6
-----
- update dependency_links
0.1.5 0.1.5
----- -----
- Add sqlite and postgres support. - Add sqlite and postgres support.

View File

@@ -1,4 +1,4 @@
checkfiles = aerich/ tests/ checkfiles = aerich/ tests/ conftest.py
black_opts = -l 100 -t py38 black_opts = -l 100 -t py38
py_warn = PYTHONDEVMODE=1 py_warn = PYTHONDEVMODE=1
MYSQL_HOST ?= "127.0.0.1" MYSQL_HOST ?= "127.0.0.1"

View File

@@ -104,6 +104,10 @@ Init db
Success create app migrate location ./migrations/models Success create app migrate location ./migrations/models
Success generate schema for app "models" Success generate schema for app "models"
.. note::
If your Tortoise-ORM app is not default ``models``, you must specify ``--app`` like ``aerich --app other_models init-db``.
Update models and make migrate Update models and make migrate
------------------------------ ------------------------------

View File

@@ -1 +1 @@
__version__ = "0.1.4" __version__ = "0.1.7"

View File

@@ -6,8 +6,9 @@ from enum import Enum
import asyncclick as click import asyncclick as click
from asyncclick import Context, UsageError from asyncclick import Context, UsageError
from tortoise import ConfigurationError, Tortoise, generate_schema_for_client from tortoise import Tortoise, generate_schema_for_client
from tortoise.transactions import in_transaction from tortoise.transactions import in_transaction
from tortoise.utils import get_schema_sql
from aerich.migrate import Migrate 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
@@ -60,10 +61,7 @@ async def cli(ctx: Context, config, app, name):
ctx.obj["location"] = location ctx.obj["location"] = location
if invoked_subcommand != "init-db": if invoked_subcommand != "init-db":
try: await Migrate.init_with_old_models(tortoise_config, app, location)
await Migrate.init_with_old_models(tortoise_config, app, location)
except ConfigurationError:
raise UsageError(ctx=ctx, message="You must exec init-db first")
@cli.command(help="Generate migrate changes file.") @cli.command(help="Generate migrate changes file.")
@@ -117,6 +115,8 @@ async def downgrade(ctx: Context):
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = json.load(f) content = json.load(f)
downgrade_query_list = content.get("downgrade") downgrade_query_list = content.get("downgrade")
if not downgrade_query_list:
return click.secho(f"No downgrade item dound", fg=Color.yellow)
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)
await last_version.delete() await last_version.delete()
@@ -198,6 +198,8 @@ async def init_db(ctx: Context, safe):
if not os.path.isdir(dirname): if not os.path.isdir(dirname):
os.mkdir(dirname) os.mkdir(dirname)
click.secho(f"Success create app migrate location {dirname}", fg=Color.green) click.secho(f"Success create app migrate location {dirname}", fg=Color.green)
else:
return click.secho(f"Inited {app} already", fg=Color.yellow)
Migrate.write_old_models(config, app, location) Migrate.write_old_models(config, app, location)
@@ -205,6 +207,15 @@ async def init_db(ctx: Context, safe):
connection = get_app_connection(config, app) connection = get_app_connection(config, app)
await generate_schema_for_client(connection, safe) await generate_schema_for_client(connection, safe)
schema = get_schema_sql(connection, safe)
version = await Migrate.generate_version()
await Aerich.create(version=version, app=app)
with open(os.path.join(dirname, version), "w") as f:
content = {
"upgrade": schema,
}
json.dump(content, f, ensure_ascii=False, indent=2)
return click.secho(f'Success generate schema for app "{app}"', fg=Color.green) return click.secho(f'Success generate schema for app "{app}"', fg=Color.green)

View File

@@ -27,6 +27,7 @@ class Migrate:
_downgrade_fk_m2m_index_operators: List[str] = [] _downgrade_fk_m2m_index_operators: List[str] = []
_upgrade_m2m: List[str] = [] _upgrade_m2m: List[str] = []
_downgrade_m2m: List[str] = [] _downgrade_m2m: List[str] = []
_aerich = Aerich.__name__
ddl: BaseDDL ddl: BaseDDL
migrate_config: dict migrate_config: dict
@@ -77,15 +78,21 @@ class Migrate:
async def _get_last_version_num(cls): async def _get_last_version_num(cls):
last_version = await cls.get_last_version() last_version = await cls.get_last_version()
if not last_version: if not last_version:
return 0 return None
version = last_version.version version = last_version.version
return version.split("_")[0] return int(version.split("_")[0])
@classmethod
async def generate_version(cls, name=None):
now = datetime.now().strftime("%Y%M%D%H%M%S").replace("/", "")
last_version_num = await cls._get_last_version_num()
if last_version_num is None:
return f"0_{now}_init.json"
return f"{last_version_num + 1}_{now}_{name}.json"
@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("/", "") version = await cls.generate_version(name)
last_version_num = await cls._get_last_version_num()
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,
@@ -105,8 +112,8 @@ class Migrate:
diff_models = apps.get(cls.diff_app) diff_models = apps.get(cls.diff_app)
app_models = apps.get(cls.app) app_models = apps.get(cls.app)
cls._diff_models(diff_models, app_models) cls.diff_models(diff_models, app_models)
cls._diff_models(app_models, diff_models, False) cls.diff_models(app_models, diff_models, False)
cls._merge_operators() cls._merge_operators()
@@ -179,15 +186,18 @@ class Migrate:
:param location: :param location:
:return: :return:
""" """
cls.app = app
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(model.replace(".", "/") + ".py") if model != "aerich.models":
old_model_files.append(model.replace(".", "/") + ".py")
cls.cp_models(app, old_model_files, os.path.join(location, app, cls.get_old_model_file())) cls.cp_models(app, old_model_files, os.path.join(location, app, cls.get_old_model_file()))
@classmethod @classmethod
def _diff_models( def diff_models(
cls, old_models: Dict[str, Type[Model]], new_models: Dict[str, Type[Model]], upgrade=True cls, old_models: Dict[str, Type[Model]], new_models: Dict[str, Type[Model]], upgrade=True
): ):
""" """
@@ -197,6 +207,9 @@ class Migrate:
:param upgrade: :param upgrade:
:return: :return:
""" """
old_models.pop(cls._aerich, None)
new_models.pop(cls._aerich, None)
for new_model_str, new_model in new_models.items(): for new_model_str, new_model in new_models.items():
if new_model_str not in old_models.keys(): if new_model_str not in old_models.keys():
cls._add_operator(cls.add_model(new_model), upgrade) cls._add_operator(cls.add_model(new_model), upgrade)

View File

@@ -1,11 +1,61 @@
import asyncio
import os import os
import pytest import pytest
from tortoise.contrib.test import finalizer, initializer from tortoise import Tortoise, expand_db_url, generate_schema_for_client
from tortoise.backends.asyncpg.schema_generator import AsyncpgSchemaGenerator
from tortoise.backends.mysql.schema_generator import MySQLSchemaGenerator
from tortoise.backends.sqlite.schema_generator import SqliteSchemaGenerator
from aerich.ddl.mysql import MysqlDDL
from aerich.ddl.postgres import PostgresDDL
from aerich.ddl.sqlite import SqliteDDL
from aerich.migrate import Migrate
db_url = os.getenv("TEST_DB", "sqlite://:memory:")
tortoise_orm = {
"connections": {"default": expand_db_url(db_url, True)},
"apps": {
"models": {
"models": ["tests.models", "aerich.models"],
"default_connection": "default",
},
},
}
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="function", autouse=True)
def initialize_tests(request): def reset_migrate():
db_url = os.getenv("TEST_DB", "sqlite://:memory:") Migrate.upgrade_operators = []
initializer(["tests.models"], db_url=db_url) Migrate.downgrade_operators = []
request.addfinalizer(finalizer) Migrate._upgrade_fk_m2m_index_operators = []
Migrate._downgrade_fk_m2m_index_operators = []
Migrate._upgrade_m2m = []
Migrate._downgrade_m2m = []
@pytest.fixture(scope="session")
def loop():
loop = asyncio.get_event_loop()
return loop
@pytest.fixture(scope="session", autouse=True)
def initialize_tests(loop, request):
tortoise_orm['connections']['diff_models'] = "sqlite://:memory:"
tortoise_orm['apps']['diff_models'] = {"models": ["tests.diff_models"], "default_connection": "diff_models"}
loop.run_until_complete(Tortoise.init(config=tortoise_orm, _create_db=True))
loop.run_until_complete(
generate_schema_for_client(Tortoise.get_connection("default"), safe=True)
)
client = Tortoise.get_connection("default")
if client.schema_generator is MySQLSchemaGenerator:
Migrate.ddl = MysqlDDL(client)
elif client.schema_generator is SqliteSchemaGenerator:
Migrate.ddl = SqliteDDL(client)
elif client.schema_generator is AsyncpgSchemaGenerator:
Migrate.ddl = PostgresDDL(client)
request.addfinalizer(lambda: loop.run_until_complete(Tortoise._drop_databases()))

53
poetry.lock generated
View File

@@ -90,14 +90,6 @@ dev = ["Cython (0.29.14)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphin
docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"]
test = ["pycodestyle (>=2.5.0,<2.6.0)", "flake8 (>=3.7.9,<3.8.0)", "uvloop (>=0.14.0,<0.15.0)"] test = ["pycodestyle (>=2.5.0,<2.6.0)", "flake8 (>=3.7.9,<3.8.0)", "uvloop (>=0.14.0,<0.15.0)"]
[[package]]
category = "dev"
description = "Enhance the standard unittest package with features for testing asyncio libraries"
name = "asynctest"
optional = false
python-versions = ">=3.5"
version = "0.13.0"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Atomic file writes." description = "Atomic file writes."
@@ -217,7 +209,7 @@ description = "the modular source code checker: pep8 pyflakes and co"
name = "flake8" name = "flake8"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
version = "3.8.1" version = "3.8.2"
[package.dependencies] [package.dependencies]
mccabe = ">=0.6.0,<0.7.0" mccabe = ">=0.6.0,<0.7.0"
@@ -412,6 +404,20 @@ wcwidth = "*"
checkqa-mypy = ["mypy (v0.761)"] checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "Pytest support for asyncio."
name = "pytest-asyncio"
optional = false
python-versions = ">= 3.5"
version = "0.12.0"
[package.dependencies]
pytest = ">=5.4.0"
[package.extras]
testing = ["async_generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "run tests in isolated forked subprocesses" description = "run tests in isolated forked subprocesses"
@@ -454,7 +460,7 @@ description = "Python 2 and 3 compatibility utilities"
name = "six" name = "six"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0" version = "1.15.0"
[[package]] [[package]]
category = "main" category = "main"
@@ -489,7 +495,7 @@ description = "Easy async ORM for python, built with relations in mind"
name = "tortoise-orm" name = "tortoise-orm"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.16.11" version = "0.16.12"
[package.dependencies] [package.dependencies]
aiosqlite = ">=0.11.0" aiosqlite = ">=0.11.0"
@@ -498,10 +504,6 @@ iso8601 = ">=0.1.12"
pypika = ">=0.36.5" pypika = ">=0.36.5"
typing-extensions = ">=3.7" typing-extensions = ">=3.7"
[package.source]
reference = "1f67b7a0ca1384365d6ff89d9e245e733166d1a6"
type = "git"
url = "https://github.com/long2ice/tortoise-orm.git"
[[package]] [[package]]
category = "dev" category = "dev"
description = "a fork of Python 2 and 3 ast modules with type comment support" description = "a fork of Python 2 and 3 ast modules with type comment support"
@@ -527,7 +529,7 @@ python-versions = "*"
version = "0.1.9" version = "0.1.9"
[metadata] [metadata]
content-hash = "58a032bbb47859e87d2bce036af24149060cc531ff9220a14f6cd48db6252f39" content-hash = "35274e9622d359af475f573760ba687b31756b1b1de70bc4d75dab5ddbc5a93d"
python-versions = "^3.8" python-versions = "^3.8"
[metadata.files] [metadata.files]
@@ -581,10 +583,6 @@ asyncpg = [
{file = "asyncpg-0.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:2af6a5a705accd36e13292ea43d08c20b15e52d684beb522cb3a7d3c9c8f3f48"}, {file = "asyncpg-0.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:2af6a5a705accd36e13292ea43d08c20b15e52d684beb522cb3a7d3c9c8f3f48"},
{file = "asyncpg-0.20.1.tar.gz", hash = "sha256:394bf19bdddbba07a38cd6fb526ebf66e120444d6b3097332b78efd5b26495b0"}, {file = "asyncpg-0.20.1.tar.gz", hash = "sha256:394bf19bdddbba07a38cd6fb526ebf66e120444d6b3097332b78efd5b26495b0"},
] ]
asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -664,8 +662,8 @@ execnet = [
{file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"},
] ]
flake8 = [ flake8 = [
{file = "flake8-3.8.1-py2.py3-none-any.whl", hash = "sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195"}, {file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"},
{file = "flake8-3.8.1.tar.gz", hash = "sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"}, {file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"},
] ]
iso8601 = [ iso8601 = [
{file = "iso8601-0.1.12-py2.py3-none-any.whl", hash = "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3"}, {file = "iso8601-0.1.12-py2.py3-none-any.whl", hash = "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3"},
@@ -766,6 +764,9 @@ pytest = [
{file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"}, {file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"},
{file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"}, {file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"},
] ]
pytest-asyncio = [
{file = "pytest-asyncio-0.12.0.tar.gz", hash = "sha256:475bd2f3dc0bc11d2463656b3cbaafdbec5a47b47508ea0b329ee693040eebd2"},
]
pytest-forked = [ pytest-forked = [
{file = "pytest-forked-1.1.3.tar.gz", hash = "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100"}, {file = "pytest-forked-1.1.3.tar.gz", hash = "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100"},
{file = "pytest_forked-1.1.3-py2.py3-none-any.whl", hash = "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87"}, {file = "pytest_forked-1.1.3-py2.py3-none-any.whl", hash = "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87"},
@@ -798,8 +799,8 @@ regex = [
{file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"}, {file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"},
] ]
six = [ six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
] ]
sniffio = [ sniffio = [
{file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"},
@@ -813,7 +814,9 @@ toml = [
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
] ]
tortoise-orm = [] tortoise-orm = [
{file = "tortoise-orm-0.16.12.tar.gz", hash = "sha256:170e4bbfe1c98223ad1fba33d7fded7923e4bb49c9d74c78bd173a0ebc861658"},
]
typed-ast = [ typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},

View File

@@ -1,18 +1,17 @@
[tool.poetry] [tool.poetry]
name = "aerich" name = "aerich"
version = "0.1.1" version = "0.1.7"
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>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
tortoise-orm = {git = "https://github.com/long2ice/tortoise-orm.git", branch = "develop"} tortoise-orm = "*"
asyncclick = "*" asyncclick = "*"
pydantic = "*" pydantic = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
taskipy = "*" taskipy = "*"
asynctest = "*"
flake8 = "*" flake8 = "*"
isort = "*" isort = "*"
black = "^19.10b0" black = "^19.10b0"
@@ -21,6 +20,7 @@ aiomysql = "*"
asyncpg = "*" asyncpg = "*"
pytest-xdist = "*" pytest-xdist = "*"
mypy = "*" mypy = "*"
pytest-asyncio = "*"
[tool.taskipy.tasks] [tool.taskipy.tasks]
export = "poetry export -f requirements.txt --without-hashes > requirements.txt" export = "poetry export -f requirements.txt --without-hashes > requirements.txt"

View File

@@ -6,7 +6,6 @@ appdirs==1.4.4
async-generator==1.10 async-generator==1.10
asyncclick==7.0.9 asyncclick==7.0.9
asyncpg==0.20.1 asyncpg==0.20.1
asynctest==0.13.0
atomicwrites==1.4.0; sys_platform == "win32" atomicwrites==1.4.0; sys_platform == "win32"
attrs==19.3.0 attrs==19.3.0
black==19.10b0 black==19.10b0
@@ -16,7 +15,7 @@ click==7.1.2
colorama==0.4.3; sys_platform == "win32" colorama==0.4.3; sys_platform == "win32"
cryptography==2.9.2 cryptography==2.9.2
execnet==1.7.1 execnet==1.7.1
flake8==3.8.1 flake8==3.8.2
iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython" iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
isort==4.3.21 isort==4.3.21
mccabe==0.6.1 mccabe==0.6.1
@@ -35,14 +34,15 @@ pymysql==0.9.2
pyparsing==2.4.7 pyparsing==2.4.7
pypika==0.37.6 pypika==0.37.6
pytest==5.4.2 pytest==5.4.2
pytest-asyncio==0.12.0
pytest-forked==1.1.3 pytest-forked==1.1.3
pytest-xdist==1.32.0 pytest-xdist==1.32.0
regex==2020.5.14 regex==2020.5.14
six==1.14.0 six==1.15.0
sniffio==1.1.0 sniffio==1.1.0
taskipy==1.2.1 taskipy==1.2.1
toml==0.10.1 toml==0.10.1
-e git+https://github.com/long2ice/tortoise-orm.git@1f67b7a0ca1384365d6ff89d9e245e733166d1a6#egg=tortoise-orm tortoise-orm==0.16.12
typed-ast==1.4.1 typed-ast==1.4.1
typing-extensions==3.7.4.2 typing-extensions==3.7.4.2
wcwidth==0.1.9 wcwidth==0.1.9

View File

@@ -7,4 +7,5 @@ iso8601==0.1.12; sys_platform == "win32" or implementation_name != "cpython"
pydantic==1.5.1 pydantic==1.5.1
pypika==0.37.6 pypika==0.37.6
sniffio==1.1.0 sniffio==1.1.0
tortoise-orm==0.16.12
typing-extensions==3.7.4.2 typing-extensions==3.7.4.2

View File

@@ -39,6 +39,6 @@ setup(
keywords=( keywords=(
'migrate Tortoise-ORM mysql' 'migrate Tortoise-ORM mysql'
), ),
dependency_links=['https://github.com/long2ice/tortoise-orm.git@develop#egg=tortoise-orm'], dependency_links=['https://github.com/tortoise-orm/tortoise-orm.git@develop#egg=tortoise-orm'],
install_requires=requirements(), install_requires=requirements(),
) )

View File

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

56
tests/diff_models.py Normal file
View File

@@ -0,0 +1,56 @@
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,)
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 Category(Model):
slug = fields.CharField(max_length=200)
user = fields.ForeignKeyField("diff_models.User", description="User")
created_at = fields.DatetimeField(auto_now_add=True)
class Product(Model):
categories = fields.ManyToManyField("diff_models.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)

View File

@@ -1,114 +1,101 @@
from tortoise import Tortoise
from tortoise.backends.asyncpg.schema_generator import AsyncpgSchemaGenerator
from tortoise.backends.mysql.schema_generator import MySQLSchemaGenerator
from tortoise.backends.sqlite.schema_generator import SqliteSchemaGenerator
from tortoise.contrib import test
from aerich.ddl.mysql import MysqlDDL from aerich.ddl.mysql import MysqlDDL
from aerich.ddl.postgres import PostgresDDL from aerich.ddl.postgres import PostgresDDL
from aerich.ddl.sqlite import SqliteDDL from aerich.ddl.sqlite import SqliteDDL
from aerich.migrate import Migrate
from tests.models import Category from tests.models import Category
class TestDDL(test.TruncationTestCase): def test_create_table():
maxDiff = None ret = Migrate.ddl.create_table(Category)
if isinstance(Migrate.ddl, MysqlDDL):
def setUp(self) -> None: assert (
client = Tortoise.get_connection("models") ret
if client.schema_generator is MySQLSchemaGenerator: == """CREATE TABLE IF NOT EXISTS `category` (
self.ddl = MysqlDDL(client)
elif client.schema_generator is SqliteSchemaGenerator:
self.ddl = SqliteDDL(client)
elif client.schema_generator is AsyncpgSchemaGenerator:
self.ddl = PostgresDDL(client)
def test_create_table(self):
ret = self.ddl.create_table(Category)
if isinstance(self.ddl, MysqlDDL):
self.assertEqual(
ret,
"""CREATE TABLE IF NOT EXISTS `category` (
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`slug` VARCHAR(200) NOT NULL, `slug` VARCHAR(200) NOT NULL,
`name` VARCHAR(200) NOT NULL, `name` VARCHAR(200) NOT NULL,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`user_id` INT NOT NULL COMMENT 'User', `user_id` INT NOT NULL COMMENT 'User',
CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) CHARACTER SET utf8mb4;""", ) CHARACTER SET utf8mb4;"""
) )
elif isinstance(self.ddl, SqliteDDL):
self.assertEqual( elif isinstance(Migrate.ddl, SqliteDDL):
ret, assert (
"""CREATE TABLE IF NOT EXISTS "category" ( ret
== """CREATE TABLE IF NOT EXISTS "category" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"slug" VARCHAR(200) NOT NULL, "slug" VARCHAR(200) NOT NULL,
"name" VARCHAR(200) NOT NULL, "name" VARCHAR(200) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE /* User */ "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE /* User */
);""", );"""
) )
elif isinstance(self.ddl, PostgresDDL):
self.assertEqual( elif isinstance(Migrate.ddl, PostgresDDL):
ret, assert (
"""CREATE TABLE IF NOT EXISTS "category" ( ret
== """CREATE TABLE IF NOT EXISTS "category" (
"id" SERIAL NOT NULL PRIMARY KEY, "id" SERIAL NOT NULL PRIMARY KEY,
"slug" VARCHAR(200) NOT NULL, "slug" VARCHAR(200) NOT NULL,
"name" VARCHAR(200) NOT NULL, "name" VARCHAR(200) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE "user_id" INT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
); );
COMMENT ON COLUMN "category"."user_id" IS 'User';""", COMMENT ON COLUMN "category"."user_id" IS 'User';"""
)
def test_drop_table(self):
ret = self.ddl.drop_table(Category)
self.assertEqual(ret, "DROP TABLE IF EXISTS category")
def test_add_column(self):
ret = self.ddl.add_column(Category, Category._meta.fields_map.get("name"))
if isinstance(self.ddl, MysqlDDL):
self.assertEqual(ret, "ALTER TABLE category ADD `name` VARCHAR(200) NOT NULL")
elif isinstance(self.ddl, PostgresDDL):
self.assertEqual(ret, 'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL')
elif isinstance(self.ddl, SqliteDDL):
self.assertEqual(ret, 'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL')
def test_drop_column(self):
ret = self.ddl.drop_column(Category, "name")
self.assertEqual(ret, "ALTER TABLE category DROP COLUMN name")
self.assertEqual(ret, "ALTER TABLE category DROP COLUMN name")
def test_add_index(self):
index = self.ddl.add_index(Category, ["name"])
index_u = self.ddl.add_index(Category, ["name"], True)
if isinstance(self.ddl, MysqlDDL):
self.assertEqual(
index, "ALTER TABLE category ADD INDEX idx_category_name_8b0cb9 (`name`)"
)
self.assertEqual(
index_u, "ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 (`name`)"
)
elif isinstance(self.ddl, SqliteDDL):
self.assertEqual(
index_u, 'ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 ("name")'
)
self.assertEqual(
index_u, 'ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 ("name")'
)
def test_drop_index(self):
ret = self.ddl.drop_index(Category, ["name"])
self.assertEqual(ret, "ALTER TABLE category DROP INDEX idx_category_name_8b0cb9")
ret = self.ddl.drop_index(Category, ["name"], True)
self.assertEqual(ret, "ALTER TABLE category DROP INDEX uid_category_name_8b0cb9")
def test_add_fk(self):
ret = self.ddl.add_fk(Category, Category._meta.fields_map.get("user"))
self.assertEqual(
ret,
"ALTER TABLE category ADD CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE",
) )
def test_drop_fk(self):
ret = self.ddl.drop_fk(Category, Category._meta.fields_map.get("user")) def test_drop_table():
self.assertEqual(ret, "ALTER TABLE category DROP FOREIGN KEY fk_category_user_e2e3874c") ret = Migrate.ddl.drop_table(Category)
assert ret == "DROP TABLE IF EXISTS category"
def test_add_column():
ret = Migrate.ddl.add_column(Category, Category._meta.fields_map.get("name"))
if isinstance(Migrate.ddl, MysqlDDL):
assert ret == "ALTER TABLE category ADD `name` VARCHAR(200) NOT NULL"
elif isinstance(Migrate.ddl, PostgresDDL):
assert ret == 'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL'
elif isinstance(Migrate.ddl, SqliteDDL):
assert ret == 'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL'
def test_drop_column():
ret = Migrate.ddl.drop_column(Category, "name")
assert ret == "ALTER TABLE category DROP COLUMN name"
assert ret == "ALTER TABLE category DROP COLUMN name"
def test_add_index():
index = Migrate.ddl.add_index(Category, ["name"])
index_u = Migrate.ddl.add_index(Category, ["name"], True)
if isinstance(Migrate.ddl, MysqlDDL):
assert index == "ALTER TABLE category ADD INDEX idx_category_name_8b0cb9 (`name`)"
assert index_u == "ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 (`name`)"
elif isinstance(Migrate.ddl, SqliteDDL):
assert index_u == 'ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 ("name")'
assert index_u == 'ALTER TABLE category ADD UNIQUE INDEX uid_category_name_8b0cb9 ("name")'
def test_drop_index():
ret = Migrate.ddl.drop_index(Category, ["name"])
assert ret == "ALTER TABLE category DROP INDEX idx_category_name_8b0cb9"
ret = Migrate.ddl.drop_index(Category, ["name"], True)
assert ret == "ALTER TABLE category DROP INDEX uid_category_name_8b0cb9"
def test_add_fk():
ret = Migrate.ddl.add_fk(Category, Category._meta.fields_map.get("user"))
assert (
ret
== "ALTER TABLE category ADD CONSTRAINT `fk_category_user_e2e3874c` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE"
)
def test_drop_fk():
ret = Migrate.ddl.drop_fk(Category, Category._meta.fields_map.get("user"))
assert ret == "ALTER TABLE category DROP FOREIGN KEY fk_category_user_e2e3874c"

41
tests/test_migrate.py Normal file
View File

@@ -0,0 +1,41 @@
from tortoise import Tortoise
from aerich.ddl.mysql import MysqlDDL
from aerich.ddl.postgres import PostgresDDL
from aerich.ddl.sqlite import SqliteDDL
from aerich.migrate import Migrate
def test_migrate():
apps = Tortoise.apps
models = apps.get("models")
diff_models = apps.get("diff_models")
Migrate.diff_models(diff_models, models)
Migrate.diff_models(models, diff_models, False)
if isinstance(Migrate.ddl, MysqlDDL):
assert Migrate.upgrade_operators == [
"ALTER TABLE category ADD `name` VARCHAR(200) NOT NULL",
"ALTER TABLE user ADD UNIQUE INDEX uid_user_usernam_9987ab (`username`)",
]
assert Migrate.downgrade_operators == [
"ALTER TABLE category DROP COLUMN name",
"ALTER TABLE user DROP INDEX uid_user_usernam_9987ab",
]
elif isinstance(Migrate.ddl, SqliteDDL):
assert Migrate.upgrade_operators == [
'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL',
'ALTER TABLE user ADD UNIQUE INDEX uid_user_usernam_9987ab ("username")',
]
assert Migrate.downgrade_operators == [
"ALTER TABLE category DROP COLUMN name",
"ALTER TABLE user DROP INDEX uid_user_usernam_9987ab",
]
elif isinstance(Migrate.ddl, PostgresDDL):
assert Migrate.upgrade_operators == [
'ALTER TABLE category ADD "name" VARCHAR(200) NOT NULL',
'ALTER TABLE user ADD UNIQUE INDEX uid_user_usernam_9987ab ("username")',
]
assert Migrate.downgrade_operators == [
"ALTER TABLE category DROP COLUMN name",
"ALTER TABLE user DROP INDEX uid_user_usernam_9987ab",
]