patched_autodetector.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. from contextlib import contextmanager
  2. from unittest import mock
  3. import django
  4. from django.db.migrations import (
  5. AddField,
  6. AlterField,
  7. CreateModel,
  8. DeleteModel,
  9. RemoveField,
  10. RenameField,
  11. )
  12. from django.db.migrations.autodetector import MigrationAutodetector
  13. from django.db.migrations.operations.fields import FieldOperation
  14. from psqlextra.models import (
  15. PostgresMaterializedViewModel,
  16. PostgresPartitionedModel,
  17. PostgresViewModel,
  18. )
  19. from psqlextra.types import PostgresPartitioningMethod
  20. from . import operations
  21. from .state import (
  22. PostgresMaterializedViewModelState,
  23. PostgresPartitionedModelState,
  24. PostgresViewModelState,
  25. )
  26. # original `MigrationAutodetector.add_operation`
  27. # function, saved here so the patched version can
  28. # call the original
  29. add_operation = MigrationAutodetector.add_operation
  30. class AddOperationHandler:
  31. """Handler for when operations are being added to a new migration.
  32. This is where we intercept operations such as
  33. :see:CreateModel to replace it with our own.
  34. """
  35. def __init__(self, autodetector, app_label, args, kwargs):
  36. self.autodetector = autodetector
  37. self.app_label = app_label
  38. self.args = args
  39. self.kwargs = kwargs
  40. def add(self, operation):
  41. """Adds the specified operation to the list of operations to execute in
  42. the migration."""
  43. return add_operation(
  44. self.autodetector,
  45. self.app_label,
  46. operation,
  47. *self.args,
  48. **self.kwargs,
  49. )
  50. def add_field(self, operation: AddField):
  51. """Adds the specified :see:AddField operation to the list of operations
  52. to execute in the migration."""
  53. return self._transform_view_field_operations(operation)
  54. def remove_field(self, operation: RemoveField):
  55. """Adds the specified :see:RemoveField operation to the list of
  56. operations to execute in the migration."""
  57. return self._transform_view_field_operations(operation)
  58. def alter_field(self, operation: AlterField):
  59. """Adds the specified :see:AlterField operation to the list of
  60. operations to execute in the migration."""
  61. return self._transform_view_field_operations(operation)
  62. def rename_field(self, operation: RenameField):
  63. """Adds the specified :see:RenameField operation to the list of
  64. operations to execute in the migration."""
  65. return self._transform_view_field_operations(operation)
  66. def _transform_view_field_operations(self, operation: FieldOperation):
  67. """Transforms operations on fields on a (materialized) view into state
  68. only operations.
  69. One cannot add/remove/delete fields on a (materialized) view,
  70. however, we do want Django's migration system to keep track of
  71. these kind of changes to the model. The :see:ApplyState
  72. operation just tells Django the operation was applied without
  73. actually applying it.
  74. """
  75. if django.VERSION >= (4, 0):
  76. model_identifier = (self.app_label, operation.model_name.lower())
  77. model_state = (
  78. self.autodetector.to_state.models.get(model_identifier)
  79. or self.autodetector.from_state.models[model_identifier]
  80. )
  81. if isinstance(model_state, PostgresViewModelState):
  82. return self.add(
  83. operations.ApplyState(state_operation=operation)
  84. )
  85. else:
  86. model = self.autodetector.new_apps.get_model(
  87. self.app_label, operation.model_name
  88. )
  89. if issubclass(model, PostgresViewModel):
  90. return self.add(
  91. operations.ApplyState(state_operation=operation)
  92. )
  93. return self.add(operation)
  94. def add_create_model(self, operation: CreateModel):
  95. """Adds the specified :see:CreateModel operation to the list of
  96. operations to execute in the migration."""
  97. if django.VERSION >= (4, 0):
  98. model_state = self.autodetector.to_state.models[
  99. self.app_label, operation.name.lower()
  100. ]
  101. if isinstance(model_state, PostgresPartitionedModelState):
  102. return self.add_create_partitioned_model(operation)
  103. elif isinstance(model_state, PostgresMaterializedViewModelState):
  104. return self.add_create_materialized_view_model(operation)
  105. elif isinstance(model_state, PostgresViewModelState):
  106. return self.add_create_view_model(operation)
  107. else:
  108. model = self.autodetector.new_apps.get_model(
  109. self.app_label, operation.name
  110. )
  111. if issubclass(model, PostgresPartitionedModel):
  112. return self.add_create_partitioned_model(operation)
  113. elif issubclass(model, PostgresMaterializedViewModel):
  114. return self.add_create_materialized_view_model(operation)
  115. elif issubclass(model, PostgresViewModel):
  116. return self.add_create_view_model(operation)
  117. return self.add(operation)
  118. def add_delete_model(self, operation: DeleteModel):
  119. """Adds the specified :see:Deletemodel operation to the list of
  120. operations to execute in the migration."""
  121. if django.VERSION >= (4, 0):
  122. model_state = self.autodetector.from_state.models[
  123. self.app_label, operation.name.lower()
  124. ]
  125. if isinstance(model_state, PostgresPartitionedModelState):
  126. return self.add_delete_partitioned_model(operation)
  127. elif isinstance(model_state, PostgresMaterializedViewModelState):
  128. return self.add_delete_materialized_view_model(operation)
  129. elif isinstance(model_state, PostgresViewModelState):
  130. return self.add_delete_view_model(operation)
  131. else:
  132. model = self.autodetector.old_apps.get_model(
  133. self.app_label, operation.name
  134. )
  135. if issubclass(model, PostgresPartitionedModel):
  136. return self.add_delete_partitioned_model(operation)
  137. elif issubclass(model, PostgresMaterializedViewModel):
  138. return self.add_delete_materialized_view_model(operation)
  139. elif issubclass(model, PostgresViewModel):
  140. return self.add_delete_view_model(operation)
  141. return self.add(operation)
  142. def add_create_partitioned_model(self, operation: CreateModel):
  143. """Adds a :see:PostgresCreatePartitionedModel operation to the list of
  144. operations to execute in the migration."""
  145. if django.VERSION >= (4, 0):
  146. model_state = self.autodetector.to_state.models[
  147. self.app_label, operation.name.lower()
  148. ]
  149. partitioning_options = model_state.partitioning_options
  150. else:
  151. model = self.autodetector.new_apps.get_model(
  152. self.app_label, operation.name
  153. )
  154. partitioning_options = model._partitioning_meta.original_attrs
  155. _, args, kwargs = operation.deconstruct()
  156. if partitioning_options["method"] != PostgresPartitioningMethod.HASH:
  157. self.add(
  158. operations.PostgresAddDefaultPartition(
  159. model_name=operation.name, name="default"
  160. )
  161. )
  162. partitioned_kwargs = {
  163. **kwargs,
  164. "partitioning_options": partitioning_options,
  165. }
  166. self.add(
  167. operations.PostgresCreatePartitionedModel(
  168. *args,
  169. **partitioned_kwargs,
  170. )
  171. )
  172. def add_delete_partitioned_model(self, operation: DeleteModel):
  173. """Adds a :see:PostgresDeletePartitionedModel operation to the list of
  174. operations to execute in the migration."""
  175. _, args, kwargs = operation.deconstruct()
  176. return self.add(
  177. operations.PostgresDeletePartitionedModel(*args, **kwargs)
  178. )
  179. def add_create_view_model(self, operation: CreateModel):
  180. """Adds a :see:PostgresCreateViewModel operation to the list of
  181. operations to execute in the migration."""
  182. if django.VERSION >= (4, 0):
  183. model_state = self.autodetector.to_state.models[
  184. self.app_label, operation.name.lower()
  185. ]
  186. view_options = model_state.view_options
  187. else:
  188. model = self.autodetector.new_apps.get_model(
  189. self.app_label, operation.name
  190. )
  191. view_options = model._view_meta.original_attrs
  192. _, args, kwargs = operation.deconstruct()
  193. view_kwargs = {**kwargs, "view_options": view_options}
  194. self.add(operations.PostgresCreateViewModel(*args, **view_kwargs))
  195. def add_delete_view_model(self, operation: DeleteModel):
  196. """Adds a :see:PostgresDeleteViewModel operation to the list of
  197. operations to execute in the migration."""
  198. _, args, kwargs = operation.deconstruct()
  199. return self.add(operations.PostgresDeleteViewModel(*args, **kwargs))
  200. def add_create_materialized_view_model(self, operation: CreateModel):
  201. """Adds a :see:PostgresCreateMaterializedViewModel operation to the
  202. list of operations to execute in the migration."""
  203. if django.VERSION >= (4, 0):
  204. model_state = self.autodetector.to_state.models[
  205. self.app_label, operation.name.lower()
  206. ]
  207. view_options = model_state.view_options
  208. else:
  209. model = self.autodetector.new_apps.get_model(
  210. self.app_label, operation.name
  211. )
  212. view_options = model._view_meta.original_attrs
  213. _, args, kwargs = operation.deconstruct()
  214. view_kwargs = {**kwargs, "view_options": view_options}
  215. self.add(
  216. operations.PostgresCreateMaterializedViewModel(
  217. *args,
  218. **view_kwargs,
  219. )
  220. )
  221. def add_delete_materialized_view_model(self, operation: DeleteModel):
  222. """Adds a :see:PostgresDeleteMaterializedViewModel operation to the
  223. list of operations to execute in the migration."""
  224. _, args, kwargs = operation.deconstruct()
  225. return self.add(
  226. operations.PostgresDeleteMaterializedViewModel(*args, **kwargs)
  227. )
  228. @contextmanager
  229. def patched_autodetector():
  230. """Patches the standard Django :seee:MigrationAutodetector for the duration
  231. of the context.
  232. The patch intercepts the `add_operation` function to
  233. customize how new operations are added.
  234. We have to do this because there is no way in Django
  235. to extend the auto detector otherwise.
  236. """
  237. autodetector_module_path = "django.db.migrations.autodetector"
  238. autodetector_class_path = (
  239. f"{autodetector_module_path}.MigrationAutodetector"
  240. )
  241. add_operation_path = f"{autodetector_class_path}.add_operation"
  242. def _patched(autodetector, app_label, operation, *args, **kwargs):
  243. handler = AddOperationHandler(autodetector, app_label, args, kwargs)
  244. if isinstance(operation, CreateModel):
  245. return handler.add_create_model(operation)
  246. if isinstance(operation, DeleteModel):
  247. return handler.add_delete_model(operation)
  248. if isinstance(operation, AddField):
  249. return handler.add_field(operation)
  250. if isinstance(operation, RemoveField):
  251. return handler.remove_field(operation)
  252. if isinstance(operation, AlterField):
  253. return handler.alter_field(operation)
  254. if isinstance(operation, RenameField):
  255. return handler.rename_field(operation)
  256. return handler.add(operation)
  257. with mock.patch(add_operation_path, new=_patched):
  258. yield