view.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
  2. from django.core.exceptions import ImproperlyConfigured
  3. from django.db import connections
  4. from django.db.models import Model
  5. from django.db.models.base import ModelBase
  6. from django.db.models.query import QuerySet
  7. from psqlextra.type_assertions import is_query_set, is_sql, is_sql_with_params
  8. from psqlextra.types import SQL, SQLWithParams
  9. from .base import PostgresModel
  10. from .options import PostgresViewOptions
  11. if TYPE_CHECKING:
  12. from psqlextra.backend.schema import PostgresSchemaEditor
  13. ViewQueryValue = Union[QuerySet, SQLWithParams, SQL]
  14. ViewQuery = Optional[Union[ViewQueryValue, Callable[[], ViewQueryValue]]]
  15. class PostgresViewModelMeta(ModelBase):
  16. """Custom meta class for :see:PostgresView and
  17. :see:PostgresMaterializedView.
  18. This meta class extracts attributes from the inner
  19. `ViewMeta` class and copies it onto a `_vew_meta`
  20. attribute. This is similar to how Django's `_meta` works.
  21. """
  22. def __new__(cls, name, bases, attrs, **kwargs):
  23. new_class = super().__new__(cls, name, bases, attrs, **kwargs)
  24. meta_class = attrs.pop("ViewMeta", None)
  25. view_query = getattr(meta_class, "query", None)
  26. sql_with_params = cls._view_query_as_sql_with_params(
  27. new_class, view_query
  28. )
  29. view_meta = PostgresViewOptions(query=sql_with_params)
  30. new_class.add_to_class("_view_meta", view_meta)
  31. return new_class
  32. @staticmethod
  33. def _view_query_as_sql_with_params(
  34. model: Model, view_query: ViewQuery
  35. ) -> Optional[SQLWithParams]:
  36. """Gets the query associated with the view as a raw SQL query with bind
  37. parameters.
  38. The query can be specified as a query set, raw SQL with params
  39. or without params. The query can also be specified as a callable
  40. which returns any of the above.
  41. When copying the meta options from the model, we convert any
  42. from the above to a raw SQL query with bind parameters. We do
  43. this is because it is what the SQL driver understands and
  44. we can easily serialize it into a migration.
  45. """
  46. # might be a callable to support delayed imports
  47. view_query = view_query() if callable(view_query) else view_query
  48. # make sure we don't do a boolean check on query sets,
  49. # because that might evaluate the query set
  50. if not is_query_set(view_query) and not view_query:
  51. return None
  52. is_valid_view_query = (
  53. is_query_set(view_query)
  54. or is_sql_with_params(view_query)
  55. or is_sql(view_query)
  56. )
  57. if not is_valid_view_query:
  58. raise ImproperlyConfigured(
  59. (
  60. "Model '%s' is not properly configured to be a view."
  61. " Set the `query` attribute on the `ViewMeta` class"
  62. " to be a valid `django.db.models.query.QuerySet`"
  63. " SQL string, or tuple of SQL string and params."
  64. )
  65. % (model.__class__.__name__)
  66. )
  67. # querysets can easily be converted into sql, params
  68. if is_query_set(view_query):
  69. return cast("QuerySet[Any]", view_query).query.sql_with_params()
  70. # query was already specified in the target format
  71. if is_sql_with_params(view_query):
  72. return cast(SQLWithParams, view_query)
  73. view_query_sql = cast(str, view_query)
  74. return view_query_sql, tuple()
  75. class PostgresViewModel(PostgresModel, metaclass=PostgresViewModelMeta):
  76. """Base class for creating a model that is a view."""
  77. _view_meta: PostgresViewOptions
  78. class Meta:
  79. abstract = True
  80. base_manager_name = "objects"
  81. class PostgresMaterializedViewModel(
  82. PostgresViewModel, metaclass=PostgresViewModelMeta
  83. ):
  84. """Base class for creating a model that is a materialized view."""
  85. class Meta:
  86. abstract = True
  87. base_manager_name = "objects"
  88. @classmethod
  89. def refresh(
  90. cls, concurrently: bool = False, using: Optional[str] = None
  91. ) -> None:
  92. """Refreshes this materialized view.
  93. Arguments:
  94. concurrently:
  95. Whether to tell PostgreSQL to refresh this
  96. materialized view concurrently.
  97. using:
  98. Optionally, the name of the database connection
  99. to use for refreshing the materialized view.
  100. """
  101. conn_name = using or "default"
  102. with connections[conn_name].schema_editor() as schema_editor:
  103. cast(
  104. "PostgresSchemaEditor", schema_editor
  105. ).refresh_materialized_view_model(cls, concurrently)