timescaledb-rails 0.1.4 → 0.1.5

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