Coverage for cc_modules/cc_alembic.py: 45%

40 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_alembic.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

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

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

30 

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

32``tools/create_database_migration.py`` instead. 

33 

34""" 

35 

36import logging 

37from typing import TYPE_CHECKING 

38import os 

39 

40from alembic.config import Config 

41from cardinal_pythonlib.fileops import preserve_cwd 

42from cardinal_pythonlib.logs import BraceStyleAdapter 

43from cardinal_pythonlib.sqlalchemy.alembic_func import ( 

44 downgrade_database, 

45 upgrade_database, 

46 stamp_allowing_unusual_version_table, 

47) 

48 

49from camcops_server.cc_modules.cc_baseconstants import ( 

50 ALEMBIC_BASE_DIR, 

51 ALEMBIC_CONFIG_FILENAME, 

52 ALEMBIC_VERSION_TABLE, 

53) 

54from camcops_server.cc_modules.cc_sqlalchemy import Base 

55 

56if TYPE_CHECKING: 

57 from sqlalchemy.sql.schema import MetaData 

58 from camcops_server.cc_modules.cc_config import CamcopsConfig 

59 

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

61 

62 

63def import_all_models(): 

64 """ 

65 Imports all SQLAlchemy models. (This has side effects including setting up 

66 the SQLAlchemy metadata properly.) 

67 """ 

68 # noinspection PyUnresolvedReferences 

69 import camcops_server.cc_modules.cc_all_models # delayed import # import side effects (ensure all models registered) # noqa 

70 

71 

72def upgrade_database_to_head(show_sql_only: bool = False) -> None: 

73 """ 

74 The primary upgrade method. Modifies the database structure from where it 

75 is, stepwise through revisions, to the head revision. 

76 

77 Args: 

78 show_sql_only: just show the SQL; don't execute it 

79 """ 

80 upgrade_database_to_revision(revision="head", show_sql_only=show_sql_only) 

81 

82 

83def upgrade_database_to_revision( 

84 revision: str, show_sql_only: bool = False 

85) -> None: 

86 """ 

87 Upgrades the database to a specific revision. Modifies the database 

88 structure from where it is, stepwise through revisions, to the specified 

89 revision. 

90 

91 Args: 

92 revision: destination revision 

93 show_sql_only: just show the SQL; don't execute it 

94 """ 

95 import_all_models() # delayed, for command-line interfaces 

96 upgrade_database( 

97 alembic_base_dir=ALEMBIC_BASE_DIR, 

98 alembic_config_filename=ALEMBIC_CONFIG_FILENAME, 

99 destination_revision=revision, 

100 version_table=ALEMBIC_VERSION_TABLE, 

101 as_sql=show_sql_only, 

102 ) 

103 # ... will get its config information from the OS environment; see 

104 # run_alembic() in alembic/env.py 

105 

106 

107def downgrade_database_to_revision( 

108 revision: str, 

109 show_sql_only: bool = False, 

110 confirm_downgrade_db: bool = False, 

111) -> None: 

112 """ 

113 Developer option. Takes the database to a specific revision. 

114 

115 Args: 

116 revision: destination revision 

117 show_sql_only: just show the SQL; don't execute it 

118 confirm_downgrade_db: has the user confirmed? Necessary for the 

119 (destructive) database operation. 

120 """ 

121 if not show_sql_only and not confirm_downgrade_db: 

122 log.critical("Destructive action not confirmed! Refusing.") 

123 return 

124 if show_sql_only: 

125 log.warning( 

126 "Current Alembic v1.0.0 bug in downgrading with " 

127 "as_sql=True; may fail" 

128 ) 

129 import_all_models() # delayed, for command-line interfaces 

130 downgrade_database( 

131 alembic_base_dir=ALEMBIC_BASE_DIR, 

132 alembic_config_filename=ALEMBIC_CONFIG_FILENAME, 

133 destination_revision=revision, 

134 version_table=ALEMBIC_VERSION_TABLE, 

135 as_sql=show_sql_only, 

136 ) 

137 # ... will get its config information from the OS environment; see 

138 # run_alembic() in alembic/env.py 

139 

140 

141@preserve_cwd 

142def create_database_from_scratch(cfg: "CamcopsConfig") -> None: 

143 """ 

144 Takes the database from nothing to the "head" revision in one step, by 

145 bypassing Alembic's revisions and taking the state directly from the 

146 SQLAlchemy ORM metadata. 

147 

148 See 

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

150 

151 This function ASSUMES that the head revision "frozen" into the latest 

152 ``alembic/version/XXX.py`` file MATCHES THE STATE OF THE SQLALCHEMY ORM 

153 METADATA as judged by ``Base.metadata``. If that's not the case, things 

154 will go awry later! (Alembic will think the database is at the state of its 

155 "head" revision, but it won't be.) 

156 

157 It also ASSUMES (as many things do) that importing ``.cc_all_models`` 

158 imports all the models (or ``Base.metadata`` will be incomplete). 

159 """ # noqa 

160 import_all_models() # delayed, for command-line interfaces 

161 

162 log.warning("Performing one-step database creation.") 

163 metadata = Base.metadata # type: MetaData 

164 engine = cfg.get_sqla_engine() 

165 metadata.create_all(engine) 

166 

167 alembic_cfg = Config(ALEMBIC_CONFIG_FILENAME) 

168 os.chdir(ALEMBIC_BASE_DIR) 

169 # command.stamp(alembic_cfg, "head") 

170 stamp_allowing_unusual_version_table( 

171 alembic_cfg, "head", version_table=ALEMBIC_VERSION_TABLE 

172 ) 

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