Compare commits
	
		
			70 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | cd176c1fd6 | ||
|  | c2819fc8dc | ||
|  | 530e7cfce5 | ||
|  | 47824a100b | ||
|  | 78a15f9f19 | ||
|  | 5ae8b9e85f | ||
|  | 55a6d4bbc7 | ||
|  | c5535f16e1 | ||
|  | 840cd71e44 | ||
|  | e0d52b1210 | ||
|  | 4dc45f723a | ||
|  | d2e0a68351 | ||
|  | ee6cc20c7d | ||
|  | 4e917495a0 | ||
|  | bfa66f6dd4 | ||
|  | f00715d4c4 | ||
|  | 6e3105690a | ||
|  | c707f7ecb2 | ||
|  | 0bbc471e00 | ||
|  | fb6cc62047 | ||
|  | e9ceaf471f | ||
|  | 85fc3b2aa2 | ||
|  | a677d506a9 | ||
|  | 9879004fee | ||
|  | 5760fe2040 | ||
|  | b229c30558 | ||
|  | 5d2f1604c3 | ||
|  | 499c4e1c02 | ||
|  | 1463ee30bc | ||
|  | 3b801932f5 | ||
|  | c2eb4dc9e3 | ||
|  | 5927febd0c | ||
|  | a1c10ff330 | ||
|  | f2013c931a | ||
|  | b21b954d32 | ||
|  | f5588a35c5 | ||
|  | f5dff84476 | ||
|  | e399821116 | ||
|  | 648f25a951 | ||
|  | fa73e132e2 | ||
|  | 1bac33cd33 | ||
|  | 4e76f12ccf | ||
|  | 724379700e | ||
|  | bb929f2b55 | ||
|  | 6339dc86a8 | ||
|  | 768747140a | ||
|  | 1fde3cd04e | ||
|  | d0ce545ff5 | ||
|  | 09b89ed7d0 | ||
|  | 86c8382593 | ||
|  | 48e3ff48a3 | ||
|  | 1bf6d45bb0 | ||
|  | 342f4cdd3b | ||
|  | 8cace21fde | ||
|  | 9889d9492b | ||
|  | 823368aea8 | ||
|  | 6b1ad46cf1 | ||
|  | ce8c0b1f06 | ||
|  | 43922d3734 | ||
|  | 48c5318737 | ||
|  | 002221e557 | ||
|  | 141d7205bf | ||
|  | af4d4be19a | ||
|  | 3b4d9b47ce | ||
|  | 4b0c4ae7d0 | ||
|  | dc821d8a02 | ||
|  | d18a6b5be0 | ||
|  | 1e56a70f21 | ||
|  | ab1e1aab75 | ||
|  | 00dd04f97d | 
							
								
								
									
										5
									
								
								.github/workflows/pypi.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/pypi.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,10 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.x' |           python-version: '3.x' | ||||||
