timescaledb-rails 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12170e03389f6ac4f3bba7816dfb20dc75a47103032ec3e3799f3c78dca25a4b
4
- data.tar.gz: f26796af01f755648e952f9e851f24d05d7adb3439e3653d793beef624ea8044
3
+ metadata.gz: 7fc0e42b6db3e42267d014923180abf3c64a643937942b8b3f17ad9b2a3bf223
4
+ data.tar.gz: 96c3105df56f184894cdd7e3ca8658b083a87e8a050711c17e52eeae6c836c3e
5
5
  SHA512:
6
- metadata.gz: 22c4c72512895fadb2a7701222e6b890b60a15948dc9c6b5c01ded24afff9865d6ebc954673452b55b95ac68b18c933dc2bf7aec6ac135285ecfa950d346e8e3
7
- data.tar.gz: f67c442f8d2eb837566f8cc3a450d809a769343799997f3979a222bd0413c2ce89fad70b9b8f3e96267ab61e8bcf3cc2c167c15a60db1f1144ea17daf9cfee36
6
+ metadata.gz: 9988f806f0512c73456e531dd42dfe3f3c5765d9ee426bcf6f4b45e3098568fcac3518884abde8fb473a42cba66d73aa4558631b2806256ea4726496a212bdb9
7
+ data.tar.gz: e85fa03def618ed5d114275a6f2b929871ca0c0ea52e95ada85cca332baa2f1252f8a6bc3fc29be544aaa8dc8225dbcda52df83903bedd530a63b3683be30e8b
data/README.md CHANGED
@@ -93,26 +93,67 @@ class AddEventReorderPolicy < ActiveRecord::Migration[7.0]
93
93
  end
94
94
  ```
95
95
 
96
+ Create continuous aggregate
97
+
98
+ ```ruby
99
+ class CreateTemperatureEventAggregate < ActiveRecord::Migration[7.0]
100
+ def up
101
+ create_continuous_aggregate(
102
+ :temperature_events,
103
+ Event.time_bucket(1.day).avg(:value).temperature.to_sql
104
+ )
105
+
106
+ add_continuous_aggregate_policy(:temperature_events, 1.month, 1.day, 1.hour)
107
+ end
108
+
109
+ def down
110
+ drop_continuous_aggregate(:temperature_events)
111
+
112
+ remove_continuous_aggregate_policy(:temperature_events)
113
+ end
114
+ end
115
+ ```
116
+
117
+ > **Reversible Migrations:**
118
+ >
119
+ > 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.
120
+
96
121
  ### Models
97
122
 
98
123
  If one of your models need TimescaleDB support, just include `Timescaledb::Rails::Model`
124
+
99
125
  ```ruby
100
- class Event < ActiveRecord::Base
126
+ class Payload < ActiveRecord::Base
101
127
  include Timescaledb::Rails::Model
128
+
129
+ self.primary_key = 'id'
102
130
  end
103
131
  ```
104
132
 
105
- If the hypertable does not belong to the default schema, don't forget to override `table_name`
133
+ When hypertable belongs to a non default schema, don't forget to override `table_name`
106
134
 
107
135
  ```ruby
108
136
  class Event < ActiveRecord::Base
109
137
  include Timescaledb::Rails::Model
110
138
 
111
- self.table_name = 'v1.events'
139
+ self.table_name = 'tdb.events'
112
140
  end
113
141
  ```
114
142
 
115
- If you need to query data for a specific time period, `Timescaledb::Rails::Model` incluldes useful scopes
143
+ Using `.find` is not recommended, to achieve more performat results, use these other find methods
144
+
145
+ ```ruby
146
+ # When you know the exact time value
147
+ Payload.find_at_time(111, Time.new(2022, 01, 01, 10, 15, 30))
148
+
149
+ # If you know that the record occurred after a given time
150
+ Payload.find_after(222, 11.days.ago)
151
+
152
+ # Lastly, if you want to scope the search by a time range
153
+ Payload.find_between(333, 1.week.ago, 1.day.ago)
154
+ ```
155
+
156
+ If you need to query data for a specific time period, `Timescaledb::Rails::Model` includes useful scopes
116
157
 
117
158
  ```ruby
