Skip to content

Commit 0fe1d3f

Browse files
authored
Merge pull request #1 from uni-intelligence/hourly_partitions
Hourly partitions
2 parents 99b2c32 + b15fb4b commit 0fe1d3f

File tree

9 files changed

+168
-10
lines changed

9 files changed

+168
-10
lines changed

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM fkrull/multi-python as build
2+
ENV PYTHONUNBUFFERED 1
3+
ARG PIP_INDEX_URL
4+
ENV PIP_INDEX_URL ${PIP_INDEX_URL}
5+
RUN pip3 --no-cache install --upgrade pip
6+
COPY setup.py .
7+
COPY psqlextra/_version.py psqlextra/_version.py
8+
COPY README.md .
9+
RUN pip3 install .[test] .[analysis] --no-cache-dir --no-cache --prefix /python-packages --no-warn-script-location
10+
11+
FROM fkrull/multi-python
12+
ENV PROJECT_DIR /project
13+
WORKDIR $PROJECT_DIR
14+
ENV PYTHONUNBUFFERED 1
15+
COPY --from=build /python-packages /usr/local
16+
COPY . .

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
test:
2+
docker compose run --rm tests

docker-compose.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
version: "3.7"
2+
3+
services:
4+
postgres:
5+
image: postgis/postgis:14-3.2
6+
ports:
7+
- "5435:5432"
8+
volumes:
9+
- pgdata:/var/lib/postgresql/data/:delegated
10+
environment:
11+
- POSTGRES_HOST_AUTH_METHOD=trust
12+
- POSTGRES_DB=psqlextra
13+
healthcheck:
14+
test: "pg_isready -h localhost -p 5432 -q -U postgres"
15+
interval: 3s
16+
timeout: 5s
17+
retries: 3
18+
19+
20+
tests:
21+
build:
22+
context: ./
23+
depends_on:
24+
- postgres
25+
command:
26+
"tox"
27+
28+
volumes:
29+
pgdata:

psqlextra/partitioning/shorthands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def partition_by_current_time(
1616
months: Optional[int] = None,
1717
weeks: Optional[int] = None,
1818
days: Optional[int] = None,
19+
hours: Optional[int] = None,
1920
max_age: Optional[relativedelta] = None,
2021
) -> PostgresPartitioningConfig:
2122
"""Short-hand for generating a partitioning config that partitions the
@@ -42,6 +43,9 @@ def partition_by_current_time(
4243
days:
4344
The amount of days each partition should contain.
4445
46+
hours:
47+
The amount of hours each partition should contain.
48+
4549
max_age:
4650
The maximum age of a partition (calculated from the
4751
start of the partition).
@@ -51,7 +55,7 @@ def partition_by_current_time(
5155
"""
5256

5357
size = PostgresTimePartitionSize(
54-
years=years, months=months, weeks=weeks, days=days
58+
years=years, months=months, weeks=weeks, days=days, hours=hours,
5559
)
5660

5761
return PostgresPartitioningConfig(

psqlextra/partitioning/time_partition.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class PostgresTimePartition(PostgresRangePartition):
1919
PostgresTimePartitionUnit.MONTHS: "%Y_%b",
2020
PostgresTimePartitionUnit.WEEKS: "%Y_week_%W",
2121
PostgresTimePartitionUnit.DAYS: "%Y_%b_%d",
22+
PostgresTimePartitionUnit.HOURS: "%Y_%b_%d_%H",
2223
}
2324

