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
34from 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