timescaledb 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a20c2f12a3673694b39fd99aa005d743bbc027031d3abc7c25fda904f71ee33f
4
- data.tar.gz: fd27816a15c114baa3d75a08bd083de5493efd7ce60ebe200d6543a3dcb266bf
3
+ metadata.gz: 33706a3c899ae608bb30b9ef62ca32875045960745b526bc0700fddec09ddc8b
4
+ data.tar.gz: 88a7cb9f1f2f2dbb68f131547fa7c9575979f6e87b0bb29f576ee95d9bb9696c
5
5
  SHA512:
6
- metadata.gz: db6642dbdb2588359fc63fff080f34479725cef75d7972f081aa2a1beb680314cf1555ba7ec94956d16cceb3ac3b7724de131e010ba1d06361350f162ed3250c
7
- data.tar.gz: df04ac079954e4b815d922d2a076929e6c4d5f5e9c11d46a54287138e8485fb171b276361a3bae43618c068709fa9f07c0ec34faa88c3afff18ac9d65d511766
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
- hypertables = Timescaledb.connection.query('SELECT * FROM timescaledb_information.hypertables')
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.includes?(hypertable.hypertable_name) }
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.includes?(hypertable.hypertable_name) }
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
- Timescaledb::ContinuousAggregates.find_each do |cagg|
40
- class_name = cagg.view_name.singularize.camelize
41
- model = Class.new(ActiveRecord::Base) do
42
- self.table_name = cagg.view_name
43
- acts_as_hypertable
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 :continuous_aggregates, -> do
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
- where(
46
- "DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
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
- where(
54
- "DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
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
- where(
62
- "DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
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
- where(
70
- "DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
71
- start_time: Date.today.in_time_zone.beginning_of_week.to_date,
72
- end_time: Date.today.in_time_zone.end_of_week.to_date
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 :yesterday, -> { where("DATE(#{time_column}) = ?", Date.yesterday.in_time_zone.to_date) }
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
@@ -1,4 +1,5 @@
1
1
  require 'singleton'
2
+ require 'ostruct'
2
3
 
3
4
  module Timescaledb
4
5
  # Minimal connection setup for Timescaledb directly with the PG.
@@ -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 conn
13
- Connection.instance.use_connection conn
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 ContinuousAggregate < ::Timescaledb::ApplicationRecord
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
@@ -1,3 +1,5 @@
1
+ require 'ostruct'
2
+
1
3
  module Timescaledb
2
4
  class Hypertable < ::Timescaledb::ApplicationRecord
3
5
  self.table_name = "timescaledb_information.hypertables"
@@ -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
- # compression_interval: '7 days'
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
- compression_interval: nil,
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
- options = ["chunk_time_interval => #{chunk_time_interval_clause(chunk_time_interval)}"]
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(table_name),
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 compress_segmentby
66
- execute <<~SQL
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
- if compression_interval
75
- execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compression_interval}')"
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, interval:)
150
- execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{interval}')"
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 chunk_time_interval_clause(chunk_time_interval)
170
- if chunk_time_interval.is_a?(Numeric)
171
- chunk_time_interval
190
+ def parse_interval(interval)
191
+ if interval.is_a?(Numeric)
192
+ interval
172
193
  else
173
- "INTERVAL '#{chunk_time_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}", interval: "#{job.config["drop_after"]}"]
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 setting.orderby_nullsfirst
110
- direction += " NULLS FIRST"
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 !setting.orderby_nullsfirst
115
- direction += " NULLS LAST"
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[:compression_interval] = job.config["compress_after"]
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 ||= time_vector_options[:value_column] || :val
12
+ @value_column ||= hypertable_options[:value_column] || :val
13
13
  end
14
14
 
15
15
  def time_column
16
- respond_to?(:time_column) && super || time_vector_options[:time_column]
16
+ respond_to?(:time_column) && super || hypertable_options[:time_column]
17
17
  end
18
18
 
19
19
  def segment_by_column
20
- time_vector_options[:segment_by]
20
+ hypertable_options[:segment_by] || hypertable_options[:compress_segment_by]
21
21
  end
22
22
 
23
23
  protected
24
24
 
25
- def define_default_scopes
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}, #{value_column}) -> sort() -> delta() -> abs() -> sum() as volatility"
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, value_column).to_sql})
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.#{value_column}, #{threshold}) -> unnest()).*
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
@@ -1,3 +1,2 @@
1
1
  require_relative "toolkit/helpers"
2
- require_relative "acts_as_time_vector"
3
2
  require_relative "toolkit/time_vector"
@@ -1,3 +1,3 @@
1
1
  module Timescaledb
2
- VERSION = '0.3.0'
2
+ VERSION = '0.3.2'
3
3
  end
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.0
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: 2024-09-06 00:00:00.000000000 Z
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
- - jonatasdp@gmail.com
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/jonatas/timescaledb
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.com/jonatas/timescaledb
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.3.7
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