123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
- from django.core.exceptions import ImproperlyConfigured
- from django.db import connections
- from django.db.models import Model
- from django.db.models.base import ModelBase
- from django.db.models.query import QuerySet
- from psqlextra.type_assertions import is_query_set, is_sql, is_sql_with_params
- from psqlextra.types import SQL, SQLWithParams
- from .base import PostgresModel
- from .options import PostgresViewOptions
- if TYPE_CHECKING:
- from psqlextra.backend.schema import PostgresSchemaEditor
- ViewQueryValue = Union[QuerySet, SQLWithParams, SQL]
- ViewQuery = Optional[Union[ViewQueryValue, Callable[[], ViewQueryValue]]]
- class PostgresViewModelMeta(ModelBase):
- """Custom meta class for :see:PostgresView and
- :see:PostgresMaterializedView.
- This meta class extracts attributes from the inner
- `ViewMeta` class and copies it onto a `_vew_meta`
- attribute. This is similar to how Django's `_meta` works.
- """
- def __new__(cls, name, bases, attrs, **kwargs):
- new_class = super().__new__(cls, name, bases, attrs, **kwargs)
- meta_class = attrs.pop("ViewMeta", None)
- view_query = getattr(meta_class, "query", None)
- sql_with_params = cls._view_query_as_sql_with_params(
- new_class, view_query
- )
- view_meta = PostgresViewOptions(query=sql_with_params)
- new_class.add_to_class("_view_meta", view_meta)
- return new_class
- @staticmethod
- def _view_query_as_sql_with_params(
- model: Model, view_query: ViewQuery
- ) -> Optional[SQLWithParams]:
- """Gets the query associated with the view as a raw SQL query with bind
- parameters.
- The query can be specified as a query set, raw SQL with params
- or without params. The query can also be specified as a callable
- which returns any of the above.
- When copying the meta options from the model, we convert any
- from the above to a raw SQL query with bind parameters. We do
- this is because it is what the SQL driver understands and
- we can easily serialize it into a migration.
- """
- # might be a callable to support delayed imports
- view_query = view_query() if callable(view_query) else view_query
- # make sure we don't do a boolean check on query sets,
- # because that might evaluate the query set
- if not is_query_set(view_query) and not view_query:
- return None
- is_valid_view_query = (
- is_query_set(view_query)
- or is_sql_with_params(view_query)
- or is_sql(view_query)
- )
- if not is_valid_view_query:
- raise ImproperlyConfigured(
- (
- "Model '%s' is not properly configured to be a view."
- " Set the `query` attribute on the `ViewMeta` class"
- " to be a valid `django.db.models.query.QuerySet`"
- " SQL string, or tuple of SQL string and params."
- )
- % (model.__class__.__name__)
- )
- # querysets can easily be converted into sql, params
- if is_query_set(view_query):
- return cast("QuerySet[Any]", view_query).query.sql_with_params()
- # query was already specified in the target format
- if is_sql_with_params(view_query):
- return cast(SQLWithParams, view_query)
- view_query_sql = cast(str, view_query)
- return view_query_sql, tuple()
- class PostgresViewModel(PostgresModel, metaclass=PostgresViewModelMeta):
- """Base class for creating a model that is a view."""
- _view_meta: PostgresViewOptions
- class Meta:
- abstract = True
- base_manager_name = "objects"
- class PostgresMaterializedViewModel(
- PostgresViewModel, metaclass=PostgresViewModelMeta
- ):
- """Base class for creating a model that is a materialized view."""
- class Meta:
- abstract = True
- base_manager_name = "objects"
- @classmethod
- def refresh(
- cls, concurrently: bool = False, using: Optional[str] = None
- ) -> None:
- """Refreshes this materialized view.
- Arguments:
- concurrently:
- Whether to tell PostgreSQL to refresh this
- materialized view concurrently.
- using:
- Optionally, the name of the database connection
- to use for refreshing the materialized view.
- """
- conn_name = using or "default"
- with connections[conn_name].schema_editor() as schema_editor:
- cast(
- "PostgresSchemaEditor", schema_editor
- ).refresh_materialized_view_model(cls, concurrently)
|