timescaledb 0.2.9 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/timescaledb/acts_as_hypertable/core.rb +1 -1
- data/lib/timescaledb/acts_as_hypertable.rb +5 -2
- data/lib/timescaledb/connection.rb +1 -0
- data/lib/timescaledb/continuous_aggregates.rb +25 -2
- data/lib/timescaledb/continuous_aggregates_helper.rb +180 -0
- data/lib/timescaledb/hypertable.rb +2 -0
- data/lib/timescaledb/migration_helpers.rb +51 -22
- data/lib/timescaledb/schema_dumper.rb +8 -10
- data/lib/timescaledb/toolkit/time_vector.rb +13 -9
- data/lib/timescaledb/toolkit.rb +0 -1
- data/lib/timescaledb/version.rb +1 -1
- data/lib/timescaledb.rb +1 -0
- metadata +22 -7
- data/lib/timescaledb/acts_as_time_vector.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f5295cfa57caa17bb740b28dd70f54fb0c239f9d011233ae04cfffa2bbfadbf
|
4
|
+
data.tar.gz: ec1b742c312d289124522e490fe47269d7aa6b9774ce0d82fe99bed206ce227c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1a0937798a8f97bccaf8a39d61268fc040f525f04b92632e1a0fc7c8d892d73b39d7f4fd7252fe430ac3f53523b5a01570bc89257aff8cef35fa68c161204e4
|
7
|
+
data.tar.gz: 460df42d48ceac9d552c47408655a9c9330b3efe9bc40d8963021e7c2f4e297d4210c54fc6fe43678f56dc156ac28836f7b06d17d11977fa82a134cb356c9127
|
@@ -22,7 +22,8 @@ module Timescaledb
|
|
22
22
|
# for configuration options
|
23
23
|
module ActsAsHypertable
|
24
24
|
DEFAULT_OPTIONS = {
|
25
|
-
time_column: :created_at
|
25
|
+
time_column: :created_at,
|
26
|
+
# Add any default time vector options here if needed
|
26
27
|
}.freeze
|
27
28
|
|
28
29
|
def acts_as_hypertable?
|
@@ -45,10 +46,12 @@ module Timescaledb
|
|
45
46
|
# @param [Hash] options The options to initialize your macro with.
|
46
47
|
# @option options [Boolean] :skip_association_scopes to avoid `.hypertable`, `.chunks` and other scopes related to metadata.
|
47
48
|
# @option options [Boolean] :skip_default_scopes to avoid the generation of default time related scopes like `last_hour`, `last_week`, `yesterday` and so on...
|
49
|
+
# @option options [Boolean] :skip_time_vector to avoid the generation of time vector related scopes
|
48
50
|
def acts_as_hypertable(options = {})
|
49
51
|
return if acts_as_hypertable?
|
50
52
|
|
51
53
|
include Timescaledb::ActsAsHypertable::Core
|
54
|
+
include Timescaledb::Toolkit::TimeVector
|
52
55
|
|
53
56
|
class_attribute :hypertable_options, instance_writer: false
|
54
57
|
|
@@ -58,8 +61,8 @@ module Timescaledb
|
|
58
61
|
|
59
62
|
define_association_scopes unless options[:skip_association_scopes]
|
60
63
|
define_default_scopes unless options[:skip_default_scopes]
|
64
|
+
define_default_vector_scopes unless options[:skip_time_vector]
|
61
65
|
end
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
65
|
-
ActiveRecord::Base.extend Timescaledb::ActsAsHypertable
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Timescaledb
|
2
|
-
class
|
2
|
+
class ContinuousAggregates < ::Timescaledb::ApplicationRecord
|
3
3
|
self.table_name = "timescaledb_information.continuous_aggregates"
|
4
4
|
self.primary_key = 'materialization_hypertable_name'
|
5
5
|
|
@@ -14,6 +14,29 @@ module Timescaledb
|
|
14
14
|
total: count
|
15
15
|
}
|
16
16
|
end
|
17
|
+
|
18
|
+
scope :hierarchical, -> do
|
19
|
+
with_recursive = <<~SQL
|
20
|
+
WITH RECURSIVE caggs AS (
|
21
|
+
SELECT mat_hypertable_id, parent_mat_hypertable_id, user_view_name
|
22
|
+
FROM _timescaledb_catalog.continuous_agg
|
23
|
+
UNION ALL
|
24
|
+
SELECT continuous_agg.mat_hypertable_id, continuous_agg.parent_mat_hypertable_id, continuous_agg.user_view_name
|
25
|
+
FROM _timescaledb_catalog.continuous_agg
|
26
|
+
JOIN caggs ON caggs.parent_mat_hypertable_id = continuous_agg.mat_hypertable_id
|
27
|
+
)
|
28
|
+
SELECT * FROM caggs
|
29
|
+
ORDER BY mat_hypertable_id
|
30
|
+
SQL
|
31
|
+
views = unscoped
|
32
|
+
.select("distinct user_view_name")
|
33
|
+
.from("(#{with_recursive}) as caggs")
|
34
|
+
.pluck(:user_view_name)
|
35
|
+
.uniq
|
36
|
+
|
37
|
+
views.map do |view|
|
38
|
+
find_by(view_name: view)
|
39
|
+
end
|
40
|
+
end
|
17
41
|
end
|
18
|
-
ContinuousAggregates = ContinuousAggregate
|
19
42
|
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Timescaledb
|
2
|
+
module ContinuousAggregatesHelper
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :rollup_rules, default: {
|
7
|
+
/count\(\*\)\s+as\s+(\w+)/ => 'sum(\1) as \1',
|
8
|
+
/sum\((\w+)\)\s+as\s+(\w+)/ => 'sum(\2) as \2',
|
9
|
+
/min\((\w+)\)\s+as\s+(\w+)/ => 'min(\2) as \2',
|
10
|
+
/max\((\w+)\)\s+as\s+(\w+)/ => 'max(\2) as \2',
|
11
|
+
/first\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'first(\3, \2) as \3',
|
12
|
+
/high\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'max(\1) as \1',
|
13
|
+
/low\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'min(\1) as \1',
|
14
|
+
/last\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'last(\3, \2) as \3',
|
15
|
+
/candlestick_agg\((\w+),\s*(\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'rollup(\4) as \4',
|
16
|
+
/stats_agg\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'rollup(\3) as \3',
|
17
|
+
/stats_agg\((\w+)\)\s+as\s+(\w+)/ => 'rollup(\2) as \2',
|
18
|
+
/state_agg\((\w+)\)\s+as\s+(\w+)/ => 'rollup(\2) as \2',
|
19
|
+
/percentile_agg\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'rollup(\3) as \3',
|
20
|
+
/heartbeat_agg\((\w+)\)\s+as\s+(\w+)/ => 'rollup(\2) as \2',
|
21
|
+
}
|
22
|
+
|
23
|
+
scope :rollup, ->(interval) do
|
24
|
+
select_values = (self.select_values - ["time"]).select{|e|!e.downcase.start_with?("time_bucket")}
|
25
|
+
if self.select_values.any?{|e|e.downcase.start_with?('time_bucket(')} || self.select_values.include?('time')
|
26
|
+
select_values = apply_rollup_rules(select_values)
|
27
|
+
select_values.gsub!(/time_bucket\((.+), (.+)\)/, "time_bucket(#{interval}, \2)")
|
28
|
+
select_values.gsub!(/\btime\b/, "time_bucket(#{interval}, time) as time")
|
29
|
+
end
|
30
|
+
group_values = self.group_values.dup
|
31
|
+
|
32
|
+
if self.segment_by_column
|
33
|
+
if !group_values.include?(self.segment_by_column)
|
34
|
+
group_values << self.segment_by_column
|
35
|
+
end
|
36
|
+
if !select_values.include?(self.segment_by_column.to_s)
|
37
|
+
select_values.insert(0, self.segment_by_column.to_s)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
where_values = self.where_values_hash
|
41
|
+
tb = "time_bucket(#{interval}, #{time_column})"
|
42
|
+
self.unscoped.select("#{tb} as #{time_column}, #{select_values.join(', ')}")
|
43
|
+
.where(where_values)
|
44
|
+
.group(tb, *group_values)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class_methods do
|
49
|
+
def continuous_aggregates(options = {})
|
50
|
+
@time_column = options[:time_column] || self.time_column
|
51
|
+
@timeframes = options[:timeframes] || [:minute, :hour, :day, :week, :month, :year]
|
52
|
+
|
53
|
+
scopes = options[:scopes] || []
|
54
|
+
@aggregates = {}
|
55
|
+
|
56
|
+
scopes.each do |scope_name|
|
57
|
+
@aggregates[scope_name] = {
|
58
|
+
scope_name: scope_name,
|
59
|
+
select: nil,
|
60
|
+
group_by: nil,
|
61
|
+
refresh_policy: options[:refresh_policy] || {}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Allow for custom aggregate definitions to override or add to scope-based ones
|
66
|
+
@aggregates.merge!(options[:aggregates] || {})
|
67
|
+
|
68
|
+
# Add custom rollup rules if provided
|
69
|
+
self.rollup_rules.merge!(options[:custom_rollup_rules] || {})
|
70
|
+
|
71
|
+
define_continuous_aggregate_classes unless options[:skip_definition]
|
72
|
+
end
|
73
|
+
|
74
|
+
def refresh_aggregates(timeframes = nil)
|
75
|
+
timeframes ||= @timeframes
|
76
|
+
@aggregates.each do |aggregate_name, _|
|
77
|
+
timeframes.each do |timeframe|
|
78
|
+
klass = const_get("#{aggregate_name}_per_#{timeframe}".classify)
|
79
|
+
klass.refresh!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_continuous_aggregates(with_data: false)
|
85
|
+
@aggregates.each do |aggregate_name, config|
|
86
|
+
@timeframes.each do |timeframe|
|
87
|
+
klass = const_get("#{aggregate_name}_per_#{timeframe}".classify)
|
88
|
+
connection.execute <<~SQL
|
89
|
+
CREATE MATERIALIZED VIEW IF NOT EXISTS #{klass.table_name}
|
90
|
+
WITH (timescaledb.continuous) AS
|
91
|
+
#{klass.base_query}
|
92
|
+
#{with_data ? 'WITH DATA' : 'WITH NO DATA'};
|
93
|
+
SQL
|
94
|
+
|
95
|
+
if (policy = klass.refresh_policy)
|
96
|
+
connection.execute <<~SQL
|
97
|
+
SELECT add_continuous_aggregate_policy('#{klass.table_name}',
|
98
|
+
start_offset => INTERVAL '#{policy[:start_offset]}',
|
99
|
+
end_offset => INTERVAL '#{policy[:end_offset]}',
|
100
|
+
schedule_interval => INTERVAL '#{policy[:schedule_interval]}');
|
101
|
+
SQL
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def apply_rollup_rules(select_values)
|
108
|
+
result = select_values.dup
|
109
|
+
rollup_rules.each do |pattern, replacement|
|
110
|
+
result.gsub!(pattern, replacement)
|
111
|
+
end
|
112
|
+
# Remove any remaining time_bucket
|
113
|
+
result.gsub!(/time_bucket\(.+?\)( as \w+)?/, '')
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
def drop_continuous_aggregates
|
118
|
+
@aggregates.each do |aggregate_name, _|
|
119
|
+
@timeframes.reverse_each do |timeframe|
|
120
|
+
view_name = "#{aggregate_name}_per_#{timeframe}"
|
121
|
+
connection.execute("DROP MATERIALIZED VIEW IF EXISTS #{view_name} CASCADE")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def define_continuous_aggregate_classes
|
129
|
+
base_model = self
|
130
|
+
@aggregates.each do |aggregate_name, config|
|
131
|
+
previous_timeframe = nil
|
132
|
+
@timeframes.each do |timeframe|
|
133
|
+
_table_name = "#{aggregate_name}_per_#{timeframe}"
|
134
|
+
class_name = "#{aggregate_name}_per_#{timeframe}".classify
|
135
|
+
const_set(class_name, Class.new(base_model) do
|
136
|
+
class << self
|
137
|
+
attr_accessor :config, :timeframe, :base_query, :base_model
|
138
|
+
end
|
139
|
+
|
140
|
+
self.table_name = _table_name
|
141
|
+
self.config = config
|
142
|
+
self.timeframe = timeframe
|
143
|
+
|
144
|
+
interval = "'1 #{timeframe.to_s}'"
|
145
|
+
self.base_model = base_model
|
146
|
+
tb = "time_bucket(#{interval}, #{time_column})"
|
147
|
+
if previous_timeframe
|
148
|
+
prev_klass = base_model.const_get("#{aggregate_name}_per_#{previous_timeframe}".classify)
|
149
|
+
select_clause = base_model.apply_rollup_rules("#{config[:select]}")
|
150
|
+
self.base_query = "SELECT #{tb} as #{time_column}, #{select_clause} FROM \"#{prev_klass.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
|
151
|
+
else
|
152
|
+
scope = base_model.public_send(config[:scope_name])
|
153
|
+
config[:select] = scope.select_values.select{|e|!e.downcase.start_with?("time_bucket")}.join(', ')
|
154
|
+
config[:group_by] = scope.group_values
|
155
|
+
self.base_query = "SELECT #{tb} as #{time_column}, #{config[:select]} FROM \"#{scope.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.refresh!(start_time = nil, end_time = nil)
|
159
|
+
if start_time && end_time
|
160
|
+
connection.execute("CALL refresh_continuous_aggregate('#{table_name}', '#{start_time}', '#{end_time}')")
|
161
|
+
else
|
162
|
+
connection.execute("CALL refresh_continuous_aggregate('#{table_name}', null, null)")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def readonly?
|
167
|
+
true
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.refresh_policy
|
171
|
+
config[:refresh_policy]&.dig(timeframe)
|
172
|
+
end
|
173
|
+
end)
|
174
|
+
previous_timeframe = timeframe
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -16,7 +16,7 @@ module Timescaledb
|
|
16
16
|
# chunk_time_interval: '1 min',
|
17
17
|
# compress_segmentby: 'identifier',
|
18
18
|
# compress_orderby: 'created_at',
|
19
|
-
#
|
19
|
+
# compress_after: '7 days'
|
20
20
|
# }
|
21
21
|
#
|
22
22
|
# create_table(:events, id: false, hypertable: options) do |t|
|
@@ -41,7 +41,8 @@ module Timescaledb
|
|
41
41
|
chunk_time_interval: '1 week',
|
42
42
|
compress_segmentby: nil,
|
43
43
|
compress_orderby: 'created_at',
|
44
|
-
|
44
|
+
compress_after: nil,
|
45
|
+
drop_after: nil,
|
45
46
|
partition_column: nil,
|
46
47
|
number_partitions: nil,
|
47
48
|
**hypertable_options)
|
@@ -49,30 +50,24 @@ module Timescaledb
|
|
49
50
|
original_logger = ActiveRecord::Base.logger
|
50
51
|
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
51
52
|
|
52
|
-
|
53
|
-
options += hypertable_options.map { |k, v| "#{k} => #{quote(v)}" }
|
53
|
+
dimension = "by_range(#{quote(time_column)}, #{parse_interval(chunk_time_interval)})"
|
54
54
|
|
55
|
-
arguments = [
|
56
|
-
quote(
|
57
|
-
quote(time_column),
|
58
|
-
(quote(partition_column) if partition_column),
|
59
|
-
(number_partitions if partition_column),
|
60
|
-
*options
|
55
|
+
arguments = [ quote(table_name), dimension,
|
56
|
+
*hypertable_options.map { |k, v| "#{k} => #{quote(v)}" }
|
61
57
|
]
|
62
58
|
|
63
59
|
execute "SELECT create_hypertable(#{arguments.compact.join(', ')})"
|
64
60
|
|
65
|
-
if
|
66
|
-
execute
|
67
|
-
ALTER TABLE #{table_name} SET (
|
68
|
-
timescaledb.compress,
|
69
|
-
timescaledb.compress_orderby = '#{compress_orderby}',
|
70
|
-
timescaledb.compress_segmentby = '#{compress_segmentby}'
|
71
|
-
)
|
72
|
-
SQL
|
61
|
+
if partition_column && number_partitions
|
62
|
+
execute "SELECT add_dimension('#{table_name}', by_hash(#{quote(partition_column)}, #{number_partitions}))"
|
73
63
|
end
|
74
|
-
|
75
|
-
|
64
|
+
|
65
|
+
if compress_segmentby || compress_after
|
66
|
+
add_compression_policy(table_name, orderby: compress_orderby, segmentby: compress_segmentby, compress_after: compress_after)
|
67
|
+
end
|
68
|
+
|
69
|
+
if drop_after
|
70
|
+
add_retention_policy(table_name, drop_after: drop_after)
|
76
71
|
end
|
77
72
|
ensure
|
78
73
|
ActiveRecord::Base.logger = original_logger if original_logger
|
@@ -146,14 +141,40 @@ module Timescaledb
|
|
146
141
|
execute "SELECT remove_continuous_aggregate_policy('#{table_name}')"
|
147
142
|
end
|
148
143
|
|
149
|
-
def create_retention_policy(table_name,
|
150
|
-
execute "SELECT add_retention_policy('#{table_name}',
|
144
|
+
def create_retention_policy(table_name, drop_after:)
|
145
|
+
execute "SELECT add_retention_policy('#{table_name}', drop_after => #{parse_interval(drop_after)})"
|
151
146
|
end
|
152
147
|
|
148
|
+
alias_method :add_retention_policy, :create_retention_policy
|
149
|
+
|
153
150
|
def remove_retention_policy(table_name)
|
154
151
|
execute "SELECT remove_retention_policy('#{table_name}')"
|
155
152
|
end
|
156
153
|
|
154
|
+
|
155
|
+
# Enable compression policy.
|
156
|
+
#
|
157
|
+
# @param table_name [String] The name of the table.
|
158
|
+
# @param orderby [String] The column to order by.
|
159
|
+
# @param segmentby [String] The column to segment by.
|
160
|
+
# @param compress_after [String] The interval to compress after.
|
161
|
+
# @param compression_chunk_time_interval [String] In case to merge chunks.
|
162
|
+
#
|
163
|
+
# @see https://docs.timescale.com/api/latest/compression/add_compression_policy/
|
164
|
+
def add_compression_policy(table_name, orderby:, segmentby:, compress_after: nil, compression_chunk_time_interval: nil)
|
165
|
+
options = []
|
166
|
+
options << 'timescaledb.compress'
|
167
|
+
options << "timescaledb.compress_orderby = '#{orderby}'" if orderby
|
168
|
+
options << "timescaledb.compress_segmentby = '#{segmentby}'" if segmentby
|
169
|
+
options << "timescaledb.compression_chunk_time_interval = INTERVAL '#{compression_chunk_time_interval}'" if compression_chunk_time_interval
|
170
|
+
execute <<~SQL
|
171
|
+
ALTER TABLE #{table_name} SET (
|
172
|
+
#{options.join(',')}
|
173
|
+
)
|
174
|
+
SQL
|
175
|
+
execute "SELECT add_compression_policy('#{table_name}', compress_after => INTERVAL '#{compress_after}')" if compress_after
|
176
|
+
end
|
177
|
+
|
157
178
|
private
|
158
179
|
|
159
180
|
# Build a string for the WITH clause of the CREATE MATERIALIZED VIEW statement.
|
@@ -165,6 +186,14 @@ module Timescaledb
|
|
165
186
|
value = options[option_key] ? 'true' : 'false'
|
166
187
|
",timescaledb.#{option_key}=#{value}"
|
167
188
|
end
|
189
|
+
|
190
|
+
def parse_interval(interval)
|
191
|
+
if interval.is_a?(Numeric)
|
192
|
+
interval
|
193
|
+
else
|
194
|
+
"INTERVAL '#{interval}'"
|
195
|
+
end
|
196
|
+
end
|
168
197
|
end
|
169
198
|
end
|
170
199
|
|
@@ -78,7 +78,7 @@ module Timescaledb
|
|
78
78
|
|
79
79
|
options = {
|
80
80
|
time_column: time.column_name,
|
81
|
-
chunk_time_interval: time.time_interval.inspect,
|
81
|
+
chunk_time_interval: time.time_interval ? time.time_interval.inspect : time.integer_interval,
|
82
82
|
**timescale_compression_settings_for(hypertable),
|
83
83
|
**timescale_space_partition_for(hypertable),
|
84
84
|
**timescale_index_options_for(hypertable)
|
@@ -106,14 +106,12 @@ module Timescaledb
|
|
106
106
|
if setting.orderby_column_index
|
107
107
|
if setting.orderby_asc
|
108
108
|
direction = "ASC"
|
109
|
-
if
|
110
|
-
|
111
|
-
end
|
109
|
+
# For ASC, default is NULLS LAST, so only add if explicitly set to FIRST
|
110
|
+
direction += " NULLS FIRST" if setting.orderby_nullsfirst == true
|
112
111
|
else
|
113
112
|
direction = "DESC"
|
114
|
-
if
|
115
|
-
|
116
|
-
end
|
113
|
+
# For DESC, default is NULLS FIRST, so only add if explicitly set to LAST
|
114
|
+
direction += " NULLS LAST" if setting.orderby_nullsfirst == false
|
117
115
|
end
|
118
116
|
|
119
117
|
compression_settings[:compress_orderby] << "#{setting.attname} #{direction}"
|
@@ -154,19 +152,19 @@ module Timescaledb
|
|
154
152
|
def timescale_continuous_aggregates(stream)
|
155
153
|
return unless Timescaledb::ContinuousAggregates.table_exists?
|
156
154
|
|
157
|
-
Timescaledb::ContinuousAggregates.
|
155
|
+
Timescaledb::ContinuousAggregates.hierarchical.each do |aggregate|
|
158
156
|
refresh_policies_opts = if (refresh_policy = aggregate.jobs.refresh_continuous_aggregate.first)
|
159
157
|
interval = timescale_interval(refresh_policy.schedule_interval)
|
160
158
|
end_offset = timescale_interval(refresh_policy.config["end_offset"])
|
161
159
|
start_offset = timescale_interval(refresh_policy.config["start_offset"])
|
162
|
-
%(refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"})
|
160
|
+
%(refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"}, )
|
163
161
|
else
|
164
162
|
""
|
165
163
|
end
|
166
164
|
|
167
165
|
with_clause_opts = "materialized_only: #{aggregate[:materialized_only]}, finalized: #{aggregate[:finalized]}"
|
168
166
|
stream.puts <<~AGG.indent(2)
|
169
|
-
create_continuous_aggregate("#{aggregate.view_name}", <<-SQL, #{refresh_policies_opts}
|
167
|
+
create_continuous_aggregate("#{aggregate.view_name}", <<-SQL, #{refresh_policies_opts}#{with_clause_opts})
|
170
168
|
#{aggregate.view_definition.strip.gsub(/;$/, '')}
|
171
169
|
SQL
|
172
170
|
AGG
|
@@ -9,23 +9,23 @@ module Timescaledb
|
|
9
9
|
|
10
10
|
module ClassMethods
|
11
11
|
def value_column
|
12
|
-
@value_column ||=
|
12
|
+
@value_column ||= hypertable_options[:value_column] || :val
|
13
13
|
end
|
14
14
|
|
15
15
|
def time_column
|
16
|
-
respond_to?(:time_column) && super ||
|
16
|
+
respond_to?(:time_column) && super || hypertable_options[:time_column]
|
17
17
|
end
|
18
18
|
|
19
19
|
def segment_by_column
|
20
|
-
|
20
|
+
hypertable_options[:segment_by] || hypertable_options[:compress_segment_by]
|
21
21
|
end
|
22
22
|
|
23
23
|
protected
|
24
24
|
|
25
|
-
def
|
26
|
-
scope :volatility, -> (segment_by: segment_by_column) do
|
25
|
+
def define_default_vector_scopes
|
26
|
+
scope :volatility, -> (segment_by: segment_by_column, value: value_column) do
|
27
27
|
select([*segment_by,
|
28
|
-
"timevector(#{time_column}, #{
|
28
|
+
"timevector(#{time_column}, #{value}) -> sort() -> delta() -> abs() -> sum() as volatility"
|
29
29
|
].join(", ")).group(segment_by)
|
30
30
|
end
|
31
31
|
|
@@ -36,11 +36,15 @@ module Timescaledb
|
|
36
36
|
.group(segment_by)
|
37
37
|
end
|
38
38
|
|
39
|
-
scope :lttb, -> (threshold:, segment_by: segment_by_column, time: time_column, value: value_column) do
|
39
|
+
scope :lttb, -> (threshold:, segment_by: segment_by_column, time: time_column, value: value_column, value_exp: value_column) do
|
40
|
+
if value =~ /(.*)\bas\b(.*)/
|
41
|
+
value_exp = $1
|
42
|
+
value = $2
|
43
|
+
end
|
40
44
|
lttb_query = <<~SQL
|
41
|
-
WITH x AS ( #{select(*segment_by, time_column,
|
45
|
+
WITH x AS ( #{select(*segment_by, time_column, value_exp || value).to_sql})
|
42
46
|
SELECT #{"x.#{segment_by}," if segment_by}
|
43
|
-
(lttb( x.#{time_column}, x.#{
|
47
|
+
(lttb( x.#{time_column}, x.#{value}, #{threshold}) -> unnest()).*
|
44
48
|
FROM x
|
45
49
|
#{"GROUP BY #{segment_by}" if segment_by}
|
46
50
|
SQL
|
data/lib/timescaledb/toolkit.rb
CHANGED
data/lib/timescaledb/version.rb
CHANGED
data/lib/timescaledb.rb
CHANGED
@@ -3,6 +3,7 @@ require 'active_record'
|
|
3
3
|
require_relative 'timescaledb/application_record'
|
4
4
|
require_relative 'timescaledb/acts_as_hypertable'
|
5
5
|
require_relative 'timescaledb/acts_as_hypertable/core'
|
6
|
+
require_relative 'timescaledb/continuous_aggregates_helper'
|
6
7
|
require_relative 'timescaledb/connection'
|
7
8
|
require_relative 'timescaledb/toolkit'
|
8
9
|
require_relative 'timescaledb/chunk'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timescaledb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jônatas Davi Paganini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ostruct
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pry
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,7 +152,7 @@ dependencies:
|
|
138
152
|
version: '0'
|
139
153
|
description: Functions from timescaledb available in the ActiveRecord models.
|
140
154
|
email:
|
141
|
-
-
|
155
|
+
- jonatas@timescale.com
|
142
156
|
executables:
|
143
157
|
- tsdb
|
144
158
|
extensions: []
|
@@ -149,13 +163,13 @@ files:
|
|
149
163
|
- lib/timescaledb.rb
|
150
164
|
- lib/timescaledb/acts_as_hypertable.rb
|
151
165
|
- lib/timescaledb/acts_as_hypertable/core.rb
|
152
|
-
- lib/timescaledb/acts_as_time_vector.rb
|
153
166
|
- lib/timescaledb/application_record.rb
|
154
167
|
- lib/timescaledb/chunk.rb
|
155
168
|
- lib/timescaledb/compression_settings.rb
|
156
169
|
- lib/timescaledb/connection.rb
|
157
170
|
- lib/timescaledb/connection_handling.rb
|
158
171
|
- lib/timescaledb/continuous_aggregates.rb
|
172
|
+
- lib/timescaledb/continuous_aggregates_helper.rb
|
159
173
|
- lib/timescaledb/database.rb
|
160
174
|
- lib/timescaledb/database/chunk_statements.rb
|
161
175
|
- lib/timescaledb/database/hypertable_statements.rb
|
@@ -181,12 +195,13 @@ files:
|
|
181
195
|
- lib/timescaledb/toolkit/helpers.rb
|
182
196
|
- lib/timescaledb/toolkit/time_vector.rb
|
183
197
|
- lib/timescaledb/version.rb
|
184
|
-
homepage: https://github.com/
|
198
|
+
homepage: https://github.com/timescale/timescaledb-ruby
|
185
199
|
licenses:
|
186
200
|
- MIT
|
187
201
|
metadata:
|
188
202
|
allowed_push_host: https://rubygems.org
|
189
|
-
homepage_uri: https://github.
|
203
|
+
homepage_uri: https://timescale.github.io/timescaledb-ruby/
|
204
|
+
source_code_uri: https://github.com/timescale/timescaledb-ruby
|
190
205
|
post_install_message:
|
191
206
|
rdoc_options: []
|
192
207
|
require_paths:
|
@@ -202,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
217
|
- !ruby/object:Gem::Version
|
203
218
|
version: '0'
|
204
219
|
requirements: []
|
205
|
-
rubygems_version: 3.
|
220
|
+
rubygems_version: 3.5.23
|
206
221
|
signing_key:
|
207
222
|
specification_version: 4
|
208
223
|
summary: TimescaleDB helpers for Ruby ecosystem.
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module Timescaledb
|
2
|
-
module ActsAsTimeVector
|
3
|
-
def acts_as_time_vector(options = {})
|
4
|
-
return if acts_as_time_vector?
|
5
|
-
|
6
|
-
include Timescaledb::Toolkit::TimeVector
|
7
|
-
|
8
|
-
class_attribute :time_vector_options, instance_writer: false
|
9
|
-
define_default_scopes
|
10
|
-
self.time_vector_options = options
|
11
|
-
end
|
12
|
-
|
13
|
-
def acts_as_time_vector?
|
14
|
-
included_modules.include?(Timescaledb::ActsAsTimeVector)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
ActiveRecord::Base.extend Timescaledb::ActsAsTimeVector
|