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

1""" 

2camcops_server/cc_modules/cc_alembic.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26**Functions to talk to Alembic; specifically, those functions that may be used 

27by users/administrators, such as to upgrade a database.** 

28 

29If you're a developer and want to create a new database migration, see 

30``tools/create_database_migration.py`` instead. 

31 

32""" 

33 

34import logging 

35from typing import TYPE_CHECKING 

36import os 

37 

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 

47 

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 

54 

55if TYPE_CHECKING: 

56 from sqlalchemy.sql.schema import MetaData 

57 from camcops_server.cc_modules.cc_config import CamcopsConfig 

58 

59log = BraceStyleAdapter(logging.getLogger(__name__)) 

60 

61 

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 

69 

70 

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. 

77 

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 ) 

85 

86 

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. 

94 

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 

111 

112 

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. 

121 

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 

148 

149 

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. 

156 

157 See 

158 https://alembic.zzzcomputing.com/en/latest/cookbook.html#building-an-up-to-date-database-from-scratch 

159 

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.) 

165 

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 

170 

171 safe_url = get_safe_url_from_url(camcops_cfg.db_url) 

172 

173 log.info(f"Performing one-step database creation for: {safe_url}") 

174 

175 # Create the tables: 

176 metadata = Base.metadata # type: MetaData 

177 engine = camcops_cfg.get_sqla_engine() 

178 metadata.create_all(engine) 

179 

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 ) 

188 

189 log.info("One-step database creation complete.")