timescaledb-rails 0.1.3 → 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 +4 -4
- data/README.md +215 -15
- data/lib/timescaledb/rails/extensions/active_record/base.rb +20 -0
- data/lib/timescaledb/rails/extensions/active_record/command_recorder.rb +80 -0
- data/lib/timescaledb/rails/extensions/active_record/postgresql_database_tasks.rb +77 -18
- data/lib/timescaledb/rails/extensions/active_record/schema_dumper.rb +75 -10
- data/lib/timescaledb/rails/extensions/active_record/schema_statements.rb +101 -7
- 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 +30 -0
- data/lib/timescaledb/rails/model/scopes.rb +66 -0
- data/lib/timescaledb/rails/model.rb +83 -0
- data/lib/timescaledb/rails/models/chunk.rb +49 -0
- data/lib/timescaledb/rails/models/concerns/durationable.rb +76 -0
- data/lib/timescaledb/rails/models/continuous_aggregate.rb +57 -0
- data/lib/timescaledb/rails/models/hypertable.rb +48 -3
- data/lib/timescaledb/rails/models/job.rb +6 -0
- data/lib/timescaledb/rails/models.rb +3 -1
- data/lib/timescaledb/rails/railtie.rb +1 -20
- data/lib/timescaledb/rails/version.rb +1 -1
- data/lib/timescaledb/rails.rb +16 -0
- metadata +26 -3
@@ -1,12 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
4
|
-
|
5
3
|
module Timescaledb
|
6
4
|
module Rails
|
7
5
|
module ActiveRecord
|
8
6
|
# :nodoc:
|
9
7
|
module SchemaStatements
|
8
|
+
# Returns an array of hypertable names defined in the database.
|
9
|
+
def hypertables
|
10
|
+
query_values('SELECT hypertable_name FROM timescaledb_information.hypertables')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Checks to see if the hypertable exists on the database.
|
14
|
+
#
|
15
|
+
# hypertable_exists?(:developers)
|
16
|
+
#
|
17
|
+
def hypertable_exists?(hypertable)
|
18
|
+
query_value(
|
19
|
+
<<-SQL.squish
|
20
|
+
SELECT COUNT(*) FROM timescaledb_information.hypertables WHERE hypertable_name = #{quote(hypertable)}
|
21
|
+
SQL
|
22
|
+
).to_i.positive?
|
23
|
+
end
|
24
|
+
|
10
25
|
# Converts given standard PG table into a hypertable.
|
11
26
|
#
|
12
27
|
# create_hypertable('readings', 'created_at', chunk_time_interval: '7 days')
|
@@ -22,7 +37,7 @@ module Timescaledb
|
|
22
37
|
create_table(table_name, id: false, primary_key: primary_key, force: force, **options, &block)
|
23
38
|
end
|
24
39
|
|
25
|
-
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});"
|
26
41
|
end
|
27
42
|
|
28
43
|
# Enables compression and sets compression options.
|
@@ -36,24 +51,103 @@ module Timescaledb
|
|
36
51
|
options << "timescaledb.compress_orderby = '#{order_by}'" unless order_by.nil?
|
37
52
|
options << "timescaledb.compress_segmentby = '#{segment_by}'" unless segment_by.nil?
|
38
53
|
|
39
|
-
execute "ALTER TABLE #{table_name} SET (#{options.join(', ')})"
|
54
|
+
execute "ALTER TABLE #{table_name} SET (#{options.join(', ')});"
|
40
55
|
|
41
|
-
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compress_after}')"
|
56
|
+
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{stringify_interval(compress_after)}');"
|
42
57
|
end
|
43
58
|
|
44
|
-
#
|
59
|
+
# Removes compression policy and disables compression from given hypertable.
|
45
60
|
#
|
46
61
|
# remove_hypertable_compression('events')
|
47
62
|
#
|
48
63
|
def remove_hypertable_compression(table_name, compress_after = nil, segment_by: nil, order_by: nil) # rubocop:disable Lint/UnusedMethodArgument
|
49
64
|
execute "SELECT remove_compression_policy('#{table_name}');"
|
65
|
+
execute "ALTER TABLE #{table_name.inspect} SET (timescaledb.compress = false);"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add a data retention policy to given hypertable.
|
69
|
+
#
|
70
|
+
# add_hypertable_retention_policy('events', 7.days)
|
71
|
+
#
|
72
|
+
def add_hypertable_retention_policy(table_name, drop_after)
|
73
|
+
execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{stringify_interval(drop_after)}');"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Removes data retention policy from given hypertable.
|
77
|
+
#
|
78
|
+
# remove_hypertable_retention_policy('events')
|
79
|
+
#
|
80
|
+
def remove_hypertable_retention_policy(table_name, _drop_after = nil)
|
81
|
+
execute "SELECT remove_retention_policy('#{table_name}');"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds a policy to reorder chunks on a given hypertable index in the background.
|
85
|
+
#
|
86
|
+
# add_hypertable_reorder_policy('events', 'index_events_on_created_at_and_name')
|
87
|
+
#
|
88
|
+
def add_hypertable_reorder_policy(table_name, index_name)
|
89
|
+
execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}');"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Removes a policy to reorder a particular hypertable.
|
93
|
+
#
|
94
|
+
# remove_hypertable_reorder_policy('events')
|
95
|
+
#
|
96
|
+
def remove_hypertable_reorder_policy(table_name, _index_name = nil)
|
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
|
50
144
|
end
|
51
145
|
|
52
146
|
# @return [String]
|
53
147
|
def hypertable_options_to_sql(options)
|
54
148
|
sql_statements = options.map do |option, value|
|
55
149
|
case option
|
56
|
-
when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{value}'"
|
150
|
+
when :chunk_time_interval then "chunk_time_interval => INTERVAL '#{stringify_interval(value)}'"
|
57
151
|
when :if_not_exists then "if_not_exists => #{value ? 'TRUE' : 'FALSE'}"
|
58
152
|
end
|
59
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
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module Model
|
6
|
+
# :nodoc:
|
7
|
+
module Scopes
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# rubocop:disable Metrics/BlockLength
|
11
|
+
included do
|
12
|
+
scope :last_year, lambda {
|
13
|
+
date = Date.current - 1.year
|
14
|
+
|
15
|
+
between(date.beginning_of_year, date.end_of_year)
|
16
|
+
}
|
17
|
+
|
18
|
+
scope :last_month, lambda {
|
19
|
+
date = Date.current - 1.month
|
20
|
+
|
21
|
+
between(date.beginning_of_month, date.end_of_month)
|
22
|
+
}
|
23
|
+
|
24
|
+
scope :last_week, lambda {
|
25
|
+
date = Date.current - 1.week
|
26
|
+
|
27
|
+
between(date.beginning_of_week, date.end_of_week)
|
28
|
+
}
|
29
|
+
|
30
|
+
scope :yesterday, lambda {
|
31
|
+
where("DATE(#{hypertable_time_column_name}) = ?", Date.current - 1.day)
|
32
|
+
}
|
33
|
+
|
34
|
+
scope :this_year, lambda {
|
35
|
+
between(Date.current.beginning_of_year, Date.current.end_of_year)
|
36
|
+
}
|
37
|
+
|
38
|
+
scope :this_month, lambda {
|
39
|
+
between(Date.current.beginning_of_month, Date.current.end_of_month)
|
40
|
+
}
|
41
|
+
|
42
|
+
scope :this_week, lambda {
|
43
|
+
between(Date.current.beginning_of_week, Date.current.end_of_week)
|
44
|
+
}
|
45
|
+
|
46
|
+
scope :today, lambda {
|
47
|
+
where("DATE(#{hypertable_time_column_name}) = ?", Date.current)
|
48
|
+
}
|
49
|
+
|
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)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
# rubocop:enable Metrics/BlockLength
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timescaledb/rails/model/finder_methods'
|
4
|
+
require 'timescaledb/rails/model/hyperfunctions'
|
5
|
+
require 'timescaledb/rails/model/scopes'
|
6
|
+
|
7
|
+
module Timescaledb
|
8
|
+
module Rails
|
9
|
+
# :nodoc:
|
10
|
+
module Model
|
11
|
+
PUBLIC_SCHEMA_NAME = 'public'
|
12
|
+
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
include Scopes
|
16
|
+
|
17
|
+
# :nodoc:
|
18
|
+
module ClassMethods
|
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
|
26
|
+
|
27
|
+
# Returns only the name of the hypertable, table_name could include
|
28
|
+
# the schema path, we need to remove it.
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
def hypertable_name
|
32
|
+
@hypertable_name ||= table_name.split('.').last
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the schema where hypertable is stored.
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
def hypertable_schema
|
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
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Timescaledb::Rails::Hypertable]
|
48
|
+
def hypertable
|
49
|
+
Timescaledb::Rails::Hypertable.find_by(hypertable_where_options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Chunk>]
|
53
|
+
def hypertable_chunks
|
54
|
+
Timescaledb::Rails::Chunk.where(hypertable_where_options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Job>]
|
58
|
+
def hypertable_jobs
|
59
|
+
Timescaledb::Rails::Job.where(hypertable_where_options)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Dimension>]
|
63
|
+
def hypertable_dimensions
|
64
|
+
Timescaledb::Rails::Dimension.where(hypertable_where_options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::CompressionSetting>]
|
68
|
+
def hypertable_compression_settings
|
69
|
+
Timescaledb::Rails::CompressionSetting.where(hypertable_where_options)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Returns hypertable name and schema.
|
75
|
+
#
|
76
|
+
# @return [Hash]
|
77
|
+
def hypertable_where_options
|
78
|
+
{ hypertable_name: hypertable_name, hypertable_schema: hypertable_schema }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
# :nodoc:
|
6
|
+
class Chunk < ::ActiveRecord::Base
|
7
|
+
self.table_name = 'timescaledb_information.chunks'
|
8
|
+
self.primary_key = 'hypertable_name'
|
9
|
+
|
10
|
+
belongs_to :hypertable, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Hypertable'
|
11
|
+
|
12
|
+
scope :compressed, -> { where(is_compressed: true) }
|
13
|
+
scope :decompressed, -> { where(is_compressed: false) }
|
14
|
+
|
15
|
+
def chunk_full_name
|
16
|
+
"#{chunk_schema}.#{chunk_name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def compress!
|
20
|
+
::ActiveRecord::Base.connection.execute(
|
21
|
+
"SELECT compress_chunk('#{chunk_full_name}')"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def decompress!
|
26
|
+
::ActiveRecord::Base.connection.execute(
|
27
|
+
"SELECT decompress_chunk('#{chunk_full_name}')"
|
28
|
+
)
|
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
|
47
|
+
end
|
48
|
+
end
|
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,16 +1,23 @@
|
|
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'
|
13
19
|
has_many :jobs, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Job'
|
20
|
+
has_many :chunks, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Chunk'
|
14
21
|
|
15
22
|
# @return [String]
|
16
23
|
def time_column_name
|
@@ -24,11 +31,29 @@ module Timescaledb
|
|
24
31
|
interval.is_a?(String) ? interval : interval.inspect
|
25
32
|
end
|
26
33
|
|
34
|
+
# @return [ActiveRecord::Relation<CompressionSetting>]
|
35
|
+
def compression_segment_settings
|
36
|
+
compression_settings.segmentby_setting
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [ActiveRecord::Relation<CompressionSetting>]
|
40
|
+
def compression_order_settings
|
41
|
+
compression_settings.orderby_setting.where.not(attname: time_column_name)
|
42
|
+
end
|
43
|
+
|
27
44
|
# @return [String]
|
28
45
|
def compression_policy_interval
|
29
|
-
|
30
|
-
|
31
|
-
|
46
|
+
parse_duration(compression_job.config['compress_after'])
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
def reorder_policy_index_name
|
51
|
+
reorder_job.config['index_name']
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String]
|
55
|
+
def retention_policy_interval
|
56
|
+
parse_duration(retention_job.config['drop_after'])
|
32
57
|
end
|
33
58
|
|
34
59
|
# @return [Boolean]
|
@@ -36,8 +61,28 @@ module Timescaledb
|
|
36
61
|
compression_job.present?
|
37
62
|
end
|
38
63
|
|
64
|
+
# @return [Boolean]
|
65
|
+
def reorder?
|
66
|
+
reorder_job.present?
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Boolean]
|
70
|
+
def retention?
|
71
|
+
retention_job.present?
|
72
|
+
end
|
73
|
+
|
39
74
|
private
|
40
75
|
|
76
|
+
# @return [Job]
|
77
|
+
def reorder_job
|
78
|
+
@reorder_job ||= jobs.policy_reorder.first
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Job]
|
82
|
+
def retention_job
|
83
|
+
@retention_job ||= jobs.policy_retention.first
|
84
|
+
end
|
85
|
+
|
41
86
|
# @return [Job]
|
42
87
|
def compression_job
|
43
88
|
@compression_job ||= jobs.policy_compression.first
|
@@ -8,8 +8,14 @@ module Timescaledb
|
|
8
8
|
self.primary_key = 'hypertable_name'
|
9
9
|
|
10
10
|
POLICY_COMPRESSION = 'policy_compression'
|
11
|
+
POLICY_REORDER = 'policy_reorder'
|
12
|
+
POLICY_RETENTION = 'policy_retention'
|
13
|
+
POLICY_REFRESH_CONTINUOUS_AGGREGATE = 'policy_refresh_continuous_aggregate'
|
11
14
|
|
12
15
|
scope :policy_compression, -> { where(proc_name: POLICY_COMPRESSION) }
|
16
|
+
scope :policy_reorder, -> { where(proc_name: POLICY_REORDER) }
|
17
|
+
scope :policy_retention, -> { where(proc_name: POLICY_RETENTION) }
|
18
|
+
scope :policy_refresh_continuous_aggregate, -> { where(proc_name: POLICY_REFRESH_CONTINUOUS_AGGREGATE) }
|
13
19
|
end
|
14
20
|
end
|
15
21
|
end
|