52 lines
2.1 KiB
Python
52 lines
2.1 KiB
Python
from sqlalchemy import create_engine, inspect, text
|
|
from sqlalchemy.engine import Engine
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from app.db.models import Base
|
|
|
|
# Columns added to existing tables after their initial release. create_all()
|
|
# only creates missing tables, never adds columns to tables that already
|
|
# exist, so a column added to a model here must also be listed below or an
|
|
# already-deployed database will never receive it and the app will crash
|
|
# reading/writing that column.
|
|
_ADDITIVE_COLUMNS: dict[str, list[tuple[str, str]]] = {
|
|
"sync_users": [
|
|
("notify_email_enabled", "BOOLEAN NOT NULL DEFAULT 0"),
|
|
("notification_email", "VARCHAR(255)"),
|
|
],
|
|
}
|
|
|
|
|
|
def create_db_engine(database_url: str) -> Engine:
|
|
connect_args = {"check_same_thread": False} if database_url.startswith("sqlite") else {}
|
|
return create_engine(database_url, connect_args=connect_args, future=True)
|
|
|
|
|
|
def create_session_factory(engine: Engine) -> sessionmaker[Session]:
|
|
return sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
|
|
|
|
|
def initialize_schema(engine: Engine) -> None:
|
|
Base.metadata.create_all(engine)
|
|
_apply_additive_migrations(engine)
|
|
|
|
|
|
def _apply_additive_migrations(engine: Engine) -> None:
|
|
if engine.dialect.name != "sqlite":
|
|
# ALTER TABLE ... ADD COLUMN syntax/type names below are only
|
|
# verified against sqlite, the only backend this app is deployed
|
|
# against; a fresh create_all() on another backend already has every
|
|
# current column, so skipping here only matters for a pre-existing
|
|
# non-sqlite database, which does not exist in practice.
|
|
return
|
|
inspector = inspect(engine)
|
|
existing_tables = set(inspector.get_table_names())
|
|
with engine.begin() as conn:
|
|
for table, columns in _ADDITIVE_COLUMNS.items():
|
|
if table not in existing_tables:
|
|
continue
|
|
existing_columns = {col["name"] for col in inspector.get_columns(table)}
|
|
for name, ddl_type in columns:
|
|
if name not in existing_columns:
|
|
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {name} {ddl_type}"))
|