timescaledb 0.2.9 → 0.3.1
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/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
|