Coverage for cc_modules/cc_alembic.py: 43%
42 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/cc_modules/cc_alembic.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26**Functions to talk to Alembic; specifically, those functions that may be used
27by users/administrators, such as to upgrade a database.**
29If you're a developer and want to create a new database migration, see
30``tools/create_database_migration.py`` instead.
32"""
34import logging
35from typing import TYPE_CHECKING
36import os
38from alembic.config import Config as AlembicConfig
39from cardinal_pythonlib.fileops import preserve_cwd
40from cardinal_pythonlib.logs import BraceStyleAdapter
41from cardinal_pythonlib.sqlalchemy.alembic_func import (
42 downgrade_database,
43 upgrade_database,
44 stamp_allowing_unusual_version_table,
45)
46from cardinal_pythonlib.sqlalchemy.session import get_safe_url_from_url
48from camcops_server.cc_modules.cc_baseconstants import (
49 ALEMBIC_BASE_DIR,
50 ALEMBIC_CONFIG_FILENAME,
51 ALEMBIC_VERSION_TABLE,
52)
53from camcops_server.cc_modules.cc_sqlalchemy import Base
55if TYPE_CHECKING:
56 from sqlalchemy.sql.schema import MetaData
57 from camcops_server.cc_modules.cc_config import CamcopsConfig
59log = BraceStyleAdapter(logging.getLogger(__name__))
62def import_all_models() -> None:
63 """
64 Imports all SQLAlchemy models. (This has side effects including setting up
65 the SQLAlchemy metadata properly.)
66 """
67 # noinspection PyUnresolvedReferences
68 import camcops_server.cc_modules.cc_all_models # delayed import # import side effects (ensure all models registered) # noqa
71def upgrade_database_to_head(
72 camcops_cfg: "CamcopsConfig", show_sql_only: bool = False
73) -> None:
74 """
75 The primary upgrade method. Modifies the database structure from where it
76 is, stepwise through revisions, to the head revision.
78 Args:
79 camcops_cfg: CamcopsConfig object (for database URL)
80 show_sql_only: just show the SQL; don't execute it
81 """
82 upgrade_database_to_revision(
83 camcops_cfg=camcops_cfg, revision="head", show_sql_only=show_sql_only
84 )
87def upgrade_database_to_revision(
88 camcops_cfg: "CamcopsConfig", revision: str, show_sql_only: bool = False
89) -> None:
90 """
91 Upgrades the database to a specific revision. Modifies the database
92 structure from where it is, stepwise through revisions, to the specified
93 revision.
95 Args:
96 camcops_cfg: CamcopsConfig object (for database URL)
97 revision: destination revision
98 show_sql_only: just show the SQL; don't execute it
99 """
100 import_all_models() # delayed, for command-line interfaces
101 upgrade_database(
102 alembic_base_dir=ALEMBIC_BASE_DIR,
103 alembic_config_filename=ALEMBIC_CONFIG_FILENAME,
104 db_url=camcops_cfg.db_url,
105 destination_revision=revision,
106 version_table=ALEMBIC_VERSION_TABLE,
107 as_sql=show_sql_only,
108 )
109 # ... will get its config information from the OS environment; see
110 # run_alembic() in alembic/env.py
113def downgrade_database_to_revision(
114 camcops_cfg: "CamcopsConfig",
115 revision: str,
116 show_sql_only: bool = False,
117 confirm_downgrade_db: bool = False,
118) -> None:
119 """
120 Developer option. Takes the database to a specific revision.
122 Args:
123 camcops_cfg: CamcopsConfig object (for database URL)
124 revision: destination revision
125 show_sql_only: just show the SQL; don't execute it
126 confirm_downgrade_db: has the user confirmed? Necessary for the
127 (destructive) database operation.
128 """
129 if not show_sql_only and not confirm_downgrade_db:
130 log.critical("Destructive action not confirmed! Refusing.")
131 return
132 if show_sql_only:
133 log.warning(
134 "Current Alembic v1.0.0 bug in downgrading with "
135 "as_sql=True; may fail"
136 )
137 import_all_models() # delayed, for command-line interfaces
138 downgrade_database(
139 alembic_base_dir=ALEMBIC_BASE_DIR,
140 alembic_config_filename=ALEMBIC_CONFIG_FILENAME,
141 db_url=camcops_cfg.db_url,
142 destination_revision=revision,
143 version_table=ALEMBIC_VERSION_TABLE,
144 as_sql=show_sql_only,
145 )
146 # ... will get its config information from the OS environment; see
147 # run_alembic() in alembic/env.py
150@preserve_cwd
151def create_database_from_scratch(camcops_cfg: "CamcopsConfig") -> None:
152 """
153 Takes the database from nothing to the "head" revision in one step, by
154 bypassing Alembic's revisions and taking the state directly from the
155 SQLAlchemy ORM metadata.
157 See
158 https://alembic.zzzcomputing.com/en/latest/cookbook.html#building-an-up-to-date-database-from-scratch
160 This function ASSUMES that the head revision "frozen" into the latest
161 ``alembic/version/XXX.py`` file MATCHES THE STATE OF THE SQLALCHEMY ORM
162 METADATA as judged by ``Base.metadata``. If that's not the case, things
163 will go awry later! (Alembic will think the database is at the state of its
164 "head" revision, but it won't be.)
166 It also ASSUMES (as many things do) that ``import_all_models()``
167 imports all the models (or ``Base.metadata`` will be incomplete).
168 """
169 import_all_models() # delayed, for command-line interfaces
171 safe_url = get_safe_url_from_url(camcops_cfg.db_url)
173 log.info(f"Performing one-step database creation for: {safe_url}")
175 # Create the tables:
176 metadata = Base.metadata # type: MetaData
177 engine = camcops_cfg.get_sqla_engine()
178 metadata.create_all(engine)
180 # Stamp the Alembic version table:
181 alembic_cfg = AlembicConfig(ALEMBIC_CONFIG_FILENAME)
182 alembic_cfg.set_main_option("sqlalchemy.url", camcops_cfg.db_url)
183 os.chdir(ALEMBIC_BASE_DIR)
184 # command.stamp(alembic_cfg, "head")
185 stamp_allowing_unusual_version_table(
186 alembic_cfg, "head", version_table=ALEMBIC_VERSION_TABLE
187 )
189 log.info("One-step database creation complete.")