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.
- psqlextra/__init__.py +15 -0
- psqlextra/_version.py +1 -0
- psqlextra/apps.py +9 -0
- psqlextra/backend/__init__.py +0 -0
- psqlextra/backend/base.py +123 -0
- psqlextra/backend/base_impl.py +112 -0
- psqlextra/backend/introspection.py +327 -0
- psqlextra/backend/migrations/__init__.py +3 -0
- psqlextra/backend/migrations/operations/__init__.py +33 -0
- psqlextra/backend/migrations/operations/add_default_partition.py +35 -0
- psqlextra/backend/migrations/operations/add_hash_partition.py +74 -0
- psqlextra/backend/migrations/operations/add_list_partition.py +60 -0
- psqlextra/backend/migrations/operations/add_range_partition.py +72 -0
- psqlextra/backend/migrations/operations/apply_state.py +41 -0
- psqlextra/backend/migrations/operations/create_materialized_view_model.py +81 -0
- psqlextra/backend/migrations/operations/create_partitioned_model.py +87 -0
- psqlextra/backend/migrations/operations/create_view_model.py +71 -0
- psqlextra/backend/migrations/operations/delete_default_partition.py +24 -0
- psqlextra/backend/migrations/operations/delete_hash_partition.py +29 -0
- psqlextra/backend/migrations/operations/delete_list_partition.py +26 -0
- psqlextra/backend/migrations/operations/delete_materialized_view_model.py +28 -0
- psqlextra/backend/migrations/operations/delete_partition.py +29 -0
- psqlextra/backend/migrations/operations/delete_partitioned_model.py +28 -0
- psqlextra/backend/migrations/operations/delete_range_partition.py +29 -0
- psqlextra/backend/migrations/operations/delete_view_model.py +28 -0
- psqlextra/backend/migrations/operations/partition.py +32 -0
- psqlextra/backend/migrations/patched_autodetector.py +329 -0
- psqlextra/backend/migrations/patched_migrations.py +18 -0
- psqlextra/backend/migrations/patched_project_state.py +66 -0
- psqlextra/backend/migrations/state/__init__.py +19 -0
- psqlextra/backend/migrations/state/materialized_view.py +16 -0
- psqlextra/backend/migrations/state/model.py +127 -0
- psqlextra/backend/migrations/state/partitioning.py +136 -0
- psqlextra/backend/migrations/state/view.py +54 -0
- psqlextra/backend/operations.py +23 -0
- psqlextra/backend/schema.py +1210 -0
- psqlextra/backend/side_effects/__init__.py +7 -0
- psqlextra/backend/side_effects/hstore_required.py +184 -0
- psqlextra/backend/side_effects/hstore_unique.py +180 -0
- psqlextra/compiler.py +491 -0
- psqlextra/contrib/__init__.py +11 -0
- psqlextra/contrib/expressions.py +47 -0
- psqlextra/contrib/model_data_migrator.py +352 -0
- psqlextra/contrib/static_row.py +97 -0
- psqlextra/contrib/transaction.py +33 -0
- psqlextra/error.py +62 -0
- psqlextra/expressions.py +235 -0
- psqlextra/fields/__init__.py +3 -0
- psqlextra/fields/hstore_field.py +78 -0
- psqlextra/indexes/__init__.py +9 -0
- psqlextra/indexes/case_insensitive_unique_index.py +38 -0
- psqlextra/indexes/conditional_unique_index.py +58 -0
- psqlextra/indexes/unique_index.py +18 -0
- psqlextra/introspect/__init__.py +8 -0
- psqlextra/introspect/fields.py +21 -0
- psqlextra/introspect/models.py +175 -0
- psqlextra/locking.py +104 -0
- psqlextra/lookups.py +34 -0
- psqlextra/management/__init__.py +0 -0
- psqlextra/management/commands/__init__.py +0 -0
- psqlextra/management/commands/pgmakemigrations.py +13 -0
- psqlextra/management/commands/pgpartition.py +134 -0
- psqlextra/management/commands/pgrefreshmv.py +49 -0
- psqlextra/manager/__init__.py +8 -0
- psqlextra/manager/manager.py +72 -0
- psqlextra/models/__init__.py +10 -0
- psqlextra/models/base.py +16 -0
- psqlextra/models/options.py +33 -0
- psqlextra/models/partitioned.py +236 -0
- psqlextra/models/view.py +138 -0
- psqlextra/partitioning/__init__.py +30 -0
- psqlextra/partitioning/config.py +21 -0
- psqlextra/partitioning/constants.py +8 -0
- psqlextra/partitioning/current_time_strategy.py +81 -0
- psqlextra/partitioning/error.py +6 -0
- psqlextra/partitioning/manager.py +156 -0
- psqlextra/partitioning/partition.py +38 -0
- psqlextra/partitioning/plan.py +116 -0
- psqlextra/partitioning/range_partition.py +46 -0
- psqlextra/partitioning/range_strategy.py +9 -0
- psqlextra/partitioning/shorthands.py +77 -0
- psqlextra/partitioning/strategy.py +24 -0
- psqlextra/partitioning/time_partition.py +61 -0
- psqlextra/partitioning/time_partition_size.py +119 -0
- psqlextra/partitioning/time_strategy.py +23 -0
- psqlextra/py.typed +0 -0
- psqlextra/query.py +633 -0
- psqlextra/schema.py +227 -0
- psqlextra/settings.py +121 -0
- psqlextra/sql.py +224 -0
- psqlextra/type_assertions.py +29 -0
- psqlextra/types.py +41 -0
- psqlextra/util.py +24 -0
- xdjango_postgres_extra-2.0.9rc5.dist-info/METADATA +202 -0
- xdjango_postgres_extra-2.0.9rc5.dist-info/RECORD +98 -0
- xdjango_postgres_extra-2.0.9rc5.dist-info/WHEEL +5 -0
- xdjango_postgres_extra-2.0.9rc5.dist-info/licenses/LICENSE +21 -0
- 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
|
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,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
|
+
)
|