118
159
  # If you want to get all records from last year
@@ -136,6 +177,19 @@ Here the list of all available scopes
136
177
  * yesterday
137
178
  * today
138
179
 
180
+ If you still need to query data by other time periods, take a look at these other scopes
181
+
182
+ ```ruby
183
+ # If you want to get all records that occurred in the last 30 minutes
184
+ Event.after(30.minutes.ago) #=> [#<Event name...>, ...]
185
+
186
+ # If you want to get records that occurred in the last 4 days, excluding today
187
+ Event.between(4.days.ago, 1.day.ago) #=> [#<Event name...>, ...]
188
+
189
+ # If you want to get records that occurred at a specific time
190
+ Event.at_time(Time.new(2023, 01, 04, 10, 20, 30)) #=> [#<Event name...>, ...]
191
+ ```
192
+
139
193
  If you need information about your hypertable, use the following helper methods to get useful information
140
194
 
141
195
  ```ruby
@@ -165,6 +219,53 @@ chunk.compress! unless chunk.is_compressed?
165
219
  chunk.decompress! if chunk.is_compressed?
166
220
  ```
167
221
 
222
+ If you need to reorder a specific chunk
223
+
224
+ ```ruby
225
+ chunk = Event.hypertable_chunks.first
226
+
227
+ # If an index is not specified, it will use the one from the reorder policy
228
+ # In case there is no reorder policy index it will raise an ArgumentError
229
+ chunk.reorder!
230
+
231
+ # If an index is specified it will use that index
232
+ chunk.reorder!(index)
233
+ ```
234
+
235
+ If you need to manually refresh a continuous aggregate
236
+
237
+ ```ruby
238
+ aggregate = Event.hypertable.continuous_aggregates.first
239
+
240
+ aggregate.refresh!(5.days.ago, 1.day.ago)
241
+ ```
242
+
243
+ ### Hyperfunctions
244
+
245
+ #### Time bucket
246
+
247
+ 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)
248
+
249
+ ```ruby
250
+ Event.time_bucket(1.day)
251
+
252
+ Event.time_bucket('1 day')
253
+
254
+ Event.time_bucket(1.day, :created_at)
255
+
256
+ Event.time_bucket(1.day, 'occurred_at')
257
+ ```
258
+
259
+ You may add aggregation like so:
260
+
261
+ ```ruby
262
+ Event.time_bucket(1.day).avg(:column)
263
+ Event.time_bucket(1.day).sum(:column)
264
+ Event.time_bucket(1.day).min(:column)
265
+ Event.time_bucket(1.day).max(:column)
266
+ Event.time_bucket(1.day).count
267
+ ```
268
+
168
269
  ## Contributing
169
270
 
170
271
  Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.
@@ -33,6 +33,22 @@ module Timescaledb
33
33
  record(:remove_hypertable_retention_policy, args, &block)
34
34
  end
35
35
 
36
+ def create_continuous_aggregate(*args, &block)
37
+ record(:create_continuous_aggregate, args, &block)
38
+ end
39
+
40
+ def drop_continuous_aggregate(*args, &block)
41
+ record(:drop_continuous_aggregate, args, &block)
42
+ end
43
+
44
+ def add_continuous_aggregate_policy(*args, &block)
45
+ record(:add_continuous_aggregate_policy, args, &block)
46
+ end
47
+
48
+ def remove_continuous_aggregate_policy(*args, &block)
49
+ record(:remove_continuous_aggregate_policy, args, &block)
50
+ end
51
+
36
52
  def invert_create_hypertable(args, &block)
37
53
  if block.nil?
38
54
  raise ::ActiveRecord::IrreversibleMigration, 'create_hypertable is only reversible if given a block (can be empty).' # rubocop:disable Layout/LineLength
@@ -76,6 +92,30 @@ module Timescaledb
76
92
 
77
93
  [:add_hypertable_reorder_policy, args, block]
78
94
  end
