v0.166.0
  1"""
  2Useful auxiliary data structures for query construction. Not useful outside
  3the SQL domain.
  4"""
  5
  6from typing import TYPE_CHECKING, Any
  7
  8from plain.postgres.dialect import quote_name
  9from plain.postgres.sql.constants import INNER, LOUTER
 10
 11if TYPE_CHECKING:
 12    from plain.postgres.connection import DatabaseConnection
 13    from plain.postgres.fields.related import ForeignKeyField
 14    from plain.postgres.fields.reverse_related import ForeignKeyRel
 15    from plain.postgres.sql.compiler import SQLCompiler
 16
 17
 18class MultiJoin(Exception):
 19    """
 20    Used by join construction code to indicate the point at which a
 21    multi-valued join was attempted (if the caller wants to treat that
 22    exceptionally).
 23    """
 24
 25    def __init__(
 26        self, names_pos: int, path_with_names: list[tuple[str, list[Any]]]
 27    ) -> None:
 28        self.level = names_pos
 29        # The path travelled, this includes the path to the multijoin.
 30        self.names_with_path = path_with_names
 31
 32
 33class Join:
 34    """
 35    Used by sql.Query and sql.SQLCompiler to generate JOIN clauses into the
 36    FROM entry. For example, the SQL generated could be
 37        LEFT OUTER JOIN "sometable" T1
 38        ON ("othertable"."sometable_id" = "sometable"."id")
 39
 40    This class is primarily used in Query.alias_map. All entries in alias_map
 41    must be Join compatible by providing the following attributes and methods:
 42        - table_name (string)
 43        - table_alias (possible alias for the table, can be None)
 44        - join_type (can be None for those entries that aren't joined from
 45          anything)
 46        - parent_alias (which table is this join's parent, can be None similarly
 47          to join_type)
 48        - as_sql()
 49        - relabeled_clone()
 50    """
 51
 52    def __init__(
 53        self,
 54        table_name: str,
 55        parent_alias: str,
 56        table_alias: str,
 57        join_type: str,
 58        join_field: ForeignKeyField | ForeignKeyRel,
 59        nullable: bool,
 60    ) -> None:
 61        # Join table
 62        self.table_name = table_name
 63        self.parent_alias = parent_alias
 64        # Note: table_alias is not necessarily known at instantiation time.
 65        self.table_alias = table_alias
 66        # LOUTER or INNER
 67        self.join_type = join_type
 68        # The (lhs_col, rhs_col) pair for the JOIN's ON clause. A relation joins
 69        # on a single column pair.
 70        self.join_col = join_field.get_joining_columns()
 71        # Along which field (or ForeignKeyRel in the reverse join case)
 72        self.join_field = join_field
 73        # Is this join nullabled?
 74        self.nullable = nullable
 75
 76    def as_sql(
 77        self, compiler: SQLCompiler, connection: DatabaseConnection
 78    ) -> tuple[str, list[Any]]:
 79        """
 80        Generate the full
 81           LEFT OUTER JOIN sometable ON sometable.somecol = othertable.othercol, params
 82        clause for this join.
 83        """
 84        join_conditions = []
 85        params = []
 86        qn = compiler.quote_name_unless_alias
 87        qn2 = quote_name
 88
 89        # Add the single join condition for this relation's column pair.
 90        lhs_col, rhs_col = self.join_col
 91        join_conditions.append(
 92            f"{qn(self.parent_alias)}.{qn2(lhs_col)} = {qn(self.table_alias)}.{qn2(rhs_col)}"
 93        )
 94
 95        on_clause_sql = " AND ".join(join_conditions)
 96        alias_str = (
 97            "" if self.table_alias == self.table_name else (f" {self.table_alias}")
 98        )
 99        sql = f"{self.join_type} {qn(self.table_name)}{alias_str} ON ({on_clause_sql})"
100        return sql, params
101
102    def relabeled_clone(self, change_map: dict[str, str]) -> Join:
103        new_parent_alias = change_map.get(self.parent_alias, self.parent_alias)
104        new_table_alias = change_map.get(self.table_alias, self.table_alias)
105        return self.__class__(
106            self.table_name,
107            new_parent_alias,
108            new_table_alias,
109            self.join_type,
110            self.join_field,
111            self.nullable,
112        )
113
114    @property
115    def identity(self) -> tuple[type[Join], str, str, Any]:
116        return (
117            self.__class__,
118            self.table_name,
119            self.parent_alias,
120            self.join_field,
121        )
122
123    def __eq__(self, other: object) -> bool:
124        if not isinstance(other, Join):
125            return NotImplemented
126        return self.identity == other.identity
127
128    def __hash__(self) -> int:
129        return hash(self.identity)
130
131    def demote(self) -> Join:
132        new = self.relabeled_clone({})
133        new.join_type = INNER
134        return new
135
136    def promote(self) -> Join:
137        new = self.relabeled_clone({})
138        new.join_type = LOUTER
139        return new
140
141
142class BaseTable:
143    """
144    The BaseTable class is used for base table references in FROM clause. For
145    example, the SQL "foo" in
146        SELECT * FROM "foo" WHERE somecond
147    could be generated by this class.
148    """
149
150    join_type = None
151    parent_alias = None
152
153    def __init__(self, table_name: str, alias: str) -> None:
154        self.table_name = table_name
155        self.table_alias = alias
156
157    def as_sql(
158        self, compiler: SQLCompiler, connection: DatabaseConnection
159    ) -> tuple[str, list[Any]]:
160        alias_str = (
161            "" if self.table_alias == self.table_name else (f" {self.table_alias}")
162        )
163        base_sql = compiler.quote_name_unless_alias(self.table_name)
164        return base_sql + alias_str, []
165
166    def relabeled_clone(self, change_map: dict[str, str]) -> BaseTable:
167        return self.__class__(
168            self.table_name, change_map.get(self.table_alias, self.table_alias)
169        )
170
171    @property
172    def identity(self) -> tuple[type[BaseTable], str, str]:
173        return self.__class__, self.table_name, self.table_alias
174
175    def __eq__(self, other: object) -> bool:
176        if not isinstance(other, BaseTable):
177            return NotImplemented
178        return self.identity == other.identity
179
180    def __hash__(self) -> int:
181        return hash(self.identity)