feat(db): Add database migration support, implements #19

Migrations allow us to keep track of database changes and upgrading databases if needed.
This commit is contained in:
Ferdinand Thiessen 2021-12-19 22:11:57 +01:00
parent e510c54bd8
commit 35a2f25e71
6 changed files with 182 additions and 7 deletions

View File

@ -101,10 +101,11 @@ def create_app(test_config=None):
CORS(app) CORS(app)
with app.app_context(): with app.app_context():
from flaschengeist.database import db from flaschengeist.database import db, migrate
configure_app(app, test_config) configure_app(app, test_config)
db.init_app(app) db.init_app(app)
migrate.init_app(app, db, compare_type=True)
__load_plugins(app) __load_plugins(app)
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])

View File

@ -1,3 +1,6 @@
import os
from flask import current_app
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData from sqlalchemy import MetaData
@ -14,6 +17,27 @@ metadata = MetaData(
db = SQLAlchemy(metadata=metadata) db = SQLAlchemy(metadata=metadata)
migrate = Migrate()
@migrate.configure
def configure_alembic(config):
# Load migration paths from plugins
migrations = [str(p.migrations_path) for p in current_app.config["FG_PLUGINS"].values() if p and p.migrations_path]
if len(migrations) > 0:
# Get configured paths
paths = config.get_main_option("version_locations")
# Get configured path seperator
sep = config.get_main_option("version_path_separator", "os")
if paths:
# Insert configured paths at the front, before plugin migrations
migrations.insert(0, config.get_main_option("version_locations"))
sep = os.pathsep if sep == "os" else " " if sep == "space" else sep
# write back seperator (we changed it if neither seperator nor locations were specified)
config.set_main_option("version_path_separator", sep)
config.set_main_option("version_locations", sep.join(migrations))
print(config.get_main_option("version_locations"))
return config
def case_sensitive(s): def case_sensitive(s):

52
migrations/alembic.ini Normal file
View File

@ -0,0 +1,52 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
version_path_separator = os
version_locations = %(here)s/versions
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

73
migrations/env.py Normal file
View File

@ -0,0 +1,73 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger("alembic.env")
config.set_main_option("sqlalchemy.url", str(current_app.extensions["migrate"].db.get_engine().url).replace("%", "%%"))
target_metadata = current_app.extensions["migrate"].db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info("No changes in schema detected.")
connectable = current_app.extensions["migrate"].db.get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions["migrate"].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

25
migrations/script.py.mako Normal file
View File

@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -23,13 +23,13 @@ python_requires = >=3.9
packages = find: packages = find:
install_requires = install_requires =
Flask >= 2.0 Flask >= 2.0
Flask-Cors >= 3.0
Flask-Migrate >= 3.1.0
Flask-SQLAlchemy >= 2.5
Pillow >= 8.4.0 Pillow >= 8.4.0
flask_cors SQLAlchemy >= 1.4.28
flask_sqlalchemy>=2.5
sqlalchemy>=1.4.26
toml toml
werkzeug werkzeug >= 2.0
[options.extras_require] [options.extras_require]
argon = argon2-cffi argon = argon2-cffi