xdjango-postgres-extra 2.0.9rc5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. psqlextra/__init__.py +15 -0
  2. psqlextra/_version.py +1 -0
  3. psqlextra/apps.py +9 -0
  4. psqlextra/backend/__init__.py +0 -0
  5. psqlextra/backend/base.py +123 -0
  6. psqlextra/backend/base_impl.py +112 -0
  7. psqlextra/backend/introspection.py +327 -0
  8. psqlextra/backend/migrations/__init__.py +3 -0
  9. psqlextra/backend/migrations/operations/__init__.py +33 -0
  10. psqlextra/backend/migrations/operations/add_default_partition.py +35 -0
  11. psqlextra/backend/migrations/operations/add_hash_partition.py +74 -0
  12. psqlextra/backend/migrations/operations/add_list_partition.py +60 -0
  13. psqlextra/backend/migrations/operations/add_range_partition.py +72 -0
  14. psqlextra/backend/migrations/operations/apply_state.py +41 -0
  15. psqlextra/backend/migrations/operations/create_materialized_view_model.py +81 -0
  16. psqlextra/backend/migrations/operations/create_partitioned_model.py +87 -0
  17. psqlextra/backend/migrations/operations/create_view_model.py +71 -0
  18. psqlextra/backend/migrations/operations/delete_default_partition.py +24 -0
  19. psqlextra/backend/migrations/operations/delete_hash_partition.py +29 -0
  20. psqlextra/backend/migrations/operations/delete_list_partition.py +26 -0
  21. psqlextra/backend/migrations/operations/delete_materialized_view_model.py +28 -0
  22. psqlextra/backend/migrations/operations/delete_partition.py +29 -0
  23. psqlextra/backend/migrations/operations/delete_partitioned_model.py +28 -0
  24. psqlextra/backend/migrations/operations/delete_range_partition.py +29 -0
  25. psqlextra/backend/migrations/operations/delete_view_model.py +28 -0
  26. psqlextra/backend/migrations/operations/partition.py +32 -0
  27. psqlextra/backend/migrations/patched_autodetector.py +329 -0
  28. psqlextra/backend/migrations/patched_migrations.py +18 -0
  29. psqlextra/backend/migrations/patched_project_state.py +66 -0
  30. psqlextra/backend/migrations/state/__init__.py +19 -0
  31. psqlextra/backend/migrations/state/materialized_view.py +16 -0
  32. psqlextra/backend/migrations/state/model.py +127 -0
  33. psqlextra/backend/migrations/state/partitioning.py +136 -0
  34. psqlextra/backend/migrations/state/view.py +54 -0
  35. psqlextra/backend/operations.py +23 -0
  36. psqlextra/backend/schema.py +1210 -0
  37. psqlextra/backend/side_effects/__init__.py +7 -0
  38. psqlextra/backend/side_effects/hstore_required.py +184 -0
  39. psqlextra/backend/side_effects/hstore_unique.py +180 -0
  40. psqlextra/compiler.py +491 -0
  41. psqlextra/contrib/__init__.py +11 -0
  42. psqlextra/contrib/expressions.py +47 -0
  43. psqlextra/contrib/model_data_migrator.py +352 -0
  44. psqlextra/contrib/static_row.py +97 -0
  45. psqlextra/contrib/transaction.py +33 -0
  46. psqlextra/error.py +62 -0
  47. psqlextra/expressions.py +235 -0
  48. psqlextra/fields/__init__.py +3 -0
  49. psqlextra/fields/hstore_field.py +78 -0
  50. psqlextra/indexes/__init__.py +9 -0
  51. psqlextra/indexes/case_insensitive_unique_index.py +38 -0
  52. psqlextra/indexes/conditional_unique_index.py +58 -0
  53. psqlextra/indexes/unique_index.py +18 -0
  54. psqlextra/introspect/__init__.py +8 -0
  55. psqlextra/introspect/fields.py +21 -0
  56. psqlextra/introspect/models.py +175 -0
  57. psqlextra/locking.py +104 -0
  58. psqlextra/lookups.py +34 -0
  59. psqlextra/management/__init__.py +0 -0
  60. psqlextra/management/commands/__init__.py +0 -0
  61. psqlextra/management/commands/pgmakemigrations.py +13 -0
  62. psqlextra/management/commands/pgpartition.py +134 -0
  63. psqlextra/management/commands/pgrefreshmv.py +49 -0
  64. psqlextra/manager/__init__.py +8 -0
  65. psqlextra/manager/manager.py +72 -0
  66. psqlextra/models/__init__.py +10 -0
  67. psqlextra/models/base.py +16 -0
  68. psqlextra/models/options.py +33 -0
  69. psqlextra/models/partitioned.py +236 -0
  70. psqlextra/models/view.py +138 -0
  71. psqlextra/partitioning/__init__.py +30 -0
  72. psqlextra/partitioning/config.py +21 -0
  73. psqlextra/partitioning/constants.py +8 -0
  74. psqlextra/partitioning/current_time_strategy.py +81 -0
  75. psqlextra/partitioning/error.py +6 -0
  76. psqlextra/partitioning/manager.py +156 -0
  77. psqlextra/partitioning/partition.py +38 -0
  78. psqlextra/partitioning/plan.py +116 -0
  79. psqlextra/partitioning/range_partition.py +46 -0
  80. psqlextra/partitioning/range_strategy.py +9 -0
  81. psqlextra/partitioning/shorthands.py +77 -0
  82. psqlextra/partitioning/strategy.py +24 -0
  83. psqlextra/partitioning/time_partition.py +61 -0
  84. psqlextra/partitioning/time_partition_size.py +119 -0
  85. psqlextra/partitioning/time_strategy.py +23 -0
  86. psqlextra/py.typed +0 -0
  87. psqlextra/query.py +633 -0
  88. psqlextra/schema.py +227 -0
  89. psqlextra/settings.py +121 -0
  90. psqlextra/sql.py +224 -0
  91. psqlextra/type_assertions.py +29 -0
  92. psqlextra/types.py +41 -0
  93. psqlextra/util.py +24 -0
  94. xdjango_postgres_extra-2.0.9rc5.dist-info/METADATA +202 -0
  95. xdjango_postgres_extra-2.0.9rc5.dist-info/RECORD +98 -0
  96. xdjango_postgres_extra-2.0.9rc5.dist-info/WHEEL +5 -0
  97. xdjango_postgres_extra-2.0.9rc5.dist-info/licenses/LICENSE +21 -0
  98. xdjango_postgres_extra-2.0.9rc5.dist-info/top_level.txt +1 -0
