timescaledb 0.3.1 → 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: 0f5295cfa57caa17bb740b28dd70f54fb0c239f9d011233ae04cfffa2bbfadbf
4
- data.tar.gz: ec1b742c312d289124522e490fe47269d7aa6b9774ce0d82fe99bed206ce227c
3
+ metadata.gz: 2a82c28a5761d43b30d50afa56a1b41b4e27e81c0280c1a79659cef76430fed8
4
+ data.tar.gz: c7b912b564a84a5904194f4a05e4767229538285298513d7f1940d93208dbfe0
5
5
  SHA512:
6
- metadata.gz: f1a0937798a8f97bccaf8a39d61268fc040f525f04b92632e1a0fc7c8d892d73b39d7f4fd7252fe430ac3f53523b5a01570bc89257aff8cef35fa68c161204e4
7
- data.tar.gz: 460df42d48ceac9d552c47408655a9c9330b3efe9bc40d8963021e7c2f4e297d4210c54fc6fe43678f56dc156ac28836f7b06d17d11977fa82a134cb356c9127
6
+ metadata.gz: 82f1e4582467e4ac74cb9a14323667a40be51b12edd4181fe50be5c73779b6256833c2885863fa10b23c1808cf82b06328f477e2cf0ca4e554db444ddb4f20ab
7
+ data.tar.gz: 51bcf8fa51b7463fcd99542d4917217e51507388eb7bb660a15406d0a0a690b7f612efd3d0514e1a6189156983a031f461fc0a58976e6145f43cfe55368f82f7
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?
@@ -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,15 +2,32 @@ 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
+ # 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
@@ -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"
@@ -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
@@ -134,25 +136,47 @@ module Timescaledb
134
136
  class_name = "#{aggregate_name}_per_#{timeframe}".classify
135
137
  const_set(class_name, Class.new(base_model) do
136
138
  class << self
137
- attr_accessor :config, :timeframe, :base_query, :base_model
139
+ attr_accessor :config, :timeframe, :base_query, :base_model, :previous_timeframe, :interval, :aggregate_name, :prev_klass
138
140
  end
139
141
 
140
142
  self.table_name = _table_name
141
143
  self.config = config
142
144
  self.timeframe = timeframe
145
+ self.previous_timeframe = previous_timeframe
146
+ self.aggregate_name = aggregate_name
143
147
 
144
- interval = "'1 #{timeframe.to_s}'"
148
+ self.interval = "'1 #{timeframe.to_s}'"
145
149
  self.base_model = base_model
146
- tb = "time_bucket(#{interval}, #{time_column})"
147
- if previous_timeframe
148
- prev_klass = base_model.const_get("#{aggregate_name}_per_#{previous_timeframe}".classify)
149
- select_clause = base_model.apply_rollup_rules("#{config[:select]}")
150
- self.base_query = "SELECT #{tb} as #{time_column}, #{select_clause} FROM \"#{prev_klass.table_name}\" GROUP BY #{[tb, *config[:group_by]].join(', ')}"
151
- else
152
- scope = base_model.public_send(config[:scope_name])
153
- config[:select] = scope.select_values.select{|e|!e.downcase.start_with?("time_bucket")}.join(', ')
154
- 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(', ')}"
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
179
+ end
156
180
  end
157
181
 
158
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)
@@ -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.
@@ -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.1'
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.1
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: 2024-12-20 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,6 +165,7 @@ 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
@@ -202,7 +202,6 @@ metadata:
202
202
  allowed_push_host: https://rubygems.org
203
203
  homepage_uri: https://timescale.github.io/timescaledb-ruby/
204
204
  source_code_uri: https://github.com/timescale/timescaledb-ruby
205
- post_install_message:
206
205
  rdoc_options: []
207
206
  require_paths:
208
207
  - lib
@@ -217,8 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
216
  - !ruby/object:Gem::Version
218
217
  version: '0'
219
218
  requirements: []
220
- rubygems_version: 3.5.23
221
- signing_key:
219
+ rubygems_version: 3.6.3
222
220
  specification_version: 4
223
221
  summary: TimescaleDB helpers for Ruby ecosystem.
224
222
  test_files: []