|       - uses: dschep/install-poetry-action@v1.3 |       - name: Install and configure Poetry | ||||||
|  |         uses: snok/install-poetry@v1.1.1 | ||||||
|  |         with: | ||||||
|  |           virtualenvs-create: false | ||||||
|       - name: Build dists |       - name: Build dists | ||||||
|         run: make build |         run: make build | ||||||
|       - name: Pypi Publish |       - name: Pypi Publish | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| name: test | name: test | ||||||
| on: [push, pull_request] | on: [ push, pull_request ] | ||||||
| jobs: | jobs: | ||||||
|   testall: |   testall: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| @@ -19,7 +19,10 @@ jobs: | |||||||
|       - uses: actions/setup-python@v2 |       - uses: actions/setup-python@v2 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.x' |           python-version: '3.x' | ||||||
|       - uses: dschep/install-poetry-action@v1.3 |       - name: Install and configure Poetry | ||||||
|  |         uses: snok/install-poetry@v1.1.1 | ||||||
|  |         with: | ||||||
|  |           virtualenvs-create: false | ||||||
|       - name: CI |       - name: CI | ||||||
|         env: |         env: | ||||||
|           MYSQL_PASS: root |           MYSQL_PASS: root | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -144,3 +144,5 @@ cython_debug/ | |||||||
| migrations | migrations | ||||||
| aerich.ini | aerich.ini | ||||||
| src | src | ||||||
|  | .vscode | ||||||
|  | .DS_Store | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,7 +1,73 @@ | |||||||
| # ChangeLog | # ChangeLog | ||||||
|  |  | ||||||
|  | ## 0.4 | ||||||
|  |  | ||||||
|  | ### 0.4.4 | ||||||
|  |  | ||||||
|  | - Fix unnecessary import. (#113) | ||||||
|  |  | ||||||
|  | ### 0.4.3 | ||||||
|  |  | ||||||
|  | - Replace migrations separator to sql standard comment. | ||||||
|  | - Add `inspectdb` command. | ||||||
|  |  | ||||||
|  | ### 0.4.2 | ||||||
|  |  | ||||||
|  | - Use `pathlib` for path resolving. (#89) | ||||||
|  | - Fix upgrade in new db. (#96) | ||||||
|  | - Fix packaging error. (#92) | ||||||
|  |  | ||||||
|  | ### 0.4.1 | ||||||
|  |  | ||||||
|  | - Bug fix. (#91 #93) | ||||||
|  |  | ||||||
|  | ### 0.4.0 | ||||||
|  |  | ||||||
|  | - Use `.sql` instead of `.json` to store version file. | ||||||
|  | - Add `rename` column support MySQL5. | ||||||
|  | - Remove callable detection for defaults. (#87) | ||||||
|  | - Fix `sqlite` stuck. (#90) | ||||||
|  |  | ||||||
|  | ## 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 table in database. | ||||||
|  |     3. Delete `migrations/{app}` folder and rerun `aerich init-db`. | ||||||
|  |     4. Update model and `aerich migrate` normally. | ||||||
|  |  | ||||||
| ## 0.2 | ## 0.2 | ||||||
|  |  | ||||||
|  | ### 0.2.5 | ||||||
|  |  | ||||||
|  | - Fix windows support. (#46) | ||||||
|  | - Support `db_constraint` in fk, m2m should manual define table with fk. (#52) | ||||||
|  |  | ||||||
|  | ### 0.2.4 | ||||||
|  |  | ||||||
|  | - Raise error with SQLite unsupported features. | ||||||
|  | - Fix Postgres alter table. (#48) | ||||||
|  | - Add `Rename` support. | ||||||
|  |  | ||||||
| ### 0.2.3 | ### 0.2.3 | ||||||
|  |  | ||||||
| - Fix tortoise ssl config. | - Fix tortoise ssl config. | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								Makefile
									
									
									
									
									
								
							| @@ -3,8 +3,10 @@ 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" | ||||||
| MYSQL_PORT ?= 3306 | MYSQL_PORT ?= 3306 | ||||||
|  | MYSQL_PASS ?= "123456" | ||||||
| POSTGRES_HOST ?= "127.0.0.1" | POSTGRES_HOST ?= "127.0.0.1" | ||||||
| POSTGRES_PORT ?= 5432 | POSTGRES_PORT ?= 5432 | ||||||
|  | POSTGRES_PASS ?= "123456" | ||||||
|  |  | ||||||
| help: | help: | ||||||
| 	@echo "Aerich development makefile" | 	@echo "Aerich development makefile" | ||||||
| @@ -22,7 +24,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) | ||||||
| @@ -40,10 +42,10 @@ test_sqlite: | |||||||
| 	$(py_warn) TEST_DB=sqlite://:memory: py.test | 	$(py_warn) TEST_DB=sqlite://:memory: py.test | ||||||
|  |  | ||||||
| test_mysql: | test_mysql: | ||||||
| 	$(py_warn) TEST_DB="mysql://root:$(MYSQL_PASS)@$(MYSQL_HOST):$(MYSQL_PORT)/test_\{\}" py.test | 	$(py_warn) TEST_DB="mysql://root:$(MYSQL_PASS)@$(MYSQL_HOST):$(MYSQL_PORT)/test_\{\}" pytest -vv -s | ||||||
|  |  | ||||||
| test_postgres: | test_postgres: | ||||||
| 	$(py_warn) TEST_DB="postgres://postgres:$(POSTGRES_PASS)@$(POSTGRES_HOST):$(POSTGRES_PORT)/test_\{\}" py.test | 	$(py_warn) TEST_DB="postgres://postgres:$(POSTGRES_PASS)@$(POSTGRES_HOST):$(POSTGRES_PORT)/test_\{\}" pytest | ||||||
|  |  | ||||||
| testall: deps test_sqlite test_postgres test_mysql | testall: deps test_sqlite test_postgres test_mysql | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										114
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								README.md
									
									
									
									
									
								
							| @@ -7,12 +7,10 @@ | |||||||
|  |  | ||||||
| ## Introduction | ## Introduction | ||||||
|  |  | ||||||
| Tortoise-ORM is the best asyncio ORM now, but it lacks a database | Aerich is a database migrations tool for Tortoise-ORM, which like alembic for SQLAlchemy, or Django ORM with it\'s own | ||||||
| migrations tool like alembic for SQLAlchemy, or Django ORM with it\'s | migrations solution. | ||||||
| own migrations tool. |  | ||||||
|  |  | ||||||
| This project aim to be a best migrations tool for Tortoise-ORM and which | **Important: You can only use absolutely import in your `models.py` to make `aerich` work.** | ||||||
| written by one of contributors of Tortoise-ORM. |  | ||||||
|  |  | ||||||
| ## Install | ## Install | ||||||
|  |  | ||||||
| @@ -25,7 +23,7 @@ Just install from pypi: | |||||||
| ## Quick Start | ## Quick Start | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich -h | > aerich -h | ||||||
|  |  | ||||||
| Usage: aerich [OPTIONS] COMMAND [ARGS]... | Usage: aerich [OPTIONS] COMMAND [ARGS]... | ||||||
|  |  | ||||||
| @@ -37,19 +35,19 @@ 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. | ||||||
|   init-db    Generate schema and generate app migrate location. |   init-db    Generate schema and generate app migrate location. | ||||||
|  |   inspectdb  Introspects the database tables to standard output as... | ||||||
|   migrate    Generate migrate changes file. |   migrate    Generate migrate changes file. | ||||||
|   upgrade    Upgrade to latest version. |   upgrade    Upgrade to latest version. | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Usage | ## Usage | ||||||
|  |  | ||||||
| You need add `aerich.models` to your `Tortoise-ORM` config first, | You need add `aerich.models` to your `Tortoise-ORM` config first, example: | ||||||
| example: |  | ||||||
|  |  | ||||||
| ```python | ```python | ||||||
| TORTOISE_ORM = { | TORTOISE_ORM = { | ||||||
| @@ -66,7 +64,7 @@ TORTOISE_ORM = { | |||||||
| ### Initialization | ### Initialization | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich init -h | > aerich init -h | ||||||
|  |  | ||||||
| Usage: aerich init [OPTIONS] | Usage: aerich init [OPTIONS] | ||||||
|  |  | ||||||
| @@ -82,7 +80,7 @@ Options: | |||||||
| Init config file and location: | Init config file and location: | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich init -t tests.backends.mysql.TORTOISE_ORM | > aerich init -t tests.backends.mysql.TORTOISE_ORM | ||||||
|  |  | ||||||
| Success create migrate location ./migrations | Success create migrate location ./migrations | ||||||
| Success generate config file aerich.ini | Success generate config file aerich.ini | ||||||
| @@ -91,7 +89,7 @@ Success generate config file aerich.ini | |||||||
| ### Init db | ### Init db | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich init-db | > aerich 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" | ||||||
| @@ -103,54 +101,114 @@ If your Tortoise-ORM app is not default `models`, you must specify | |||||||
| ### Update models and make migrate | ### Update models and make migrate | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich migrate --name drop_column | > aerich migrate --name drop_column | ||||||
|  |  | ||||||
| Success migrate 1_202029051520102929_drop_column.json | Success migrate 1_202029051520102929_drop_column.sql | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Format of migrate filename is | Format of migrate filename is | ||||||
| `{version_num}_{datetime}_{name|update}.json` | `{version_num}_{datetime}_{name|update}.sql`. | ||||||
|  |  | ||||||
|  | And if `aerich` guess you are renaming a column, it will ask `Rename {old_column} to {new_column} [True]`, you can | ||||||
|  | choice `True` to rename column without column drop, or choice `False` to drop column then create. | ||||||
|  |  | ||||||
| ### Upgrade to latest version | ### Upgrade to latest version | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich upgrade | > aerich upgrade | ||||||
|  |  | ||||||
| Success upgrade 1_202029051520102929_drop_column.json | Success upgrade 1_202029051520102929_drop_column.sql | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Now your db is migrated to latest. | Now your db is migrated to latest. | ||||||
|  |  | ||||||
| ### Downgrade to previous version | ### Downgrade to specified version | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich downgrade | > aerich init -h | ||||||
|  |  | ||||||
| Success downgrade 1_202029051520102929_drop_column.json | Usage: aerich downgrade [OPTIONS] | ||||||
|  |  | ||||||
|  |   Downgrade to specified version. | ||||||
|  |  | ||||||
|  | Options: | ||||||
|  |   -v, --version INTEGER  Specified version, default to last.  [default: -1] | ||||||
|  |   -d, --delete           Delete version files at the same time.  [default: | ||||||
|  |                          False] | ||||||
|  |  | ||||||
|  |   --yes                  Confirm the action without prompting. | ||||||
|  |   -h, --help             Show this message and exit. | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Now your db rollback to previous version. | ```shell | ||||||
|  | > aerich downgrade | ||||||
|  |  | ||||||
|  | Success downgrade 1_202029051520102929_drop_column.sql | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Now your db rollback to specified version. | ||||||
|  |  | ||||||
| ### Show history | ### Show history | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich history | > aerich history | ||||||
|  |  | ||||||
| 1_202029051520102929_drop_column.json | 1_202029051520102929_drop_column.sql | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Show heads to be migrated | ### Show heads to be migrated | ||||||
|  |  | ||||||
| ```shell | ```shell | ||||||
| $ aerich heads | > aerich heads | ||||||
|  |  | ||||||
| 1_202029051520102929_drop_column.json | 1_202029051520102929_drop_column.sql | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Support this project | ### Inspect db tables to TortoiseORM model | ||||||
|  |  | ||||||
| - Just give a star! | ```shell | ||||||
| - Donation. | Usage: aerich inspectdb [OPTIONS] | ||||||
|  |  | ||||||
|  |   Introspects the database tables to standard output as TortoiseORM model. | ||||||
|  |  | ||||||
|  | Options: | ||||||
|  |   -t, --table TEXT  Which tables to inspect. | ||||||
|  |   -h, --help        Show this message and exit. | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Inspect all tables and print to console: | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | aerich --app models inspectdb -t user  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Inspect a specified table in default app and redirect to `models.py`: | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | aerich inspectdb -t user > models.py | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Note that this command is restricted, which is not supported in some solutions, such as `IntEnumField` | ||||||
|  | and `ForeignKeyField` and so on. | ||||||
|  |  | ||||||
|  | ### Multiple databases | ||||||
|  |  | ||||||
|  | ```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                                                           | | | AliPay                                                                                 | WeChatPay                                                                                 | PayPal                                                           | | ||||||
| | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "0.2.3" | __version__ = "0.4.4" | ||||||
|   | |||||||
							
								
								
									
										219
									
								
								aerich/cli.py
									
									
									
									
									
								
							
							
						
						
									
										219
									
								
								aerich/cli.py
									
									
									
									
									
								
							| @@ -1,47 +1,56 @@ | |||||||
| import functools | import asyncio | ||||||
| import json |  | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
| from configparser import ConfigParser | from configparser import ConfigParser | ||||||
| from enum import Enum | from functools import wraps | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import List | ||||||
|  |  | ||||||
| import asyncclick as click | import click | ||||||
| from asyncclick import Context, UsageError | from click import Context, UsageError | ||||||
| from tortoise import Tortoise, generate_schema_for_client | from tortoise import Tortoise, generate_schema_for_client | ||||||
| from tortoise.exceptions import OperationalError | from tortoise.exceptions import OperationalError | ||||||
| from tortoise.transactions import in_transaction | from tortoise.transactions import in_transaction | ||||||
| from tortoise.utils import get_schema_sql | from tortoise.utils import get_schema_sql | ||||||
|  |  | ||||||
|  | from aerich.inspectdb import InspectDb | ||||||
| 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, | ||||||
|  |     get_version_content_from_file, | ||||||
|  |     write_version_file, | ||||||
|  | ) | ||||||
|  |  | ||||||
| from . import __version__ | from . import __version__ | ||||||
|  | from .enums import Color | ||||||
| from .models import Aerich | from .models import Aerich | ||||||
|  |  | ||||||
|  |  | ||||||
| class Color(str, Enum): |  | ||||||
|     green = "green" |  | ||||||
|     red = "red" |  | ||||||
|     yellow = "yellow" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| parser = ConfigParser() | parser = ConfigParser() | ||||||
|  |  | ||||||
|  |  | ||||||
| def close_db(func): | def coro(f): | ||||||
|     @functools.wraps(func) |     @wraps(f) | ||||||
|     async def close_db_inner(*args, **kwargs): |     def wrapper(*args, **kwargs): | ||||||
|         result = await func(*args, **kwargs) |         loop = asyncio.get_event_loop() | ||||||
|         await Tortoise.close_connections() |         ctx = args[0] | ||||||
|         return result |         loop.run_until_complete(f(*args, **kwargs)) | ||||||
|  |         app = ctx.obj.get("app") | ||||||
|  |         if app: | ||||||
|  |             Migrate.remove_old_model_file(app, ctx.obj["location"]) | ||||||
|  |  | ||||||
|     return close_db_inner |     return wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
| @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( | ||||||
| @@ -52,6 +61,7 @@ def close_db(func): | |||||||
|     help="Name of section in .ini file to use for aerich config.", |     help="Name of section in .ini file to use for aerich config.", | ||||||
| ) | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
|  | @coro | ||||||
| async def cli(ctx: Context, config, app, name): | async def cli(ctx: Context, config, app, name): | ||||||
|     ctx.ensure_object(dict) |     ctx.ensure_object(dict) | ||||||
|     ctx.obj["config_file"] = config |     ctx.obj["config_file"] = config | ||||||
| @@ -59,7 +69,7 @@ async def cli(ctx: Context, config, app, name): | |||||||
|  |  | ||||||
|     invoked_subcommand = ctx.invoked_subcommand |     invoked_subcommand = ctx.invoked_subcommand | ||||||
|     if invoked_subcommand != "init": |     if invoked_subcommand != "init": | ||||||
|         if not os.path.exists(config): |         if not Path(config).exists(): | ||||||
|             raise UsageError("You must exec init first", ctx=ctx) |             raise UsageError("You must exec init first", ctx=ctx) | ||||||
|         parser.read(config) |         parser.read(config) | ||||||
|  |  | ||||||
| @@ -68,85 +78,113 @@ 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 | ||||||
|  |         Migrate.app = app | ||||||
|         if invoked_subcommand != "init-db": |         if invoked_subcommand != "init-db": | ||||||
|  |             if not Path(location, app).exists(): | ||||||
|  |                 raise UsageError("You must exec init-db first", ctx=ctx) | ||||||
|             await Migrate.init_with_old_models(tortoise_config, app, location) |             await Migrate.init_with_old_models(tortoise_config, app, location) | ||||||
|  |  | ||||||
|  |  | ||||||
| @cli.command(help="Generate migrate changes file.") | @cli.command(help="Generate migrate changes file.") | ||||||
| @click.option("--name", default="update", show_default=True, help="Migrate name.") | @click.option("--name", default="update", show_default=True, help="Migrate name.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
| @close_db | @coro | ||||||
| async def migrate(ctx: Context, name): | async def migrate(ctx: Context, name): | ||||||
|     config = ctx.obj["config"] |  | ||||||
|     location = ctx.obj["location"] |  | ||||||
|     app = ctx.obj["app"] |  | ||||||
|  |  | ||||||
|     ret = await Migrate.migrate(name) |     ret = await Migrate.migrate(name) | ||||||
|     if not ret: |     if not ret: | ||||||
|         return click.secho("No changes detected", fg=Color.yellow) |         return click.secho("No changes detected", fg=Color.yellow) | ||||||
|     Migrate.write_old_models(config, app, location) |  | ||||||
|     click.secho(f"Success migrate {ret}", fg=Color.green) |     click.secho(f"Success migrate {ret}", fg=Color.green) | ||||||
|  |  | ||||||
|  |  | ||||||
| @cli.command(help="Upgrade to latest version.") | @cli.command(help="Upgrade to specified version.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
| @close_db | @coro | ||||||
| async def upgrade(ctx: Context): | async def upgrade(ctx: Context): | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|  |     location = ctx.obj["location"] | ||||||
|     migrated = False |     migrated = False | ||||||
|     for version in Migrate.get_all_version_files(): |     for version_file in Migrate.get_all_version_files(): | ||||||
|         try: |         try: | ||||||
|             exists = await Aerich.exists(version=version, app=app) |             exists = await Aerich.exists(version=version_file, app=app) | ||||||
|         except OperationalError: |         except OperationalError: | ||||||
|             exists = False |             exists = False | ||||||
|         if not exists: |         if not exists: | ||||||
|             async with in_transaction(get_app_connection_name(config, app)) as conn: |             async with in_transaction(get_app_connection_name(config, app)) as conn: | ||||||
|                 file_path = os.path.join(Migrate.migrate_location, version) |                 file_path = Path(Migrate.migrate_location, version_file) | ||||||
|                 with open(file_path, "r", encoding="utf-8") as f: |                 content = get_version_content_from_file(file_path) | ||||||
|                     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_script(upgrade_query) |                     await conn.execute_script(upgrade_query) | ||||||
|                 await Aerich.create(version=version, app=app) |                 await Aerich.create( | ||||||
|             click.secho(f"Success upgrade {version}", fg=Color.green) |                     version=version_file, | ||||||
|  |                     app=app, | ||||||
|  |                     content=Migrate.get_models_content(config, app, location), | ||||||
|  |                 ) | ||||||
|  |             click.secho(f"Success upgrade {version_file}", fg=Color.green) | ||||||
|             migrated = True |             migrated = True | ||||||
|     if not migrated: |     if not migrated: | ||||||
|         click.secho("No migrate items", fg=Color.yellow) |         click.secho("No upgrade items found", fg=Color.yellow) | ||||||
|  |  | ||||||
|  |  | ||||||
| @cli.command(help="Downgrade to previous version.") | @cli.command(help="Downgrade to specified version.") | ||||||
|  | @click.option( | ||||||
|  |     "-v", | ||||||
|  |     "--version", | ||||||
|  |     default=-1, | ||||||
|  |     type=int, | ||||||
|  |     show_default=True, | ||||||
|  |     help="Specified version, default to last.", | ||||||
|  | ) | ||||||
|  | @click.option( | ||||||
|  |     "-d", | ||||||
|  |     "--delete", | ||||||
|  |     is_flag=True, | ||||||
|  |     default=False, | ||||||
|  |     show_default=True, | ||||||
|  |     help="Delete version files at the same time.", | ||||||
|  | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
| @close_db | @click.confirmation_option( | ||||||
| async def downgrade(ctx: Context): |     prompt="Downgrade is dangerous, which maybe lose your data, are you sure?", | ||||||
|  | ) | ||||||
|  | @coro | ||||||
|  | async def downgrade(ctx: Context, version: int, delete: bool): | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     last_version = await Migrate.get_last_version() |     if version == -1: | ||||||
|     if not last_version: |         specified_version = await Migrate.get_last_version() | ||||||
|         return click.secho("No last version found", fg=Color.yellow) |     else: | ||||||
|     file = last_version.version |         specified_version = await Aerich.filter(app=app, version__startswith=f"{version}_").first() | ||||||
|  |     if not specified_version: | ||||||
|  |         return click.secho("No specified version found", fg=Color.yellow) | ||||||
|  |     if version == -1: | ||||||
|  |         versions = [specified_version] | ||||||
|  |     else: | ||||||
|  |         versions = await Aerich.filter(app=app, pk__gte=specified_version.pk) | ||||||
|  |     for version in versions: | ||||||
|  |         file = 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: | ||||||
|         file_path = os.path.join(Migrate.migrate_location, file) |             file_path = Path(Migrate.migrate_location, file) | ||||||
|         with open(file_path, "r", encoding="utf-8") as f: |             content = get_version_content_from_file(file_path) | ||||||
|             content = json.load(f) |  | ||||||
|             downgrade_query_list = content.get("downgrade") |             downgrade_query_list = content.get("downgrade") | ||||||
|             if not downgrade_query_list: |             if not downgrade_query_list: | ||||||
|                 return click.secho("No downgrade item found", fg=Color.yellow) |                 click.secho("No downgrade items found", fg=Color.yellow) | ||||||
|  |                 return | ||||||
|             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 version.delete() | ||||||
|         return click.secho(f"Success downgrade {file}", fg=Color.green) |             if delete: | ||||||
|  |                 os.unlink(file_path) | ||||||
|  |             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 | ||||||
| @close_db | @coro | ||||||
| async def heads(ctx: Context): | async def heads(ctx: Context): | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|     versions = Migrate.get_all_version_files() |     versions = Migrate.get_all_version_files() | ||||||
| @@ -156,12 +194,12 @@ async def heads(ctx: Context): | |||||||
|             click.secho(version, fg=Color.green) |             click.secho(version, fg=Color.green) | ||||||
|             is_heads = True |             is_heads = True | ||||||
|     if not is_heads: |     if not is_heads: | ||||||
|         click.secho("No available heads,try migrate", fg=Color.green) |         click.secho("No available heads,try migrate first", fg=Color.green) | ||||||
|  |  | ||||||
|  |  | ||||||
| @cli.command(help="List all migrate items.") | @cli.command(help="List all migrate items.") | ||||||
| @click.pass_context | @click.pass_context | ||||||
| @close_db | @coro | ||||||
| async def history(ctx: Context): | async def history(ctx: Context): | ||||||
|     versions = Migrate.get_all_version_files() |     versions = Migrate.get_all_version_files() | ||||||
|     for version in versions: |     for version in versions: | ||||||
| @@ -178,15 +216,21 @@ 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 | ||||||
| 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"] | ||||||
|     if os.path.exists(config_file): |     if Path(config_file).exists(): | ||||||
|         return click.secho("You have inited", fg=Color.yellow) |         return click.secho("You have inited", fg=Color.yellow) | ||||||
|  |  | ||||||
|     parser.add_section(name) |     parser.add_section(name) | ||||||
| @@ -196,8 +240,7 @@ async def init( | |||||||
|     with open(config_file, "w", encoding="utf-8") as f: |     with open(config_file, "w", encoding="utf-8") as f: | ||||||
|         parser.write(f) |         parser.write(f) | ||||||
|  |  | ||||||
|     if not os.path.isdir(location): |     Path(location).mkdir(parents=True, exist_ok=True) | ||||||
|         os.mkdir(location) |  | ||||||
|  |  | ||||||
|     click.secho(f"Success create migrate location {location}", fg=Color.green) |     click.secho(f"Success create migrate location {location}", fg=Color.green) | ||||||
|     click.secho(f"Success generate config file {config_file}", fg=Color.green) |     click.secho(f"Success generate config file {config_file}", fg=Color.green) | ||||||
| @@ -212,20 +255,20 @@ async def init( | |||||||
|     show_default=True, |     show_default=True, | ||||||
| ) | ) | ||||||
| @click.pass_context | @click.pass_context | ||||||
| @close_db | @coro | ||||||
| async def init_db(ctx: Context, safe): | async def init_db(ctx: Context, safe): | ||||||
|     config = ctx.obj["config"] |     config = ctx.obj["config"] | ||||||
|     location = ctx.obj["location"] |     location = ctx.obj["location"] | ||||||
|     app = ctx.obj["app"] |     app = ctx.obj["app"] | ||||||
|  |  | ||||||
|     dirname = os.path.join(location, app) |     dirname = Path(location, app) | ||||||
|     if not os.path.isdir(dirname): |     try: | ||||||
|         os.mkdir(dirname) |         dirname.mkdir(parents=True) | ||||||
|         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: |     except FileExistsError: | ||||||
|         return click.secho(f"Inited {app} already", fg=Color.yellow) |         return click.secho( | ||||||
|  |             f"Inited {app} already, or delete {dirname} and try again.", fg=Color.yellow | ||||||
|     Migrate.write_old_models(config, app, location) |         ) | ||||||
|  |  | ||||||
|     await Tortoise.init(config=config) |     await Tortoise.init(config=config) | ||||||
|     connection = get_app_connection(config, app) |     connection = get_app_connection(config, app) | ||||||
| @@ -234,15 +277,41 @@ async def init_db(ctx: Context, safe): | |||||||
|     schema = get_schema_sql(connection, safe) |     schema = get_schema_sql(connection, safe) | ||||||
|  |  | ||||||
|     version = await Migrate.generate_version() |     version = await Migrate.generate_version() | ||||||
|     await Aerich.create(version=version, app=app) |     await Aerich.create( | ||||||
|     with open(os.path.join(dirname, version), "w", encoding="utf-8") as f: |         version=version, | ||||||
|  |         app=app, | ||||||
|  |         content=Migrate.get_models_content(config, app, location), | ||||||
|  |     ) | ||||||
|     content = { |     content = { | ||||||
|         "upgrade": [schema], |         "upgrade": [schema], | ||||||
|     } |     } | ||||||
|         json.dump(content, f, ensure_ascii=False, indent=2) |     write_version_file(Path(dirname, version), content) | ||||||
|     return click.secho(f'Success generate schema for app "{app}"', fg=Color.green) |     click.secho(f'Success generate schema for app "{app}"', fg=Color.green) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @cli.command(help="Introspects the database tables to standard output as TortoiseORM model.") | ||||||
|  | @click.option( | ||||||
|  |     "-t", | ||||||
|  |     "--table", | ||||||
|  |     help="Which tables to inspect.", | ||||||
|  |     multiple=True, | ||||||
|  |     required=False, | ||||||
|  | ) | ||||||
|  | @click.pass_context | ||||||
|  | @coro | ||||||
|  | async def inspectdb(ctx: Context, table: List[str]): | ||||||
|  |     config = ctx.obj["config"] | ||||||
|  |     app = ctx.obj["app"] | ||||||
|  |     connection = get_app_connection(config, app) | ||||||
|  |  | ||||||
|  |     inspect = InspectDb(connection, table) | ||||||
|  |     await inspect.inspect() | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     sys.path.insert(0, ".") |     sys.path.insert(0, ".") | ||||||
|     cli(_anyio_backend="asyncio") |     cli() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from typing import List, Type | |||||||
|  |  | ||||||
| from tortoise import BaseDBAsyncClient, ForeignKeyFieldInstance, ManyToManyFieldInstance, Model | from tortoise import BaseDBAsyncClient, ForeignKeyFieldInstance, ManyToManyFieldInstance, Model | ||||||
| from tortoise.backends.base.schema_generator import BaseSchemaGenerator | from tortoise.backends.base.schema_generator import BaseSchemaGenerator | ||||||
| from tortoise.fields import Field, JSONField, TextField, UUIDField | from tortoise.fields import CASCADE, Field, JSONField, TextField, UUIDField | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseDDL: | class BaseDDL: | ||||||
| @@ -11,14 +11,20 @@ class BaseDDL: | |||||||
|     _DROP_TABLE_TEMPLATE = 'DROP TABLE IF EXISTS "{table_name}"' |     _DROP_TABLE_TEMPLATE = 'DROP TABLE IF EXISTS "{table_name}"' | ||||||
|     _ADD_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" ADD {column}' |     _ADD_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" ADD {column}' | ||||||
|     _DROP_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" DROP COLUMN "{column_name}"' |     _DROP_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" DROP COLUMN "{column_name}"' | ||||||
|  |     _RENAME_COLUMN_TEMPLATE = ( | ||||||
|  |         'ALTER TABLE "{table_name}" RENAME COLUMN "{old_column_name}" TO "{new_column_name}"' | ||||||
|  |     ) | ||||||
|     _ADD_INDEX_TEMPLATE = ( |     _ADD_INDEX_TEMPLATE = ( | ||||||
|         'ALTER TABLE "{table_name}" ADD {unique} INDEX "{index_name}" ({column_names})' |         'ALTER TABLE "{table_name}" ADD {unique} INDEX "{index_name}" ({column_names})' | ||||||
|     ) |     ) | ||||||
|     _DROP_INDEX_TEMPLATE = 'ALTER TABLE "{table_name}" DROP INDEX "{index_name}"' |     _DROP_INDEX_TEMPLATE = 'ALTER TABLE "{table_name}" DROP INDEX "{index_name}"' | ||||||
|     _ADD_FK_TEMPLATE = 'ALTER TABLE "{table_name}" ADD CONSTRAINT "{fk_name}" FOREIGN KEY ("{db_column}") REFERENCES "{table}" ("{field}") ON DELETE {on_delete}' |     _ADD_FK_TEMPLATE = 'ALTER TABLE "{table_name}" ADD CONSTRAINT "{fk_name}" FOREIGN KEY ("{db_column}") REFERENCES "{table}" ("{field}") ON DELETE {on_delete}' | ||||||
|     _DROP_FK_TEMPLATE = 'ALTER TABLE "{table_name}" DROP FOREIGN KEY "{fk_name}"' |     _DROP_FK_TEMPLATE = 'ALTER TABLE "{table_name}" DROP FOREIGN KEY "{fk_name}"' | ||||||
|     _M2M_TABLE_TEMPLATE = 'CREATE TABLE "{table_name}" ("{backward_key}" {backward_type} NOT NULL REFERENCES "{backward_table}" ("{backward_field}") ON DELETE CASCADE,"{forward_key}" {forward_type} NOT NULL REFERENCES "{forward_table}" ("{forward_field}") ON DELETE CASCADE){extra}{comment};' |     _M2M_TABLE_TEMPLATE = 'CREATE TABLE "{table_name}" ("{backward_key}" {backward_type} NOT NULL REFERENCES "{backward_table}" ("{backward_field}") ON DELETE CASCADE,"{forward_key}" {forward_type} NOT NULL REFERENCES "{forward_table}" ("{forward_field}") ON DELETE {on_delete}){extra}{comment};' | ||||||
|     _MODIFY_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" MODIFY COLUMN {column}' |     _MODIFY_COLUMN_TEMPLATE = 'ALTER TABLE "{table_name}" MODIFY COLUMN {column}' | ||||||
|  |     _CHANGE_COLUMN_TEMPLATE = ( | ||||||
|  |         'ALTER TABLE "{table_name}" CHANGE {old_column_name} {new_column_name} {new_column_type}' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     def __init__(self, client: "BaseDBAsyncClient"): |     def __init__(self, client: "BaseDBAsyncClient"): | ||||||
|         self.client = client |         self.client = client | ||||||
| @@ -41,6 +47,7 @@ class BaseDDL: | |||||||
|             backward_type=model._meta.pk.get_for_dialect(self.DIALECT, "SQL_TYPE"), |             backward_type=model._meta.pk.get_for_dialect(self.DIALECT, "SQL_TYPE"), | ||||||
|             forward_key=field.forward_key, |             forward_key=field.forward_key, | ||||||
|             forward_type=field.related_model._meta.pk.get_for_dialect(self.DIALECT, "SQL_TYPE"), |             forward_type=field.related_model._meta.pk.get_for_dialect(self.DIALECT, "SQL_TYPE"), | ||||||
|  |             on_delete=CASCADE, | ||||||
|             extra=self.schema_generator._table_generate_extra(table=field.through), |             extra=self.schema_generator._table_generate_extra(table=field.through), | ||||||
|             comment=self.schema_generator._table_comment_generator( |             comment=self.schema_generator._table_comment_generator( | ||||||
|                 table=field.through, comment=field.description |                 table=field.through, comment=field.description | ||||||
| @@ -125,6 +132,23 @@ class BaseDDL: | |||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def rename_column(self, model: "Type[Model]", old_column_name: str, new_column_name: str): | ||||||
|  |         return self._RENAME_COLUMN_TEMPLATE.format( | ||||||
|  |             table_name=model._meta.db_table, | ||||||
|  |             old_column_name=old_column_name, | ||||||
|  |             new_column_name=new_column_name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def change_column( | ||||||
|  |         self, model: "Type[Model]", old_column_name: str, new_column_name: str, new_column_type: str | ||||||
|  |     ): | ||||||
|  |         return self._CHANGE_COLUMN_TEMPLATE.format( | ||||||
|  |             table_name=model._meta.db_table, | ||||||
|  |             old_column_name=old_column_name, | ||||||
|  |             new_column_name=new_column_name, | ||||||
|  |             new_column_type=new_column_type, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def add_index(self, model: "Type[Model]", field_names: List[str], unique=False): |     def add_index(self, model: "Type[Model]", field_names: List[str], unique=False): | ||||||
|         return self._ADD_INDEX_TEMPLATE.format( |         return self._ADD_INDEX_TEMPLATE.format( | ||||||
|             unique="UNIQUE" if unique else "", |             unique="UNIQUE" if unique else "", | ||||||
|   | |||||||
| @@ -9,6 +9,9 @@ class MysqlDDL(BaseDDL): | |||||||
|     _DROP_TABLE_TEMPLATE = "DROP TABLE IF EXISTS `{table_name}`" |     _DROP_TABLE_TEMPLATE = "DROP TABLE IF EXISTS `{table_name}`" | ||||||
|     _ADD_COLUMN_TEMPLATE = "ALTER TABLE `{table_name}` ADD {column}" |     _ADD_COLUMN_TEMPLATE = "ALTER TABLE `{table_name}` ADD {column}" | ||||||
|     _DROP_COLUMN_TEMPLATE = "ALTER TABLE `{table_name}` DROP COLUMN `{column_name}`" |     _DROP_COLUMN_TEMPLATE = "ALTER TABLE `{table_name}` DROP COLUMN `{column_name}`" | ||||||
|  |     _RENAME_COLUMN_TEMPLATE = ( | ||||||
|  |         "ALTER TABLE `{table_name}` RENAME COLUMN `{old_column_name}` TO `{new_column_name}`" | ||||||
|  |     ) | ||||||
|     _ADD_INDEX_TEMPLATE = ( |     _ADD_INDEX_TEMPLATE = ( | ||||||
|         "ALTER TABLE `{table_name}` ADD {unique} INDEX `{index_name}` ({column_names})" |         "ALTER TABLE `{table_name}` ADD {unique} INDEX `{index_name}` ({column_names})" | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -1,8 +1,19 @@ | |||||||
|  | from typing import Type | ||||||
|  |  | ||||||
|  | from tortoise import Model | ||||||
| from tortoise.backends.sqlite.schema_generator import SqliteSchemaGenerator | from tortoise.backends.sqlite.schema_generator import SqliteSchemaGenerator | ||||||
|  | from tortoise.fields import Field | ||||||
|  |  | ||||||
| from aerich.ddl import BaseDDL | from aerich.ddl import BaseDDL | ||||||
|  | from aerich.exceptions import NotSupportError | ||||||
|  |  | ||||||
|  |  | ||||||
| class SqliteDDL(BaseDDL): | class SqliteDDL(BaseDDL): | ||||||
|     schema_generator_cls = SqliteSchemaGenerator |     schema_generator_cls = SqliteSchemaGenerator | ||||||
|     DIALECT = SqliteSchemaGenerator.DIALECT |     DIALECT = SqliteSchemaGenerator.DIALECT | ||||||
|  |  | ||||||
|  |     def drop_column(self, model: "Type[Model]", column_name: str): | ||||||
|  |         raise NotSupportError("Drop column is unsupported in SQLite.") | ||||||
|  |  | ||||||
|  |     def modify_column(self, model: "Type[Model]", field_object: Field): | ||||||
|  |         raise NotSupportError("Modify column is unsupported in SQLite.") | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								aerich/enums.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								aerich/enums.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Color(str, Enum): | ||||||
|  |     green = "green" | ||||||
|  |     red = "red" | ||||||
|  |     yellow = "yellow" | ||||||
| @@ -1,6 +1,4 @@ | |||||||
| class ConfigurationError(Exception): | class NotSupportError(Exception): | ||||||
|     """ |     """ | ||||||
|     config error |     raise when features not support | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     pass |  | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								aerich/inspectdb.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								aerich/inspectdb.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | import sys | ||||||
|  | from typing import List, Optional | ||||||
|  |  | ||||||
|  | from ddlparse import DdlParse | ||||||
|  | from tortoise import BaseDBAsyncClient | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InspectDb: | ||||||
|  |     _table_template = "class {table}(Model):\n" | ||||||
|  |     _field_template_mapping = { | ||||||
|  |         "INT": "    {field} = fields.IntField({pk}{unique}{comment})", | ||||||
|  |         "SMALLINT": "    {field} = fields.IntField({pk}{unique}{comment})", | ||||||
|  |         "TINYINT": "    {field} = fields.BooleanField({null}{default}{comment})", | ||||||
|  |         "VARCHAR": "    {field} = fields.CharField({pk}{unique}{length}{null}{default}{comment})", | ||||||
|  |         "LONGTEXT": "    {field} = fields.TextField({null}{default}{comment})", | ||||||
|  |         "TEXT": "    {field} = fields.TextField({null}{default}{comment})", | ||||||
|  |         "DATETIME": "    {field} = fields.DatetimeField({null}{default}{comment})", | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def __init__(self, conn: BaseDBAsyncClient, tables: Optional[List[str]] = None): | ||||||
|  |         self.conn = conn | ||||||
|  |         self.tables = tables | ||||||
|  |         self.DIALECT = conn.schema_generator.DIALECT | ||||||
|  |  | ||||||
|  |     async def show_create_tables(self): | ||||||
|  |         if self.DIALECT == "mysql": | ||||||
|  |             if not self.tables: | ||||||
|  |                 sql_tables = f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{self.conn.database}';"  # nosec: B608 | ||||||
|  |                 ret = await self.conn.execute_query(sql_tables) | ||||||
|  |                 self.tables = map(lambda x: x["TABLE_NAME"], ret[1]) | ||||||
|  |             for table in self.tables: | ||||||
|  |                 sql_show_create_table = f"SHOW CREATE TABLE {table}" | ||||||
|  |                 ret = await self.conn.execute_query(sql_show_create_table) | ||||||
|  |                 yield ret[1][0]["Create Table"] | ||||||
|  |         else: | ||||||
|  |             raise NotImplementedError("Currently only support MySQL") | ||||||
|  |  | ||||||
|  |     async def inspect(self): | ||||||
|  |         ddl_list = self.show_create_tables() | ||||||
|  |         result = "from tortoise import Model, fields\n\n\n" | ||||||
|  |         tables = [] | ||||||
|  |         async for ddl in ddl_list: | ||||||
|  |             parser = DdlParse(ddl, DdlParse.DATABASE.mysql) | ||||||
|  |             table = parser.parse() | ||||||
|  |             name = table.name.title() | ||||||
|  |             columns = table.columns | ||||||
|  |             fields = [] | ||||||
|  |             model = self._table_template.format(table=name) | ||||||
|  |             for column_name, column in columns.items(): | ||||||
|  |                 comment = default = length = unique = null = pk = "" | ||||||
|  |                 if column.primary_key: | ||||||
|  |                     pk = "pk=True, " | ||||||
|  |                 if column.unique: | ||||||
|  |                     unique = "unique=True, " | ||||||
|  |                 if column.data_type == "VARCHAR": | ||||||
|  |                     length = f"max_length={column.length}, " | ||||||
|  |                 if not column.not_null: | ||||||
|  |                     null = "null=True, " | ||||||
|  |                 if column.default is not None: | ||||||
|  |                     if column.data_type == "TINYINT": | ||||||
|  |                         default = f"default={'True' if column.default == '1' else 'False'}, " | ||||||
|  |                     elif column.data_type == "DATETIME": | ||||||
|  |                         if "CURRENT_TIMESTAMP" in column.default: | ||||||
|  |                             if "ON UPDATE CURRENT_TIMESTAMP" in ddl: | ||||||
|  |                                 default = "auto_now_add=True, " | ||||||
|  |                             else: | ||||||
|  |                                 default = "auto_now=True, " | ||||||
|  |                     else: | ||||||
|  |                         default = f"default={column.default}, " | ||||||
|  |  | ||||||
|  |                 if column.comment: | ||||||
|  |                     comment = f"description='{column.comment}', " | ||||||
|  |  | ||||||
|  |                 field = self._field_template_mapping[column.data_type].format( | ||||||
|  |                     field=column_name, | ||||||
|  |                     pk=pk, | ||||||
|  |                     unique=unique, | ||||||
|  |                     length=length, | ||||||
|  |                     null=null, | ||||||
|  |                     default=default, | ||||||
|  |                     comment=comment, | ||||||
|  |                 ) | ||||||
|  |                 fields.append(field) | ||||||
|  |             tables.append(model + "\n".join(fields)) | ||||||
|  |         sys.stdout.write(result + "\n\n\n".join(tables)) | ||||||
| @@ -1,23 +1,29 @@ | |||||||
| import json | import inspect | ||||||
| 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 typing import Dict, List, Tuple, Type | from io import StringIO | ||||||
|  | from pathlib import Path | ||||||
|  | from types import ModuleType | ||||||
|  | from typing import Dict, List, Optional, Tuple, Type | ||||||
|  |  | ||||||
|  | import click | ||||||
| from tortoise import ( | from tortoise import ( | ||||||
|     BackwardFKRelation, |     BackwardFKRelation, | ||||||
|     BackwardOneToOneRelation, |     BackwardOneToOneRelation, | ||||||
|  |     BaseDBAsyncClient, | ||||||
|     ForeignKeyFieldInstance, |     ForeignKeyFieldInstance, | ||||||
|     ManyToManyFieldInstance, |     ManyToManyFieldInstance, | ||||||
|     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 | ||||||
| from aerich.models import MAX_VERSION_LENGTH, Aerich | from aerich.models import MAX_VERSION_LENGTH, Aerich | ||||||
| from aerich.utils import get_app_connection | from aerich.utils import get_app_connection, write_version_file | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migrate: | class Migrate: | ||||||
| @@ -28,6 +34,8 @@ class Migrate: | |||||||
|     _upgrade_m2m: List[str] = [] |     _upgrade_m2m: List[str] = [] | ||||||
|     _downgrade_m2m: List[str] = [] |     _downgrade_m2m: List[str] = [] | ||||||
|     _aerich = Aerich.__name__ |     _aerich = Aerich.__name__ | ||||||
|  |     _rename_old = [] | ||||||
|  |     _rename_new = [] | ||||||
|  |  | ||||||
|     ddl: BaseDDL |     ddl: BaseDDL | ||||||
|     migrate_config: dict |     migrate_config: dict | ||||||
| @@ -36,30 +44,53 @@ class Migrate: | |||||||
|     app: str |     app: str | ||||||
|     migrate_location: str |     migrate_location: str | ||||||
|     dialect: str |     dialect: str | ||||||
|  |     _db_version: Optional[str] = None | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_old_model_file(cls): |     def get_old_model_file(cls, app: str, location: str): | ||||||
|         return cls.old_models + ".py" |         return Path(location, app, cls.old_models + ".py") | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_all_version_files(cls) -> List[str]: |     def get_all_version_files(cls) -> List[str]: | ||||||
|         return sorted( |         return sorted( | ||||||
|             filter(lambda x: x.endswith("json"), os.listdir(cls.migrate_location)), |             filter(lambda x: x.endswith("sql"), os.listdir(cls.migrate_location)), | ||||||
|             key=lambda x: int(x.split("_")[0]), |             key=lambda x: int(x.split("_")[0]), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     async def get_last_version(cls) -> Aerich: |     async def get_last_version(cls) -> Optional[Aerich]: | ||||||
|  |         try: | ||||||
|             return await Aerich.filter(app=cls.app).first() |             return await Aerich.filter(app=cls.app).first() | ||||||
|  |         except OperationalError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def remove_old_model_file(cls, app: str, location: str): | ||||||
|  |         try: | ||||||
|  |             os.unlink(cls.get_old_model_file(app, location)) | ||||||
|  |         except (OSError, FileNotFoundError): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     async def _get_db_version(cls, connection: BaseDBAsyncClient): | ||||||
|  |         if cls.dialect == "mysql": | ||||||
|  |             sql = "select version() as version" | ||||||
|  |             ret = await connection.execute_query(sql) | ||||||
|  |             cls._db_version = ret[1][0].get("version") | ||||||
|  |  | ||||||
|     @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): | ||||||
|         migrate_config = cls._get_migrate_config(config, app, location) |         await Tortoise.init(config=config) | ||||||
|  |         last_version = await cls.get_last_version() | ||||||
|         cls.app = app |         cls.app = app | ||||||
|         cls.migrate_config = migrate_config |         cls.migrate_location = Path(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) | ||||||
|  |  | ||||||
|  |             migrate_config = cls._get_migrate_config(config, app, location) | ||||||
|  |             cls.migrate_config = migrate_config | ||||||
|             await Tortoise.init(config=migrate_config) |             await Tortoise.init(config=migrate_config) | ||||||
|  |  | ||||||
|         connection = get_app_connection(config, app) |         connection = get_app_connection(config, app) | ||||||
| @@ -76,6 +107,7 @@ class Migrate: | |||||||
|             from aerich.ddl.postgres import PostgresDDL |             from aerich.ddl.postgres import PostgresDDL | ||||||
|  |  | ||||||
|             cls.ddl = PostgresDDL(connection) |             cls.ddl = PostgresDDL(connection) | ||||||
|  |         await cls._get_db_version(connection) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     async def _get_last_version_num(cls): |     async def _get_last_version_num(cls): | ||||||
| @@ -90,8 +122,8 @@ class Migrate: | |||||||
|         now = datetime.now().strftime("%Y%m%d%H%M%S").replace("/", "") |         now = datetime.now().strftime("%Y%m%d%H%M%S").replace("/", "") | ||||||
|         last_version_num = await cls._get_last_version_num() |         last_version_num = await cls._get_last_version_num() | ||||||
|         if last_version_num is None: |         if last_version_num is None: | ||||||
|             return f"0_{now}_init.json" |             return f"0_{now}_init.sql" | ||||||
|         version = f"{last_version_num + 1}_{now}_{name}.json" |         version = f"{last_version_num + 1}_{now}_{name}.sql" | ||||||
|         if len(version) > MAX_VERSION_LENGTH: |         if len(version) > MAX_VERSION_LENGTH: | ||||||
|             raise ValueError(f"Version name exceeds maximum length ({MAX_VERSION_LENGTH})") |             raise ValueError(f"Version name exceeds maximum length ({MAX_VERSION_LENGTH})") | ||||||
|         return version |         return version | ||||||
| @@ -99,12 +131,15 @@ class Migrate: | |||||||
|     @classmethod |     @classmethod | ||||||
|     async def _generate_diff_sql(cls, name): |     async def _generate_diff_sql(cls, name): | ||||||
|         version = await cls.generate_version(name) |         version = await cls.generate_version(name) | ||||||
|  |         # delete if same version exists | ||||||
|  |         for version_file in cls.get_all_version_files(): | ||||||
|  |             if version_file.startswith(version.split("_")[0]): | ||||||
|  |                 os.unlink(Path(cls.migrate_location, version_file)) | ||||||
|         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, version), "w", encoding="utf-8") as f: |         write_version_file(Path(cls.migrate_location, version), content) | ||||||
|             json.dump(content, f, indent=2, ensure_ascii=False) |  | ||||||
|         return version |         return version | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
| @@ -129,7 +164,7 @@ class Migrate: | |||||||
|         return await cls._generate_diff_sql(name) |         return await cls._generate_diff_sql(name) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _add_operator(cls, operator: str, upgrade=True, fk=False): |     def _add_operator(cls, operator: str, upgrade=True, fk_m2m=False): | ||||||
|         """ |         """ | ||||||
|         add operator,differentiate fk because fk is order limit |         add operator,differentiate fk because fk is order limit | ||||||
|         :param operator: |         :param operator: | ||||||
| @@ -138,36 +173,16 @@ class Migrate: | |||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|         if upgrade: |         if upgrade: | ||||||
|             if fk: |             if fk_m2m: | ||||||
|                 cls._upgrade_fk_m2m_index_operators.append(operator) |                 cls._upgrade_fk_m2m_index_operators.append(operator) | ||||||
|             else: |             else: | ||||||
|                 cls.upgrade_operators.append(operator) |                 cls.upgrade_operators.append(operator) | ||||||
|         else: |         else: | ||||||
|             if fk: |             if fk_m2m: | ||||||
|                 cls._downgrade_fk_m2m_index_operators.append(operator) |                 cls._downgrade_fk_m2m_index_operators.append(operator) | ||||||
|             else: |             else: | ||||||
|                 cls.downgrade_operators.append(operator) |                 cls.downgrade_operators.append(operator) | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def cp_models( |  | ||||||
|         cls, app: str, model_files: List[str], old_model_file, |  | ||||||
|     ): |  | ||||||
|         """ |  | ||||||
|         cp currents models to old_model_files |  | ||||||
|         :param app: |  | ||||||
|         :param model_files: |  | ||||||
|         :param old_model_file: |  | ||||||
|         :return: |  | ||||||
|         """ |  | ||||||
|         pattern = rf"(\n)?('|\")({app})(.\w+)('|\")" |  | ||||||
|         for i, model_file in enumerate(model_files): |  | ||||||
|             with open(model_file, "r", encoding="utf-8") as f: |  | ||||||
|                 content = f.read() |  | ||||||
|             ret = re.sub(pattern, rf"\2{cls.diff_app}\4\5", content) |  | ||||||
|             mode = "w" if i == 0 else "a" |  | ||||||
|             with open(old_model_file, mode, encoding="utf-8") as f: |  | ||||||
|                 f.write(ret) |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_migrate_config(cls, config: dict, app: str, location: str): |     def _get_migrate_config(cls, config: dict, app: str, location: str): | ||||||
|         """ |         """ | ||||||
| @@ -177,8 +192,7 @@ class Migrate: | |||||||
|         :param location: |         :param location: | ||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|         path = os.path.join(location, app, cls.old_models) |         path = Path(location, app, cls.old_models).as_posix().replace("/", ".") | ||||||
|         path = path.replace(os.sep, ".").lstrip(".") |  | ||||||
|         config["apps"][cls.diff_app] = { |         config["apps"][cls.diff_app] = { | ||||||
|             "models": [path], |             "models": [path], | ||||||
|             "default_connection": config.get("apps").get(app).get("default_connection", "default"), |             "default_connection": config.get("apps").get(app).get("default_connection", "default"), | ||||||
| @@ -186,7 +200,7 @@ class Migrate: | |||||||
|         return config |         return config | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def write_old_models(cls, config: dict, app: str, location: str): |     def get_models_content(cls, config: dict, app: str, location: str): | ||||||
|         """ |         """ | ||||||
|         write new models to old models |         write new models to old models | ||||||
|         :param config: |         :param config: | ||||||
| @@ -194,14 +208,29 @@ 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(import_module(model).__file__) |             if isinstance(model, ModuleType): | ||||||
|  |                 module = model | ||||||
|         cls.cp_models(app, old_model_files, os.path.join(location, app, cls.get_old_model_file())) |             else: | ||||||
|  |                 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): | ||||||
|  |             with open(model_file, "r", encoding="utf-8") as f: | ||||||
|  |                 content = f.read() | ||||||
|  |             ret = re.sub(pattern, rf"\2{cls.diff_app}\4\5", content) | ||||||
|  |             str_io.write(f"{ret}\n") | ||||||
|  |         return str_io.getvalue() | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def diff_models( |     def diff_models( | ||||||
| @@ -264,10 +293,47 @@ class Migrate: | |||||||
|             if cls._exclude_field(new_field, upgrade): |             if cls._exclude_field(new_field, upgrade): | ||||||
|                 continue |                 continue | ||||||
|             if new_key not in old_keys: |             if new_key not in old_keys: | ||||||
|  |                 new_field_dict = new_field.describe(serializable=True) | ||||||
|  |                 new_field_dict.pop("name", None) | ||||||
|  |                 new_field_dict.pop("db_column", None) | ||||||
|  |                 for diff_key in old_keys - new_keys: | ||||||
|  |                     old_field = old_fields_map.get(diff_key) | ||||||
|  |                     old_field_dict = old_field.describe(serializable=True) | ||||||
|  |                     old_field_dict.pop("name", None) | ||||||
|  |                     old_field_dict.pop("db_column", None) | ||||||
|  |                     if old_field_dict == new_field_dict: | ||||||
|  |                         if upgrade: | ||||||
|  |                             is_rename = click.prompt( | ||||||
|  |                                 f"Rename {diff_key} to {new_key}?", | ||||||
|  |                                 default=True, | ||||||
|  |                                 type=bool, | ||||||
|  |                                 show_choices=True, | ||||||
|  |                             ) | ||||||
|  |                             cls._rename_new.append(new_key) | ||||||
|  |                             cls._rename_old.append(diff_key) | ||||||
|  |                         else: | ||||||
|  |                             is_rename = diff_key in cls._rename_new | ||||||
|  |                         if is_rename: | ||||||
|  |                             if ( | ||||||
|  |                                 cls.dialect == "mysql" | ||||||
|  |                                 and cls._db_version | ||||||
|  |                                 and cls._db_version.startswith("5.") | ||||||
|  |                             ): | ||||||
|  |                                 cls._add_operator( | ||||||
|  |                                     cls._change_field(new_model, old_field, new_field), | ||||||
|  |                                     upgrade, | ||||||
|  |                                 ) | ||||||
|  |                             else: | ||||||
|  |                                 cls._add_operator( | ||||||
|  |                                     cls._rename_field(new_model, old_field, new_field), | ||||||
|  |                                     upgrade, | ||||||
|  |                                 ) | ||||||
|  |                             break | ||||||
|  |                 else: | ||||||
|                     cls._add_operator( |                     cls._add_operator( | ||||||
|                         cls._add_field(new_model, new_field), |                         cls._add_field(new_model, new_field), | ||||||
|                         upgrade, |                         upgrade, | ||||||
|                     isinstance(new_field, (ForeignKeyFieldInstance, ManyToManyFieldInstance)), |                         cls._is_fk_m2m(new_field), | ||||||
|                     ) |                     ) | ||||||
|             else: |             else: | ||||||
|                 old_field = old_fields_map.get(new_key) |                 old_field = old_fields_map.get(new_key) | ||||||
| @@ -283,7 +349,9 @@ class Migrate: | |||||||
|                             cls._add_operator( |                             cls._add_operator( | ||||||
|                                 cls._alter_null(new_model, new_field), upgrade=upgrade |                                 cls._alter_null(new_model, new_field), upgrade=upgrade | ||||||
|                             ) |                             ) | ||||||
|                         if new_field.default != old_field.default: |                         if new_field.default != old_field.default and not callable( | ||||||
|  |                             new_field.default | ||||||
|  |                         ): | ||||||
|                             cls._add_operator( |                             cls._add_operator( | ||||||
|                                 cls._alter_default(new_model, new_field), upgrade=upgrade |                                 cls._alter_default(new_model, new_field), upgrade=upgrade | ||||||
|                             ) |                             ) | ||||||
| @@ -291,6 +359,11 @@ class Migrate: | |||||||
|                             cls._add_operator( |                             cls._add_operator( | ||||||
|                                 cls._set_comment(new_model, new_field), upgrade=upgrade |                                 cls._set_comment(new_model, new_field), upgrade=upgrade | ||||||
|                             ) |                             ) | ||||||
|  |                         if new_field.field_type != old_field.field_type: | ||||||
|  |                             cls._add_operator( | ||||||
|  |                                 cls._modify_field(new_model, new_field), upgrade=upgrade | ||||||
|  |                             ) | ||||||
|  |                     else: | ||||||
|                         cls._add_operator(cls._modify_field(new_model, new_field), upgrade=upgrade) |                         cls._add_operator(cls._modify_field(new_model, new_field), upgrade=upgrade) | ||||||
|                 if (old_field.index and not new_field.index) or ( |                 if (old_field.index and not new_field.index) or ( | ||||||
|                     old_field.unique and not new_field.unique |                     old_field.unique and not new_field.unique | ||||||
| @@ -310,17 +383,41 @@ class Migrate: | |||||||
|                         upgrade, |                         upgrade, | ||||||
|                         cls._is_fk_m2m(new_field), |                         cls._is_fk_m2m(new_field), | ||||||
|                     ) |                     ) | ||||||
|  |                 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, | ||||||
|  |                         ) | ||||||
|  |                     if new_field.db_constraint and not old_field.db_constraint: | ||||||
|  |                         cls._add_operator( | ||||||
|  |                             cls._add_fk(new_model, new_field), | ||||||
|  |                             upgrade, | ||||||
|  |                             True, | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|         for old_key in old_keys: |         for old_key in old_keys: | ||||||
|             field = old_fields_map.get(old_key) |             field = old_fields_map.get(old_key) | ||||||
|             if old_key not in new_keys and not cls._exclude_field(field, upgrade): |             if old_key not in new_keys and not cls._exclude_field(field, upgrade): | ||||||
|  |                 if (upgrade and old_key not in cls._rename_old) or ( | ||||||
|  |                     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) | ||||||
| @@ -400,6 +497,10 @@ class Migrate: | |||||||
|     def _modify_field(cls, model: Type[Model], field: Field): |     def _modify_field(cls, model: Type[Model], field: Field): | ||||||
|         return cls.ddl.modify_column(model, field) |         return cls.ddl.modify_column(model, field) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _drop_fk(cls, model: Type[Model], field: ForeignKeyFieldInstance): | ||||||
|  |         return cls.ddl.drop_fk(model, field) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _remove_field(cls, model: Type[Model], field: Field): |     def _remove_field(cls, model: Type[Model], field: Field): | ||||||
|         if isinstance(field, ForeignKeyFieldInstance): |         if isinstance(field, ForeignKeyFieldInstance): | ||||||
| @@ -408,6 +509,19 @@ class Migrate: | |||||||
|             return cls.ddl.drop_m2m(field) |             return cls.ddl.drop_m2m(field) | ||||||
|         return cls.ddl.drop_column(model, field.model_field_name) |         return cls.ddl.drop_column(model, field.model_field_name) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _rename_field(cls, model: Type[Model], old_field: Field, new_field: Field): | ||||||
|  |         return cls.ddl.rename_column(model, old_field.model_field_name, new_field.model_field_name) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def _change_field(cls, model: Type[Model], old_field: Field, new_field: Field): | ||||||
|  |         return cls.ddl.change_column( | ||||||
|  |             model, | ||||||
|  |             old_field.model_field_name, | ||||||
|  |             new_field.model_field_name, | ||||||
|  |             new_field.get_for_dialect(cls.dialect, "SQL_TYPE"), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _add_fk(cls, model: Type[Model], field: ForeignKeyFieldInstance): |     def _add_fk(cls, model: Type[Model], field: ForeignKeyFieldInstance): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ MAX_VERSION_LENGTH = 255 | |||||||
| class Aerich(Model): | class Aerich(Model): | ||||||
|     version = fields.CharField(max_length=MAX_VERSION_LENGTH) |     version = fields.CharField(max_length=MAX_VERSION_LENGTH) | ||||||
|     app = fields.CharField(max_length=20) |     app = fields.CharField(max_length=20) | ||||||
|  |     content = fields.TextField() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         ordering = ["-id"] |         ordering = ["-id"] | ||||||
|   | |||||||
| @@ -1,17 +1,24 @@ | |||||||
| import importlib | import importlib | ||||||
|  | from typing import Dict | ||||||
|  |  | ||||||
| from asyncclick import BadOptionUsage, Context | from click import BadOptionUsage, Context | ||||||
| from tortoise import BaseDBAsyncClient, Tortoise | from tortoise import BaseDBAsyncClient, Tortoise | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_app_connection_name(config, app) -> str: | def get_app_connection_name(config, app_name: str) -> str: | ||||||
|     """ |     """ | ||||||
|     get connection name |     get connection name | ||||||
|     :param config: |     :param config: | ||||||
|     :param app: |     :param app_name: | ||||||
|     :return: |     :return: | ||||||
|     """ |     """ | ||||||
|     return config.get("apps").get(app).get("default_connection", "default") |     app = config.get("apps").get(app_name) | ||||||
|  |     if app: | ||||||
|  |         return app.get("default_connection", "default") | ||||||
|  |     raise BadOptionUsage( | ||||||
|  |         option_name="--app", | ||||||
|  |         message=f'Can\'t get app named "{app_name}"', | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_app_connection(config, app) -> BaseDBAsyncClient: | def get_app_connection(config, app) -> BaseDBAsyncClient: | ||||||
| @@ -49,3 +56,55 @@ def get_tortoise_config(ctx: Context, tortoise_orm: str) -> dict: | |||||||
|             ctx=ctx, |             ctx=ctx, | ||||||
|         ) |         ) | ||||||
|     return config |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _UPGRADE = "-- upgrade --\n" | ||||||
|  | _DOWNGRADE = "-- downgrade --\n" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_version_content_from_file(version_file: str) -> Dict: | ||||||
|  |     """ | ||||||
|  |     get version content | ||||||
|  |     :param version_file: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     with open(version_file, "r", encoding="utf-8") as f: | ||||||
|  |         content = f.read() | ||||||
|  |         first = content.index(_UPGRADE) | ||||||
|  |         try: | ||||||
|  |             second = content.index(_DOWNGRADE) | ||||||
|  |         except ValueError: | ||||||
|  |             second = len(content) - 1 | ||||||
|  |         upgrade_content = content[first + len(_UPGRADE) : second].strip()  # noqa:E203 | ||||||
|  |         downgrade_content = content[second + len(_DOWNGRADE) :].strip()  # noqa:E203 | ||||||
|  |         ret = { | ||||||
|  |             "upgrade": list(filter(lambda x: x or False, upgrade_content.split(";\n"))), | ||||||
|  |             "downgrade": list(filter(lambda x: x or False, downgrade_content.split(";\n"))), | ||||||
|  |         } | ||||||
|  |         return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_version_file(version_file: str, content: Dict): | ||||||
|  |     """ | ||||||
|  |     write version file | ||||||
|  |     :param version_file: | ||||||
|  |     :param content: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     with open(version_file, "w", encoding="utf-8") as f: | ||||||
|  |         f.write(_UPGRADE) | ||||||
|  |         upgrade = content.get("upgrade") | ||||||
|  |         if len(upgrade) > 1: | ||||||
|  |             f.write(";\n".join(upgrade) + ";\n") | ||||||
|  |         else: | ||||||
|  |             f.write(f"{upgrade[0]}") | ||||||
|  |             if not upgrade[0].endswith(";"): | ||||||
|  |                 f.write(";") | ||||||
|  |             f.write("\n") | ||||||
|  |         downgrade = content.get("downgrade") | ||||||
|  |         if downgrade: | ||||||
|  |             f.write(_DOWNGRADE) | ||||||
|  |             if len(downgrade) > 1: | ||||||
|  |                 f.write(";\n".join(downgrade) + ";\n") | ||||||
|  |             else: | ||||||
|  |                 f.write(f"{downgrade[0]};\n") | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								conftest.py
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								conftest.py
									
									
									
									
									
								
							| @@ -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"}, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -32,23 +37,28 @@ def reset_migrate(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="session") | @pytest.fixture(scope="session") | ||||||
| def loop(): | def event_loop(): | ||||||
|     loop = asyncio.get_event_loop() |     policy = asyncio.get_event_loop_policy() | ||||||
|     return loop |     res = policy.new_event_loop() | ||||||
|  |     asyncio.set_event_loop(res) | ||||||
|  |     res._close = res.close | ||||||
|  |     res.close = lambda: None | ||||||
|  |  | ||||||
|  |     yield res | ||||||
|  |  | ||||||
|  |     res._close() | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(scope="session", autouse=True) | @pytest.fixture(scope="session", autouse=True) | ||||||
| def initialize_tests(loop, request): | async def initialize_tests(event_loop, request): | ||||||
|     tortoise_orm["connections"]["diff_models"] = "sqlite://:memory:" |     tortoise_orm["connections"]["diff_models"] = "sqlite://:memory:" | ||||||
|     tortoise_orm["apps"]["diff_models"] = { |     tortoise_orm["apps"]["diff_models"] = { | ||||||
|         "models": ["tests.diff_models"], |         "models": ["tests.diff_models"], | ||||||
|         "default_connection": "diff_models", |         "default_connection": "diff_models", | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     loop.run_until_complete(Tortoise.init(config=tortoise_orm, _create_db=True)) |     await Tortoise.init(config=tortoise_orm, _create_db=True) | ||||||
|     loop.run_until_complete( |     await generate_schema_for_client(Tortoise.get_connection("default"), safe=True) | ||||||
|         generate_schema_for_client(Tortoise.get_connection("default"), safe=True) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     client = Tortoise.get_connection("default") |     client = Tortoise.get_connection("default") | ||||||
|     if client.schema_generator is MySQLSchemaGenerator: |     if client.schema_generator is MySQLSchemaGenerator: | ||||||
| @@ -57,5 +67,5 @@ def initialize_tests(loop, request): | |||||||
|         Migrate.ddl = SqliteDDL(client) |         Migrate.ddl = SqliteDDL(client) | ||||||
|     elif client.schema_generator is AsyncpgSchemaGenerator: |     elif client.schema_generator is AsyncpgSchemaGenerator: | ||||||
|         Migrate.ddl = PostgresDDL(client) |         Migrate.ddl = PostgresDDL(client) | ||||||
|  |     Migrate.dialect = Migrate.ddl.DIALECT | ||||||
|     request.addfinalizer(lambda: loop.run_until_complete(Tortoise._drop_databases())) |     request.addfinalizer(lambda: event_loop.run_until_complete(Tortoise._drop_databases())) | ||||||
|   | |||||||
							
								
								
									
										887
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										887
									
								
								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.2.3" | version = "0.4.4" | ||||||
| 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" | ||||||
| @@ -17,15 +17,16 @@ include = ["CHANGELOG.md", "LICENSE", "README.md"] | |||||||
| [tool.poetry.dependencies] | [tool.poetry.dependencies] | ||||||
| python = "^3.7" | python = "^3.7" | ||||||
| tortoise-orm = "*" | tortoise-orm = "*" | ||||||
| asyncclick = "*" | click = "*" | ||||||
| pydantic = "*" | pydantic = "*" | ||||||
| aiomysql = {version = "*", optional = true} | aiomysql = {version = "*", optional = true} | ||||||
| asyncpg = {version = "*", optional = true} | asyncpg = {version = "*", optional = true} | ||||||
|  | ddlparse = "*" | ||||||
|  |  | ||||||
| [tool.poetry.dev-dependencies] | [tool.poetry.dev-dependencies] | ||||||
| flake8 = "*" | flake8 = "*" | ||||||
| isort = "*" | isort = "*" | ||||||
| black = "^19.10b0" | black = "^20.8b1" | ||||||
| pytest = "*" | pytest = "*" | ||||||
| pytest-xdist = "*" | pytest-xdist = "*" | ||||||
| pytest-asyncio = "*" | pytest-asyncio = "*" | ||||||
|   | |||||||
| @@ -24,13 +24,19 @@ class Status(IntEnum): | |||||||
| class User(Model): | class User(Model): | ||||||
|     username = fields.CharField(max_length=20) |     username = fields.CharField(max_length=20) | ||||||
|     password = fields.CharField(max_length=200) |     password = fields.CharField(max_length=200) | ||||||
|     last_login = fields.DatetimeField(description="Last Login", default=datetime.datetime.now) |     last_login_at = fields.DatetimeField(description="Last Login", default=datetime.datetime.now) | ||||||
|     is_active = fields.BooleanField(default=True, description="Is Active") |     is_active = fields.BooleanField(default=True, description="Is Active") | ||||||
|     is_superuser = fields.BooleanField(default=False, description="Is SuperUser") |     is_superuser = fields.BooleanField(default=False, description="Is SuperUser") | ||||||
|     avatar = fields.CharField(max_length=200, default="") |     avatar = fields.CharField(max_length=200, default="") | ||||||
|     intro = fields.TextField(default="") |     intro = fields.TextField(default="") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Email(Model): | ||||||
|  |     email = fields.CharField(max_length=200) | ||||||
|  |     is_primary = fields.BooleanField(default=False) | ||||||
|  |     user = fields.ForeignKeyField("diff_models.User", db_constraint=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Category(Model): | class Category(Model): | ||||||
|     slug = fields.CharField(max_length=200) |     slug = fields.CharField(max_length=200) | ||||||
|     user = fields.ForeignKeyField("diff_models.User", description="User") |     user = fields.ForeignKeyField("diff_models.User", description="User") | ||||||
|   | |||||||
| @@ -31,6 +31,12 @@ class User(Model): | |||||||
|     intro = fields.TextField(default="") |     intro = fields.TextField(default="") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Email(Model): | ||||||
|  |     email = fields.CharField(max_length=200) | ||||||
|  |     is_primary = fields.BooleanField(default=False) | ||||||
|  |     user = fields.ForeignKeyField("models.User", db_constraint=False) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Category(Model): | class Category(Model): | ||||||
|     slug = fields.CharField(max_length=200) |     slug = fields.CharField(max_length=200) | ||||||
|     name = fields.CharField(max_length=200) |     name = fields.CharField(max_length=200) | ||||||
|   | |||||||
							
								
								
									
										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) | ||||||
| @@ -1,6 +1,9 @@ | |||||||
|  | import pytest | ||||||
|  |  | ||||||
| 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.exceptions import NotSupportError | ||||||
| from aerich.migrate import Migrate | from aerich.migrate import Migrate | ||||||
| from tests.models import Category, User | from tests.models import Category, User | ||||||
|  |  | ||||||
| @@ -39,7 +42,7 @@ def test_create_table(): | |||||||
|     "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" TIMESTAMPTZ 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';""" | ||||||
| @@ -63,27 +66,26 @@ def test_add_column(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_modify_column(): | def test_modify_column(): | ||||||
|     ret = Migrate.ddl.modify_column(Category, Category._meta.fields_map.get("name")) |     if isinstance(Migrate.ddl, SqliteDDL): | ||||||
|     if isinstance(Migrate.ddl, MysqlDDL): |         with pytest.raises(NotSupportError): | ||||||
|         assert ret == "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200) NOT NULL" |             ret0 = Migrate.ddl.modify_column(Category, Category._meta.fields_map.get("name")) | ||||||
|     elif isinstance(Migrate.ddl, PostgresDDL): |             ret1 = Migrate.ddl.modify_column(User, User._meta.fields_map.get("is_active")) | ||||||
|         assert ret == 'ALTER TABLE "category" ALTER COLUMN "name" TYPE VARCHAR(200)' |  | ||||||
|     else: |     else: | ||||||
|         assert ret == 'ALTER TABLE "category" MODIFY COLUMN "name" VARCHAR(200) NOT NULL' |         ret0 = Migrate.ddl.modify_column(Category, Category._meta.fields_map.get("name")) | ||||||
|  |         ret1 = Migrate.ddl.modify_column(User, User._meta.fields_map.get("is_active")) | ||||||
|  |     if isinstance(Migrate.ddl, MysqlDDL): | ||||||
|  |         assert ret0 == "ALTER TABLE `category` MODIFY COLUMN `name` VARCHAR(200) NOT NULL" | ||||||
|  |     elif isinstance(Migrate.ddl, PostgresDDL): | ||||||
|  |         assert ret0 == 'ALTER TABLE "category" ALTER COLUMN "name" TYPE VARCHAR(200)' | ||||||
|  |  | ||||||
|     ret = Migrate.ddl.modify_column(User, User._meta.fields_map.get("is_active")) |  | ||||||
|     if isinstance(Migrate.ddl, MysqlDDL): |     if isinstance(Migrate.ddl, MysqlDDL): | ||||||
|         assert ( |         assert ( | ||||||
|             ret |             ret1 | ||||||
|             == "ALTER TABLE `user` MODIFY COLUMN `is_active` BOOL NOT NULL  COMMENT 'Is Active' DEFAULT 1" |             == "ALTER TABLE `user` MODIFY COLUMN `is_active` BOOL NOT NULL  COMMENT 'Is Active' DEFAULT 1" | ||||||
|         ) |         ) | ||||||
|     elif isinstance(Migrate.ddl, PostgresDDL): |     elif isinstance(Migrate.ddl, PostgresDDL): | ||||||
|         assert ret == 'ALTER TABLE "user" ALTER COLUMN "is_active" TYPE BOOL' |         assert ret1 == 'ALTER TABLE "user" ALTER COLUMN "is_active" TYPE BOOL' | ||||||
|     else: |  | ||||||
|         assert ( |  | ||||||
|             ret |  | ||||||
|             == 'ALTER TABLE "user" MODIFY COLUMN "is_active" INT NOT NULL  DEFAULT 1 /* Is Active */' |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_alter_column_default(): | def test_alter_column_default(): | ||||||
| @@ -131,10 +133,14 @@ def test_set_comment(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_drop_column(): | def test_drop_column(): | ||||||
|  |     if isinstance(Migrate.ddl, SqliteDDL): | ||||||
|  |         with pytest.raises(NotSupportError): | ||||||
|  |             ret = Migrate.ddl.drop_column(Category, "name") | ||||||
|  |     else: | ||||||
|         ret = Migrate.ddl.drop_column(Category, "name") |         ret = Migrate.ddl.drop_column(Category, "name") | ||||||
|     if isinstance(Migrate.ddl, MysqlDDL): |     if isinstance(Migrate.ddl, MysqlDDL): | ||||||
|         assert ret == "ALTER TABLE `category` DROP COLUMN `name`" |         assert ret == "ALTER TABLE `category` DROP COLUMN `name`" | ||||||
|     else: |     elif isinstance(Migrate.ddl, PostgresDDL): | ||||||
|         assert ret == 'ALTER TABLE "category" DROP COLUMN "name"' |         assert ret == 'ALTER TABLE "category" DROP COLUMN "name"' | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,61 +1,79 @@ | |||||||
|  | import pytest | ||||||
|  | from pytest_mock import MockerFixture | ||||||
| from tortoise import Tortoise | from tortoise import Tortoise | ||||||
|  |  | ||||||
| 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.exceptions import NotSupportError | ||||||
| from aerich.migrate import Migrate | from aerich.migrate import Migrate | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_migrate(): | def test_migrate(mocker: MockerFixture): | ||||||
|  |     mocker.patch("click.prompt", return_value=True) | ||||||
|     apps = Tortoise.apps |     apps = Tortoise.apps | ||||||
|     models = apps.get("models") |     models = apps.get("models") | ||||||
|     diff_models = apps.get("diff_models") |     diff_models = apps.get("diff_models") | ||||||
|     Migrate.diff_models(diff_models, models) |     Migrate.diff_models(diff_models, models) | ||||||
|  |     if isinstance(Migrate.ddl, SqliteDDL): | ||||||
|  |         with pytest.raises(NotSupportError): | ||||||
|             Migrate.diff_models(models, diff_models, False) |             Migrate.diff_models(models, diff_models, False) | ||||||
|  |     else: | ||||||
|  |         Migrate.diff_models(models, diff_models, False) | ||||||
|  |     Migrate._merge_operators() | ||||||
|     if isinstance(Migrate.ddl, MysqlDDL): |     if isinstance(Migrate.ddl, MysqlDDL): | ||||||
|         assert Migrate.upgrade_operators == [ |         assert Migrate.upgrade_operators == [ | ||||||
|  |             "ALTER TABLE `email` DROP FOREIGN KEY `fk_email_user_5b58673d`", | ||||||
|             "ALTER TABLE `category` ADD `name` VARCHAR(200) NOT NULL", |             "ALTER TABLE `category` ADD `name` VARCHAR(200) NOT NULL", | ||||||
|             "ALTER TABLE `user` ADD UNIQUE INDEX `uid_user_usernam_9987ab` (`username`)", |             "ALTER TABLE `user` ADD UNIQUE INDEX `uid_user_usernam_9987ab` (`username`)", | ||||||
|  |             "ALTER TABLE `user` RENAME COLUMN `last_login_at` TO `last_login`", | ||||||
|         ] |         ] | ||||||
|         assert Migrate.downgrade_operators == [ |         assert Migrate.downgrade_operators == [ | ||||||
|             "ALTER TABLE `category` DROP COLUMN `name`", |             "ALTER TABLE `category` DROP COLUMN `name`", | ||||||
|             "ALTER TABLE `user` DROP INDEX `uid_user_usernam_9987ab`", |             "ALTER TABLE `user` DROP INDEX `uid_user_usernam_9987ab`", | ||||||
|  |             "ALTER TABLE `user` RENAME COLUMN `last_login` TO `last_login_at`", | ||||||
|  |             "ALTER TABLE `email` ADD CONSTRAINT `fk_email_user_5b58673d` FOREIGN KEY " | ||||||
|  |             "(`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE", | ||||||
|         ] |         ] | ||||||
|     elif isinstance(Migrate.ddl, PostgresDDL): |     elif isinstance(Migrate.ddl, PostgresDDL): | ||||||
|         assert Migrate.upgrade_operators == [ |         assert Migrate.upgrade_operators == [ | ||||||
|  |             'ALTER TABLE "email" DROP CONSTRAINT "fk_email_user_5b58673d"', | ||||||
|             'ALTER TABLE "category" ADD "name" VARCHAR(200) NOT NULL', |             'ALTER TABLE "category" ADD "name" VARCHAR(200) NOT NULL', | ||||||
|             'ALTER TABLE "user" ADD CONSTRAINT "uid_user_usernam_9987ab" UNIQUE ("username")', |             'ALTER TABLE "user" ADD CONSTRAINT "uid_user_usernam_9987ab" UNIQUE ("username")', | ||||||
|  |             'ALTER TABLE "user" RENAME COLUMN "last_login_at" TO "last_login"', | ||||||
|         ] |         ] | ||||||
|         assert Migrate.downgrade_operators == [ |         assert Migrate.downgrade_operators == [ | ||||||
|             'ALTER TABLE "category" DROP COLUMN "name"', |             'ALTER TABLE "category" DROP COLUMN "name"', | ||||||
|             'ALTER TABLE "user" DROP CONSTRAINT "uid_user_usernam_9987ab"', |             'ALTER TABLE "user" DROP CONSTRAINT "uid_user_usernam_9987ab"', | ||||||
|  |             'ALTER TABLE "user" RENAME COLUMN "last_login" TO "last_login_at"', | ||||||
|  |             'ALTER TABLE "email" ADD CONSTRAINT "fk_email_user_5b58673d" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE', | ||||||
|         ] |         ] | ||||||
|     else: |     elif isinstance(Migrate.ddl, SqliteDDL): | ||||||
|         assert Migrate.upgrade_operators == [ |         assert Migrate.upgrade_operators == [ | ||||||
|  |             'ALTER TABLE "email" DROP FOREIGN KEY "fk_email_user_5b58673d"', | ||||||
|             'ALTER TABLE "category" ADD "name" VARCHAR(200) NOT NULL', |             'ALTER TABLE "category" ADD "name" VARCHAR(200) NOT NULL', | ||||||
|             'ALTER TABLE "user" ADD UNIQUE INDEX "uid_user_usernam_9987ab" ("username")', |             'ALTER TABLE "user" ADD UNIQUE INDEX "uid_user_usernam_9987ab" ("username")', | ||||||
|  |             'ALTER TABLE "user" RENAME COLUMN "last_login_at" TO "last_login"', | ||||||
|         ] |         ] | ||||||
|         assert Migrate.downgrade_operators == [ |         assert Migrate.downgrade_operators == [] | ||||||
|             'ALTER TABLE "category" DROP COLUMN "name"', |  | ||||||
|             'ALTER TABLE "user" DROP INDEX "uid_user_usernam_9987ab"', |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_sort_all_version_files(mocker): | def test_sort_all_version_files(mocker): | ||||||
|     mocker.patch( |     mocker.patch( | ||||||
|         "os.listdir", |         "os.listdir", | ||||||
|         return_value=[ |         return_value=[ | ||||||
|             "1_datetime_update.json", |             "1_datetime_update.sql", | ||||||
|             "11_datetime_update.json", |             "11_datetime_update.sql", | ||||||
|             "10_datetime_update.json", |             "10_datetime_update.sql", | ||||||
|             "2_datetime_update.json", |             "2_datetime_update.sql", | ||||||
|         ], |         ], | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     Migrate.migrate_location = "." |     Migrate.migrate_location = "." | ||||||
|  |  | ||||||
|     assert Migrate.get_all_version_files() == [ |     assert Migrate.get_all_version_files() == [ | ||||||
|         "1_datetime_update.json", |         "1_datetime_update.sql", | ||||||
|         "2_datetime_update.json", |         "2_datetime_update.sql", | ||||||
|         "10_datetime_update.json", |         "10_datetime_update.sql", | ||||||
|         "11_datetime_update.json", |         "11_datetime_update.sql", | ||||||
|     ] |     ] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user