95
+
96
+ def invert_create_continuous_aggregate(args, &block)
97
+ [:drop_continuous_aggregate, args, block]
98
+ end
99
+
100
+ def invert_drop_continuous_aggregate(args, &block)
101
+ if args.size < 2
102
+ raise ::ActiveRecord::IrreversibleMigration, 'drop_continuous_aggregate is only reversible if given view name and view query.' # rubocop:disable Layout/LineLength
103
+ end
104
+
105
+ [:create_continuous_aggregate, args, block]
106
+ end
107
+
108
+ def invert_add_continuous_aggregate_policy(args, &block)
109
+ [:remove_continuous_aggregate_policy, args, block]
110
+ end
111
+
112
+ def invert_remove_continuous_aggregate_policy(args, &block)
113
+ if args.size < 4
114
+ 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
115
+ end
116
+
117
+ [:add_continuous_aggregate_policy, args, block]
118
+ end
79
119
  end
80
120
  end
81
121
  end
@@ -7,16 +7,22 @@ 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) # rubocop:disable Metrics/MethodLength
13
+ def structure_dump(filename, extra_flags)
13
14
  extra_flags = Array(extra_flags)
14
- extra_flags << timescale_structure_dump_default_flags if timescale_enabled?
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)
@@ -28,17 +34,26 @@ module Timescaledb
28
34
  end
29
35
  end
30
36
 
37
+ def continuous_aggregates(filename)
38
+ File.open(filename, 'a') do |file|
39
+ Timescaledb::Rails::ContinuousAggregate.all.each do |continuous_aggregate|
40
+ create_continuous_aggregate_statement(continuous_aggregate, file)
41
+ add_continuous_aggregate_policy_statement(continuous_aggregate, file)
42
+ end
43
+ end
44
+ end
45
+
31
46
  def drop_ts_insert_trigger_statment(hypertable, file)
32
47
  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" # rubocop:disable Layout/LineLength
48
+ file << "--- Drop ts_insert_blocker previously created by pg_dump to avoid pg errors, create_hypertable will re-create it again.\n"
34
49
  file << "---\n\n"
35
- file << "DROP TRIGGER IF EXISTS ts_insert_blocker ON #{hypertable.hypertable_name};\n"
50
+ file << "DROP TRIGGER IF EXISTS ts_insert_blocker ON #{hypertable.hypertable_schema}.#{hypertable.hypertable_name};\n"
36
51
  end
37
52
 
38
53
  def create_hypertable_statement(hypertable, file)
39
54
  options = hypertable_options(hypertable)
40
55
 
41
- file << "SELECT create_hypertable('#{hypertable.hypertable_name}', '#{hypertable.time_column_name}', #{options});\n\n" # rubocop:disable Layout/LineLength
56
+ file << "SELECT create_hypertable('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', '#{hypertable.time_column_name}', #{options});\n\n"
42
57
  end
43
58
 
44
59
  def add_hypertable_compression_statement(hypertable, file)
@@ -46,20 +61,35 @@ module Timescaledb
46
61
 
47
62
  options = hypertable_compression_options(hypertable)
48
63
 
49
- file << "ALTER TABLE #{hypertable.hypertable_name} SET (#{options});\n\n"
50
- file << "SELECT add_compression_policy('#{hypertable.hypertable_name}', INTERVAL '#{hypertable.compression_policy_interval}');\n\n" # rubocop:disable Layout/LineLength
64
+ file << "ALTER TABLE #{hypertable.hypertable_schema}.#{hypertable.hypertable_name} SET (#{options});\n\n"
65
+ file << "SELECT add_compression_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', INTERVAL '#{hypertable.compression_policy_interval}');\n\n"
51
66
  end
52
67
 
53
68
  def add_hypertable_reorder_policy_statement(hypertable, file)
54
69
  return unless hypertable.reorder?
55
70
 
56
- file << "SELECT add_reorder_policy('#{hypertable.hypertable_name}', '#{hypertable.reorder_policy_index_name}');\n\n" # rubocop:disable Layout/LineLength
71
+ file << "SELECT add_reorder_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', '#{hypertable.reorder_policy_index_name}');\n\n"
57
72
  end
58
73
 
59
74
  def add_hypertable_retention_policy_statement(hypertable, file)
60
75
  return unless hypertable.retention?
61
76
 
62
- file << "SELECT add_retention_policy('#{hypertable.hypertable_name}', INTERVAL '#{hypertable.retention_policy_interval}');\n\n" # rubocop:disable Layout/LineLength
77
+ file << "SELECT add_retention_policy('#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}', INTERVAL '#{hypertable.retention_policy_interval}');\n\n"
78
+ end
79
+
80
+ def create_continuous_aggregate_statement(continuous_aggregate, file)
81
+ file << "CREATE MATERIALIZED VIEW #{continuous_aggregate.view_schema}.#{continuous_aggregate.view_name} WITH (timescaledb.continuous) AS\n"
82
+ file << "#{continuous_aggregate.view_definition.strip.indent(2)}\n\n"
83
+ end
84
+
85
+ def add_continuous_aggregate_policy_statement(continuous_aggregate, file)
86
+ return unless continuous_aggregate.refresh?
87
+
88
+ start_offset = continuous_aggregate.refresh_start_offset
89
+ end_offset = continuous_aggregate.refresh_end_offset
90
+ schedule_interval = continuous_aggregate.refresh_schedule_interval
91
+
92
+ 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
93
  end
64
94
 
65
95
  def hypertable_options(hypertable)
@@ -93,11 +123,18 @@ module Timescaledb
93
123
  hypertable.compression_segment_settings.map(&:attname)
94
124
  end
95
125
 
96
- # Returns `pg_dump` flag to exclude `_timescaledb_internal` schema tables.
126
+ # Returns `pg_dump` flags to exclude `_timescaledb_internal` schema tables and
127
+ # exclude the corresponding continuous aggregate views.
97
128
  #
98
- # @return [String]
129
+ # @return [Array<String>]
99
130
  def timescale_structure_dump_default_flags
100
- '--exclude-schema=_timescaledb_internal'
131
+ flags = ['--exclude-schema=_timescaledb_internal']
132
+
133
+ Timescaledb::Rails::ContinuousAggregate.pluck(:view_schema, :view_name).each do |view_schema, view_name|
134
+ flags << "--exclude-table=#{view_schema}.#{view_name}"
135
+ end
136
+
137
+ flags
101
138
  end
102
139
 
103
140
  # @return [Boolean]
@@ -105,6 +142,7 @@ module Timescaledb
105
142
  Timescaledb::Rails::Hypertable.table_exists?
106
143
  end
107
144
  end
145
+ # rubocop:enable Layout/LineLength
108
146
  end
109
147
  end
110
148
  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.all.each do |continuous_aggregate|
23
+ continuous_aggregate(continuous_aggregate, stream)
24
+ continuous_aggregate_policy(continuous_aggregate, 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
 
@@ -37,7 +37,7 @@ 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
43
  # Enables compression and sets compression options.
@@ -51,17 +51,18 @@ module Timescaledb
51
51
  options << "timescaledb.compress_orderby = '#{order_by}'" unless order_by.nil?
52
52
  options << "timescaledb.compress_segmentby = '#{segment_by}'" unless segment_by.nil?
53
53
 
54
- execute "ALTER TABLE #{table_name} SET (#{options.join(', ')})"
54
+ execute "ALTER TABLE #{table_name} SET (#{options.join(', ')});"
55
55
 
56
- execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compress_after.inspect}')"
56
+ execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{stringify_interval(compress_after)}');"
57
57
  end
58
58
 
59
- # Disables compression from given table.
59
+ # Removes compression policy and disables compression from given hypertable.
60
60
  #
61
61
  # remove_hypertable_compression('events')
62
62
  #
63
63
  def remove_hypertable_compression(table_name, compress_after = nil, segment_by: nil, order_by: nil) # rubocop:disable Lint/UnusedMethodArgument
64
- execute "SELECT remove_compression_policy('#{table_name.inspect}');"
64
+ execute "SELECT remove_compression_policy('#{table_name}');"
65
+ execute "ALTER TABLE #{table_name.inspect} SET (timescaledb.compress = false);"
65
66
  end
66
67
 
67
68
  # Add a data retention policy to given hypertable.
@@ -69,7 +70,7 @@ module Timescaledb
69
70
  # add_hypertable_retention_policy('events', 7.days)
70
71
  #
71
72
  def add_hypertable_retention_policy(table_name, drop_after)
72
- execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{drop_after.inspect}')"
73
+ execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{stringify_interval(drop_after)}');"
73
74
  end
74
75
 
75
76
  # Removes data retention policy from given hypertable.
@@ -77,7 +78,7 @@ module Timescaledb
77
78
  # remove_hypertable_retention_policy('events')
78
79
  #
79
80
  def remove_hypertable_retention_policy(table_name, _drop_after = nil)
80
- execute "SELECT remove_retention_policy('#{table_name}')"
81
+ execute "SELECT remove_retention_policy('#{table_name}');"
81
82
  end
82
83
 
83
84
  # Adds a policy to reorder chunks on a given hypertable index in the background.
@@ -85,7 +86,7 @@ module Timescaledb
85
86
  # add_hypertable_reorder_policy('events', 'index_events_on_created_at_and_name')
86
87
  #
87
88
  def add_hypertable_reorder_policy(table_name, index_name)
88
- execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}')"
89
+ execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}');"
89
90
  end
90
91
 
91
92
  # Removes a policy to reorder a particular hypertable.
@@ -93,14 +94,60 @@ module Timescaledb
93
94
  # remove_hypertable_reorder_policy('events')
94
95
  #
95
96
  def remove_hypertable_reorder_policy(table_name, _index_name = nil)
96
- execute "SELECT remove_reorder_policy('#{table_name}')"
97
+ execute "SELECT remove_reorder_policy('#{table_name}');"
98
+ end
99
+
100
+ # Creates a continuous aggregate
101
+ #
102
+ # create_continuous_aggregate(
103
+ # 'temperature_events', "SELECT * FROM events where event_type = 'temperature'"
104
+ # )
105
+ #
106
+ def create_continuous_aggregate(view_name, view_query)
107
+ execute "CREATE MATERIALIZED VIEW #{view_name} WITH (timescaledb.continuous) AS #{view_query};"
108
+ end
109
+
110
+ # Drops a continuous aggregate
111
+ #
112
+ # drop_continuous_aggregate('temperature_events')
113
+ #
114
+ def drop_continuous_aggregate(view_name, _view_query = nil)
115
+ execute "DROP MATERIALIZED VIEW #{view_name};"
116
+ end
117
+
118
+ # Adds refresh continuous aggregate policy
119
+ #
120
+ # add_continuous_aggregate_policy('temperature_events', 1.month, 1.day, 1.hour)
121
+ #
122
+ def add_continuous_aggregate_policy(view_name, start_offset, end_offset, schedule_interval)
123
+ start_offset = start_offset.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(start_offset)}'"
124
+ end_offset = end_offset.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(end_offset)}'"
125
+ schedule_interval = schedule_interval.nil? ? 'NULL' : "INTERVAL '#{stringify_interval(schedule_interval)}'"
126
+
127
+ execute "SELECT add_continuous_aggregate_policy('#{view_name}', start_offset => #{start_offset}, end_offset => #{end_offset}, schedule_interval => #{schedule_interval});" # rubocop:disable Layout/LineLength
128
+ end
129
+
130
+ # Removes refresh continuous aggregate policy
131
+ #
132
+ # remove_continuous_aggregate_policy('temperature_events')
133
+ #
134
+ def remove_continuous_aggregate_policy(view_name, _start_offset = nil,
135
+ _end_offset = nil, _schedule_interval = nil)
136
+ execute "SELECT remove_continuous_aggregate_policy('#{view_name}');"
137
+ end
138
+
139
+ private
140
+
141
+ # @param [ActiveSupport::Duration|String] interval
142
+ def stringify_interval(interval)
143
+ interval.is_a?(ActiveSupport::Duration) ? interval.inspect : interval
97
144
  end
98
145
 
99
146
  # @return [String]
100
147
  def hypertable_options_to_sql(options)
