|
| 1 | +from contextlib import contextmanager |
1 | 2 | from dataclasses import dataclass |
2 | | -from typing import List, Optional, Tuple |
| 3 | +from typing import Dict, List, Optional, Tuple |
| 4 | + |
| 5 | +from django.db import transaction |
3 | 6 |
|
4 | 7 | from psqlextra.types import PostgresPartitioningMethod |
5 | 8 |
|
@@ -48,6 +51,22 @@ def partition_by_name( |
48 | 51 | class PostgresIntrospection(base_impl.introspection()): |
49 | 52 | """Adds introspection features specific to PostgreSQL.""" |
50 | 53 |
|
| 54 | + # TODO: This class is a mess, both here and in the |
| 55 | + # the base. |
| 56 | + # |
| 57 | + # Some methods return untyped dicts, some named tuples, |
| 58 | + # some flat lists of strings. It's horribly inconsistent. |
| 59 | + # |
| 60 | + # Most methods are poorly named. For example; `get_table_description` |
| 61 | + # does not return a complete table description. It merely returns |
| 62 | + # the columns. |
| 63 | + # |
| 64 | + # We do our best in this class to stay consistent with |
| 65 | + # the base in Django by respecting its naming scheme |
| 66 | + # and commonly used return types. Creating an API that |
| 67 | + # matches the look&feel from the Django base class |
| 68 | + # is more important than fixing those issues. |
| 69 | + |
51 | 70 | def get_partitioned_tables( |
52 | 71 | self, cursor |
53 | 72 | ) -> PostgresIntrospectedPartitonedTable: |
@@ -172,6 +191,9 @@ def get_partition_key(self, cursor, table_name: str) -> List[str]: |
172 | 191 | cursor.execute(sql, (table_name,)) |
173 | 192 | return [row[0] for row in cursor.fetchall()] |
174 | 193 |
|
| 194 | + def get_columns(self, cursor, table_name: str): |
| 195 | + return self.get_table_description(cursor, table_name) |
| 196 | + |
175 | 197 | def get_constraints(self, cursor, table_name: str): |
176 | 198 | """Retrieve any constraints or keys (unique, pk, fk, check, index) |
177 | 199 | across one or more columns. |
@@ -202,15 +224,93 @@ def get_constraints(self, cursor, table_name: str): |
202 | 224 | def get_table_locks(self, cursor) -> List[Tuple[str, str, str]]: |
203 | 225 | cursor.execute( |
204 | 226 | """ |
205 | | - SELECT |
206 | | - n.nspname, |
207 | | - t.relname, |
208 | | - l.mode |
209 | | - FROM pg_locks l |
210 | | - INNER JOIN pg_class t ON t.oid = l.relation |
211 | | - INNER JOIN pg_namespace n ON n.oid = t.relnamespace |
212 | | - WHERE t.relnamespace >= 2200 |
213 | | - ORDER BY n.nspname, t.relname, l.mode""" |
| 227 | + SELECT |
| 228 | + n.nspname, |
| 229 | + t.relname, |
| 230 | + l.mode |
| 231 | + FROM pg_locks l |
| 232 | + INNER JOIN pg_class t ON t.oid = l.relation |
| 233 | + INNER JOIN pg_namespace n ON n.oid = t.relnamespace |
| 234 | + WHERE t.relnamespace >= 2200 |
| 235 | + ORDER BY n.nspname, t.relname, l.mode |
| 236 | + """ |
214 | 237 | ) |
215 | 238 |
|
216 | 239 | return cursor.fetchall() |
| 240 | + |
| 241 | + def get_storage_settings(self, cursor, table_name: str) -> Dict[str, str]: |
| 242 | + sql = """ |
| 243 | + SELECT |
| 244 | + unnest(c.reloptions || array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x)) |
| 245 | + FROM |
| 246 | + pg_catalog.pg_class c |
| 247 | + LEFT JOIN |
| 248 | + pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid) |
| 249 | + LEFT JOIN |
| 250 | + pg_catalog.pg_am am ON (c.relam = am.oid) |
| 251 | + WHERE |
| 252 | + c.relname::text = %s |
| 253 | + """ |
| 254 | + |
| 255 | + cursor.execute(sql, (table_name,)) |
| 256 | + |
| 257 | + storage_settings = {} |
| 258 | + for row in cursor.fetchall(): |
| 259 | + # It's hard to believe, but storage settings are really |
| 260 | + # represented as `key=value` strings in Postgres. |
| 261 | + # See: https://www.postgresql.org/docs/current/catalog-pg-class.html |
| 262 | + name, value = row[0].split("=") |
| 263 | + storage_settings[name] = value |
| 264 | + |
| 265 | + return storage_settings |
| 266 | + |
| 267 | + def get_relations(self, cursor, table_name: str): |
| 268 | + """Gets a dictionary {field_name: (field_name_other_table, |
| 269 | + other_table)} representing all relations in the specified table. |
| 270 | +
|
| 271 | + This is overriden because the query in Django does not handle |
| 272 | + relations between tables in different schemas properly. |
| 273 | + """ |
| 274 | + |
| 275 | + cursor.execute( |
| 276 | + """ |
| 277 | + SELECT a1.attname, c2.relname, a2.attname |
| 278 | + FROM pg_constraint con |
| 279 | + LEFT JOIN pg_class c1 ON con.conrelid = c1.oid |
| 280 | + LEFT JOIN pg_class c2 ON con.confrelid = c2.oid |
| 281 | + LEFT JOIN pg_attribute a1 ON c1.oid = a1.attrelid AND a1.attnum = con.conkey[1] |
| 282 | + LEFT JOIN pg_attribute a2 ON c2.oid = a2.attrelid AND a2.attnum = con.confkey[1] |
| 283 | + WHERE |
| 284 | + con.conrelid = %s::regclass AND |
| 285 | + con.contype = 'f' AND |
| 286 | + pg_catalog.pg_table_is_visible(c1.oid) |
| 287 | + """, |
| 288 | + [table_name], |
| 289 | + ) |
| 290 | + return {row[0]: (row[2], row[1]) for row in cursor.fetchall()} |
| 291 | + |
| 292 | + @contextmanager |
| 293 | + def in_search_path(self, search_path: List[str]): |
| 294 | + """Changes the Postgres `search_path` within the context and switches |
| 295 | + it back when it exits.""" |
| 296 | + |
| 297 | + # Wrap in a transaction so a savepoint is created. If |
| 298 | + # something goes wrong, the `SET LOCAL search_path` |
| 299 | + # statement will be rolled back. |
| 300 | + with transaction.atomic(using=self.connection.alias): |
| 301 | + with self.connection.cursor() as cursor: |
| 302 | + cursor.execute("SHOW search_path") |
| 303 | + (original_search_path,) = cursor.fetchone() |
| 304 | + |
| 305 | + # Syntax in Postgres is a bit weird here. It isn't really |
| 306 | + # a list of names like in `WHERE bla in (val1, val2)`. |
| 307 | + placeholder = ", ".join(["%s" for _ in search_path]) |
| 308 | + cursor.execute( |
| 309 | + f"SET LOCAL search_path = {placeholder}", search_path |
| 310 | + ) |
| 311 | + |
| 312 | + yield self |
| 313 | + |
| 314 | + cursor.execute( |
| 315 | + f"SET LOCAL search_path = {original_search_path}" |
| 316 | + ) |
0 commit comments