timescaledb 0.3.2 → 0.3.3

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: 33706a3c899ae608bb30b9ef62ca32875045960745b526bc0700fddec09ddc8b
4
- data.tar.gz: 88a7cb9f1f2f2dbb68f131547fa7c9575979f6e87b0bb29f576ee95d9bb9696c
3
+ metadata.gz: 2a82c28a5761d43b30d50afa56a1b41b4e27e81c0280c1a79659cef76430fed8
4
+ data.tar.gz: c7b912b564a84a5904194f4a05e4767229538285298513d7f1940d93208dbfe0
5
5
  SHA512:
6
- metadata.gz: a700d4b61d11af54856fb02e19db676a720ba5ff3c2367e7eb9ba153bc14ae1c3c67d9130bfc47e1ecac786a007274df9c3595a6b10fe797fb06b13ae30ac957
7
- data.tar.gz: 05c45d270e86c24d40984cea92e8870869441580d35426b5028ca17b346a47ad7468482337adec69eaf0d6daecdd4292d7108868677a7388f35cf2529d7e631a
6
+ metadata.gz: 82f1e4582467e4ac74cb9a14323667a40be51b12edd4181fe50be5c73779b6256833c2885863fa10b23c1808cf82b06328f477e2cf0ca4e554db444ddb4f20ab
7
+ data.tar.gz: 51bcf8fa51b7463fcd99542d4917217e51507388eb7bb660a15406d0a0a690b7f612efd3d0514e1a6189156983a031f461fc0a58976e6145f43cfe55368f82f7
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Timescaledb
4
+ class Configuration
5
+ attr_accessor :scenic_integration
6
+
7
+ DEFAULTS = {
8
+ scenic_integration: :enabled
9
+ }.freeze
10
+
11
+ def initialize
12
+ @scenic_integration = DEFAULTS[:scenic_integration]
13
+ end
14
+
15
+ def enable_scenic_integration?
16
+ case @scenic_integration
17
+ when :enabled then scenic_detected?
18
+ else false # :disabled, :false, nil, etc.
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def scenic_detected?
25
+ # Try to require scenic to see if it's available
26
+ require 'scenic'
27
+ true
28
+ rescue LoadError
29
+ false
30
+ end
31
+ end
32
+ end
@@ -2,12 +2,12 @@ module Timescaledb
2
2
  class ConnectionNotEstablishedError < StandardError; end
3
3
 
4
4
  module_function
5
-
5
+
6
6
  # @param [String] config with the postgres connection string.
7
7
  def establish_connection(config)
8
8
  # Establish connection for Timescaledb
9
9
  Connection.instance.config = config
10
-
10
+
11
11
  # Also establish connection for ActiveRecord if it's defined
12
12
  if defined?(ActiveRecord::Base)
13
13
  ActiveRecord::Base.establish_connection(config)
@@ -17,12 +17,12 @@ module Timescaledb
17
17
  # @param [PG::Connection] to use it directly from a raw connection
18
18
  def use_connection(conn)
19
19
  Connection.instance.use_connection(conn)
20
-
20
+
21
21
  # Also set ActiveRecord connection if it's defined
22
22
  if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
23
23
  ar_conn = ActiveRecord::Base.connection
24
24
  current_conn = ar_conn.raw_connection
25
-
25
+
26
26
  # Only set if it's different to avoid redundant assignment
27
27
  if current_conn != conn
28
28
  ar_conn.instance_variable_set(:@raw_connection, conn)
@@ -3,8 +3,7 @@ module Timescaledb
3
3
  self.table_name = "timescaledb_information.continuous_aggregates"
4
4
  self.primary_key = 'materialization_hypertable_name'
5
5
 
6
- has_many :jobs, foreign_key: "hypertable_name",
7
- class_name: "Timescaledb::Job"
6
+ has_many :jobs, foreign_key: 'hypertable_name', primary_key: 'view_name', class_name: 'Timescaledb::Job'
8
7
 
9
8
  has_many :chunks, foreign_key: "hypertable_name",
10
9
  class_name: "Timescaledb::Chunk"
@@ -136,34 +136,47 @@ module Timescaledb
136
136
  class_name = "#{aggregate_name}_per_#{timeframe}".classify