101
148
  sql_statements = options.map do |option, value|
102
149
  case option
103
- when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{value}'"
150
+ when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{stringify_interval(value)}'"
104
151
  when :if_not_exists then "if_not_exists => #{value ? 'TRUE' : 'FALSE'}"
105
152
  end
106
153
  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,30 @@
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)
14
+ target_column ||= hypertable_time_column_name
15
+
16
+ select("time_bucket('#{format_interval_value(interval)}', #{target_column}) as #{TIME_BUCKET_ALIAS}")
17
+ .group(TIME_BUCKET_ALIAS)
18
+ .order(TIME_BUCKET_ALIAS)
19
+ .extending(AggregateFunctions)
20
+ end
21
+
22
+ private
23
+
24
+ def format_interval_value(value)
25
+ value.is_a?(ActiveSupport::Duration) ? value.inspect : value
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -12,19 +12,19 @@ module Timescaledb
12
12
  scope :last_year, lambda {
13
13
  date = Date.current - 1.year
14
14
 
15
- between_time_column(date.beginning_of_year, date.end_of_year)
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
- between_time_column(date.beginning_of_month, date.end_of_month)
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
- between_time_column(date.beginning_of_week, date.end_of_week)
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
- between_time_column(Date.current.beginning_of_year, Date.current.end_of_year)
35
+ between(Date.current.beginning_of_year, Date.current.end_of_year)
36
36
  }
37
37
 
38
38
  scope :this_month, lambda {
39
- between_time_column(Date.current.beginning_of_month, Date.current.end_of_month)
39
+ between(Date.current.beginning_of_month, Date.current.end_of_month)
40
40
  }
41
41
 
