Skip to content

Commit 757f163

Browse files
authored
Merge pull request #14 from daadu/migrate_to_hypertable
proper migrate to hypertable
2 parents 41c7ac2 + b82f4cf commit 757f163

File tree

4 files changed

+216
-35
lines changed

4 files changed

+216
-35
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class Metric(TimescaleModel):
7979

8080
```
8181

82-
If you already have a table and want to just add a field you can add the TimescaleDateTimeField to your model. This also triggers the creation of a hypertable.
82+
If you already have a table, you can either add `time` field of type `TimescaleDateTimeField` to your model or rename (if not already named `time`) and change type of existing `DateTimeField` (rename first then run `makemigrations` and then change the type, so that `makemigrations` considers it as change in same field instead of removing and adding new field). This also triggers the creation of a hypertable.
8383

8484
```python
8585
from timescale.db.models.fields import TimescaleDateTimeField
@@ -153,4 +153,5 @@ As such the use of the Django's ORM is perfectally suited to this type of data.
153153
## Contributors
154154

155155
* [Ben Cleary](https://github.com/bencleary)
156-
* [Jonathan Sundqvist](https://github.com/jonathan-s)
156+
* [Jonathan Sundqvist](https://github.com/jonathan-s)
157+
* [Harsh Bhikadia](https://github.com/daadu)

README.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,14 @@ Implementation would look like this
7777
temperature = models.FloatField()
7878
7979
80-
If you already have a table and want to just add a field you can add the
81-
TimescaleDateTimeField to your model. This also triggers the creation of
82-
a hypertable.
80+
If you already have a table, you can either add `time`
81+
field of type `TimescaleDateTimeField` to your model or
82+
rename (if not already named `time`) and change type of
83+
existing `DateTimeField` (rename first then run
84+
`makemigrations` and then change the type, so that
85+
`makemigrations` considers it as change in same field
86+
instead of removing and adding new field). This also
87+
triggers the creation of a hypertable.
8388

8489
.. code:: python
8590

timescale/db/backends/postgis/schema.py

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,61 @@
44

55

66
class TimescaleSchemaEditor(PostGISSchemaEditor):
7+
sql_is_hypertable = 'SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = {table}'
8+
9+
sql_assert_is_hypertable = (
10+
'DO $do$ BEGIN '
11+
'IF EXISTS ( '
12+
+ sql_is_hypertable +
13+
') '
14+
'THEN NULL; '
15+
'ELSE RAISE EXCEPTION {error_message}; '
16+
'END IF;'
17+
'END; $do$'
18+
)
19+
sql_assert_is_not_hypertable = (
20+
'DO $do$ BEGIN '
21+
'IF EXISTS ( '
22+
+ sql_is_hypertable +
23+
') '
24+
'THEN RAISE EXCEPTION {error_message}; '
25+
'ELSE NULL; '
26+
'END IF;'
27+
'END; $do$'
28+
)
29+
30+
sql_drop_primary_key = 'ALTER TABLE {table} DROP CONSTRAINT {pkey}'
31+
732
sql_add_hypertable = (
833
"SELECT create_hypertable("
934
"{table}, {partition_column}, "
10-
"chunk_time_interval => interval {interval})"
35+
"chunk_time_interval => interval {interval}, "
36+
"migrate_data => {migrate})"
1137
)
1238

13-
sql_drop_primary_key = (
14-
'ALTER TABLE {table} '
15-
'DROP CONSTRAINT {pkey}'
16-
)
39+
sql_set_chunk_time_interval = 'SELECT set_chunk_time_interval({table}, interval {interval})'
1740

18-
def drop_primary_key(self, model):
41+
def _assert_is_hypertable(self, model):
42+
"""
43+
Assert if the table is a hyper table
44+
"""
45+
table = self.quote_value(model._meta.db_table)
46+
error_message = self.quote_value("assert failed - " + table + " should be a hyper table")
47+
48+
sql = self.sql_assert_is_hypertable.format(table=table, error_message=error_message)
49+
self.execute(sql)
50+
51+
def _assert_is_not_hypertable(self, model):
52+
"""
53+
Assert if the table is not a hyper table
54+
"""
55+
table = self.quote_value(model._meta.db_table)
56+
error_message = self.quote_value("assert failed - " + table + " should not be a hyper table")
57+
58+
sql = self.sql_assert_is_not_hypertable.format(table=table, error_message=error_message)
59+
self.execute(sql)
60+
61+
def _drop_primary_key(self, model):
1962
"""
2063
Hypertables can't partition if the primary key is not
2164
the partition column.
@@ -29,26 +72,70 @@ def drop_primary_key(self, model):
2972

3073
self.execute(sql)
3174

32-
def create_hypertable(self, model, field):
75+
def _create_hypertable(self, model, field, should_migrate=False):
3376
"""
3477
Create the hypertable with the partition column being the field.
3578
"""
79+
# assert that the table is not already a hypertable
80+
self._assert_is_not_hypertable(model)
81+
82+
# drop primary key of the table
83+
self._drop_primary_key(model)
84+
3685
partition_column = self.quote_value(field.column)
3786
interval = self.quote_value(field.interval)
3887
table = self.quote_value(model._meta.db_table)
88+
migrate = "true" if should_migrate else "false"
89+
90+
if should_migrate and getattr(settings, "TIMESCALE_MIGRATE_HYPERTABLE_WITH_FRESH_TABLE", False):
91+
# TODO migrate with fresh table [https://github.com/schlunsen/django-timescaledb/issues/16]
92+
raise NotImplementedError()
93+
else:
94+
sql = self.sql_add_hypertable.format(
95+
table=table, partition_column=partition_column, interval=interval, migrate=migrate
96+
)
97+
self.execute(sql)
98+
99+
def _set_chunk_time_interval(self, model, field):
100+
"""
101+
Change time interval for hypertable
102+
"""
103+
# assert if already a hypertable
104+
self._assert_is_hypertable(model)
39105

40-
sql = self.sql_add_hypertable.format(
41-
table=table, partition_column=partition_column, interval=interval
42-
)
106+
table = self.quote_value(model._meta.db_table)
107+
interval = self.quote_value(field.interval)
43108

109+
sql = self.sql_set_chunk_time_interval.format(table=table, interval=interval)
44110
self.execute(sql)
45111

46112
def create_model(self, model):
47113
super().create_model(model)
48114

115+
# scan if any field is of instance `TimescaleDateTimeField`
49116
for field in model._meta.local_fields:
50-
if not isinstance(field, TimescaleDateTimeField):
51-
continue
117+
if isinstance(field, TimescaleDateTimeField):
118+
# create hypertable, with the field as partition column
119+
self._create_hypertable(model, field)
120+
break
121+
122+
def add_field(self, model, field):
123+
super().add_field(model, field)
124+
125+
# check if this field is type `TimescaleDateTimeField`
126+
if isinstance(field, TimescaleDateTimeField):
127+
# migrate existing table to hypertable
128+
self._create_hypertable(model, field, True)
129+
130+
def alter_field(self, model, old_field, new_field, strict=False):
131+
super().alter_field(model, old_field, new_field, strict)
52132

53-
self.drop_primary_key(model)
54-
self.create_hypertable(model, field)
133+
# check if old_field is not type `TimescaleDateTimeField` and new_field is
134+
if not isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField):
135+
# migrate existing table to hypertable
136+
self._create_hypertable(model, new_field, True)
137+
# check if old_field and new_field is type `TimescaleDateTimeField` and `interval` is changed
138+
elif isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField) \
139+
and old_field.interval != new_field.interval:
140+
# change chunk-size
141+
self._set_chunk_time_interval(model, new_field)
Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,65 @@
1-
from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor
1+
from django.conf import settings
2+
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
23

34
from timescale.db.models.fields import TimescaleDateTimeField
45

56

6-
class TimescaleSchemaEditor(PostGISSchemaEditor):
7+
class TimescaleSchemaEditor(DatabaseSchemaEditor):
8+
sql_is_hypertable = 'SELECT * FROM timescaledb_information.hypertables WHERE hypertable_name = {table}'
9+
10+
sql_assert_is_hypertable = (
11+
'DO $do$ BEGIN '
12+
'IF EXISTS ( '
13+
+ sql_is_hypertable +
14+
') '
15+
'THEN NULL; '
16+
'ELSE RAISE EXCEPTION {error_message}; '
17+
'END IF;'
18+
'END; $do$'
19+
)
20+
sql_assert_is_not_hypertable = (
21+
'DO $do$ BEGIN '
22+
'IF EXISTS ( '
23+
+ sql_is_hypertable +
24+
') '
25+
'THEN RAISE EXCEPTION {error_message}; '
26+
'ELSE NULL; '
27+
'END IF;'
28+
'END; $do$'
29+
)
30+
31+
sql_drop_primary_key = 'ALTER TABLE {table} DROP CONSTRAINT {pkey}'
32+
733
sql_add_hypertable = (
834
"SELECT create_hypertable("
935
"{table}, {partition_column}, "
10-
"chunk_time_interval => interval {interval})"
36+
"chunk_time_interval => interval {interval}, "
37+
"migrate_data => {migrate})"
1138
)
1239

13-
sql_drop_primary_key = (
14-
'ALTER TABLE {table} '
15-
'DROP CONSTRAINT {pkey}'
16-
)
40+
sql_set_chunk_time_interval = 'SELECT set_chunk_time_interval({table}, interval {interval})'
1741

18-
def drop_primary_key(self, model):
42+
def _assert_is_hypertable(self, model):
43+
"""
44+
Assert if the table is a hyper table
45+
"""
46+
table = self.quote_value(model._meta.db_table)
47+
error_message = self.quote_value("assert failed - " + table + " should be a hyper table")
48+
49+
sql = self.sql_assert_is_hypertable.format(table=table, error_message=error_message)
50+
self.execute(sql)
51+
52+
def _assert_is_not_hypertable(self, model):
53+
"""
54+
Assert if the table is not a hyper table
55+
"""
56+
table = self.quote_value(model._meta.db_table)
57+
error_message = self.quote_value("assert failed - " + table + " should not be a hyper table")
58+
59+
sql = self.sql_assert_is_not_hypertable.format(table=table, error_message=error_message)
60+
self.execute(sql)
61+
62+
def _drop_primary_key(self, model):
1963
"""
2064
Hypertables can't partition if the primary key is not
2165
the partition column.
@@ -29,26 +73,70 @@ def drop_primary_key(self, model):
2973

3074
self.execute(sql)
3175

32-
def create_hypertable(self, model, field):
76+
def _create_hypertable(self, model, field, should_migrate=False):
3377
"""
3478
Create the hypertable with the partition column being the field.
3579
"""
80+
# assert that the table is not already a hypertable
81+
self._assert_is_not_hypertable(model)
82+
83+
# drop primary key of the table
84+
self._drop_primary_key(model)
85+
3686
partition_column = self.quote_value(field.column)
3787
interval = self.quote_value(field.interval)
3888
table = self.quote_value(model._meta.db_table)
89+
migrate = "true" if should_migrate else "false"
90+
91+
if should_migrate and getattr(settings, "TIMESCALE_MIGRATE_HYPERTABLE_WITH_FRESH_TABLE", False):
92+
# TODO migrate with fresh table [https://github.com/schlunsen/django-timescaledb/issues/16]
93+
raise NotImplementedError()
94+
else:
95+
sql = self.sql_add_hypertable.format(
96+
table=table, partition_column=partition_column, interval=interval, migrate=migrate
97+
)
98+
self.execute(sql)
99+
100+
def _set_chunk_time_interval(self, model, field):
101+
"""
102+
Change time interval for hypertable
103+
"""
104+
# assert if already a hypertable
105+
self._assert_is_hypertable(model)
39106

40-
sql = self.sql_add_hypertable.format(
41-
table=table, partition_column=partition_column, interval=interval
42-
)
107+
table = self.quote_value(model._meta.db_table)
108+
interval = self.quote_value(field.interval)
43109

110+
sql = self.sql_set_chunk_time_interval.format(table=table, interval=interval)
44111
self.execute(sql)
45112

46113
def create_model(self, model):
47114
super().create_model(model)
48115

116+
# scan if any field is of instance `TimescaleDateTimeField`
49117
for field in model._meta.local_fields:
50-
if not isinstance(field, TimescaleDateTimeField):
51-
continue
118+
if isinstance(field, TimescaleDateTimeField):
119+
# create hypertable, with the field as partition column
120+
self._create_hypertable(model, field)
121+
break
122+
123+
def add_field(self, model, field):
124+
super().add_field(model, field)
125+
126+
# check if this field is type `TimescaleDateTimeField`
127+
if isinstance(field, TimescaleDateTimeField):
128+
# migrate existing table to hypertable
129+
self._create_hypertable(model, field, True)
130+
131+
def alter_field(self, model, old_field, new_field, strict=False):
132+
super().alter_field(model, old_field, new_field, strict)
52133

53-
self.drop_primary_key(model)
54-
self.create_hypertable(model, field)
134+
# check if old_field is not type `TimescaleDateTimeField` and new_field is
135+
if not isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField):
136+
# migrate existing table to hypertable
137+
self._create_hypertable(model, new_field, True)
138+
# check if old_field and new_field is type `TimescaleDateTimeField` and `interval` is changed
139+
elif isinstance(old_field, TimescaleDateTimeField) and isinstance(new_field, TimescaleDateTimeField) \
140+
and old_field.interval != new_field.interval:
141+
# change chunk-size
142+
self._set_chunk_time_interval(model, new_field)

0 commit comments

Comments
 (0)