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:
		
							parent
							
								
									e510c54bd8
								
							
						
					
					
						commit
						35a2f25e71
					
				|  | @ -101,10 +101,11 @@ def create_app(test_config=None): | |||
|     CORS(app) | ||||
| 
 | ||||
|     with app.app_context(): | ||||
|         from flaschengeist.database import db | ||||
|         from flaschengeist.database import db, migrate | ||||
| 
 | ||||
|         configure_app(app, test_config) | ||||
|         db.init_app(app) | ||||
|         migrate.init_app(app, db, compare_type=True) | ||||
|         __load_plugins(app) | ||||
| 
 | ||||
|     @app.route("/", methods=["GET"]) | ||||
|  |  | |||
|  | @ -1,3 +1,6 @@ | |||
| import os | ||||
| from flask import current_app | ||||
| from flask_migrate import Migrate | ||||
| from flask_sqlalchemy import SQLAlchemy | ||||
| from sqlalchemy import MetaData | ||||
| 
 | ||||
|  | @ -14,6 +17,27 @@ 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): | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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() | ||||
|  | @ -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"} | ||||
							
								
								
									
										12
									
								
								setup.cfg
								
								
								
								
							
							
						
						
									
										12
									
								
								setup.cfg
								
								
								
								
							|  | @ -23,13 +23,13 @@ python_requires = >=3.9 | |||
| packages = find: | ||||
| install_requires = | ||||
|     Flask >= 2.0 | ||||
|     Pillow>=8.4.0 | ||||
|     flask_cors | ||||
|     flask_sqlalchemy>=2.5 | ||||
|     sqlalchemy>=1.4.26 | ||||
|     Flask-Cors >= 3.0 | ||||
|     Flask-Migrate >= 3.1.0  | ||||
|     Flask-SQLAlchemy >= 2.5 | ||||
|     Pillow >= 8.4.0 | ||||
|     SQLAlchemy >= 1.4.28 | ||||
|     toml | ||||
|     werkzeug | ||||
|      | ||||
|     werkzeug >= 2.0 | ||||
| 
 | ||||
| [options.extras_require] | ||||
| argon = argon2-cffi | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue