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 +4 -4
- data/lib/timescaledb/configuration.rb +32 -0
- data/lib/timescaledb/connection_handling.rb +4 -4
- data/lib/timescaledb/continuous_aggregates.rb +1 -2
- data/lib/timescaledb/continuous_aggregates_helper.rb +33 -20
- data/lib/timescaledb/migration_helpers.rb +9 -3
- data/lib/timescaledb/scenic/extension.rb +0 -2
- data/lib/timescaledb/schema_dumper.rb +4 -1
- data/lib/timescaledb/version.rb +1 -1
- data/lib/timescaledb.rb +43 -10
- metadata +4 -7
- data/lib/timescaledb/counter_cache.rb +0 -88
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a82c28a5761d43b30d50afa56a1b41b4e27e81c0280c1a79659cef76430fed8
|
|
4
|
+
data.tar.gz: c7b912b564a84a5904194f4a05e4767229538285298513d7f1940d93208dbfe0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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]
|
|
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
|
-
#{
|
|
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;
|
|
@@ -162,7 +162,10 @@ module Timescaledb
|
|
|
162
162
|
""
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
|
|
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(/;$/, '')}
|
data/lib/timescaledb/version.rb
CHANGED
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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
|