137
137
  const_set(class_name, Class.new(base_model) do
138
138
  class << self
139
- attr_accessor :config, :timeframe, :base_query, :base_model
139
+ attr_accessor :config, :timeframe, :base_query, :base_model, :previous_timeframe, :interval, :aggregate_name, :prev_klass
140
140
  end
141
141
 
142
142
  self.table_name = _table_name
143
143
  self.config = config
144
144
  self.timeframe = timeframe
145
+ self.previous_timeframe = previous_timeframe
146
+ self.aggregate_name = aggregate_name
145
147
 
146
- interval = "'1 #{timeframe.to_s}'"
148
+ self.interval = "'1 #{timeframe.to_s}'"
147
149
  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
150
+
151
+ def self.prev_klass
152
+ base_model.const_get("#{aggregate_name}_per_#{previous_timeframe}".classify)
153
+ end
154
+
155
+ def self.base_query
156
+ @base_query ||= begin
157
+ tb = "time_bucket(#{interval}, #{time_column})"
158
+ if previous_timeframe
159
+ select_clause = base_model.apply_rollup_rules("#{config[:select]}")
160
+ # Note there's no where clause here, because we're using the previous timeframe's data
161
+ "SELECT #{tb} as #{time_column}, #{select_clause} FROM \"#{prev_klass.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
162
+ else
163
+ scope = base_model.public_send(config[:scope_name])
164
+ config[:select] = scope.select_values.select{|e|!e.downcase.start_with?("time_bucket")}.join(', ')
165
+ config[:group_by] = scope.group_values
166
+ config[:where] =
167
+ if scope.where_values_hash.present?
168
+ scope.where_values_hash.map { |key, value| "#{key} = '#{value}'" }.join(' AND ')
169
+ elsif scope.where_clause.ast.present? && scope.where_clause.ast.to_sql.present?
170
+ scope.where_clause.ast.to_sql
171
+ end
172
+
173
+ sql = "SELECT #{tb} as #{time_column}, #{config[:select]}"
174
+ sql += " FROM \"#{base_model.table_name}\""
175
+ sql += " WHERE #{config[:where]}" if config[:where]
176
+ sql += " GROUP BY #{[tb, *config[:group_by]].join(', ')}"
177
+ sql
178
+ end
162
179
  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
180
  end
168
181
 
169
182
  def self.refresh!(start_time = nil, end_time = nil)
@@ -48,7 +48,7 @@ module Timescaledb
48
48
  **hypertable_options)
49
49
 
50
50
  original_logger = ActiveRecord::Base.logger
51
- ActiveRecord::Base.logger = Logger.new(STDOUT)
51
+ ActiveRecord::Base.logger = Logger.new(STDOUT) unless original_logger.nil?
52
52
 
53
53
  dimension = "by_range(#{quote(time_column)}, #{parse_interval(chunk_time_interval)})"
54
54
 
@@ -84,7 +84,9 @@ module Timescaledb
84
84
  # @option refresh_policies [String] schedule_interval: INTERVAL
85
85
  # @option materialized_only [Boolean] Override the WITH clause 'timescaledb.materialized_only'
86
86
  # @option create_group_indexes [Boolean] Override the WITH clause 'timescaledb.create_group_indexes'
87
- # @option finalized [Boolean] Override the WITH clause 'timescaledb.finalized'
87
+ # @option finalized [Boolean] Set to false for legacy (non-finalized) format. Note: the finalized
88
+ # parameter was removed in TimescaleDB 2.14+ where all aggregates are finalized by default.
89
+ # Only use finalized: false on TimescaleDB 2.7-2.13 for legacy compatibility.
88
90
  #
89
91
  # @see https://docs.timescale.com/api/latest/continuous-aggregates/create_materialized_view/
90
92
  # @see https://docs.timescale.com/api/latest/continuous-aggregates/add_continuous_aggregate_policy/
@@ -99,13 +101,17 @@ module Timescaledb
99
101
  # SQL
100
102
  #
101
103
  def create_continuous_aggregate(table_name, query, **options)
104
+ # Only include finalized when explicitly false (legacy format).
105
+ # The parameter was removed in TimescaleDB 2.14+ where all aggregates are finalized by default.
106
+ finalized_clause = options[:finalized] == false ? ",timescaledb.finalized=false" : ""
107
+
102
108
  execute <<~SQL
103
109
  CREATE MATERIALIZED VIEW #{table_name}
104
110
  WITH (
105
111
  timescaledb.continuous
106
112
  #{build_with_clause_option_string(:materialized_only, options)}
107
113
  #{build_with_clause_option_string(:create_group_indexes, options)}
108
- #{build_with_clause_option_string(:finalized, options)}
114
+ #{finalized_clause}
109
115
  ) AS
110
116
  #{query.respond_to?(:to_sql) ? query.to_sql : query}
111
117
  WITH #{'NO' unless options[:with_data]} DATA;
@@ -67,5 +67,3 @@ module Timescaledb
67
67
  end
68
68
 
69
69
 
70
- Scenic::Adapters::Postgres.include(Timescaledb::Scenic::Extension)
71
- ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Timescaledb::Scenic::MigrationHelpers)
@@ -162,7 +162,10 @@ module Timescaledb
162
162
  ""
163
163
  end
164
164
 
