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 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