timescaledb-rails 0.1.4 → 0.1.6
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.
- checksums.yaml +4 -4
- data/README.md +115 -8
- data/lib/timescaledb/rails/extensions/active_record/command_recorder.rb +58 -31
- data/lib/timescaledb/rails/extensions/active_record/postgresql_database_tasks.rb +59 -15
- data/lib/timescaledb/rails/extensions/active_record/schema_dumper.rb +56 -8
- data/lib/timescaledb/rails/extensions/active_record/schema_statements.rb +84 -17
- data/lib/timescaledb/rails/model/aggregate_functions.rb +30 -0
- data/lib/timescaledb/rails/model/finder_methods.rb +44 -0
- data/lib/timescaledb/rails/model/hyperfunctions.rb +36 -0
- data/lib/timescaledb/rails/model/scopes.rb +16 -9
- data/lib/timescaledb/rails/model.rb +16 -7
- data/lib/timescaledb/rails/models/application_record.rb +24 -0
- data/lib/timescaledb/rails/models/chunk.rb +22 -3
- data/lib/timescaledb/rails/models/compression_setting.rb +1 -1
- data/lib/timescaledb/rails/models/concerns/durationable.rb +78 -0
- data/lib/timescaledb/rails/models/continuous_aggregate.rb +66 -0
- data/lib/timescaledb/rails/models/dimension.rb +1 -1
- data/lib/timescaledb/rails/models/hypertable.rb +13 -9
- data/lib/timescaledb/rails/models/job.rb +3 -1
- data/lib/timescaledb/rails/models.rb +3 -0
- data/lib/timescaledb/rails/version.rb +1 -1
- metadata +13 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e350967c0553e59b57a773226c1b532f9e1e2a93a03b1908c7228816c942d39
|
4
|
+
data.tar.gz: 6636c247f01919bfd344f8ff4afb7a74eaff050f4ec3246b64f24904c3046752
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a96ee3099632fa74997735f6bbef27971be5f65d0e54e2978c90d4df7ae0ea64a7478acd0ffec787d05bcb0c7126a0ee51937fa4d59b03fc1fbb5c80bb20189c
|
7
|
+
data.tar.gz: 6bdd3e8bfe645fea36dc66461d6c4773d560d86d9af79c0bbb5a7a6d1d11eaf424898b6f3e07591b03828a2b2a521581b6a47783bb60e8dfa3b2b2a8e3ff8721
|
data/README.md
CHANGED
@@ -51,16 +51,20 @@ class CreatePayloadHypertable < ActiveRecord::Migration[7.0]
|
|
51
51
|
end
|
52
52
|
```
|
53
53
|
|
54
|
-
Add hypertable compression
|
54
|
+
Add hypertable compression policy
|
55
55
|
|
56
56
|
```ruby
|
57
|
-
class
|
57
|
+
class AddEventCompressionPolicy < ActiveRecord::Migration[7.0]
|
58
58
|
def up
|
59
|
-
|
59
|
+
enable_hypertable_compression :events, segment_by: :name, order_by: 'occurred_at DESC'
|
60
|
+
|
61
|
+
add_hypertable_compression_policy :events, 20.days
|
60
62
|
end
|
61
63
|
|
62
64
|
def down
|
63
|
-
|
65
|
+
remove_hypertable_compression_policy :events
|
66
|
+
|
67
|
+
disable_hypertable_compression :events
|
64
68
|
end
|
65
69
|
end
|
66
70
|
```
|
@@ -93,26 +97,69 @@ class AddEventReorderPolicy < ActiveRecord::Migration[7.0]
|
|
93
97
|
end
|
94
98
|
```
|
95
99
|
|
100
|
+
Create continuous aggregate
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class CreateTemperatureEventAggregate < ActiveRecord::Migration[7.0]
|
104
|
+
disable_ddl_transaction!
|
105
|
+
|
106
|
+
def up
|
107
|
+
create_continuous_aggregate(
|
108
|
+
:temperature_events,
|
109
|
+
Event.time_bucket(1.day).avg(:value).temperature.to_sql
|
110
|
+
)
|
111
|
+
|
112
|
+
add_continuous_aggregate_policy(:temperature_events, 1.month, 1.day, 1.hour)
|
113
|
+
end
|
114
|
+
|
115
|
+
def down
|
116
|
+
drop_continuous_aggregate(:temperature_events)
|
117
|
+
|
118
|
+
remove_continuous_aggregate_policy(:temperature_events)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
> **Reversible Migrations:**
|
124
|
+
>
|
125
|
+
> Above examples implement `up`/`down` methods to better document all the different APIs. Feel free to use `change` method, timescaledb-rails defines all the reverse calls for each API method so Active Record can automatically figure out how to reverse your migration.
|
126
|
+
|
96
127
|
### Models
|
97
128
|
|
98
129
|
If one of your models need TimescaleDB support, just include `Timescaledb::Rails::Model`
|
130
|
+
|
99
131
|
```ruby
|
100
|
-
class
|
132
|
+
class Payload < ActiveRecord::Base
|
101
133
|
include Timescaledb::Rails::Model
|
134
|
+
|
135
|
+
self.primary_key = 'id'
|
102
136
|
end
|
103
137
|
```
|
104
138
|
|
105
|
-
|
139
|
+
When hypertable belongs to a non default schema, don't forget to override `table_name`
|
106
140
|
|
107
141
|
```ruby
|
108
142
|
class Event < ActiveRecord::Base
|
109
143
|
include Timescaledb::Rails::Model
|
110
144
|
|
111
|
-
self.table_name = '
|
145
|
+
self.table_name = 'tdb.events'
|
112
146
|
end
|
113
147
|
```
|
114
148
|
|
115
|
-
|
149
|
+
Using `.find` is not recommended, to achieve more performat results, use these other find methods
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# When you know the exact time value
|
153
|
+
Payload.find_at_time(111, Time.new(2022, 01, 01, 10, 15, 30))
|
154
|
+
|
155
|
+
# If you know that the record occurred after a given time
|
156
|
+
Payload.find_after(222, 11.days.ago)
|
157
|
+
|
158
|
+
# Lastly, if you want to scope the search by a time range
|
159
|
+
Payload.find_between(333, 1.week.ago, 1.day.ago)
|
160
|
+
```
|
161
|
+
|
162
|
+
If you need to query data for a specific time period, `Timescaledb::Rails::Model` includes useful scopes
|
116
163
|
|
117
164
|
```ruby
|
118
165
|
# If you want to get all records from last year
|
@@ -136,6 +183,19 @@ Here the list of all available scopes
|
|
136
183
|
* yesterday
|
137
184
|
* today
|
138
185
|
|
186
|
+
If you still need to query data by other time periods, take a look at these other scopes
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
# If you want to get all records that occurred in the last 30 minutes
|
190
|
+
Event.after(30.minutes.ago) #=> [#<Event name...>, ...]
|
191
|
+
|
192
|
+
# If you want to get records that occurred in the last 4 days, excluding today
|
193
|
+
Event.between(4.days.ago, 1.day.ago) #=> [#<Event name...>, ...]
|
194
|
+
|
195
|
+
# If you want to get records that occurred at a specific time
|
196
|
+
Event.at_time(Time.new(2023, 01, 04, 10, 20, 30)) #=> [#<Event name...>, ...]
|
197
|
+
```
|
198
|
+
|
139
199
|
If you need information about your hypertable, use the following helper methods to get useful information
|
140
200
|
|
141
201
|
```ruby
|
@@ -165,6 +225,53 @@ chunk.compress! unless chunk.is_compressed?
|
|
165
225
|
chunk.decompress! if chunk.is_compressed?
|
166
226
|
```
|
167
227
|
|
228
|
+
If you need to reorder a specific chunk
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
chunk = Event.hypertable_chunks.first
|
232
|
+
|
233
|
+
# If an index is not specified, it will use the one from the reorder policy
|
234
|
+
# In case there is no reorder policy index it will raise an ArgumentError
|
235
|
+
chunk.reorder!
|
236
|
+
|
237
|
+
# If an index is specified it will use that index
|
238
|
+
chunk.reorder!(index)
|
239
|
+
```
|
240
|
+
|
241
|
+
If you need to manually refresh a continuous aggregate
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
aggregate = Event.hypertable.continuous_aggregates.first
|
245
|
+
|
246
|
+
aggregate.refresh!(5.days.ago, 1.day.ago)
|
247
|
+
```
|
248
|
+
|
249
|
+
### Hyperfunctions
|
250
|
+
|
251
|
+
#### Time bucket
|
252
|
+
|
253
|
+
You can call the time bucket function with an interval (note that leaving the target column blank will use the default time column of the hypertable)
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
Event.time_bucket(1.day)
|
257
|
+
|
258
|
+
Event.time_bucket('1 day')
|
259
|
+
|
260
|
+
Event.time_bucket(1.day, :created_at)
|
261
|
+
|
262
|
+
Event.time_bucket(1.day, 'occurred_at')
|
263
|
+
```
|
264
|
+
|
265
|
+
You may add aggregation like so:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
Event.time_bucket(1.day).avg(:column)
|
269
|
+
Event.time_bucket(1.day).sum(:column)
|
270
|
+
Event.time_bucket(1.day).min(:column)
|
271
|
+
Event.time_bucket(1.day).max(:column)
|
272
|
+
Event.time_bucket(1.day).count
|
273
|
+
```
|
274
|
+
|
168
275
|
## Contributing
|
169
276
|
|
170
277
|
Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.
|
@@ -5,32 +5,27 @@ module Timescaledb
|
|
5
5
|
module ActiveRecord
|
6
6
|
# :nodoc:
|
7
7
|
module CommandRecorder
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
record(:add_hypertable_retention_policy, args, &block)
|
30
|
-
end
|
31
|
-
|
32
|
-
def remove_hypertable_retention_policy(*args, &block)
|
33
|
-
record(:remove_hypertable_retention_policy, args, &block)
|
8
|
+
%w[
|
9
|
+
create_hypertable
|
10
|
+
enable_hypertable_compression
|
11
|
+
disable_hypertable_compression
|
12
|
+
add_hypertable_compression_policy
|
13
|
+
remove_hypertable_compression_policy
|
14
|
+
add_hypertable_reorder_policy
|
15
|
+
remove_hypertable_reorder_policy
|
16
|
+
add_hypertable_retention_policy
|
17
|
+
remove_hypertable_retention_policy
|
18
|
+
create_continuous_aggregate
|
19
|
+
drop_continuous_aggregate
|
20
|
+
add_continuous_aggregate_policy
|
21
|
+
remove_continuous_aggregate_policy
|
22
|
+
].each do |method|
|
23
|
+
module_eval <<-METHOD, __FILE__, __LINE__ + 1
|
24
|
+
def #{method}(*args, &block) # def create_table(*args, &block)
|
25
|
+
record(:"#{method}", args, &block) # record(:create_table, args, &block)
|
26
|
+
end # end
|
27
|
+
METHOD
|
28
|
+
ruby2_keywords(method) if respond_to?(:ruby2_keywords)
|
34
29
|
end
|
35
30
|
|
36
31
|
def invert_create_hypertable(args, &block)
|
@@ -41,16 +36,24 @@ module Timescaledb
|
|
41
36
|
[:drop_table, args.first, block]
|
42
37
|
end
|
43
38
|
|
44
|
-
def
|
45
|
-
[:
|
39
|
+
def invert_enable_hypertable_compression(args, &block)
|
40
|
+
[:disable_hypertable_compression, args, block]
|
46
41
|
end
|
47
42
|
|
48
|
-
def
|
43
|
+
def invert_disable_hypertable_compression(args, &block)
|
44
|
+
[:enable_hypertable_compression, args, block]
|
45
|
+
end
|
46
|
+
|
47
|
+
def invert_add_hypertable_compression_policy(args, &block)
|
48
|
+
[:remove_hypertable_compression_policy, args, block]
|
49
|
+
end
|
50
|
+
|
51
|
+
def invert_remove_hypertable_compression_policy(args, &block)
|
49
52
|
if args.size < 2
|
50
|
-
raise ::ActiveRecord::IrreversibleMigration, '
|
53
|
+
raise ::ActiveRecord::IrreversibleMigration, 'remove_hypertable_compression_policy is only reversible if given table name and compress period.' # rubocop:disable Layout/LineLength
|
51
54
|
end
|
52
55
|
|
53
|
-
[:
|
56
|
+
[:add_hypertable_compression_policy, args, block]
|
54
57
|
end
|
55
58
|
|
56
59
|
def invert_add_hypertable_retention_policy(args, &block)
|
@@ -76,6 +79,30 @@ module Timescaledb
|
|
76
79
|
|
77
80
|
[:add_hypertable_reorder_policy, args, block]
|
78
81
|
end
|
82
|
+
|
83
|
+
def invert_create_continuous_aggregate(args, &block)
|
84
|
+
[:drop_continuous_aggregate, args, block]
|
85
|
+
end
|
86
|
+
|
87
|
+
def invert_drop_continuous_aggregate(args, &block)
|
88
|
+
if args.size < 2
|
89
|
+
raise ::ActiveRecord::IrreversibleMigration, 'drop_continuous_aggregate is only reversible if given view name and view query.' # rubocop:disable Layout/LineLength
|
90
|
+
end
|
91
|
+
|
92
|
+
[:create_continuous_aggregate, args, block]
|
93
|
+
end
|
94
|
+
|
95
|
+
def invert_add_continuous_aggregate_policy(args, &block)
|
96
|
+
[:remove_continuous_aggregate_policy, args, block]
|
97
|
+
end
|
98
|
+
|
99
|
+
def invert_remove_continuous_aggregate_policy(args, &block)
|
100
|
+
if args.size < 4
|
101
|
+
raise ::ActiveRecord::IrreversibleMigration, 'remove_continuous_aggregate_policy is only reversible if given view name, start offset, end offset and schedule interval.' # rubocop:disable Layout/LineLength
|
102
|
+
end
|
103
|
+
|
104
|
+
[:add_continuous_aggregate_policy, args, block]
|
105
|
+
end
|
79
106
|
end
|
80
107
|
end
|
81
108
|
end
|
@@ -7,59 +7,95 @@ module Timescaledb
|
|
7
7
|
module Rails
|
8
8
|
module ActiveRecord
|
9
9
|
# :nodoc:
|
10
|
+
# rubocop:disable Layout/LineLength
|
10
11
|
module PostgreSQLDatabaseTasks
|
11
12
|
# @override
|
12
|
-
def structure_dump(filename, extra_flags)
|
13
|
+
def structure_dump(filename, extra_flags)
|
13
14
|
extra_flags = Array(extra_flags)
|
14
|
-
extra_flags
|
15
|
+
extra_flags |= timescale_structure_dump_default_flags if timescale_enabled?
|
15
16
|
|
16
17
|
super(filename, extra_flags)
|
17
18
|
|
18
19
|
return unless timescale_enabled?
|
19
20
|
|
21
|
+
hypertables(filename)
|
22
|
+
continuous_aggregates(filename)
|
23
|
+
end
|
24
|
+
|
25
|
+
def hypertables(filename)
|
20
26
|
File.open(filename, 'a') do |file|
|
21
27
|
Timescaledb::Rails::Hypertable.all.each do |hypertable|
|
22
28
|
drop_ts_insert_trigger_statment(hypertable, file)
|
23
29
|
create_hypertable_statement(hypertable, file)
|
24
|
-
|
30
|
+
enable_hypertable_compression_statement(hypertable, file)
|
31
|
+
add_hypertable_compression_policy_statement(hypertable, file)
|
25
32
|
add_hypertable_reorder_policy_statement(hypertable, file)
|
26
33
|
add_hypertable_retention_policy_statement(hypertable, file)
|
27
34
|
end
|
28
35
|
end
|
29
36
|
end
|
30
37
|
|
38
|
+
def continuous_aggregates(filename)
|
39
|
+
File.open(filename, 'a') do |file|
|
40
|
+
Timescaledb::Rails::ContinuousAggregate.dependency_ordered.each do |continuous_aggregate|
|
41
|
+
create_continuous_aggregate_statement(continuous_aggregate, file)
|
42
|
+
add_continuous_aggregate_policy_statement(continuous_aggregate, file)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
31
47
|
def drop_ts_insert_trigger_statment(hypertable, file)
|
32
48
|
file << "---\n"
|
33
|
-
file << "--- Drop ts_insert_blocker previously created by pg_dump to avoid pg errors, create_hypertable will re-create it again.\n"
|
49
|
+
file << "--- Drop ts_insert_blocker previously created by pg_dump to avoid pg errors, create_hypertable will re-create it again.\n"
|
34
50
|
file << "---\n\n"
|
35
|
-
file << "DROP TRIGGER IF EXISTS ts_insert_blocker ON #{hypertable.hypertable_name};\n"
|
51
|
+
file << "DROP TRIGGER IF EXISTS ts_insert_blocker ON #{hypertable.hypertable_schema}.#{hypertable.hypertable_name};\n"
|
36
52
|
end
|
37
53
|
|
38
54
|
def create_hypertable_statement(hypertable, file)
|
39
55
|
options = hypertable_options(hypertable)
|
40
56
|
|
41
|
-
file << "SELECT create_hypertable('#{hypertable.hypertable_name}', '#{hypertable.time_column_name}', #{options});\n\n"
|
57
|
+
file << "SELECT create_hypertable('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', '#{hypertable.time_column_name}', #{options});\n\n"
|
42
58
|
end
|
43
59
|
|
44
|
-
def
|
60
|
+
def enable_hypertable_compression_statement(hypertable, file)
|
45
61
|
return unless hypertable.compression?
|
46
62
|
|
47
63
|
options = hypertable_compression_options(hypertable)
|
48
64
|
|
49
|
-
file << "ALTER TABLE #{hypertable.hypertable_name} SET (#{options});\n\n"
|
50
|
-
|
65
|
+
file << "ALTER TABLE #{hypertable.hypertable_schema}.#{hypertable.hypertable_name} SET (#{options});\n\n"
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_hypertable_compression_policy_statement(hypertable, file)
|
69
|
+
return unless hypertable.compression_policy?
|
70
|
+
|
71
|
+
file << "SELECT add_compression_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', INTERVAL '#{hypertable.compression_policy_interval}');\n\n"
|
51
72
|
end
|
52
73
|
|
53
74
|
def add_hypertable_reorder_policy_statement(hypertable, file)
|
54
75
|
return unless hypertable.reorder?
|
55
76
|
|
56
|
-
file << "SELECT add_reorder_policy('#{hypertable.hypertable_name}', '#{hypertable.reorder_policy_index_name}');\n\n"
|
77
|
+
file << "SELECT add_reorder_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', '#{hypertable.reorder_policy_index_name}');\n\n"
|
57
78
|
end
|
58
79
|
|
59
80
|
def add_hypertable_retention_policy_statement(hypertable, file)
|
60
81
|
return unless hypertable.retention?
|
61
82
|
|
62
|
-
file << "SELECT add_retention_policy('#{hypertable.hypertable_name}', INTERVAL '#{hypertable.retention_policy_interval}');\n\n"
|
83
|
+
file << "SELECT add_retention_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', INTERVAL '#{hypertable.retention_policy_interval}');\n\n"
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_continuous_aggregate_statement(continuous_aggregate, file)
|
87
|
+
file << "CREATE MATERIALIZED VIEW #{continuous_aggregate.view_schema}.#{continuous_aggregate.view_name} WITH (timescaledb.continuous) AS\n"
|
88
|
+
file << "#{continuous_aggregate.view_definition.strip.indent(2)}\n\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_continuous_aggregate_policy_statement(continuous_aggregate, file)
|
92
|
+
return unless continuous_aggregate.refresh?
|
93
|
+
|
94
|
+
start_offset = continuous_aggregate.refresh_start_offset
|
95
|
+
end_offset = continuous_aggregate.refresh_end_offset
|
96
|
+
schedule_interval = continuous_aggregate.refresh_schedule_interval
|
97
|
+
|
98
|
+
file << "SELECT add_continuous_aggregate_policy('#{continuous_aggregate.view_schema}.#{continuous_aggregate.view_name}', start_offset => INTERVAL '#{start_offset}', end_offset => INTERVAL '#{end_offset}', schedule_interval => INTERVAL '#{schedule_interval}');\n\n"
|
63
99
|
end
|
64
100
|
|
65
101
|
def hypertable_options(hypertable)
|
@@ -93,18 +129,26 @@ module Timescaledb
|
|
93
129
|
hypertable.compression_segment_settings.map(&:attname)
|
94
130
|
end
|
95
131
|
|
96
|
-
# Returns `pg_dump`
|
132
|
+
# Returns `pg_dump` flags to exclude `_timescaledb_internal` schema tables and
|
133
|
+
# exclude the corresponding continuous aggregate views.
|
97
134
|
#
|
98
|
-
# @return [String]
|
135
|
+
# @return [Array<String>]
|
99
136
|
def timescale_structure_dump_default_flags
|
100
|
-
'--exclude-schema=_timescaledb_internal'
|
137
|
+
flags = ['--exclude-schema=_timescaledb_internal']
|
138
|
+
|
139
|
+
Timescaledb::Rails::ContinuousAggregate.pluck(:view_schema, :view_name).each do |view_schema, view_name|
|
140
|
+
flags << "--exclude-table=#{view_schema}.#{view_name}"
|
141
|
+
end
|
142
|
+
|
143
|
+
flags
|
101
144
|
end
|
102
145
|
|
103
146
|
# @return [Boolean]
|
104
147
|
def timescale_enabled?
|
105
|
-
|
148
|
+
ApplicationRecord.timescale_connection?(connection) && Hypertable.table_exists?
|
106
149
|
end
|
107
150
|
end
|
151
|
+
# rubocop:enable Layout/LineLength
|
108
152
|
end
|
109
153
|
end
|
110
154
|
end
|
@@ -7,7 +7,46 @@ module Timescaledb
|
|
7
7
|
module Rails
|
8
8
|
module ActiveRecord
|
9
9
|
# :nodoc:
|
10
|
-
module SchemaDumper
|
10
|
+
module SchemaDumper # rubocop:disable Metrics/ModuleLength
|
11
|
+
# @override
|
12
|
+
def tables(stream)
|
13
|
+
super
|
14
|
+
|
15
|
+
continuous_aggregates(stream)
|
16
|
+
stream
|
17
|
+
end
|
18
|
+
|
19
|
+
def continuous_aggregates(stream)
|
20
|
+
return unless timescale_enabled?
|
21
|
+
|
22
|
+
Timescaledb::Rails::ContinuousAggregate.dependency_ordered.each do |ca|
|
23
|
+
continuous_aggregate(ca, stream)
|
24
|
+
continuous_aggregate_policy(ca, stream)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def continuous_aggregate(continuous_aggregate, stream)
|
29
|
+
stream.puts " create_continuous_aggregate #{continuous_aggregate.view_name.inspect}, <<-SQL"
|
30
|
+
stream.puts " #{continuous_aggregate.view_definition.strip.indent(2)}"
|
31
|
+
stream.puts ' SQL'
|
32
|
+
stream.puts
|
33
|
+
end
|
34
|
+
|
35
|
+
def continuous_aggregate_policy(continuous_aggregate, stream)
|
36
|
+
return unless continuous_aggregate.refresh?
|
37
|
+
|
38
|
+
options = [
|
39
|
+
continuous_aggregate.view_name.inspect,
|
40
|
+
continuous_aggregate.refresh_start_offset.inspect,
|
41
|
+
continuous_aggregate.refresh_end_offset.inspect,
|
42
|
+
continuous_aggregate.refresh_schedule_interval.inspect
|
43
|
+
]
|
44
|
+
|
45
|
+
stream.puts " add_continuous_aggregate_policy #{options.join(', ')}"
|
46
|
+
stream.puts
|
47
|
+
end
|
48
|
+
|
49
|
+
# @override
|
11
50
|
def table(table, stream)
|
12
51
|
super(table, stream)
|
13
52
|
|
@@ -16,8 +55,9 @@ module Timescaledb
|
|
16
55
|
|
17
56
|
hypertable(hypertable, stream)
|
18
57
|
hypertable_compression(hypertable, stream)
|
19
|
-
|
20
|
-
|
58
|
+
hypertable_compression_policy(hypertable, stream)
|
59
|
+
hypertable_reorder_policy(hypertable, stream)
|
60
|
+
hypertable_retention_policy(hypertable, stream)
|
21
61
|
end
|
22
62
|
|
23
63
|
private
|
@@ -33,14 +73,22 @@ module Timescaledb
|
|
33
73
|
def hypertable_compression(hypertable, stream)
|
34
74
|
return unless hypertable.compression?
|
35
75
|
|
76
|
+
options = [hypertable.hypertable_name.inspect, hypertable_compression_options(hypertable)]
|
77
|
+
|
78
|
+
stream.puts " enable_hypertable_compression #{options.join(', ')}"
|
79
|
+
stream.puts
|
80
|
+
end
|
81
|
+
|
82
|
+
def hypertable_compression_policy(hypertable, stream)
|
83
|
+
return unless hypertable.compression_policy?
|
84
|
+
|
36
85
|
options = [hypertable.hypertable_name.inspect, hypertable.compression_policy_interval.inspect]
|
37
|
-
options |= hypertable_compression_options(hypertable)
|
38
86
|
|
39
|
-
stream.puts "
|
87
|
+
stream.puts " add_hypertable_compression_policy #{options.join(', ')}"
|
40
88
|
stream.puts
|
41
89
|
end
|
42
90
|
|
43
|
-
def
|
91
|
+
def hypertable_reorder_policy(hypertable, stream)
|
44
92
|
return unless hypertable.reorder?
|
45
93
|
|
46
94
|
options = [hypertable.hypertable_name.inspect, hypertable.reorder_policy_index_name.inspect]
|
@@ -49,7 +97,7 @@ module Timescaledb
|
|
49
97
|
stream.puts
|
50
98
|
end
|
51
99
|
|
52
|
-
def
|
100
|
+
def hypertable_retention_policy(hypertable, stream)
|
53
101
|
return unless hypertable.retention?
|
54
102
|
|
55
103
|
options = [hypertable.hypertable_name.inspect, hypertable.retention_policy_interval.inspect]
|
@@ -101,7 +149,7 @@ module Timescaledb
|
|
101
149
|
end
|
102
150
|
|
103
151
|
def timescale_enabled?
|
104
|
-
|
152
|
+
ApplicationRecord.timescale_connection?(@connection) && Hypertable.table_exists?
|
105
153
|
end
|
106
154
|
end
|
107
155
|
end
|
@@ -37,31 +37,45 @@ module Timescaledb
|
|
37
37
|
create_table(table_name, id: false, primary_key: primary_key, force: force, **options, &block)
|
38
38
|
end
|
39
39
|
|
40
|
-
execute "SELECT create_hypertable('#{table_name}', '#{time_column_name}', #{options_as_sql})"
|
40
|
+
execute "SELECT create_hypertable('#{table_name}', '#{time_column_name}', #{options_as_sql});"
|
41
41
|
end
|
42
42
|
|
43
|
-
# Enables compression
|
43
|
+
# Enables compression on given hypertable.
|
44
44
|
#
|
45
|
-
#
|
45
|
+
# enable_hypertable_compression('events', segment_by: :created_at, order_by: :name)
|
46
46
|
#
|
47
|
-
def
|
48
|
-
compress_after = compress_after.inspect if compress_after.is_a?(ActiveSupport::Duration)
|
49
|
-
|
47
|
+
def enable_hypertable_compression(table_name, segment_by: nil, order_by: nil)
|
50
48
|
options = ['timescaledb.compress']
|
51
49
|
options << "timescaledb.compress_orderby = '#{order_by}'" unless order_by.nil?
|
52
50
|
options << "timescaledb.compress_segmentby = '#{segment_by}'" unless segment_by.nil?
|
53
51
|
|
54
|
-
execute "ALTER TABLE #{table_name} SET (#{options.join(', ')})"
|
52
|
+
execute "ALTER TABLE #{table_name} SET (#{options.join(', ')});"
|
53
|
+
end
|
55
54
|
|
56
|
-
|
55
|
+
# Disables compression on given hypertable.
|
56
|
+
#
|
57
|
+
# disable_hypertable_compression('events')
|
58
|
+
#
|
59
|
+
def disable_hypertable_compression(table_name, segment_by: nil, order_by: nil) # rubocop:disable Lint/UnusedMethodArgument
|
60
|
+
execute "ALTER TABLE #{table_name} SET (timescaledb.compress = false);"
|
57
61
|
end
|
58
62
|
|
59
|
-
#
|
63
|
+
# Adds compression policy to given hypertable.
|
60
64
|
#
|
61
|
-
#
|
65
|
+
# add_hypertable_compression_policy('events', 7.days)
|
62
66
|
#
|
63
|
-
def
|
64
|
-
|
67
|
+
def add_hypertable_compression_policy(table_name, compress_after)
|
68
|
+
compress_after = compress_after.inspect if compress_after.is_a?(ActiveSupport::Duration)
|
69
|
+
|
70
|
+
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{stringify_interval(compress_after)}');"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Removes compression policy from the given hypertable.
|
74
|
+
#
|
75
|
+
# remove_hypertable_compression_policy('events')
|
76
|
+
#
|
77
|
+
def remove_hypertable_compression_policy(table_name, _compress_after = nil)
|
78
|
+
execute "SELECT remove_compression_policy('#{table_name}');"
|
65
79
|
end
|
66
80
|
|
67
81
|
# Add a data retention policy to given hypertable.
|
@@ -69,7 +83,7 @@ module Timescaledb
|
|
69
83
|
# add_hypertable_retention_policy('events', 7.days)
|
70
84
|
#
|
71
85
|
def add_hypertable_retention_policy(table_name, drop_after)
|
72
|
-
execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{drop_after
|
86
|
+
execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{stringify_interval(drop_after)}');"
|
73
87
|
end
|
74
88
|
|
75
89
|
# Removes data retention policy from given hypertable.
|
@@ -77,7 +91,7 @@ module Timescaledb
|
|
77
91
|
# remove_hypertable_retention_policy('events')
|
78
92
|
#
|
79
93
|
def remove_hypertable_retention_policy(table_name, _drop_after = nil)
|
80
|
-
execute "SELECT remove_retention_policy('#{table_name}')"
|
94
|
+
execute "SELECT remove_retention_policy('#{table_name}');"
|
81
95
|
end
|
82
96
|
|
83
97
|
# Adds a policy to reorder chunks on a given hypertable index in the background.
|
@@ -85,7 +99,7 @@ module Timescaledb
|
|
85
99
|
# add_hypertable_reorder_policy('events', 'index_events_on_created_at_and_name')
|
86
100
|
#
|
87
101
|
def add_hypertable_reorder_policy(table_name, index_name)
|
88
|
-
execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}')"
|
102
|
+
execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}');"
|
89
103
|
end
|
90
104
|
|
91
105
|
# Removes a policy to reorder a particular hypertable.
|
@@ -93,14 +107,67 @@ module Timescaledb
|
|
93
107
|
# remove_hypertable_reorder_policy('events')
|
94
108
|
#
|
95
109
|
def remove_hypertable_reorder_policy(table_name, _index_name = nil)
|
96
|
-
execute "SELECT remove_reorder_policy('#{table_name}')"
|
110
|
+
execute "SELECT remove_reorder_policy('#{table_name}');"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates a continuous aggregate
|
114
|
+
#
|
115
|
+
# create_continuous_aggregate(
|
116
|
+
# 'temperature_events', "SELECT * FROM events where event_type = 'temperature'"
|
117
|
+
# )
|
118
|
+
#
|
119
|
+
def create_continuous_aggregate(view_name, view_query, force: false)
|
120
|
+
if force
|
121
|
+
execute "DROP MATERIALIZED VIEW #{quote_table_name(view_name)} CASCADE;" if view_exists? view_name
|
122
|
+
else
|
123
|
+
schema_cache.clear_data_source_cache!(view_name.to_s)
|
124
|
+
end
|
125
|
+
|
126
|
+
execute "CREATE MATERIALIZED VIEW #{quote_table_name(view_name)} " \
|
127
|
+
"WITH (timescaledb.continuous) AS #{view_query};"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Drops a continuous aggregate
|
131
|
+
#
|
132
|
+
# drop_continuous_aggregate('temperature_events')
|
133
|
+
#
|
134
|
+
def drop_continuous_aggregate(view_name, _view_query = nil, force: false) # rubocop:disable Lint/UnusedMethodArgument
|
135
|
+
execute "DROP MATERIALIZED VIEW #{view_name};"
|
136
|
+
end
|
137
|
+
|
138
|
+
# Adds refresh continuous aggregate policy
|
139
|
+
#
|
140
|
+
# add_continuous_aggregate_policy('temperature_events', 1.month, 1.day, 1.hour)
|
141
|
+
#
|
142
|
+
def add_continuous_aggregate_policy(view_name, start_offset, end_offset, schedule_interval)
|
143
|
+
start_offset = start_offset.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(start_offset)}'"
|
144
|
+
end_offset = end_offset.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(end_offset)}'"
|
145
|
+
schedule_interval = schedule_interval.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(schedule_interval)}'"
|
146
|
+
|
147
|
+
execute "SELECT add_continuous_aggregate_policy('#{view_name}', start_offset => #{start_offset}, end_offset => #{end_offset}, schedule_interval => #{schedule_interval});" # rubocop:disable Layout/LineLength
|
148
|
+
end
|
149
|
+
|
150
|
+
# Removes refresh continuous aggregate policy
|
151
|
+
#
|
152
|
+
# remove_continuous_aggregate_policy('temperature_events')
|
153
|
+
#
|
154
|
+
def remove_continuous_aggregate_policy(view_name, _start_offset = nil,
|
155
|
+
_end_offset = nil, _schedule_interval = nil)
|
156
|
+
execute "SELECT remove_continuous_aggregate_policy('#{view_name}');"
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# @param [ActiveSupport::Duration|String] interval
|
162
|
+
def stringify_interval(interval)
|
163
|
+
interval.is_a?(ActiveSupport::Duration) ? interval.inspect : interval
|
97
164
|
end
|
98
165
|
|
99
166
|
# @return [String]
|
100
167
|
def hypertable_options_to_sql(options)
|
101
168
|
sql_statements = options.map do |option, value|
|
102
169
|
case option
|
103
|
-
when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{value}'"
|
170
|
+
when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{stringify_interval(value)}'"
|
104
171
|
when :if_not_exists then "if_not_exists => #{value ? 'TRUE' : 'FALSE'}"
|
105
172
|
end
|
106
173
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module Model
|
6
|
+
# :nodoc:
|
7
|
+
module AggregateFunctions
|
8
|
+
def count(alias_name = 'count')
|
9
|
+
select("COUNT(1) AS #{alias_name}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def avg(column_name, alias_name = 'avg')
|
13
|
+
select("AVG(#{column_name}) AS #{alias_name}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def sum(column_name, alias_name = 'sum')
|
17
|
+
select("SUM(#{column_name}) AS #{alias_name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def min(column_name, alias_name = 'min')
|
21
|
+
select("MIN(#{column_name}) AS #{alias_name}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def max(column_name, alias_name = 'max')
|
25
|
+
select("MAX(#{column_name}) AS #{alias_name}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module Model
|
6
|
+
# :nodoc:
|
7
|
+
module FinderMethods
|
8
|
+
# Adds a warning message to avoid calling find without filtering by time.
|
9
|
+
#
|
10
|
+
# @override
|
11
|
+
def find(*args)
|
12
|
+
warn "WARNING: Calling `.find` without filtering by `#{hypertable_time_column_name}` could cause performance issues, use built-in find_(at_time|between|after) methods for more performant results." # rubocop:disable Layout/LineLength
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Finds records by primary key and chunk time.
|
18
|
+
#
|
19
|
+
# @param [Array<Integer>, Integer] id The primary key values.
|
20
|
+
# @param [Time, Date, Integer] time The chunk time value.
|
21
|
+
def find_at_time(id, time)
|
22
|
+
at_time(time).find(id)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Finds records by primary key and chunk time occurring between given time range.
|
26
|
+
#
|
27
|
+
# @param [Array<Integer>, Integer] id The primary key values.
|
28
|
+
# @param [Time, Date, Integer] from The chunk from time value.
|
29
|
+
# @param [Time, Date, Integer] to The chunk to time value.
|
30
|
+
def find_between(id, from, to)
|
31
|
+
between(from, to).find(id)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Finds records by primary key and chunk time occurring after given time.
|
35
|
+
#
|
36
|
+
# @param [Array<Integer>, Integer] id The primary key values.
|
37
|
+
# @param [Time, Date, Integer] from The chunk from time value.
|
38
|
+
def find_after(id, from)
|
39
|
+
after(from).find(id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timescaledb/rails/model/aggregate_functions'
|
4
|
+
|
5
|
+
module Timescaledb
|
6
|
+
module Rails
|
7
|
+
module Model
|
8
|
+
# :nodoc:
|
9
|
+
module Hyperfunctions
|
10
|
+
TIME_BUCKET_ALIAS = 'time_bucket'
|
11
|
+
|
12
|
+
# @return [ActiveRecord::Relation<ActiveRecord::Base>]
|
13
|
+
def time_bucket(interval, target_column = nil, select_alias: TIME_BUCKET_ALIAS)
|
14
|
+
target_column &&= Arel.sql(target_column.to_s)
|
15
|
+
target_column ||= arel_table[hypertable_time_column_name]
|
16
|
+
|
17
|
+
time_bucket = Arel::Nodes::NamedFunction.new(
|
18
|
+
'time_bucket',
|
19
|
+
[Arel::Nodes.build_quoted(format_interval_value(interval)), target_column]
|
20
|
+
)
|
21
|
+
|
22
|
+
select(time_bucket.dup.as(select_alias))
|
23
|
+
.group(time_bucket)
|
24
|
+
.order(time_bucket)
|
25
|
+
.extending(AggregateFunctions)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def format_interval_value(value)
|
31
|
+
value.is_a?(ActiveSupport::Duration) ? value.inspect : value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -12,19 +12,19 @@ module Timescaledb
|
|
12
12
|
scope :last_year, lambda {
|
13
13
|
date = Date.current - 1.year
|
14
14
|
|
15
|
-
|
15
|
+
between(date.beginning_of_year, date.end_of_year)
|
16
16
|
}
|
17
17
|
|
18
18
|
scope :last_month, lambda {
|
19
19
|
date = Date.current - 1.month
|
20
20
|
|
21
|
-
|
21
|
+
between(date.beginning_of_month, date.end_of_month)
|
22
22
|
}
|
23
23
|
|
24
24
|
scope :last_week, lambda {
|
25
25
|
date = Date.current - 1.week
|
26
26
|
|
27
|
-
|
27
|
+
between(date.beginning_of_week, date.end_of_week)
|
28
28
|
}
|
29
29
|
|
30
30
|
scope :yesterday, lambda {
|
@@ -32,24 +32,31 @@ module Timescaledb
|
|
32
32
|
}
|
33
33
|
|
34
34
|
scope :this_year, lambda {
|
35
|
-
|
35
|
+
between(Date.current.beginning_of_year, Date.current.end_of_year)
|
36
36
|
}
|
37
37
|
|
38
38
|
scope :this_month, lambda {
|
39
|
-
|
39
|
+
between(Date.current.beginning_of_month, Date.current.end_of_month)
|
40
40
|
}
|
41
41
|
|
42
42
|
scope :this_week, lambda {
|
43
|
-
|
43
|
+
between(Date.current.beginning_of_week, Date.current.end_of_week)
|
44
44
|
}
|
45
45
|
|
46
46
|
scope :today, lambda {
|
47
47
|
where("DATE(#{hypertable_time_column_name}) = ?", Date.current)
|
48
48
|
}
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
scope :after, lambda { |time|
|
51
|
+
where("#{hypertable_time_column_name} > ?", time)
|
52
|
+
}
|
53
|
+
|
54
|
+
scope :at_time, lambda { |time|
|
55
|
+
where(hypertable_time_column_name => time)
|
56
|
+
}
|
57
|
+
|
58
|
+
scope :between, lambda { |from, to|
|
59
|
+
where(hypertable_time_column_name => from..to)
|
53
60
|
}
|
54
61
|
end
|
55
62
|
# rubocop:enable Metrics/BlockLength
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'timescaledb/rails/model/finder_methods'
|
4
|
+
require 'timescaledb/rails/model/hyperfunctions'
|
3
5
|
require 'timescaledb/rails/model/scopes'
|
4
6
|
|
5
7
|
module Timescaledb
|
@@ -14,25 +16,32 @@ module Timescaledb
|
|
14
16
|
|
15
17
|
# :nodoc:
|
16
18
|
module ClassMethods
|
17
|
-
|
19
|
+
include FinderMethods
|
20
|
+
include Hyperfunctions
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
def hypertable_time_column_name
|
24
|
+
@hypertable_time_column_name ||= hypertable&.time_column_name
|
25
|
+
end
|
18
26
|
|
19
27
|
# Returns only the name of the hypertable, table_name could include
|
20
28
|
# the schema path, we need to remove it.
|
21
29
|
#
|
22
30
|
# @return [String]
|
23
31
|
def hypertable_name
|
24
|
-
table_name.split('.').last
|
32
|
+
@hypertable_name ||= table_name.split('.').last
|
25
33
|
end
|
26
34
|
|
27
35
|
# Returns the schema where hypertable is stored.
|
28
36
|
#
|
29
37
|
# @return [String]
|
30
38
|
def hypertable_schema
|
31
|
-
|
32
|
-
table_name.split('.')
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
@hypertable_schema ||=
|
40
|
+
if table_name.split('.').size > 1
|
41
|
+
table_name.split('.')[0..-2].join('.')
|
42
|
+
else
|
43
|
+
PUBLIC_SCHEMA_NAME
|
44
|
+
end
|
36
45
|
end
|
37
46
|
|
38
47
|
# @return [Timescaledb::Rails::Hypertable]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
# :nodoc:
|
6
|
+
class ApplicationRecord < ::ActiveRecord::Base
|
7
|
+
self.abstract_class = true
|
8
|
+
|
9
|
+
def self.timescale_connection?(connection)
|
10
|
+
pool_name = lambda do |pool|
|
11
|
+
if pool.respond_to?(:db_config)
|
12
|
+
pool.db_config.name
|
13
|
+
elsif pool.respond_to?(:spec)
|
14
|
+
pool.spec.name
|
15
|
+
else
|
16
|
+
raise "Don't know how to get pool name from #{pool.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
pool_name[connection.pool] == pool_name[self.connection.pool]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -3,10 +3,12 @@
|
|
3
3
|
module Timescaledb
|
4
4
|
module Rails
|
5
5
|
# :nodoc:
|
6
|
-
class Chunk <
|
6
|
+
class Chunk < ApplicationRecord
|
7
7
|
self.table_name = 'timescaledb_information.chunks'
|
8
8
|
self.primary_key = 'hypertable_name'
|
9
9
|
|
10
|
+
belongs_to :hypertable, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Hypertable'
|
11
|
+
|
10
12
|
scope :compressed, -> { where(is_compressed: true) }
|
11
13
|
scope :decompressed, -> { where(is_compressed: false) }
|
12
14
|
|
@@ -15,16 +17,33 @@ module Timescaledb
|
|
15
17
|
end
|
16
18
|
|
17
19
|
def compress!
|
18
|
-
|
20
|
+
self.class.connection.execute(
|
19
21
|
"SELECT compress_chunk('#{chunk_full_name}')"
|
20
22
|
)
|
21
23
|
end
|
22
24
|
|
23
25
|
def decompress!
|
24
|
-
|
26
|
+
self.class.connection.execute(
|
25
27
|
"SELECT decompress_chunk('#{chunk_full_name}')"
|
26
28
|
)
|
27
29
|
end
|
30
|
+
|
31
|
+
# @param index [String] The name of the index to order by
|
32
|
+
#
|
33
|
+
def reorder!(index = nil)
|
34
|
+
if index.blank? && !hypertable.reorder?
|
35
|
+
raise ArgumentError, 'Index name is required if reorder policy is not set'
|
36
|
+
end
|
37
|
+
|
38
|
+
index ||= hypertable.reorder_policy_index_name
|
39
|
+
|
40
|
+
options = ["'#{chunk_full_name}'"]
|
41
|
+
options << "'#{index}'" if index.present?
|
42
|
+
|
43
|
+
self.class.connection.execute(
|
44
|
+
"SELECT reorder_chunk(#{options.join(', ')})"
|
45
|
+
)
|
46
|
+
end
|
28
47
|
end
|
29
48
|
end
|
30
49
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module Models
|
6
|
+
# :nodoc:
|
7
|
+
module Durationable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
HOUR_MINUTE_SECOND_REGEX = /^\d+:\d+:\d+$/.freeze
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
def parse_duration(duration)
|
14
|
+
return if duration.nil?
|
15
|
+
|
16
|
+
duration_in_seconds = duration_in_seconds(duration)
|
17
|
+
|
18
|
+
duration_to_interval(
|
19
|
+
ActiveSupport::Duration.build(duration_in_seconds)
|
20
|
+
)
|
21
|
+
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
|
22
|
+
duration
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Converts different interval formats into seconds.
|
28
|
+
#
|
29
|
+
# duration_in_seconds('P1D') #=> 86400
|
30
|
+
# duration_in_seconds('24:00:00') #=> 86400
|
31
|
+
# duration_in_seconds(1.day) #=> 86400
|
32
|
+
#
|
33
|
+
# @param [ActiveSupport::Duration|String] duration
|
34
|
+
# @return [Integer]
|
35
|
+
def duration_in_seconds(duration)
|
36
|
+
return duration.to_i if duration.is_a?(ActiveSupport::Duration)
|
37
|
+
|
38
|
+
if (duration =~ HOUR_MINUTE_SECOND_REGEX).present?
|
39
|
+
hours, minutes, seconds = duration.split(':').map(&:to_i)
|
40
|
+
|
41
|
+
(hours.hour + minutes.minute + seconds.second).to_i
|
42
|
+
else
|
43
|
+
ActiveSupport::Duration.parse(duration).to_i
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Converts given duration into a human interval readable format.
|
48
|
+
#
|
49
|
+
# duration_to_interval(1.day) #=> '1 day'
|
50
|
+
# duration_to_interval(2.weeks + 6.days) #=> '20 days'
|
51
|
+
# duration_to_interval(1.years + 3.months) #=> '1 year 3 months'
|
52
|
+
#
|
53
|
+
# @param [ActiveSupport::Duration] duration
|
54
|
+
# @return [String]
|
55
|
+
def duration_to_interval(duration)
|
56
|
+
parts = duration.parts
|
57
|
+
|
58
|
+
# Combine days and weeks if both present
|
59
|
+
#
|
60
|
+
# "1 week 2 days" => "9 days"
|
61
|
+
parts[:days] += parts.delete(:weeks) * 7 if parts.key?(:weeks) && parts.key?(:days)
|
62
|
+
|
63
|
+
parts.map do |(unit, quantity)|
|
64
|
+
"#{quantity} #{humanize_duration_unit(unit.to_s, quantity)}"
|
65
|
+
end.join(' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
# Pluralize or singularize given duration unit based on given count.
|
69
|
+
#
|
70
|
+
# @param [String] duration_unit
|
71
|
+
# @param [Integer] count
|
72
|
+
def humanize_duration_unit(duration_unit, count)
|
73
|
+
count > 1 ? duration_unit.pluralize : duration_unit.singularize
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timescaledb/rails/models/concerns/durationable'
|
4
|
+
|
5
|
+
module Timescaledb
|
6
|
+
module Rails
|
7
|
+
# :nodoc:
|
8
|
+
class ContinuousAggregate < ApplicationRecord
|
9
|
+
include Timescaledb::Rails::Models::Durationable
|
10
|
+
|
11
|
+
self.table_name = 'timescaledb_information.continuous_aggregates'
|
12
|
+
self.primary_key = 'materialization_hypertable_name'
|
13
|
+
|
14
|
+
has_many :jobs, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Job'
|
15
|
+
|
16
|
+
def self.dependency_ordered
|
17
|
+
deps = find_each.index_by(&:materialization_hypertable_name)
|
18
|
+
|
19
|
+
TSort.tsort_each(
|
20
|
+
->(&b) { deps.each_value.sort_by(&:hypertable_name).each(&b) },
|
21
|
+
->(n, &b) { Array.wrap(deps[n.hypertable_name]).each(&b) }
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Manually refresh a continuous aggregate.
|
26
|
+
#
|
27
|
+
# @param [DateTime] start_time
|
28
|
+
# @param [DateTime] end_time
|
29
|
+
#
|
30
|
+
def refresh!(start_time = 'NULL', end_time = 'NULL')
|
31
|
+
self.class.connection.execute(
|
32
|
+
"CALL refresh_continuous_aggregate('#{view_name}', #{start_time}, #{end_time});"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
def refresh_start_offset
|
38
|
+
parse_duration(refresh_job.config['start_offset'])
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String]
|
42
|
+
def refresh_end_offset
|
43
|
+
parse_duration(refresh_job.config['end_offset'])
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String]
|
47
|
+
def refresh_schedule_interval
|
48
|
+
interval = refresh_job.schedule_interval
|
49
|
+
|
50
|
+
interval.is_a?(String) ? parse_duration(interval) : interval.inspect
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Boolean]
|
54
|
+
def refresh?
|
55
|
+
refresh_job.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @return [Job]
|
61
|
+
def refresh_job
|
62
|
+
@refresh_job ||= jobs.policy_refresh_continuous_aggregate.first
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,12 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'timescaledb/rails/models/concerns/durationable'
|
4
|
+
|
3
5
|
module Timescaledb
|
4
6
|
module Rails
|
5
7
|
# :nodoc:
|
6
|
-
class Hypertable <
|
8
|
+
class Hypertable < ApplicationRecord
|
9
|
+
include Timescaledb::Rails::Models::Durationable
|
10
|
+
|
7
11
|
self.table_name = 'timescaledb_information.hypertables'
|
8
12
|
self.primary_key = 'hypertable_name'
|
9
13
|
|
14
|
+
has_many :continuous_aggregates, foreign_key: 'hypertable_name',
|
15
|
+
class_name: 'Timescaledb::Rails::ContinuousAggregate'
|
10
16
|
has_many :compression_settings, foreign_key: 'hypertable_name',
|
11
17
|
class_name: 'Timescaledb::Rails::CompressionSetting'
|
12
18
|
has_many :dimensions, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Dimension'
|
@@ -51,10 +57,15 @@ module Timescaledb
|
|
51
57
|
end
|
52
58
|
|
53
59
|
# @return [Boolean]
|
54
|
-
def
|
60
|
+
def compression_policy?
|
55
61
|
compression_job.present?
|
56
62
|
end
|
57
63
|
|
64
|
+
# @return [Boolean]
|
65
|
+
def compression?
|
66
|
+
compression_settings.any?
|
67
|
+
end
|
68
|
+
|
58
69
|
# @return [Boolean]
|
59
70
|
def reorder?
|
60
71
|
reorder_job.present?
|
@@ -86,13 +97,6 @@ module Timescaledb
|
|
86
97
|
def time_dimension
|
87
98
|
@time_dimension ||= dimensions.time.first
|
88
99
|
end
|
89
|
-
|
90
|
-
# @return [String]
|
91
|
-
def parse_duration(duration)
|
92
|
-
ActiveSupport::Duration.parse(duration).inspect
|
93
|
-
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
|
94
|
-
duration
|
95
|
-
end
|
96
100
|
end
|
97
101
|
end
|
98
102
|
end
|
@@ -3,17 +3,19 @@
|
|
3
3
|
module Timescaledb
|
4
4
|
module Rails
|
5
5
|
# :nodoc:
|
6
|
-
class Job <
|
6
|
+
class Job < ApplicationRecord
|
7
7
|
self.table_name = 'timescaledb_information.jobs'
|
8
8
|
self.primary_key = 'hypertable_name'
|
9
9
|
|
10
10
|
POLICY_COMPRESSION = 'policy_compression'
|
11
11
|
POLICY_REORDER = 'policy_reorder'
|
12
12
|
POLICY_RETENTION = 'policy_retention'
|
13
|
+
POLICY_REFRESH_CONTINUOUS_AGGREGATE = 'policy_refresh_continuous_aggregate'
|
13
14
|
|
14
15
|
scope :policy_compression, -> { where(proc_name: POLICY_COMPRESSION) }
|
15
16
|
scope :policy_reorder, -> { where(proc_name: POLICY_REORDER) }
|
16
17
|
scope :policy_retention, -> { where(proc_name: POLICY_RETENTION) }
|
18
|
+
scope :policy_refresh_continuous_aggregate, -> { where(proc_name: POLICY_REFRESH_CONTINUOUS_AGGREGATE) }
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './models/application_record'
|
4
|
+
|
3
5
|
require_relative './models/chunk'
|
4
6
|
require_relative './models/compression_setting'
|
7
|
+
require_relative './models/continuous_aggregate'
|
5
8
|
require_relative './models/dimension'
|
6
9
|
require_relative './models/hypertable'
|
7
10
|
require_relative './models/job'
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timescaledb-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Iván Etchart
|
8
8
|
- Santiago Doldán
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2023-04-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- - "~>"
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '2.15'
|
126
|
-
description:
|
126
|
+
description:
|
127
127
|
email: oss@crunchloop.io
|
128
128
|
executables: []
|
129
129
|
extensions: []
|
@@ -139,10 +139,16 @@ files:
|
|
139
139
|
- lib/timescaledb/rails/extensions/active_record/schema_dumper.rb
|
140
140
|
- lib/timescaledb/rails/extensions/active_record/schema_statements.rb
|
141
141
|
- lib/timescaledb/rails/model.rb
|
142
|
+
- lib/timescaledb/rails/model/aggregate_functions.rb
|
143
|
+
- lib/timescaledb/rails/model/finder_methods.rb
|
144
|
+
- lib/timescaledb/rails/model/hyperfunctions.rb
|
142
145
|
- lib/timescaledb/rails/model/scopes.rb
|
143
146
|
- lib/timescaledb/rails/models.rb
|
147
|
+
- lib/timescaledb/rails/models/application_record.rb
|
144
148
|
- lib/timescaledb/rails/models/chunk.rb
|
145
149
|
- lib/timescaledb/rails/models/compression_setting.rb
|
150
|
+
- lib/timescaledb/rails/models/concerns/durationable.rb
|
151
|
+
- lib/timescaledb/rails/models/continuous_aggregate.rb
|
146
152
|
- lib/timescaledb/rails/models/dimension.rb
|
147
153
|
- lib/timescaledb/rails/models/hypertable.rb
|
148
154
|
- lib/timescaledb/rails/models/job.rb
|
@@ -154,7 +160,7 @@ licenses:
|
|
154
160
|
- MIT
|
155
161
|
metadata:
|
156
162
|
rubygems_mfa_required: 'true'
|
157
|
-
post_install_message:
|
163
|
+
post_install_message:
|
158
164
|
rdoc_options: []
|
159
165
|
require_paths:
|
160
166
|
- lib
|
@@ -169,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
175
|
- !ruby/object:Gem::Version
|
170
176
|
version: '0'
|
171
177
|
requirements: []
|
172
|
-
rubygems_version: 3.3.
|
173
|
-
signing_key:
|
178
|
+
rubygems_version: 3.3.7
|
179
|
+
signing_key:
|
174
180
|
specification_version: 4
|
175
181
|
summary: TimescaleDB Rails integration
|
176
182
|
test_files: []
|