timescaledb 0.3.0 → 0.3.2
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/bin/tsdb +16 -13
- data/lib/timescaledb/acts_as_hypertable/core.rb +24 -24
- data/lib/timescaledb/acts_as_hypertable.rb +4 -1
- data/lib/timescaledb/connection.rb +1 -0
- data/lib/timescaledb/connection_handling.rb +19 -2
- data/lib/timescaledb/continuous_aggregates.rb +1 -2
- data/lib/timescaledb/continuous_aggregates_helper.rb +191 -0
- data/lib/timescaledb/counter_cache.rb +88 -0
- data/lib/timescaledb/hypertable.rb +2 -0
- data/lib/timescaledb/migration_helpers.rb +47 -26
- data/lib/timescaledb/schema_dumper.rb +6 -8
- 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 +23 -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: 33706a3c899ae608bb30b9ef62ca32875045960745b526bc0700fddec09ddc8b
|
4
|
+
data.tar.gz: 88a7cb9f1f2f2dbb68f131547fa7c9575979f6e87b0bb29f576ee95d9bb9696c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a700d4b61d11af54856fb02e19db676a720ba5ff3c2367e7eb9ba153bc14ae1c3c67d9130bfc47e1ecac786a007274df9c3595a6b10fe797fb06b13ae30ac957
|
7
|
+
data.tar.gz: 05c45d270e86c24d40984cea92e8870869441580d35426b5028ca17b346a47ad7468482337adec69eaf0d6daecdd4292d7108868677a7388f35cf2529d7e631a
|
data/bin/tsdb
CHANGED
@@ -6,17 +6,19 @@ require "pry"
|
|
6
6
|
|
7
7
|
Timescaledb.establish_connection(ARGV[0])
|
8
8
|
|
9
|
-
|
9
|
+
ActiveSupport.on_load(:active_record) { extend Timescaledb::ActsAsHypertable }
|
10
|
+
|
10
11
|
|
11
12
|
if ARGV.index("--stats")
|
13
|
+
hypertables = Timescaledb::Hypertable.all
|
12
14
|
if (only = ARGV.index("--only"))
|
13
15
|
only_hypertables = ARGV[only+1].split(",")
|
14
16
|
|
15
|
-
hypertables.select! { |hypertable| only_hypertables.
|
17
|
+
hypertables.select! { |hypertable| only_hypertables.include?(hypertable.hypertable_name) }
|
16
18
|
elsif (except = ARGV.index("--except"))
|
17
19
|
except_hypertables = ARGV[except+1].split(",")
|
18
20
|
|
19
|
-
hypertables.select! { |hypertable| except_hypertables.
|
21
|
+
hypertables.select! { |hypertable| except_hypertables.include?(hypertable.hypertable_name) }
|
20
22
|
end
|
21
23
|
|
22
24
|
stats = Timescaledb::Stats.new(hypertables).to_h
|
@@ -25,25 +27,26 @@ if ARGV.index("--stats")
|
|
25
27
|
end
|
26
28
|
|
27
29
|
if ARGV.index("--console")
|
28
|
-
ActiveRecord::Base.establish_connection(ARGV[0])
|
29
|
-
|
30
30
|
Timescaledb::Hypertable.find_each do |hypertable|
|
31
31
|
class_name = hypertable.hypertable_name.singularize.camelize
|
32
32
|
model = Class.new(ActiveRecord::Base) do
|
33
|
-
self.table_name = hypertable.hypertable_name
|
33
|
+
self.table_name = "#{hypertable.hypertable_schema}.#{hypertable.hypertable_name}"
|
34
|
+
|
34
35
|
acts_as_hypertable time_column: hypertable.main_dimension.column_name
|
35
36
|
end
|
36
|
-
Timescaledb.const_set(class_name, model)
|
37
|
-
end
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
Timescaledb::ContinuousAggregate.where( hypertable_schema: hypertable.hypertable_schema, hypertable_name: hypertable.hypertable_name).hierarchical.find_each do |cagg|
|
39
|
+
cagg_model_name = cagg.view_name.singularize.camelize
|
40
|
+
cagg_model = Class.new(ActiveRecord::Base) do
|
41
|
+
self.table_name = cagg.view_name
|
42
|
+
self.schema_name = cagg.view_schema
|
43
|
+
acts_as_hypertable
|
44
|
+
end
|
45
|
+
model.const_set(cagg_model_name, cagg_model)
|
44
46
|
end
|
45
47
|
Timescaledb.const_set(class_name, model)
|
46
48
|
end
|
47
49
|
|
50
|
+
|
48
51
|
Pry.start(Timescaledb)
|
49
52
|
end
|
@@ -35,47 +35,47 @@ module Timescaledb
|
|
35
35
|
CompressionSettings.where(hypertable_name: table_name)
|
36
36
|
end
|
37
37
|
|
38
|
-
scope :
|
38
|
+
scope :caggs, -> do
|
39
39
|
ContinuousAggregates.where(hypertable_name: table_name)
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
def define_default_scopes
|
44
|
+
scope :between, ->(start_time, end_time) do
|
45
|
+
where("#{time_column} BETWEEN ? AND ?", start_time, end_time)
|
46
|
+
end
|
47
|
+
|
44
48
|
scope :previous_month, -> do
|
45
|
-
|
46
|
-
|
47
|
-
start_time: Date.today.last_month.in_time_zone.beginning_of_month.to_date,
|
48
|
-
end_time: Date.today.last_month.in_time_zone.end_of_month.to_date
|
49
|
-
)
|
49
|
+
ref = 1.month.ago.in_time_zone
|
50
|
+
between(ref.beginning_of_month, ref.end_of_month)
|
50
51
|
end
|
51
52
|
|
52
53
|
scope :previous_week, -> do
|
53
|
-
|
54
|
-
|
55
|
-
start_time: Date.today.last_week.in_time_zone.beginning_of_week.to_date,
|
56
|
-
end_time: Date.today.last_week.in_time_zone.end_of_week.to_date
|
57
|
-
)
|
54
|
+
ref = 1.week.ago.in_time_zone
|
55
|
+
between(ref.beginning_of_week, ref.end_of_week)
|
58
56
|
end
|
59
57
|
|
60
58
|
scope :this_month, -> do
|
61
|
-
|
62
|
-
|
63
|
-
start_time: Date.today.in_time_zone.beginning_of_month.to_date,
|
64
|
-
end_time: Date.today.in_time_zone.end_of_month.to_date
|
65
|
-
)
|
59
|
+
ref = Time.now.in_time_zone
|
60
|
+
between(ref.beginning_of_month, ref.end_of_month)
|
66
61
|
end
|
67
62
|
|
68
63
|
scope :this_week, -> do
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
64
|
+
ref = Time.now.in_time_zone
|
65
|
+
between(ref.beginning_of_week, ref.end_of_week)
|
66
|
+
end
|
67
|
+
|
68
|
+
scope :yesterday, -> do
|
69
|
+
ref = 1.day.ago.in_time_zone
|
70
|
+
between(ref.yesterday, ref.yesterday)
|
71
|
+
end
|
72
|
+
|
73
|
+
scope :today, -> do
|
74
|
+
ref = Time.now.in_time_zone
|
75
|
+
between(ref.beginning_of_day, ref.end_of_day)
|
74
76
|
end
|
75
77
|
|
76
|
-
scope :
|
77
|
-
scope :today, -> { where("DATE(#{time_column}) = ?", Date.today.in_time_zone.to_date) }
|
78
|
-
scope :last_hour, -> { where("#{time_column} between ? and ?", 1.hour.ago.in_time_zone, Time.now.end_of_hour.in_time_zone) }
|
78
|
+
scope :last_hour, -> { where("#{time_column} > ?", 1.hour.ago.in_time_zone) }
|
79
79
|
end
|
80
80
|
|
81
81
|
def normalize_hypertable_options
|
@@ -22,7 +22,7 @@ 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
26
|
}.freeze
|
27
27
|
|
28
28
|
def acts_as_hypertable?
|
@@ -45,10 +45,12 @@ module Timescaledb
|
|
45
45
|
# @param [Hash] options The options to initialize your macro with.
|
46
46
|
# @option options [Boolean] :skip_association_scopes to avoid `.hypertable`, `.chunks` and other scopes related to metadata.
|
47
47
|
# @option options [Boolean] :skip_default_scopes to avoid the generation of default time related scopes like `last_hour`, `last_week`, `yesterday` and so on...
|
48
|
+
# @option options [Boolean] :skip_time_vector to avoid the generation of time vector related scopes
|
48
49
|
def acts_as_hypertable(options = {})
|
49
50
|
return if acts_as_hypertable?
|
50
51
|
|
51
52
|
include Timescaledb::ActsAsHypertable::Core
|
53
|
+
include Timescaledb::Toolkit::TimeVector
|
52
54
|
|
53
55
|
class_attribute :hypertable_options, instance_writer: false
|
54
56
|
|
@@ -58,6 +60,7 @@ module Timescaledb
|
|
58
60
|
|
59
61
|
define_association_scopes unless options[:skip_association_scopes]
|
60
62
|
define_default_scopes unless options[:skip_default_scopes]
|
63
|
+
define_default_vector_scopes unless options[:skip_time_vector]
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
@@ -5,12 +5,29 @@ module Timescaledb
|
|
5
5
|
|
6
6
|
# @param [String] config with the postgres connection string.
|
7
7
|
def establish_connection(config)
|
8
|
+
# Establish connection for Timescaledb
|
8
9
|
Connection.instance.config = config
|
10
|
+
|
11
|
+
# Also establish connection for ActiveRecord if it's defined
|
12
|
+
if defined?(ActiveRecord::Base)
|
13
|
+
ActiveRecord::Base.establish_connection(config)
|
14
|
+
end
|
9
15
|
end
|
10
16
|
|
11
17
|
# @param [PG::Connection] to use it directly from a raw connection
|
12
|
-
def use_connection
|
13
|
-
Connection.instance.use_connection
|
18
|
+
def use_connection(conn)
|
19
|
+
Connection.instance.use_connection(conn)
|
20
|
+
|
21
|
+
# Also set ActiveRecord connection if it's defined
|
22
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
23
|
+
ar_conn = ActiveRecord::Base.connection
|
24
|
+
current_conn = ar_conn.raw_connection
|
25
|
+
|
26
|
+
# Only set if it's different to avoid redundant assignment
|
27
|
+
if current_conn != conn
|
28
|
+
ar_conn.instance_variable_set(:@raw_connection, conn)
|
29
|
+
end
|
30
|
+
end
|
14
31
|
end
|
15
32
|
|
16
33
|
def connection
|
@@ -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
|
|
@@ -39,5 +39,4 @@ module Timescaledb
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
42
|
-
ContinuousAggregates = ContinuousAggregate
|
43
42
|
end
|
@@ -0,0 +1,191 @@
|
|
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
|
+
/stats_agg\(([^)]+)\)\s+(as\s+(\w+))/ => 'rollup(\3) \2',
|
22
|
+
/stats_agg\((.*)\)\s+(as\s+(\w+))/ => 'rollup(\3) \2'
|
23
|
+
}
|
24
|
+
|
25
|
+
scope :rollup, ->(interval) do
|
26
|
+
select_values = (self.select_values - ["time"]).select{|e|!e.downcase.start_with?("time_bucket")}
|
27
|
+
if self.select_values.any?{|e|e.downcase.start_with?('time_bucket(')} || self.select_values.include?('time')
|
28
|
+
select_values = apply_rollup_rules(select_values)
|
29
|
+
select_values.gsub!(/time_bucket\((.+), (.+)\)/, "time_bucket(#{interval}, \2)")
|
30
|
+
select_values.gsub!(/\btime\b/, "time_bucket(#{interval}, time) as time")
|
31
|
+
end
|
32
|
+
group_values = self.group_values.dup
|
33
|
+
|
34
|
+
if self.segment_by_column
|
35
|
+
if !group_values.include?(self.segment_by_column)
|
36
|
+
group_values << self.segment_by_column
|
37
|
+
end
|
38
|
+
if !select_values.include?(self.segment_by_column.to_s)
|
39
|
+
select_values.insert(0, self.segment_by_column.to_s)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
where_values = self.where_values_hash
|
43
|
+
tb = "time_bucket(#{interval}, #{time_column})"
|
44
|
+
self.unscoped.select("#{tb} as #{time_column}, #{select_values.join(', ')}")
|
45
|
+
.where(where_values)
|
46
|
+
.group(tb, *group_values)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class_methods do
|
51
|
+
def continuous_aggregates(options = {})
|
52
|
+
@time_column = options[:time_column] || self.time_column
|
53
|
+
@timeframes = options[:timeframes] || [:minute, :hour, :day, :week, :month, :year]
|
54
|
+
|
55
|
+
scopes = options[:scopes] || []
|
56
|
+
@aggregates = {}
|
57
|
+
|
58
|
+
scopes.each do |scope_name|
|
59
|
+
@aggregates[scope_name] = {
|
60
|
+
scope_name: scope_name,
|
61
|
+
select: nil,
|
62
|
+
group_by: nil,
|
63
|
+
refresh_policy: options[:refresh_policy] || {}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Allow for custom aggregate definitions to override or add to scope-based ones
|
68
|
+
@aggregates.merge!(options[:aggregates] || {})
|
69
|
+
|
70
|
+
# Add custom rollup rules if provided
|
71
|
+
self.rollup_rules.merge!(options[:custom_rollup_rules] || {})
|
72
|
+
|
73
|
+
define_continuous_aggregate_classes unless options[:skip_definition]
|
74
|
+
end
|
75
|
+
|
76
|
+
def refresh_aggregates(timeframes = nil)
|
77
|
+
timeframes ||= @timeframes
|
78
|
+
@aggregates.each do |aggregate_name, _|
|
79
|
+
timeframes.each do |timeframe|
|
80
|
+
klass = const_get("#{aggregate_name}_per_#{timeframe}".classify)
|
81
|
+
klass.refresh!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_continuous_aggregates(with_data: false)
|
87
|
+
@aggregates.each do |aggregate_name, config|
|
88
|
+
@timeframes.each do |timeframe|
|
89
|
+
klass = const_get("#{aggregate_name}_per_#{timeframe}".classify)
|
90
|
+
connection.execute <<~SQL
|
91
|
+
CREATE MATERIALIZED VIEW IF NOT EXISTS #{klass.table_name}
|
92
|
+
WITH (timescaledb.continuous) AS
|
93
|
+
#{klass.base_query}
|
94
|
+
#{with_data ? 'WITH DATA' : 'WITH NO DATA'};
|
95
|
+
SQL
|
96
|
+
|
97
|
+
if (policy = klass.refresh_policy)
|
98
|
+
connection.execute <<~SQL
|
99
|
+
SELECT add_continuous_aggregate_policy('#{klass.table_name}',
|
100
|
+
start_offset => INTERVAL '#{policy[:start_offset]}',
|
101
|
+
end_offset => INTERVAL '#{policy[:end_offset]}',
|
102
|
+
schedule_interval => INTERVAL '#{policy[:schedule_interval]}');
|
103
|
+
SQL
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def apply_rollup_rules(select_values)
|
110
|
+
result = select_values.dup
|
111
|
+
rollup_rules.each do |pattern, replacement|
|
112
|
+
result.gsub!(pattern, replacement)
|
113
|
+
end
|
114
|
+
# Remove any remaining time_bucket
|
115
|
+
result.gsub!(/time_bucket\(.+?\)( as \w+)?/, '')
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
def drop_continuous_aggregates
|
120
|
+
@aggregates.each do |aggregate_name, _|
|
121
|
+
@timeframes.reverse_each do |timeframe|
|
122
|
+
view_name = "#{aggregate_name}_per_#{timeframe}"
|
123
|
+
connection.execute("DROP MATERIALIZED VIEW IF EXISTS #{view_name} CASCADE")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def define_continuous_aggregate_classes
|
131
|
+
base_model = self
|
132
|
+
@aggregates.each do |aggregate_name, config|
|
133
|
+
previous_timeframe = nil
|
134
|
+
@timeframes.each do |timeframe|
|
135
|
+
_table_name = "#{aggregate_name}_per_#{timeframe}"
|
136
|
+
class_name = "#{aggregate_name}_per_#{timeframe}".classify
|
137
|
+
const_set(class_name, Class.new(base_model) do
|
138
|
+
class << self
|
139
|
+
attr_accessor :config, :timeframe, :base_query, :base_model
|
140
|
+
end
|
141
|
+
|
142
|
+
self.table_name = _table_name
|
143
|
+
self.config = config
|
144
|
+
self.timeframe = timeframe
|
145
|
+
|
146
|
+
interval = "'1 #{timeframe.to_s}'"
|
147
|
+
self.base_model = base_model
|
148
|
+
tb = "time_bucket(#{interval}, #{time_column})"
|
149
|
+
if previous_timeframe
|
150
|
+
prev_klass = base_model.const_get("#{aggregate_name}_per_#{previous_timeframe}".classify)
|
151
|
+
select_clause = base_model.apply_rollup_rules("#{config[:select]}")
|
152
|
+
# Note there's no where clause here, because we're using the previous timeframe's data
|
153
|
+
self.base_query = "SELECT #{tb} as #{time_column}, #{select_clause} FROM \"#{prev_klass.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
|
154
|
+
else
|
155
|
+
scope = base_model.public_send(config[:scope_name])
|
156
|
+
config[:select] = scope.select_values.select{|e|!e.downcase.start_with?("time_bucket")}.join(', ')
|
157
|
+
config[:group_by] = scope.group_values
|
158
|
+
config[:where] = if scope.where_values_hash.present?
|
159
|
+
scope.where_values_hash.to_sql
|
160
|
+
elsif scope.where_clause.ast.present? && scope.where_clause.ast.to_sql.present?
|
161
|
+
scope.where_clause.ast.to_sql
|
162
|
+
end
|
163
|
+
self.base_query = "SELECT #{tb} as #{time_column}, #{config[:select]}"
|
164
|
+
self.base_query += " FROM \"#{base_model.table_name}\""
|
165
|
+
self.base_query += " WHERE #{config[:where]}" if config[:where]
|
166
|
+
self.base_query += " GROUP BY #{[tb, *config[:group_by]].join(', ')}"
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.refresh!(start_time = nil, end_time = nil)
|
170
|
+
if start_time && end_time
|
171
|
+
connection.execute("CALL refresh_continuous_aggregate('#{table_name}', '#{start_time}', '#{end_time}')")
|
172
|
+
else
|
173
|
+
connection.execute("CALL refresh_continuous_aggregate('#{table_name}', null, null)")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def readonly?
|
178
|
+
true
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.refresh_policy
|
182
|
+
config[:refresh_policy]&.dig(timeframe)
|
183
|
+
end
|
184
|
+
end)
|
185
|
+
previous_timeframe = timeframe
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Timescaledb
|
2
|
+
module CounterCache
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :counter_cache_options, default: {}
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def belongs_to_with_counter_cache(name, scope = nil, **options)
|
11
|
+
if options[:counter_cache] == :timescaledb || options[:counter_cache].is_a?(Array)
|
12
|
+
setup_timescaledb_counter_cache(name, options)
|
13
|
+
options.delete(:counter_cache)
|
14
|
+
end
|
15
|
+
|
16
|
+
belongs_to(name, scope, **options)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def setup_timescaledb_counter_cache(association_name, options)
|
22
|
+
timeframes = if options[:counter_cache] == :timescaledb
|
23
|
+
[:hour, :day] # Default timeframes
|
24
|
+
else
|
25
|
+
options[:counter_cache]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Store counter cache configuration
|
29
|
+
self.counter_cache_options[association_name] = {
|
30
|
+
timeframes: timeframes,
|
31
|
+
foreign_key: options[:foreign_key] || "#{association_name}_id"
|
32
|
+
}
|
33
|
+
|
34
|
+
# Setup continuous aggregate for counting
|
35
|
+
setup_counter_aggregate(association_name, timeframes)
|
36
|
+
|
37
|
+
# Setup associations on the target class
|
38
|
+
setup_target_associations(association_name, timeframes)
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_counter_aggregate(association_name, timeframes)
|
42
|
+
scope_name = "#{association_name}_count"
|
43
|
+
|
44
|
+
# Define the base counting scope
|
45
|
+
scope scope_name, -> { select(counter_cache_options[association_name][:foreign_key], "count(*) as count").group(1) }
|
46
|
+
|
47
|
+
# Create continuous aggregates for each timeframe
|
48
|
+
continuous_aggregates(
|
49
|
+
scopes: [scope_name],
|
50
|
+
timeframes: timeframes,
|
51
|
+
refresh_policy: {
|
52
|
+
start_offset: "1 day",
|
53
|
+
end_offset: "1 hour",
|
54
|
+
schedule_interval: "1 hour"
|
55
|
+
}
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup_target_associations(association_name, timeframes)
|
60
|
+
target_class = reflect_on_association(association_name).klass
|
61
|
+
|
62
|
+
timeframes.each do |timeframe|
|
63
|
+
view_name = "#{table_name}_#{association_name}_count_per_#{timeframe}"
|
64
|
+
|
65
|
+
target_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
66
|
+
has_many :#{view_name.pluralize},
|
67
|
+
class_name: "#{self}::#{association_name.to_s.classify}CountPer#{timeframe.to_s.classify}",
|
68
|
+
foreign_key: :#{counter_cache_options[association_name][:foreign_key]}
|
69
|
+
|
70
|
+
def #{view_name}_total
|
71
|
+
#{view_name.pluralize}.sum(:count)
|
72
|
+
end
|
73
|
+
RUBY
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Extend ActiveRecord with counter cache functionality
|
81
|
+
ActiveSupport.on_load(:active_record) do
|
82
|
+
extend Timescaledb::CounterCache
|
83
|
+
|
84
|
+
class << self
|
85
|
+
alias_method :belongs_to_without_counter_cache, :belongs_to
|
86
|
+
alias_method :belongs_to, :belongs_to_with_counter_cache
|
87
|
+
end
|
88
|
+
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.
|
@@ -166,11 +187,11 @@ module Timescaledb
|
|
166
187
|
",timescaledb.#{option_key}=#{value}"
|
167
188
|
end
|
168
189
|
|
169
|
-
def
|
170
|
-
if
|
171
|
-
|
190
|
+
def parse_interval(interval)
|
191
|
+
if interval.is_a?(Numeric)
|
192
|
+
interval
|
172
193
|
else
|
173
|
-
"INTERVAL '#{
|
194
|
+
"INTERVAL '#{interval}'"
|
174
195
|
end
|
175
196
|
end
|
176
197
|
end
|
@@ -90,7 +90,7 @@ module Timescaledb
|
|
90
90
|
|
91
91
|
def timescale_retention_policy(hypertable, stream)
|
92
92
|
hypertable.jobs.where(proc_name: "policy_retention").each do |job|
|
93
|
-
stream.puts %Q[ create_retention_policy "#{job.hypertable_name}",
|
93
|
+
stream.puts %Q[ create_retention_policy "#{job.hypertable_name}", drop_after: "#{job.config["drop_after"]}"]
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
@@ -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}"
|
@@ -121,7 +119,7 @@ module Timescaledb
|
|
121
119
|
end
|
122
120
|
|
123
121
|
hypertable.jobs.compression.each do |job|
|
124
|
-
compression_settings[:
|
122
|
+
compression_settings[:compress_after] = job.config["compress_after"]
|
125
123
|
end
|
126
124
|
|
127
125
|
# Pack the compression setting arrays into a comma-separated string instead.
|
@@ -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.3.
|
4
|
+
version: 0.3.2
|
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:
|
11
|
+
date: 2025-02-18 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,14 @@ 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
|
173
|
+
- lib/timescaledb/counter_cache.rb
|
159
174
|
- lib/timescaledb/database.rb
|
160
175
|
- lib/timescaledb/database/chunk_statements.rb
|
161
176
|
- lib/timescaledb/database/hypertable_statements.rb
|
@@ -181,12 +196,13 @@ files:
|
|
181
196
|
- lib/timescaledb/toolkit/helpers.rb
|
182
197
|
- lib/timescaledb/toolkit/time_vector.rb
|
183
198
|
- lib/timescaledb/version.rb
|
184
|
-
homepage: https://github.com/
|
199
|
+
homepage: https://github.com/timescale/timescaledb-ruby
|
185
200
|
licenses:
|
186
201
|
- MIT
|
187
202
|
metadata:
|
188
203
|
allowed_push_host: https://rubygems.org
|
189
|
-
homepage_uri: https://github.
|
204
|
+
homepage_uri: https://timescale.github.io/timescaledb-ruby/
|
205
|
+
source_code_uri: https://github.com/timescale/timescaledb-ruby
|
190
206
|
post_install_message:
|
191
207
|
rdoc_options: []
|
192
208
|
require_paths:
|
@@ -202,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
218
|
- !ruby/object:Gem::Version
|
203
219
|
version: '0'
|
204
220
|
requirements: []
|
205
|
-
rubygems_version: 3.
|
221
|
+
rubygems_version: 3.5.23
|
206
222
|
signing_key:
|
207
223
|
specification_version: 4
|
208
224
|
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
|