Coverage for cc_modules/cc_alembic.py: 43%

1"""

2 camcops_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

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

28

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

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

31

32"""

33

34 import logging

35 from typing import TYPE_CHECKING

36 import os

37

38 from alembic.config import Config as AlembicConfig

39 from cardinal_pythonlib.fileops import preserve_cwd

40 from cardinal_pythonlib.logs import BraceStyleAdapter

41 from cardinal_pythonlib.sqlalchemy.alembic_func import (

42 downgrade_database,

43 upgrade_database,

44 stamp_allowing_unusual_version_table,

45)

46 from cardinal_pythonlib.sqlalchemy.session import get_safe_url_from_url

47

48 from camcops_server.cc_modules.cc_baseconstants import (

49 ALEMBIC_BASE_DIR,

50 ALEMBIC_CONFIG_FILENAME,

51 ALEMBIC_VERSION_TABLE,

52)

53 from camcops_server.cc_modules.cc_sqlalchemy import Base

54

55 if TYPE_CHECKING:

56 from sqlalchemy.sql.schema import MetaData

57 from camcops_server.cc_modules.cc_config import CamcopsConfig

58

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

60

61

62 def 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

71 def 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

87 def 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

113 def 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

151 def 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.")