psqlextra/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ import django
2
+
3
+ from ._version import __version__
4
+
5
+ if django.VERSION < (3, 2): # pragma: no cover
6
+ default_app_config = "psqlextra.apps.PostgresExtraAppConfig"
7
+
8
+ __all__ = [
9
+ "default_app_config",
10
+ "__version__",
11
+ ]
12
+ else:
13
+ __all__ = [
14
+ "__version__",
15
+ ]
psqlextra/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.9rc5"
psqlextra/apps.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class PostgresExtraAppConfig(AppConfig):
5
+ name = "psqlextra"
6
+ verbose_name = "PostgreSQL Extra"
7
+
8
+ def ready(self) -> None:
9
+ from .lookups import InValuesLookup # noqa
File without changes
@@ -0,0 +1,123 @@
1
+ import logging
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from django import VERSION
6
+ from django.conf import settings
7
+ from django.contrib.postgres.signals import (
8
+ get_hstore_oids,
9
+ register_type_handlers,
10
+ )
11
+ from django.db import ProgrammingError
12
+
13
+ from . import base_impl
14
+ from .introspection import PostgresIntrospection
15
+ from .operations import PostgresOperations
16
+ from .schema import PostgresSchemaEditor
17
+
18
+ from django.db.backends.postgresql.base import ( # isort:skip
19
+ DatabaseWrapper as PostgresDatabaseWrapper,
20
+ )
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ if TYPE_CHECKING:
27
+
28
+ class Wrapper(PostgresDatabaseWrapper):
29
+ pass
30
+
31
+ else:
32
+ Wrapper = base_impl.backend()
33
+
34
+
35
+ class DatabaseWrapper(Wrapper):
36
+ """Wraps the standard PostgreSQL database back-end.
37
+
38
+ Overrides the schema editor with our custom schema editor and makes
39
+ sure the `hstore` extension is enabled.
40
+ """
41
+
42
+ SchemaEditorClass = PostgresSchemaEditor # type: ignore[assignment]
43
+ introspection_class = PostgresIntrospection
44
+ ops_class = PostgresOperations
45
+
46
+ def __init__(self, *args, **kwargs):
47
+ super().__init__(*args, **kwargs)
48
+
49
+ if VERSION >= (5, 0):
50
+ return
51
+
52
+ # Some base back-ends such as the PostGIS back-end don't properly
53
+ # set `ops_class` and `introspection_class` and initialize these
54
+ # classes themselves.
55
+ #
56
+ # This can lead to broken functionality. We fix this automatically.
57
+
58
+ if not isinstance(self.introspection, self.introspection_class):
59
+ self.introspection = self.introspection_class(self)
60
+
61
+ if not isinstance(self.ops, self.ops_class):
62
+ self.ops = self.ops_class(self)
63
+
64
+ for expected_compiler_class in self.ops.compiler_classes:
65
+ compiler_class = self.ops.compiler(expected_compiler_class.__name__)
66
+
67
+ if not issubclass(compiler_class, expected_compiler_class):
68
+ logger.warning(
69
+ "Compiler '%s.%s' is not properly deriving from '%s.%s'."
70
+ % (
71
+ compiler_class.__module__,
72
+ compiler_class.__name__,
73
+ expected_compiler_class.__module__,
74
+ expected_compiler_class.__name__,
75
+ )
76
+ )
77
+
78
+ def prepare_database(self):
79
+ """Ran to prepare the configured database.
80
+
81
+ This is where we enable the `hstore` extension if it wasn't
82
+ enabled yet.
83
+ """
84
+
85
+ super().prepare_database()
86
+
87
+ setup_ext = getattr(
88
+ settings, "POSTGRES_EXTRA_AUTO_EXTENSION_SET_UP", True
89
+ )
90
+ if not setup_ext:
91
+ return False
92
+
93
+ with self.cursor() as cursor:
94
+ try:
95
+ cursor.execute("CREATE EXTENSION IF NOT EXISTS hstore")
96
+ except ProgrammingError: # permission denied
97
+ logger.warning(
98
+ 'Failed to create "hstore" extension. '
99
+ "Tables with hstore columns may fail to migrate. "
100
+ "If hstore is needed, make sure you are connected "
101
+ "to the database as a superuser "
102
+ "or add the extension manually.",
103
+ exc_info=True,
104
+ )
105
+ return
106
+
107
+ # Clear old (non-existent), stale oids.
108
+ get_hstore_oids.cache_clear()
109
+
110
+ # Verify that we (and Django) can find the OIDs
111
+ # for hstore.
112
+ oids, _ = get_hstore_oids(self.alias)
113
+ if not oids:
114
+ logger.warning(
115
+ '"hstore" extension was created, but we cannot find the oids'
116
+ "in the database. Something went wrong.",
117
+ )
118
+ return
119
+
120
+ # We must trigger Django into registering the type handlers now
121
+ # so that any subsequent code can properly use the newly
122
+ # registered types.
123
+ register_type_handlers(self)
@@ -0,0 +1,112 @@
1
+ import importlib
2
+
3
+ from django.conf import settings
4
+ from django.core.exceptions import ImproperlyConfigured
5
+ from django.db import DEFAULT_DB_ALIAS, connections
6
+ from django.db.backends.postgresql.base import DatabaseWrapper
7
+ from django.db.backends.postgresql.introspection import ( # type: ignore[import]
8
+ DatabaseIntrospection,
9
+ )
10
+ from django.db.backends.postgresql.operations import DatabaseOperations
11
+ from django.db.backends.postgresql.schema import ( # type: ignore[import]
12
+ DatabaseSchemaEditor,
13
+ )
14
+
15
+ from django.db.backends.postgresql.base import ( # isort:skip
16
+ DatabaseWrapper as Psycopg2DatabaseWrapper,
17
+ )
18
+
19
+
20
+ def base_backend_instance():
21
+ """Gets an instance of the base class for the custom database back-end.
22
+
23
+ This should be the Django PostgreSQL back-end. However,
24
+ some people are already using a custom back-end from
25
+ another package. We are nice people and expose an option
26
+ that allows them to configure the back-end we base upon.
27
+
28
+ As long as the specified base eventually also has
29
+ the PostgreSQL back-end as a base, then everything should
30
+ work as intended.
31
+
32
+ We create an instance to inspect what classes to subclass
33
+ because not all back-ends set properties such as `ops_class`
34
+ properly. The PostGIS back-end is a good example.
35
+ """
36
+ base_class_name = getattr(
37
+ settings,
38
+ "POSTGRES_EXTRA_DB_BACKEND_BASE",
39
+ "django.db.backends.postgresql",
40
+ )
41
+
42
+ base_class_module = importlib.import_module(base_class_name + ".base")
43
+ base_class = getattr(base_class_module, "DatabaseWrapper", None)
44
+
45
+ if not base_class:
46
+ raise ImproperlyConfigured(
47
+ (
48
+ "'%s' is not a valid database back-end."
49
+ " The module does not define a DatabaseWrapper class."
50
+ " Check the value of POSTGRES_EXTRA_DB_BACKEND_BASE."
51
+ )
52
+ % base_class_name
53
+ )
54
+
55
+ if isinstance(base_class, Psycopg2DatabaseWrapper):
56
+ raise ImproperlyConfigured(
57
+ (
58
+ "'%s' is not a valid database back-end."
59
+ " It does inherit from the PostgreSQL back-end."
60
+ " Check the value of POSTGRES_EXTRA_DB_BACKEND_BASE."
61
+ )
62
+ % base_class_name
63
+ )
64
+
65
+ base_instance = base_class(connections.databases[DEFAULT_DB_ALIAS])
66
+ if base_instance.connection:
67
+ raise ImproperlyConfigured(
68
+ (
69
+ "'%s' establishes a connection during initialization."
70
+ " This is not expected and can lead to more connections"
71
+ " being established than neccesarry."
72
+ )
73
+ % base_class_name
74
+ )
75
+
76
+ return base_instance
77
+
78
+
79
+ def backend() -> DatabaseWrapper:
80
+ """Gets the base class for the database back-end."""
81
+
82
+ return base_backend_instance().__class__
83
+
84
+
85
+ def schema_editor() -> DatabaseSchemaEditor:
86
+ """Gets the base class for the schema editor.
87
+
88
+ We have to use the configured base back-end's schema editor for
89
+ this.
90
+ """
91
+
92
+ return base_backend_instance().SchemaEditorClass
93
+
94
+
95
+ def introspection() -> DatabaseIntrospection:
96
+ """Gets the base class for the introspection class.
97
+
98
+ We have to use the configured base back-end's introspection class
99
+ for this.
100
+ """
101
+
102
+ return base_backend_instance().introspection.__class__
103
+
104
+
105
+ def operations() -> DatabaseOperations:
106
+ """Gets the base class for the operations class.
107
+
108
+ We have to use the configured base back-end's operations class for
109
+ this.
110
+ """
111
+
112
+ return base_backend_instance().ops.__class__
@@ -0,0 +1,327 @@
1
+ from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
3
+
4
+ from django.db.backends.postgresql.introspection import ( # type: ignore[import]
5
+ DatabaseIntrospection,
6
+ )
7
+
8
+ from psqlextra.types import PostgresPartitioningMethod
9
+
10
+ from . import base_impl
11
+
12
+ PARTITIONING_STRATEGY_TO_METHOD = {
13
+ "r": PostgresPartitioningMethod.RANGE,
14
+ "l": PostgresPartitioningMethod.LIST,
15
+ "h": PostgresPartitioningMethod.HASH,
16
+ }
17
+
18
+
19
+ @dataclass
20
+ class PostgresIntrospectedPartitionTable:
21
+ """Data container for information about a partition."""
22
+
23
+ name: str
24
+ full_name: str
25
+ comment: Optional[str]
26
+
27
+
28
+ @dataclass
29
+ class PostgresIntrospectedPartitonedTable:
30
+ """Data container for information about a partitioned table."""
31
+
32
+ name: str
33
+ method: PostgresPartitioningMethod
34
+ key: List[str]
35
+ partitions: List[PostgresIntrospectedPartitionTable]
36
+
37
+ def partition_by_name(
38
+ self, name: str
39
+ ) -> Optional[PostgresIntrospectedPartitionTable]:
40
+ """Finds the partition with the specified name."""
41
+
42
+ return next(
43
+ (
44
+ partition
45
+ for partition in self.partitions
46
+ if partition.name == name
47
+ ),
48
+ None,
49
+ )
50
+
51
+
52
+ if TYPE_CHECKING:
53
+
54
+ class Introspection(DatabaseIntrospection):
55
+ pass
56
+
57
+ else:
58
+ Introspection = base_impl.introspection()
59
+
60
+
61
+ class PostgresIntrospection(Introspection):
62
+ """Adds introspection features specific to PostgreSQL."""
63
+
64
+ # TODO: This class is a mess, both here and in the
65
+ # the base.
66
+ #
67
+ # Some methods return untyped dicts, some named tuples,
68
+ # some flat lists of strings. It's horribly inconsistent.
69
+ #
70
+ # Most methods are poorly named. For example; `get_table_description`
71
+ # does not return a complete table description. It merely returns
72
+ # the columns.
73
+ #
74
+ # We do our best in this class to stay consistent with
75
+ # the base in Django by respecting its naming scheme
76
+ # and commonly used return types. Creating an API that
77
+ # matches the look&feel from the Django base class
78
+ # is more important than fixing those issues.
79
+
80
+ def get_partitioned_tables(
81
+ self, cursor
82
+ ) -> List[PostgresIntrospectedPartitonedTable]:
83
+ """Gets a list of partitioned tables."""
84
+
85
+ cursor.execute(
86
+ """
87
+ SELECT
88
+ pg_class.relname,
89
+ pg_partitioned_table.partstrat
90
+ FROM
91
+ pg_partitioned_table
92
+ JOIN
93
+ pg_class
94
+ ON
95
+ pg_class.oid = pg_partitioned_table.partrelid
96
+ ORDER BY
97
+ pg_partitioned_table.partrelid
98
+ """
99
+ )
100
+
101
+ return [
102
+ PostgresIntrospectedPartitonedTable(
103
+ name=row[0],
104
+ method=PARTITIONING_STRATEGY_TO_METHOD[row[1]],
105
+ key=self.get_partition_key(cursor, row[0]),
106
+ partitions=self.get_partitions(cursor, row[0]),
107
+ )
108
+ for row in cursor.fetchall()
109
+ ]
110
+
111
+ def get_partitioned_table(self, cursor, table_name: str):
112
+ """Gets a single partitioned table."""
113
+
114
+ return next(
115
+ (
116
+ table
117
+ for table in self.get_partitioned_tables(cursor)
118
+ if table.name == table_name
119
+ ),
120
+ None,
121
+ )
122
+
123
+ def get_partitions(
124
+ self, cursor, table_name
125
+ ) -> List[PostgresIntrospectedPartitionTable]:
126
+ """Gets a list of partitions belonging to the specified partitioned
127
+ table."""
128
+
129
+ sql = """
130
+ SELECT
131
+ child.relname,
132
+ pg_description.description
133
+ FROM pg_inherits
134
+ JOIN
135
+ pg_class parent
136
+ ON
137
+ pg_inherits.inhparent = parent.oid
138
+ JOIN
139
+ pg_class child
140
+ ON
141
+ pg_inherits.inhrelid = child.oid
142
+ JOIN
143
+ pg_namespace nmsp_parent
144
+ ON
145
+ nmsp_parent.oid = parent.relnamespace
146
+ JOIN
147
+ pg_namespace nmsp_child
148
+ ON
149
+ nmsp_child.oid = child.relnamespace
150
+ LEFT JOIN
151
+ pg_description
152
+ ON
153
+ pg_description.objoid = child.oid
154
+ WHERE
155
+ parent.relname = %s
156
+ ORDER BY
157
+ child.oid,
158
+ child.relname
159
+ """
160
+
161
+ cursor.execute(sql, (table_name,))
162
+
163
+ return [
164
+ PostgresIntrospectedPartitionTable(
165
+ name=row[0].replace(f"{table_name}_", ""),
166
+ full_name=row[0],
167
+ comment=row[1] or None,
168
+ )
169
+ for row in cursor.fetchall()
170
+ ]
171
+
172
+ def get_partition_key(self, cursor, table_name: str) -> List[str]:
173
+ """Gets the partition key for the specified partitioned table.
174
+
175
+ Returns:
176
+ A list of column names that are part of the
177
+ partition key.
178
+ """
179
+
180
+ sql = """
181
+ SELECT
182
+ col.column_name
183
+ FROM
184
+ (SELECT partrelid,
185
+ partnatts,
186
+ CASE partstrat
187
+ WHEN 'l' THEN 'list'
188
+ WHEN 'r' THEN 'range'
189
+ WHEN 'h' THEN 'hash'
190
+ END AS partition_strategy,
191
+ Unnest(partattrs) column_index
192
+ FROM pg_partitioned_table) pt
193
+ JOIN
194
+ pg_class par
195
+ ON par.oid = pt.partrelid
196
+ JOIN
197
+ information_schema.COLUMNS col
198
+ ON
199
+ col.table_schema = par.relnamespace :: regnamespace :: text
200
+ AND col.table_name = par.relname
201
+ AND ordinal_position = pt.column_index
202
+ WHERE
203
+ table_name = %s
204
+ ORDER BY
205
+ col.ordinal_position,
206
+ col.column_name
207
+ """
208
+
209
+ cursor.execute(sql, (table_name,))
210
+ return [row[0] for row in cursor.fetchall()]
211
+
212
+ def get_columns(self, cursor, table_name: str):
213
+ return self.get_table_description(cursor, table_name)
214
+
215
+ def get_schema_list(self, cursor) -> List[str]:
216
+ """A flat list of available schemas."""
217
+
218
+ cursor.execute(
219
+ """
220
+ SELECT
221
+ schema_name
222
+ FROM
223
+ information_schema.schemata
224
+ ORDER BY
225
+ schema_name,
226
+ catalog_name
227
+ """,
228
+ tuple(),
229
+ )
230
+
231
+ return [name for name, in cursor.fetchall()]
232
+
233
+ def get_constraints(self, cursor, table_name: str):
234
+ """Retrieve any constraints or keys (unique, pk, fk, check, index)
235
+ across one or more columns.
236
+
237
+ Also retrieve the definition of expression-based indexes.
238
+ """
239
+
240
+ constraints = super().get_constraints(cursor, table_name)
241
+
242
+ # standard Django implementation does not return the definition
243
+ # for indexes, only for constraints, let's patch that up
244
+ cursor.execute(
245
+ "SELECT indexname, indexdef FROM pg_indexes WHERE tablename = %s",
246
+ (table_name,),
247
+ )
248
+ for index_name, definition in cursor.fetchall():
249
+ # PostgreSQL 13 or older won't give a definition if the
250
+ # index is actually a primary key.
251
+ constraint = constraints.get(index_name)
252
+ if not constraint:
253
+ continue
254
+
255
+ if constraint.get("definition") is None:
256
+ constraint["definition"] = definition
257
+
258
+ return constraints
259
+
260
+ def get_table_locks(self, cursor) -> List[Tuple[str, str, str]]:
261
+ cursor.execute(
262
+ """
263
+ SELECT
264
+ n.nspname,
265
+ t.relname,
266
+ l.mode
267
+ FROM pg_locks l
268
+ INNER JOIN pg_class t ON t.oid = l.relation
269
+ INNER JOIN pg_namespace n ON n.oid = t.relnamespace
270
+ WHERE t.relnamespace >= 2200
271
+ ORDER BY n.nspname, t.relname, l.mode
272
+ """
273
+ )
274
+
275
+ return cursor.fetchall()
276
+
277
+ def get_storage_settings(self, cursor, table_name: str) -> Dict[str, str]:
278
+ sql = """
279
+ SELECT
280
+ unnest(c.reloptions || array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x))
281
+ FROM
282
+ pg_catalog.pg_class c
283
+ LEFT JOIN
284
+ pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)
285
+ LEFT JOIN
286
+ pg_catalog.pg_am am ON (c.relam = am.oid)
287
+ WHERE
288
+ c.relname::text = %s
289
+ AND pg_catalog.pg_table_is_visible(c.oid)
290
+ """
291
+
292
+ cursor.execute(sql, (table_name,))
293
+
294
+ storage_settings = {}
295
+ for row in cursor.fetchall():
296
+ # It's hard to believe, but storage settings are really
297
+ # represented as `key=value` strings in Postgres.
298
+ # See: https://www.postgresql.org/docs/current/catalog-pg-class.html
299
+ name, value = row[0].split("=")
300
+ storage_settings[name] = value
301
+
302
+ return storage_settings
303
+
304
+ def get_relations(self, cursor, table_name: str):
305
+ """Gets a dictionary {field_name: (field_name_other_table,
306
+ other_table)} representing all relations in the specified table.
307
+
308
+ This is overriden because the query in Django does not handle
309
+ relations between tables in different schemas properly.
310
+ """
311
+
312
+ cursor.execute(
313
+ """
314
+ SELECT a1.attname, c2.relname, a2.attname
315
+ FROM pg_constraint con
316
+ LEFT JOIN pg_class c1 ON con.conrelid = c1.oid
317
+ LEFT JOIN pg_class c2 ON con.confrelid = c2.oid
318
+ LEFT JOIN pg_attribute a1 ON c1.oid = a1.attrelid AND a1.attnum = con.conkey[1]
319
+ LEFT JOIN pg_attribute a2 ON c2.oid = a2.attrelid AND a2.attnum = con.confkey[1]
320
+ WHERE
321
+ con.conrelid = %s::regclass AND
322
+ con.contype = 'f' AND
323
+ pg_catalog.pg_table_is_visible(c1.oid)
324
+ """,
325
+ [table_name],
326
+ )
327
+ return {row[0]: (row[2], row[1]) for row in cursor.fetchall()}
@@ -0,0 +1,3 @@
1
+ from .patched_migrations import postgres_patched_migrations
2
+
3
+ __all__ = ["postgres_patched_migrations"]
@@ -0,0 +1,33 @@
1
+ from .add_default_partition import PostgresAddDefaultPartition
2
+ from .add_hash_partition import PostgresAddHashPartition
3
+ from .add_list_partition import PostgresAddListPartition
4
+ from .add_range_partition import PostgresAddRangePartition
5
+ from .apply_state import ApplyState
6
+ from .create_materialized_view_model import PostgresCreateMaterializedViewModel
7
+ from .create_partitioned_model import PostgresCreatePartitionedModel
8
+ from .create_view_model import PostgresCreateViewModel
9
+ from .delete_default_partition import PostgresDeleteDefaultPartition
10
+ from .delete_hash_partition import PostgresDeleteHashPartition
11
+ from .delete_list_partition import PostgresDeleteListPartition
12
+ from .delete_materialized_view_model import PostgresDeleteMaterializedViewModel
13
+ from .delete_partitioned_model import PostgresDeletePartitionedModel
14
+ from .delete_range_partition import PostgresDeleteRangePartition
15
+ from .delete_view_model import PostgresDeleteViewModel
16
+
17
+ __all__ = [
18
+ "ApplyState",
19
+ "PostgresAddHashPartition",
20
+ "PostgresAddListPartition",
21
+ "PostgresAddRangePartition",
22
+ "PostgresAddDefaultPartition",
23
+ "PostgresDeleteDefaultPartition",
24
+ "PostgresDeleteHashPartition",
25
+ "PostgresDeleteListPartition",
26
+ "PostgresDeleteRangePartition",
27
+ "PostgresCreatePartitionedModel",
28
+ "PostgresDeletePartitionedModel",
29
+ "PostgresCreateViewModel",
30
+ "PostgresCreateMaterializedViewModel",
31
+ "PostgresDeleteViewModel",
32
+ "PostgresDeleteMaterializedViewModel",
33
+ ]
@@ -0,0 +1,35 @@
1
+ from psqlextra.backend.migrations.state import PostgresPartitionState
2
+
3
+ from .partition import PostgresPartitionOperation
4
+
5
+
6
+ class PostgresAddDefaultPartition(PostgresPartitionOperation):
7
+ """Adds a new default partition to a :see:PartitionedPostgresModel."""
8
+
9
+ def state_forwards(self, app_label, state):
10
+ model_state = state.models[(app_label, self.model_name_lower)]
11
+ model_state.add_partition(
12
+ PostgresPartitionState(
13
+ app_label=app_label, model_name=self.model_name, name=self.name
14
+ )
15
+ )
16
+
17
+ state.reload_model(app_label, self.model_name_lower)
18
+
19
+ def database_forwards(self, app_label, schema_editor, from_state, to_state):
20
+ model = to_state.apps.get_model(app_label, self.model_name)
21
+ if self.allow_migrate_model(schema_editor.connection.alias, model):
22
+ schema_editor.add_default_partition(model, self.name)
23
+
24
+ def database_backwards(
25
+ self, app_label, schema_editor, from_state, to_state
26
+ ):
27
+ model = from_state.apps.get_model(app_label, self.model_name)
28
+ if self.allow_migrate_model(schema_editor.connection.alias, model):
29
+ schema_editor.delete_partition(model, self.name)
30
+
31
+ def describe(self) -> str:
32
+ return "Creates default partition '%s' on %s" % (
33
+ self.name,
34
+ self.model_name,
35
+ )