2425
def __init__(
@@ -27,8 +28,8 @@ def __init__(
2728
end_datetime = start_datetime + size.as_delta()
2829

2930
super().__init__(
30-
from_values=start_datetime.strftime("%Y-%m-%d"),
31-
to_values=end_datetime.strftime("%Y-%m-%d"),
31+
from_values=start_datetime.strftime("%Y-%m-%d %H:00"),
32+
to_values=end_datetime.strftime("%Y-%m-%d %H:00"),
3233
)
3334

3435
self.size = size

psqlextra/partitioning/time_partition_size.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class PostgresTimePartitionUnit(enum.Enum):
1313
MONTHS = "months"
1414
WEEKS = "weeks"
1515
DAYS = "days"
16+
HOURS = "hours"
1617

1718

1819
class PostgresTimePartitionSize:
@@ -27,8 +28,10 @@ def __init__(
2728
months: Optional[int] = None,
2829
weeks: Optional[int] = None,
2930
days: Optional[int] = None,
31+
hours: Optional[int] = None,
32+
3033
) -> None:
31-
sizes = [years, months, weeks, days]
34+
sizes = [years, months, weeks, days, hours]
3235

3336
if not any(sizes):
3437
raise PostgresPartitioningError("Partition cannot be 0 in size.")
@@ -50,6 +53,9 @@ def __init__(
5053
elif days:
5154
self.unit = PostgresTimePartitionUnit.DAYS
5255
self.value = days
56+
elif hours:
57+
self.unit = PostgresTimePartitionUnit.HOURS
58+
self.value = hours
5359
else:
5460
raise PostgresPartitioningError(
5561
"Unsupported time partitioning unit"
@@ -68,25 +74,32 @@ def as_delta(self) -> relativedelta:
6874
if self.unit == PostgresTimePartitionUnit.DAYS:
6975
return relativedelta(days=self.value)
7076

77+
if self.unit == PostgresTimePartitionUnit.HOURS:
78+
return relativedelta(hours=self.value)
79+
7180
raise PostgresPartitioningError(
7281
"Unsupported time partitioning unit: %s" % self.unit
7382
)
7483

7584
def start(self, dt: datetime) -> datetime:
7685
if self.unit == PostgresTimePartitionUnit.YEARS:
77-
return self._ensure_datetime(dt.replace(month=1, day=1))
86+
return self._ensure_datetime(dt.replace(month=1, day=1, hour=0))
7887

7988
if self.unit == PostgresTimePartitionUnit.MONTHS:
80-
return self._ensure_datetime(dt.replace(day=1))
89+
return self._ensure_datetime(dt.replace(day=1, hour=0))
8190

8291
if self.unit == PostgresTimePartitionUnit.WEEKS:
83-
return self._ensure_datetime(dt - relativedelta(days=dt.weekday()))
92+
week_dt = dt - relativedelta(days=dt.weekday())
93+
return self._ensure_datetime(week_dt.replace(hour=0))
94+
95+
if self.unit == PostgresTimePartitionUnit.DAYS:
96+
return self._ensure_datetime(dt.replace(hour=0))
8497

8598
return self._ensure_datetime(dt)
8699

87100
@staticmethod
88101
def _ensure_datetime(dt: Union[date, datetime]) -> datetime:
89-
return datetime(year=dt.year, month=dt.month, day=dt.day)
102+
return datetime(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour)
90103

91104
def __repr__(self) -> str:
92105
return "PostgresTimePartitionSize<%s, %s>" % (self.unit, self.value)

settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
99

1010
DATABASES = {
11-
'default': dj_database_url.config(default='postgres:///psqlextra'),
11+
'default': dj_database_url.config(default='postgresql://postgres/?user=postgres'),
1212
}
1313

1414
DATABASES['default']['ENGINE'] = 'psqlextra.backend'

tests/test_partitioning_time.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,56 @@ def test_partitioning_time_daily_apply():
215215
assert table.partitions[6].name == "2019_jun_04"
216216

217217

218+
@pytest.mark.postgres_version(lt=110000)
219+
def test_partitioning_time_hourly_apply():
220+
"""Tests whether automatically creating new partitions ahead hourly works as
221+
expected."""
222+
223+
model = define_fake_partitioned_model(
224+
{"timestamp": models.DateTimeField()}, {"key": ["timestamp"]}
225+
)
226+
227+
schema_editor = connection.schema_editor()
228+
schema_editor.create_partitioned_model(model)
229+
230+
# create partitions for the next 4 hours (including the current)
231+
with freezegun.freeze_time("2019-1-23 22:00"):
232+
manager = PostgresPartitioningManager(
233+
[partition_by_current_time(model, hours=1, count=4)]
234+
)
235+
manager.plan().apply()
236+
237+
table = _get_partitioned_table(model)
238+
assert len(table.partitions) == 4
239+
assert table.partitions[0].name == "2019_jan_23_22"
240+
assert table.partitions[1].name == "2019_jan_23_23"
241+
assert table.partitions[2].name == "2019_jan_24_00"
242+
assert table.partitions[3].name == "2019_jan_24_01"
243+
244+
# re-running it with 5, should just create one additional partition
245+
with freezegun.freeze_time("2019-1-23 22:59"):
246+
manager = PostgresPartitioningManager(
247+
[partition_by_current_time(model, hours=1, count=5)]
248+
)
249+
manager.plan().apply()
250+
251+
table = _get_partitioned_table(model)
252+
assert len(table.partitions) == 5
253+
assert table.partitions[4].name == "2019_jan_24_02"
254+
255+
# it's june now, we want to partition two hours ahead
256+
with freezegun.freeze_time("2019-06-03"):
257+
manager = PostgresPartitioningManager(
258+
[partition_by_current_time(model, hours=1, count=2)]
259+
)
260+
manager.plan().apply()
261+
262+
table = _get_partitioned_table(model)
263+
assert len(table.partitions) == 7
264+
assert table.partitions[5].name == "2019_jun_03_00"
265+
assert table.partitions[6].name == "2019_jun_03_01"
266+
267+
218268
@pytest.mark.postgres_version(lt=110000)
219269
def test_partitioning_time_monthly_apply_insert():
220270
"""Tests whether automatically created monthly partitions line up
@@ -333,6 +383,45 @@ def test_partitioning_time_daily_apply_insert():
333383
model.objects.create(timestamp=datetime.date(2019, 1, 10))
334384

335385

386+
@pytest.mark.postgres_version(lt=110000)
387+
def test_partitioning_time_hourly_apply_insert():
388+
"""Tests whether automatically created hourly partitions line up
389+
perfectly."""
390+
391+
model = define_fake_partitioned_model(
392+
{"timestamp": models.DateTimeField()}, {"key": ["timestamp"]}
393+
)
394+
395+
schema_editor = connection.schema_editor()
396+
schema_editor.create_partitioned_model(model)
397+
398+
with freezegun.freeze_time("2019-1-07 13:59"):
399+
manager = PostgresPartitioningManager(
400+
[partition_by_current_time(model, hours=2, count=2)]
401+
)
402+
manager.plan().apply()
403+
404+
table = _get_partitioned_table(model)
405+
assert len(table.partitions) == 2
406+
407+
model.objects.create(timestamp=datetime.datetime(2019, 1, 7, 13))
408+
model.objects.create(timestamp=datetime.datetime(2019, 1, 7, 16, 59))
409+
410+
with transaction.atomic():
411+
with pytest.raises(IntegrityError):
412+
model.objects.create(timestamp=datetime.datetime(2019, 1, 7, 15))
413+
model.objects.create(timestamp=datetime.datetime(2019, 1, 8))
414+
415+
with freezegun.freeze_time("2019-1-07 13:00"):
416+
manager = PostgresPartitioningManager(
417+
[partition_by_current_time(model, hours=2, count=4)]
418+
)
419+
manager.plan().apply()
420+
421+
model.objects.create(timestamp=datetime.datetime(2019, 1, 7, 17))
422+
model.objects.create(timestamp=datetime.datetime(2019, 1, 7, 20, 59))
423+
424+
336425
@pytest.mark.postgres_version(lt=110000)
337426
@pytest.mark.parametrize(
338427
"kwargs,partition_names",

tox.ini

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
[tox]
22
envlist = py36-dj{20,21,22,30,31,32}, py37-dj{20,21,22,30,31,32}, py38-dj{20,21,22,30,31,32,40}, py39-dj{21,22,30,31,32,40}, py310-dj{21,22,30,31,32,40}
33

4+
; evaluation, flight_tracks
5+
;envlist = py37-dj{21}, py39-dj{32}
6+
7+
48
[testenv]
59
deps =
610
dj20: Django~=2.0.0
@@ -13,5 +17,5 @@ deps =
1317
.[test]
1418
setenv =
1519
DJANGO_SETTINGS_MODULE=settings
16-
passenv = DATABASE_URL
20+
passenv = postgresql://postgres:5432/?user=postgres
1721
commands = python setup.py test

0 commit comments

Comments
 (0)