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)