42
42
  scope :this_week, lambda {
43
- between_time_column(Date.current.beginning_of_week, Date.current.end_of_week)
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
- # @!visibility private
51
- scope :between_time_column, lambda { |from, to|
52
- where("DATE(#{hypertable_time_column_name}) BETWEEN ? AND ?", from, to)
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
- delegate :time_column_name, to: :hypertable, prefix: true
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
- if table_name.split('.').size > 1
32
- table_name.split('.')[0..-2].join('.')
33
- else
34
- PUBLIC_SCHEMA_NAME
35
- end
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]
@@ -7,6 +7,8 @@ module Timescaledb
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
 
@@ -25,6 +27,23 @@ module Timescaledb
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
+ ::ActiveRecord::Base.connection.execute(
44
+ "SELECT reorder_chunk(#{options.join(', ')})"
45
+ )
46
+ end
28
47
  end
29
48
  end
30
49
  end
@@ -0,0 +1,76 @@
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
+ duration_in_seconds = duration_in_seconds(duration)
15
+
16
+ duration_to_interval(
17
+ ActiveSupport::Duration.build(duration_in_seconds)
18
+ )
19
+ rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
20
+ duration
21
+ end
22
+
23
+ private
24
+
25
+ # Converts different interval formats into seconds.
26
+ #
27
+ # duration_in_seconds('P1D') #=> 86400
28
+ # duration_in_seconds('24:00:00') #=> 86400
29
+ # duration_in_seconds(1.day) #=> 86400
30
+ #
31
+ # @param [ActiveSupport::Duration|String] duration
32
+ # @return [Integer]
33
+ def duration_in_seconds(duration)
34
+ return duration.to_i if duration.is_a?(ActiveSupport::Duration)
35
+
36
+ if (duration =~ HOUR_MINUTE_SECOND_REGEX).present?
37
+ hours, minutes, seconds = duration.split(':').map(&:to_i)
38
+
39
+ (hours.hour + minutes.minute + seconds.second).to_i
40
+ else
41
+ ActiveSupport::Duration.parse(duration).to_i
42
+ end
43
+ end
44
+
45
+ # Converts given duration into a human interval readable format.
46
+ #
47
+ # duration_to_interval(1.day) #=> '1 day'
48
+ # duration_to_interval(2.weeks + 6.days) #=> '20 days'
49
+ # duration_to_interval(1.years + 3.months) #=> '1 year 3 months'
50
+ #
51
+ # @param [ActiveSupport::Duration] duration
52
+ # @return [String]
53
+ def duration_to_interval(duration)
54
+ parts = duration.parts
55
+
56
+ # Combine days and weeks if both present
57
+ #
58
+ # "1 week 2 days" => "9 days"
59
+ parts[:days] += parts.delete(:weeks) * 7 if parts.key?(:weeks) && parts.key?(:days)
60
+
61
+ parts.map do |(unit, quantity)|
62
+ "#{quantity} #{humanize_duration_unit(unit.to_s, quantity)}"
63
+ end.join(' ')
64
+ end
65
+
66
+ # Pluralize or singularize given duration unit based on given count.
67
+ #
68
+ # @param [String] duration_unit
69
+ # @param [Integer] count
70
+ def humanize_duration_unit(duration_unit, count)
71
+ count > 1 ? duration_unit.pluralize : duration_unit.singularize
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,57 @@
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 < ::ActiveRecord::Base
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
+ # Manually refresh a continuous aggregate.
17
+ #
18
+ # @param [DateTime] start_time
19
+ # @param [DateTime] end_time
20
+ #
21
+ def refresh!(start_time = 'NULL', end_time = 'NULL')
22
+ ::ActiveRecord::Base.connection.execute(
23
+ "CALL refresh_continuous_aggregate('#{view_name}', #{start_time}, #{end_time});"
24
+ )
25
+ end
26
+
27
+ # @return [String]
28
+ def refresh_start_offset
29
+ parse_duration(refresh_job.config['start_offset'])
30
+ end
31
+
32
+ # @return [String]
33
+ def refresh_end_offset
34
+ parse_duration(refresh_job.config['end_offset'])
35
+ end
36
+
37
+ # @return [String]
38
+ def refresh_schedule_interval
39
+ interval = refresh_job.schedule_interval
40
+
41
+ interval.is_a?(String) ? parse_duration(interval) : interval.inspect
42
+ end
43
+
44
+ # @return [Boolean]
45
+ def refresh?
46
+ refresh_job.present?
47
+ end
48
+
49
+ private
50
+
51
+ # @return [Job]
52
+ def refresh_job
53
+ @refresh_job ||= jobs.policy_refresh_continuous_aggregate.first
54
+ end
55
+ end
56
+ end
57
+ 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
8
  class Hypertable < ::ActiveRecord::Base
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'
@@ -86,13 +92,6 @@ module Timescaledb
86
92
  def time_dimension
87
93
  @time_dimension ||= dimensions.time.first
88
94
  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
95
  end
97
96
  end
98
97
  end
@@ -10,10 +10,12 @@ module Timescaledb
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative './models/chunk'
4
4
  require_relative './models/compression_setting'
5
+ require_relative './models/continuous_aggregate'
5
6
  require_relative './models/dimension'
6
7
  require_relative './models/hypertable'
7
8
  require_relative './models/job'
@@ -3,6 +3,6 @@
3
3
  module Timescaledb
4
4
  # :nodoc:
5
5
  module Rails
6
- VERSION = '0.1.4'
6
+ VERSION = '0.1.5'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timescaledb-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Iván Etchart
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-12-30 00:00:00.000000000 Z
12
+ date: 2023-01-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -139,10 +139,15 @@ 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
144
147
  - lib/timescaledb/rails/models/chunk.rb
145
148
  - lib/timescaledb/rails/models/compression_setting.rb
149
+ - lib/timescaledb/rails/models/concerns/durationable.rb
150
+ - lib/timescaledb/rails/models/continuous_aggregate.rb
146
151
  - lib/timescaledb/rails/models/dimension.rb
147
152
  - lib/timescaledb/rails/models/hypertable.rb
148
153
  - lib/timescaledb/rails/models/job.rb
@@ -169,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
174
  - !ruby/object:Gem::Version
170
175
  version: '0'
171
176
  requirements: []
172
- rubygems_version: 3.3.26
177
+ rubygems_version: 3.0.3.1
173
178
  signing_key:
174
179
  specification_version: 4
175
180
  summary: TimescaleDB Rails integration