timescaledb 0.3.1 → 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: 0f5295cfa57caa17bb740b28dd70f54fb0c239f9d011233ae04cfffa2bbfadbf
4
- data.tar.gz: ec1b742c312d289124522e490fe47269d7aa6b9774ce0d82fe99bed206ce227c
3
+ metadata.gz: 33706a3c899ae608bb30b9ef62ca32875045960745b526bc0700fddec09ddc8b
4
+ data.tar.gz: 88a7cb9f1f2f2dbb68f131547fa7c9575979f6e87b0bb29f576ee95d9bb9696c
5
5
  SHA512:
6
- metadata.gz: f1a0937798a8f97bccaf8a39d61268fc040f525f04b92632e1a0fc7c8d892d73b39d7f4fd7252fe430ac3f53523b5a01570bc89257aff8cef35fa68c161204e4
7
- data.tar.gz: 460df42d48ceac9d552c47408655a9c9330b3efe9bc40d8963021e7c2f4e297d4210c54fc6fe43678f56dc156ac28836f7b06d17d11977fa82a134cb356c9127
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
@@ -41,41 +41,41 @@ module Timescaledb
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
@@ -23,7 +23,6 @@ module Timescaledb
23
23
  module ActsAsHypertable
24
24
  DEFAULT_OPTIONS = {
25
25
  time_column: :created_at,
26
- # Add any default time vector options here if needed
27
26
  }.freeze
28
27
 
29
28
  def acts_as_hypertable?
@@ -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
@@ -18,6 +18,8 @@ module Timescaledb
18
18
  /state_agg\((\w+)\)\s+as\s+(\w+)/ => 'rollup(\2) as \2',
19
19
  /percentile_agg\((\w+),\s*(\w+)\)\s+as\s+(\w+)/ => 'rollup(\3) as \3',
20
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'
21
23
  }
22
24
 
23
25
  scope :rollup, ->(interval) do
@@ -147,12 +149,21 @@ module Timescaledb
147
149
  if previous_timeframe
148
150
  prev_klass = base_model.const_get("#{aggregate_name}_per_#{previous_timeframe}".classify)
149
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
150
153
  self.base_query = "SELECT #{tb} as #{time_column}, #{select_clause} FROM \"#{prev_klass.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
151
154
  else
152
155
  scope = base_model.public_send(config[:scope_name])
153
156
  config[:select] = scope.select_values.select{|e|!e.downcase.start_with?("time_bucket")}.join(', ')
154
157
  config[:group_by] = scope.group_values
155
- self.base_query = "SELECT #{tb} as #{time_column}, #{config[:select]} FROM \"#{scope.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
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(', ')}"
156
167
  end
157
168
 
158
169
  def self.refresh!(start_time = nil, end_time = nil)
@@ -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
@@ -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
 
@@ -119,7 +119,7 @@ module Timescaledb
119
119
  end
120
120
 
121
121
  hypertable.jobs.compression.each do |job|
122
- compression_settings[:compression_interval] = job.config["compress_after"]
122
+ compression_settings[:compress_after] = job.config["compress_after"]
123
123
  end
124
124
 
125
125
  # Pack the compression setting arrays into a comma-separated string instead.
@@ -1,3 +1,3 @@
1
1
  module Timescaledb
2
- VERSION = '0.3.1'
2
+ VERSION = '0.3.2'
3
3
  end
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.1
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-12-20 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
@@ -170,6 +170,7 @@ files:
170
170
  - lib/timescaledb/connection_handling.rb
171
171
  - lib/timescaledb/continuous_aggregates.rb
172
172
  - lib/timescaledb/continuous_aggregates_helper.rb
173
+ - lib/timescaledb/counter_cache.rb
173
174
  - lib/timescaledb/database.rb
174
175
  - lib/timescaledb/database/chunk_statements.rb
175
176
  - lib/timescaledb/database/hypertable_statements.rb