Skip to content

Commit 2ffa621

Browse files
AnexenPhotonios
authored andcommitted
Support for hash partioning
1 parent 582f9ab commit 2ffa621

File tree

11 files changed

+203
-10
lines changed

11 files changed

+203
-10
lines changed

docs/source/table_partitioning.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The following partitioning methods are available:
2020

2121
* ``PARTITION BY RANGE``
2222
* ``PARTITION BY LIST``
23+
* ``PARTITION BY HASH``
2324

2425
.. note::
2526

psqlextra/backend/introspection.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
from . import base_impl
77

8+
PARTITIONING_STRATEGY_TO_METHOD = {
9+
"r": PostgresPartitioningMethod.RANGE,
10+
"l": PostgresPartitioningMethod.LIST,
11+
"h": PostgresPartitioningMethod.HASH,
12+
}
813

914
@dataclass
1015
class PostgresIntrospectedPartitionTable:
@@ -64,9 +69,7 @@ def get_partitioned_tables(
6469
return [
6570
PostgresIntrospectedPartitonedTable(
6671
name=row[0],
67-
method=PostgresPartitioningMethod.RANGE
68-
if row[1] == "r"
69-
else PostgresPartitioningMethod.LIST,
72+
method=PARTITIONING_STRATEGY_TO_METHOD[row[1]],
7073
key=self.get_partition_key(cursor, row[0]),
7174
partitions=self.get_partitions(cursor, row[0]),
7275
)

psqlextra/backend/migrations/operations/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .add_default_partition import PostgresAddDefaultPartition
2+
from .add_hash_partition import PostgresAddHashPartition
23
from .add_list_partition import PostgresAddListPartition
34
from .add_range_partition import PostgresAddRangePartition
45
from .apply_state import ApplyState
56
from .create_materialized_view_model import PostgresCreateMaterializedViewModel
67
from .create_partitioned_model import PostgresCreatePartitionedModel
78
from .create_view_model import PostgresCreateViewModel
89
from .delete_default_partition import PostgresDeleteDefaultPartition
10+
from .delete_hash_partition import PostgresDeleteHashPartition
911
from .delete_list_partition import PostgresDeleteListPartition
1012
from .delete_materialized_view_model import PostgresDeleteMaterializedViewModel
1113
from .delete_partitioned_model import PostgresDeletePartitionedModel
@@ -14,12 +16,14 @@
1416

1517
__all__ = [
1618
"ApplyState",
17-
"PostgresAddRangePartition",
19+
"PostgresAddHashPartition",
1820
"PostgresAddListPartition",
21+
"PostgresAddRangePartition",
1922
"PostgresAddDefaultPartition",
2023
"PostgresDeleteDefaultPartition",
21-
"PostgresDeleteRangePartition",
24+
"PostgresDeleteHashPartition",
2225
"PostgresDeleteListPartition",
26+
"PostgresDeleteRangePartition",
2327
"PostgresCreatePartitionedModel",
2428
"PostgresDeletePartitionedModel",
2529
"PostgresCreateViewModel",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from psqlextra.backend.migrations.state import PostgresHashPartitionState
2+
3+
from .partition import PostgresPartitionOperation
4+
5+
6+
class PostgresAddHashPartition(PostgresPartitionOperation):
7+
"""
8+
Adds a new hash partition to a :see:PartitionedPostgresModel.
9+
Each partition will hold the rows for which the hash value of the partition
10+
key divided by the specified modulus will produce the specified remainder.
11+
"""
12+
13+
def __init__(self, model_name: str, name: str, modulus: int, remainder: int):
14+
"""Initializes new instance of :see:AddHashPartition.
15+
Arguments:
16+
model_name:
17+
The name of the :see:PartitionedPostgresModel.
18+
19+
name:
20+
The name to give to the new partition table.
21+
22+
modulus:
23+
Integer value by which the key is divided.
24+
25+
remainder:
26+
The remainder of the hash value when divided by modulus.
27+
"""
28+
29+
super().__init__(model_name, name)
30+
31+
self.modulus = modulus
32+
self.remainder = remainder
33+
34+
def state_forwards(self, app_label, state):
35+
model = state.models[(app_label, self.model_name_lower)]
36+
model.add_partition(
37+
PostgresHashPartitionState(
38+
app_label=app_label,
39+
model_name=self.model_name,
40+
name=self.name,
41+
modulus=self.modulus,
42+
remainder=self.remainder,
43+
)
44+
)
45+
46+
state.reload_model(app_label, self.model_name_lower)
47+
48+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
49+
model = to_state.apps.get_model(app_label, self.model_name)
50+
if self.allow_migrate_model(schema_editor.connection.alias, model):
51+
schema_editor.add_hash_partition(
52+
model, self.name, self.modulus, self.remainder
53+
)
54+
55+
def database_backwards(
56+
self, app_label, schema_editor, from_state, to_state
57+
):
58+
model = from_state.apps.get_model(app_label, self.model_name)
59+
if self.allow_migrate_model(schema_editor.connection.alias, model):
60+
schema_editor.delete_partition(model, self.name)
61+
62+
def deconstruct(self):
63+
name, args, kwargs = super().deconstruct()
64+
65+
kwargs["modulus"] = self.modulus
66+
kwargs["remainder"] = self.remainder
67+
68+
return name, args, kwargs
69+
70+
def describe(self) -> str:
71+
return "Creates hash partition %s on %s" % (self.name, self.model_name)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from .delete_partition import PostgresDeletePartition
2+
3+
4+
class PostgresDeleteHashPartition(PostgresDeletePartition):
5+
"""Deletes a hash partition that's part of a.
6+
7+
:see:PartitionedPostgresModel.
8+
"""
9+
10+
def database_backwards(
11+
self, app_label, schema_editor, from_state, to_state
12+
):
13+
model = to_state.apps.get_model(app_label, self.model_name)
14+
model_state = to_state.models[(app_label, self.model_name_lower)]
15+
16+
if self.allow_migrate_model(schema_editor.connection.alias, model):
17+
partition_state = model_state.partitions[self.name]
18+
schema_editor.add_hash_partition(
19+
model,
20+
partition_state.name,
21+
partition_state.modulus,
22+
partition_state.remainder,
23+
)
24+
25+
def describe(self) -> str:
26+
return "Deletes hash partition '%s' on %s" % (
27+
self.name,
28+
self.model_name,
29+
)

psqlextra/backend/migrations/state/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .materialized_view import PostgresMaterializedViewModelState
22
from .partitioning import (
3+
PostgresHashPartitionState,
34
PostgresListPartitionState,
45
PostgresPartitionedModelState,
56
PostgresPartitionState,
@@ -10,6 +11,7 @@
1011
__all__ = [
1112
"PostgresPartitionState",
1213
"PostgresRangePartitionState",
14+
"PostgresHashPartitionState",
1315
"PostgresListPartitionState",
1416
"PostgresPartitionedModelState",
1517
"PostgresViewModelState",

psqlextra/backend/migrations/state/partitioning.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ def __init__(self, app_label: str, model_name: str, name: str, values):
3838
self.values = values
3939

4040

41+
class PostgresHashPartitionState(PostgresPartitionState):
42+
"""Represents the state of a hash partition for a
43+
:see:PostgresPartitionedModel during a migration."""
44+
45+
def __init__(
46+
self,
47+
app_label: str,
48+
model_name: str,
49+
name: str,
50+
modulus: int,
51+
remainder: int,
52+
):
53+
super().__init__(app_label, model_name, name)
54+
55+
self.modulus = modulus
56+
self.remainder = remainder
57+
58+
4159
class PostgresPartitionedModelState(PostgresModelState):
4260
"""Represents the state of a :see:PostgresPartitionedModel in the
4361
migrations."""

psqlextra/backend/schema.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class PostgresSchemaEditor(base_impl.schema_editor()):
3838
)
3939
sql_partition_by = " PARTITION BY %s (%s)"
4040
sql_add_default_partition = "CREATE TABLE %s PARTITION OF %s DEFAULT"
41+
sql_add_hash_partition = "CREATE TABLE %s PARTITION OF %s FOR VALUES WITH (MODULUS %s, REMAINDER %s)"
4142
sql_add_range_partition = (
4243
"CREATE TABLE %s PARTITION OF %s FOR VALUES FROM (%s) TO (%s)"
4344
)
@@ -283,6 +284,52 @@ def add_list_partition(
283284
if comment:
284285
self.set_comment_on_table(table_name, comment)
285286

287+
def add_hash_partition(
288+
self,
289+
model: Model,
290+
name: str,
291+
modulus: int,
292+
remainder: int,
293+
comment: Optional[str] = None,
294+
) -> None:
295+
"""Creates a new hash partition for the specified partitioned model.
296+
297+
Arguments:
298+
model:
299+
Partitioned model to create a partition for.
300+
301+
name:
302+
Name to give to the new partition.
303+
Final name will be "{table_name}_{partition_name}"
304+
305+
modulus:
306+
Integer value by which the key is divided.
307+
308+
remainder:
309+
The remainder of the hash value when divided by modulus.
310+
311+
comment:
312+
Optionally, a comment to add on this partition table.
313+
"""
314+
315+
# asserts the model is a model set up for partitioning
316+
self._partitioning_properties_for_model(model)
317+
318+
table_name = self.create_partition_table_name(model, name)
319+
320+
sql = self.sql_add_hash_partition % (
321+
self.quote_name(table_name),
322+
self.quote_name(model._meta.db_table),
323+
"%s",
324+
"%s",
325+
)
326+
327+
with transaction.atomic():
328+
self.execute(sql, (modulus, remainder))
329+
330+
if comment:
331+
self.set_comment_on_table(table_name, comment)
332+
286333
def add_default_partition(
287334
self, model: Model, name: str, comment: Optional[str] = None
288335
) -> None:

psqlextra/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ class PostgresPartitioningMethod(StrEnum):
3535

3636
RANGE = "range"
3737
LIST = "list"
38+
HASH = "hash"

tests/test_make_migrations.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
method=PostgresPartitioningMethod.RANGE, key="timestamp"
3838
),
3939
),
40+
dict(
41+
fields={"artist_id": models.IntegerField()},
42+
partitioning_options=dict(
43+
method=PostgresPartitioningMethod.HASH, key="artist_id"
44+
)
45+
)
4046
],
4147
)
4248
@postgres_patched_migrations()

0 commit comments

Comments
 (0)