165
- with_clause_opts = "materialized_only: #{aggregate[:materialized_only]}, finalized: #{aggregate[:finalized]}"
165
+ # Only output finalized when false (legacy format) - the parameter was
166
+ # removed in TimescaleDB 2.14+ where all aggregates are finalized by default
167
+ with_clause_opts = "materialized_only: #{aggregate[:materialized_only]}"
168
+ with_clause_opts += ", finalized: false" if aggregate[:finalized] == false
166
169
  stream.puts <<~AGG.indent(2)
167
170
  create_continuous_aggregate("#{aggregate.view_name}", <<-SQL, #{refresh_policies_opts}#{with_clause_opts})
168
171
  #{aggregate.view_definition.strip.gsub(/;$/, '')}
@@ -1,3 +1,3 @@
1
1
  module Timescaledb
2
- VERSION = '0.3.2'
2
+ VERSION = '0.3.3'
3
3
  end
data/lib/timescaledb.rb CHANGED
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
5
+ require_relative 'timescaledb/configuration'
3
6
  require_relative 'timescaledb/application_record'
4
7
  require_relative 'timescaledb/acts_as_hypertable'
5
8
  require_relative 'timescaledb/acts_as_hypertable/core'
@@ -22,6 +25,39 @@ require_relative 'timescaledb/extension'
22
25
  require_relative 'timescaledb/version'
23
26
 
24
27
  module Timescaledb
28
+ class << self
29
+ def configure
30
+ yield(configuration) if block_given?
31
+ end
32
+
33
+ def configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def setup_scenic_integration
38
+ return unless configuration.enable_scenic_integration?
39
+ return if @scenic_integration_setup
40
+
41
+ begin
42
+ require 'scenic'
43
+ require_relative 'timescaledb/scenic/adapter'
44
+ require_relative 'timescaledb/scenic/extension'
45
+
46
+ ::Scenic.configure do |config|
47
+ config.database = Timescaledb::Scenic::Adapter.new
48
+ end
49
+
50
+ ::Scenic::Adapters::Postgres.include(Timescaledb::Scenic::Extension)
51
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Timescaledb::Scenic::MigrationHelpers)
52
+
53
+ @scenic_integration_setup = true
54
+ rescue LoadError
55
+ # This is expected when the scenic gem is not being used
56
+ @scenic_integration_setup = false
57
+ end
58
+ end
59
+ end
60
+
25
61
  module_function
26
62
 
27
63
  def connection
@@ -65,15 +101,12 @@ module Timescaledb
65
101
  end
66
102
  end
67
103
 
68
- begin
69
- require 'scenic'
70
- require_relative 'timescaledb/scenic/adapter'
71
- require_relative 'timescaledb/scenic/extension'
72
-
73
- Scenic.configure do |config|
74
- config.database = Timescaledb::Scenic::Adapter.new
104
+ # Delay scenic integration setup to respect user configuration when using Rails
105
+ if defined?(ActiveSupport) && ActiveSupport.respond_to?(:on_load)
106
+ ActiveSupport.on_load(:active_record) do
107
+ Timescaledb.setup_scenic_integration
75
108
  end
76
-
77
- rescue LoadError
78
- # This is expected when the scenic gem is not being used
109
+ else
110
+ # For non-Rails usage, setup immediately
111
+ Timescaledb.setup_scenic_integration
79
112
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timescaledb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jônatas Davi Paganini
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-02-18 00:00:00.000000000 Z
10
+ date: 2026-02-11 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: pg
@@ -166,11 +165,11 @@ files:
166
165
  - lib/timescaledb/application_record.rb
167
166
  - lib/timescaledb/chunk.rb
168
167
  - lib/timescaledb/compression_settings.rb
168
+ - lib/timescaledb/configuration.rb
169
169
  - lib/timescaledb/connection.rb
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
174
173
  - lib/timescaledb/database.rb
175
174
  - lib/timescaledb/database/chunk_statements.rb
176
175
  - lib/timescaledb/database/hypertable_statements.rb
@@ -203,7 +202,6 @@ metadata:
203
202
  allowed_push_host: https://rubygems.org
204
203
  homepage_uri: https://timescale.github.io/timescaledb-ruby/
205
204
  source_code_uri: https://github.com/timescale/timescaledb-ruby
206
- post_install_message:
207
205
  rdoc_options: []
208
206
  require_paths:
209
207
  - lib
@@ -218,8 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
216
  - !ruby/object:Gem::Version
219
217
  version: '0'
220
218
  requirements: []
221
- rubygems_version: 3.5.23
222
- signing_key:
219
+ rubygems_version: 3.6.3
223
220
  specification_version: 4
224
221
  summary: TimescaleDB helpers for Ruby ecosystem.
225
222
  test_files: []
@@ -1,88 +0,0 @@
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