timescaledb 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9281749307a9161088f45af47b26ac9577d693e03958c351eee257467b706428
4
- data.tar.gz: 5a3ef9e484416b56091d62eaff9c3063ca19d441a893e022c10ebe17d4997241
3
+ metadata.gz: 240bc1a55d3955c734d79946868b5e878cacfa354f7b0a843b72f78c26a85c83
4
+ data.tar.gz: 23ccc7e91c0da1ea522e5c8a71cd4522478aa1c73ff0e9ee6f61e42b26f9955c
5
5
  SHA512:
6
- metadata.gz: 37f96329e32067e2d891f1ea8c40f3b2af4e6b4422f1a6a47593013b70092f95207e9d1b6ddc8708f342fede9395e92951b52a04d57bafb1eb4bdeecd29c481b
7
- data.tar.gz: 4fdb61ef0fae73b217c2f14791f29bfb1dfe0b447c3a26c95f2a78d2ecf9ed97de2f14b8c3af0741dab91dac3746430a5281e0f98c4a97279fd62506fb2ceba7
6
+ metadata.gz: 3ae2cda69cc099ad24a47aa4125ee10db96eb8186e261125f1e56ad6f4f2d805888d02d9eee92d48d18eab49d628918fef4e33234edc48ddb4ecb402d4518207
7
+ data.tar.gz: 65ee70a4b17944880979e71d1abee756a5f398f48cb048b575afbe4a0412f91d993ec97723c2b10d472ceafd405d7d013480824702c2a5db11e23f6eacb7f69a
data/bin/tsdb CHANGED
@@ -1,48 +1,49 @@
1
1
  #!/usr/bin/env ruby
2
+
2
3
  require "bundler/setup"
3
4
  require "timescaledb"
4
5
  require "pry"
5
6
 
6
- ActiveRecord::Base.establish_connection(ARGV[0])
7
-
8
- Timescaledb::Hypertable.find_each do |hypertable|
9
- class_name = hypertable.hypertable_name.singularize.camelize
10
- model = Class.new(ActiveRecord::Base) do
11
- self.table_name = hypertable.hypertable_name
12
- acts_as_hypertable time_column: hypertable.main_dimension.column_name
13
- end
14
- Timescaledb.const_set(class_name, model)
15
- end
16
-
17
- Timescaledb::ContinuousAggregates.find_each do |cagg|
18
- class_name = cagg.view_name.singularize.camelize
19
- model = Class.new(ActiveRecord::Base) do
20
- self.table_name = cagg.view_name
21
- acts_as_hypertable
22
- end
23
- Timescaledb.const_set(class_name, model)
24
- end
7
+ Timescaledb.establish_connection(ARGV[0])
25
8
 
26
- def show(obj)
27
- Pry::ColorPrinter.pp(obj)
28
- end
9
+ hypertables = Timescaledb.connection.query('SELECT * FROM timescaledb_information.hypertables')
29
10
 
30
11
  if ARGV.index("--stats")
31
- scope = Timescaledb::Hypertable.all
32
-
33
12
  if (only = ARGV.index("--only"))
34
13
  only_hypertables = ARGV[only+1].split(",")
35
- scope = scope.where({hypertable_name: only_hypertables})
36
- end
37
14
 
38
- if (except = ARGV.index("--except"))
15
+ hypertables.select! { |hypertable| only_hypertables.includes?(hypertable.hypertable_name) }
16
+ elsif (except = ARGV.index("--except"))
39
17
  except_hypertables = ARGV[except+1].split(",")
40
- scope = scope.where.not(hypertable_name: except_hypertables)
18
+
19
+ hypertables.select! { |hypertable| except_hypertables.includes?(hypertable.hypertable_name) }
41
20
  end
42
21
 
43
- show(Timescaledb.stats(scope))
22
+ stats = Timescaledb::Stats.new(hypertables).to_h
23
+
24
+ Pry::ColorPrinter.pp(stats)
44
25
  end
45
26
 
46
27
  if ARGV.index("--console")
28
+ ActiveRecord::Base.establish_connection(ARGV[0])
29
+
30
+ Timescaledb::Hypertable.find_each do |hypertable|
31
+ class_name = hypertable.hypertable_name.singularize.camelize
32
+ model = Class.new(ActiveRecord::Base) do
33
+ self.table_name = hypertable.hypertable_name
34
+ acts_as_hypertable time_column: hypertable.main_dimension.column_name
35
+ end
36
+ Timescaledb.const_set(class_name, model)
37
+ end
38
+
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
44
+ end
45
+ Timescaledb.const_set(class_name, model)
46
+ end
47
+
47
48
  Pry.start(Timescaledb)
48
49
  end
@@ -42,6 +42,9 @@ module Timescaledb
42
42
  # acts_as_hypertable time_column: :timestamp
43
43
  # end
44
44
  #
45
+ # @param [Hash] options The options to initialize your macro with.
46
+ # @option options [Boolean] :skip_association_scopes to avoid `.hypertable`, `.chunks` and other scopes related to metadata.
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...
45
48
  def acts_as_hypertable(options = {})
46
49
  return if acts_as_hypertable?
47
50
 
@@ -53,8 +56,8 @@ module Timescaledb
53
56
  hypertable_options.merge!(options)
54
57
  normalize_hypertable_options
55
58
 
56
- define_association_scopes
57
- define_default_scopes
59
+ define_association_scopes unless options[:skip_association_scopes]
60
+ define_default_scopes unless options[:skip_default_scopes]
58
61
  end
59
62
  end
60
63
  end
@@ -0,0 +1,43 @@
1
+ require 'singleton'
2
+
3
+ module Timescaledb
4
+ class Connection
5
+ include Singleton
6
+
7
+ attr_writer :config
8
+
9
+ # @param [String] query The SQL raw query.
10
+ # @param [Array] params The SQL query parameters.
11
+ # @return [Array<OpenStruct>] The SQL result.
12
+ def query(query, params = [])
13
+ query = params.empty? ? connection.exec(query) : connection.exec_params(query, params)
14
+
15
+ query.map(&OpenStruct.method(:new))
16
+ end
17
+
18
+ # @param [String] query The SQL raw query.
19
+ # @param [Array] params The SQL query parameters.
20
+ # @return [OpenStruct] The first SQL result.
21
+ def query_first(query, params = [])
22
+ query(query, params).first
23
+ end
24
+
25
+ # @param [String] query The SQL raw query.
26
+ # @param [Array] params The SQL query parameters.
27
+ # @return [Integr] The count value from SQL result.
28
+ def query_count(query, params = [])
29
+ query_first(query, params).count.to_i
30
+ end
31
+
32
+ # @param [Boolean] True if the connection singleton was configured, otherwise returns false.
33
+ def connected?
34
+ !@config.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def connection
40
+ @connection ||= PG.connect(@config)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ module Timescaledb
2
+ class ConnectionNotEstablishedError < StandardError; end
3
+
4
+ # @param [String] config The postgres connection string.
5
+ def establish_connection(config)
6
+ Connection.instance.config = config
7
+ end
8
+ module_function :establish_connection
9
+
10
+ def connection
11
+ raise ConnectionNotEstablishedError.new unless Connection.instance.connected?
12
+
13
+ Connection.instance
14
+ end
15
+ module_function :connection
16
+ end
@@ -0,0 +1,21 @@
1
+ module Timescaledb
2
+ class Database
3
+ module ChunkStatements
4
+ # @see https://docs.timescale.com/api/latest/compression/compress_chunk/
5
+ #
6
+ # @param [String] chunk_name The name of the chunk to be compressed
7
+ # @return [String] The compress_chunk SQL statement
8
+ def compress_chunk_sql(chunk_name)
9
+ "SELECT compress_chunk(#{quote(chunk_name)});"
10
+ end
11
+
12
+ # @see https://docs.timescale.com/api/latest/compression/decompress_chunk/
13
+ #
14
+ # @param [String] chunk_name The name of the chunk to be decompressed
15
+ # @return [String] The decompress_chunk SQL statement
16
+ def decompress_chunk_sql(chunk_name)
17
+ "SELECT decompress_chunk(#{quote(chunk_name)});"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module Timescaledb
2
+ class Database
3
+ module HypertableStatements
4
+ # @see https://docs.timescale.com/api/latest/hypertable/hypertable_size/
5
+ #
6
+ # @param [String] hypertable The hypertable to show size of
7
+ # @return [String] The hypertable_size SQL statement
8
+ def hypertable_size_sql(hypertable)
9
+ "SELECT hypertable_size(#{quote(hypertable)});"
10
+ end
11
+
12
+ # @see https://docs.timescale.com/api/latest/hypertable/hypertable_detailed_size/
13
+ #
14
+ # @param [String] hypertable The hypertable to show detailed size of
15
+ # @return [String] The hypertable_detailed_size SQL statementh
16
+ def hypertable_detailed_size_sql(hypertable)
17
+ "SELECT * FROM hypertable_detailed_size(#{quote(hypertable)});"
18
+ end
19
+
20
+ # @see https://docs.timescale.com/api/latest/hypertable/hypertable_index_size/
21
+ #
22
+ # @param [String] index_name The name of the index on a hypertable
23
+ # @return [String] The hypertable_detailed_size SQL statementh
24
+ def hypertable_index_size_sql(index_name)
25
+ "SELECT hypertable_index_size(#{quote(index_name)});"
26
+ end
27
+
28
+ # @see https://docs.timescale.com/api/latest/hypertable/chunks_detailed_size/
29
+ #
30
+ # @param [String] hypertable The name of the hypertable
31
+ # @return [String] The chunks_detailed_size SQL statementh
32
+ def chunks_detailed_size_sql(hypertable)
33
+ "SELECT * FROM chunks_detailed_size(#{quote(hypertable)});"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -15,7 +15,7 @@ module Timescaledb
15
15
 
16
16
  arguments = [quote(relation), quote(time_column_name)]
17
17
  arguments += [quote(partitioning_column), number_partitions] if partitioning_column && number_partitions
18
- arguments += cast_create_hypertable_optional_arguments(options)
18
+ arguments += create_hypertable_options_to_named_notation_sql(options)
19
19
 
20
20
  "SELECT create_hypertable(#{arguments.join(', ')});"
21
21
  end
@@ -56,7 +56,7 @@ module Timescaledb
56
56
  options.transform_keys!(&:to_sym)
57
57
 
58
58
  arguments = [quote(hypertable), interval_to_sql(compress_after)]
59
- arguments += cast_policy_optional_arguments(options)
59
+ arguments += policy_options_to_named_notation_sql(options)
60
60
 
61
61
  "SELECT add_compression_policy(#{arguments.join(', ')});"
62
62
  end
@@ -70,7 +70,7 @@ module Timescaledb
70
70
  options.transform_keys!(&:to_sym)
71
71
 
72
72
  arguments = [quote(hypertable)]
73
- arguments += cast_policy_optional_arguments(options)
73
+ arguments += policy_options_to_named_notation_sql(options)
74
74
 
75
75
  "SELECT remove_compression_policy(#{arguments.join(', ')});"
76
76
  end
@@ -85,7 +85,7 @@ module Timescaledb
85
85
  options.transform_keys!(&:to_sym)
86
86
 
87
87
  arguments = [quote(hypertable), interval_to_sql(drop_after)]
88
- arguments += cast_policy_optional_arguments(options)
88
+ arguments += policy_options_to_named_notation_sql(options)
89
89
 
90
90
  "SELECT add_retention_policy(#{arguments.join(', ')});"
91
91
  end
@@ -99,7 +99,7 @@ module Timescaledb
99
99
  options.transform_keys!(&:to_sym)
100
100
 
101
101
  arguments = [quote(hypertable)]
102
- arguments += cast_policy_optional_arguments(options)
102
+ arguments += policy_options_to_named_notation_sql(options)
103
103
 
104
104
  "SELECT remove_retention_policy(#{arguments.join(', ')});"
105
105
  end
@@ -114,7 +114,7 @@ module Timescaledb
114
114
  options.transform_keys!(&:to_sym)
115
115
 
116
116
  arguments = [quote(hypertable), quote(index_name)]
117
- arguments += cast_policy_optional_arguments(options)
117
+ arguments += policy_options_to_named_notation_sql(options)
118
118
 
119
119
  "SELECT add_reorder_policy(#{arguments.join(', ')});"
120
120
  end
@@ -128,41 +128,119 @@ module Timescaledb
128
128
  options.transform_keys!(&:to_sym)
129
129
 
130
130
  arguments = [quote(hypertable)]
131
- arguments += cast_policy_optional_arguments(options)
131
+ arguments += policy_options_to_named_notation_sql(options)
132
132
 
133
133
  "SELECT remove_reorder_policy(#{arguments.join(', ')});"
134
134
  end
135
135
 
136
+ # @see https://docs.timescale.com/api/latest/continuous-aggregates/create_materialized_view
137
+ #
138
+ # @param [String] continuous_aggregate The name of the continuous aggregate view to be created
139
+ # @param [Hash] options The optional arguments
140
+ # @return [String] The create materialized view SQL statement
141
+ def create_continuous_aggregate_sql(continuous_aggregate, sql, **options)
142
+ options.transform_keys!(&:to_sym)
143
+
144
+ with_data_opts = %w[WITH DATA]
145
+ with_data_opts.insert(1, 'NO') if options.key?(:with_no_data)
146
+
147
+ <<~SQL
148
+ CREATE MATERIALIZED VIEW #{continuous_aggregate}
149
+ WITH (timescaledb.continuous) AS
150
+ #{sql.strip}
151
+ #{with_data_opts.join(' ')};
152
+ SQL
153
+ end
154
+
155
+ # @see https://docs.timescale.com/api/latest/continuous-aggregates/drop_materialized_view
156
+ #
157
+ # @param [String] continuous_aggregate The name of the continuous aggregate view to be dropped
158
+ # @param [Boolean] cascade A boolean to drop objects that depend on the continuous aggregate view
159
+ # @return [String] The drop materialized view SQL statement
160
+ def drop_continuous_aggregate_sql(continuous_aggregate, cascade: false)
161
+ arguments = [continuous_aggregate]
162
+ arguments << 'CASCADE' if cascade
163
+
164
+ "DROP MATERIALIZED VIEW #{arguments.join(' ')};"
165
+ end
166
+
167
+ # @see https://docs.timescale.com/api/latest/continuous-aggregates/add_continuous_aggregate_policy
168
+ #
169
+ # @param [String] continuous_aggregate The name of the continuous aggregate to add the policy for
170
+ # @param [String] start_offset The start of the refresh window as an interval relative to the time when the policy is executed
171
+ # @param [String] end_offset The end of the refresh window as an interval relative to the time when the policy is executed
172
+ # @param [String] schedule_interval The interval between refresh executions in wall-clock time
173
+ # @param [Hash] options The optional arguments
174
+ # @return [String] The add_continuous_aggregate_policy SQL statement
175
+ def add_continuous_aggregate_policy_sql(continuous_aggregate, start_offset: nil, end_offset: nil, schedule_interval:, **options)
176
+ options.transform_keys!(&:to_sym)
177
+
178
+ arguments = [quote(continuous_aggregate)]
179
+ arguments << named_notation_sql(name: :start_offset, value: interval_to_sql(start_offset))
180
+ arguments << named_notation_sql(name: :end_offset, value: interval_to_sql(end_offset))
181
+ arguments << named_notation_sql(name: :schedule_interval, value: interval_to_sql(schedule_interval))
182
+ arguments += continuous_aggregate_policy_options_to_named_notation_sql(options)
183
+
184
+ "SELECT add_continuous_aggregate_policy(#{arguments.join(', ')});"
185
+ end
186
+
187
+ # @see https://docs.timescale.com/api/latest/continuous-aggregates/remove_continuous_aggregate_policy
188
+ #
189
+ # @param [String] continuous_aggregate The name of the continuous aggregate the policy should be removed from
190
+ # @param [Hash] options The optional arguments
191
+ # @return [String] The remove_continuous_aggregate_policy SQL statement
192
+ def remove_continuous_aggregate_policy_sql(continuous_aggregate, **options)
193
+ options.transform_keys!(&:to_sym)
194
+
195
+ arguments = [quote(continuous_aggregate)]
196
+ arguments += policy_options_to_named_notation_sql(options)
197
+
198
+ "SELECT remove_continuous_aggregate_policy(#{arguments.join(', ')});"
199
+ end
200
+
136
201
  private
137
202
 
138
203
  # @param [Array<Hash<Symbol, Object>>] options The policy optional arguments.
139
204
  # @return [Array<String>]
140
- def cast_policy_optional_arguments(options)
205
+ def policy_options_to_named_notation_sql(options)
141
206
  options.map do |option, value|
142
207
  case option
143
- when :if_not_exists, :if_exists then "#{option} => #{boolean_to_sql(value)}"
144
- when :initial_start, :timezone then "#{option} => #{quote(value)}"
208
+ when :if_not_exists, :if_exists then named_notation_sql(name: option, value: boolean_to_sql(value))
209
+ when :initial_start, :timezone then named_notation_sql(name: option, value: quote(value))
145
210
  end
146
211
  end.compact
147
212
  end
148
213
 
149
214
  # @param [Array<Hash<Symbol, Object>>] options The create_hypertable optional arguments.
150
215
  # @return [Array<String>]
151
- def cast_create_hypertable_optional_arguments(options)
216
+ def create_hypertable_options_to_named_notation_sql(options)
152
217
  options.map do |option, value|
153
218
  case option
154
219
  when :chunk_time_interval
155
- "#{option} => #{interval_to_sql(value)}"
220
+ named_notation_sql(name: option, value: interval_to_sql(value))
156
221
  when :if_not_exists, :create_default_indexes, :migrate_data, :distributed
157
- "#{option} => #{boolean_to_sql(value)}"
222
+ named_notation_sql(name: option, value: boolean_to_sql(value))
158
223
  when :partitioning_func, :associated_schema_name,
159
224
  :associated_table_prefix, :time_partitioning_func
160
- "#{option} => #{quote(value)}"
161
- when :replication_factor
162
- "#{option} => #{value}"
225
+ named_notation_sql(name: option, value: quote(value))
226
+ end
227
+ end.compact
228
+ end
229
+
230
+ # @param [Array<Hash<Symbol, Object>>] options The continuous aggregate policy arguments.
231
+ # @return [Array<String>]
232
+ def continuous_aggregate_policy_options_to_named_notation_sql(options)
233
+ options.map do |option, value|
234
+ case option
235
+ when :if_not_exists then named_notation_sql(name: option, value: boolean_to_sql(value))
236
+ when :initial_start, :timezone then named_notation_sql(name: option, value: quote(value))
163
237
  end
164
238
  end.compact
165
239
  end
240
+
241
+ def named_notation_sql(name:, value:)
242
+ "#{name} => #{value}"
243
+ end
166
244
  end
167
245
  end
168
246
  end
@@ -1,9 +1,12 @@
1
1
  module Timescaledb
2
2
  class Database
3
3
  module Types
4
- # @param [String] interval The interval value
4
+ # @param [String, Integer] interval The interval value
5
5
  # @return [String]
6
6
  def interval_to_sql(interval)
7
+ return 'NULL' if interval.nil?
8
+ return interval if interval.kind_of?(Integer)
9
+
7
10
  "INTERVAL #{quote(interval)}"
8
11
  end
9
12
 
@@ -1,9 +1,13 @@
1
+ require_relative 'database/chunk_statements'
2
+ require_relative 'database/hypertable_statements'
1
3
  require_relative 'database/quoting'
2
4
  require_relative 'database/schema_statements'
3
5
  require_relative 'database/types'
4
6
 
5
7
  module Timescaledb
6
8
  class Database
9
+ extend ChunkStatements
10
+ extend HypertableStatements
7
11
  extend Quoting
8
12
  extend SchemaStatements
9
13
  extend Types
@@ -29,6 +29,11 @@ module Timescaledb
29
29
  create_hypertable(table_name, **options[:hypertable]) if options.key?(:hypertable)
30
30
  end
31
31
 
32
+ # Override the valid_table_definition_options to include hypertable.
33
+ def valid_table_definition_options # :nodoc:
34
+ super + [:hypertable]
35
+ end
36
+
32
37
  # Setup hypertable from options
33
38
  # @see create_table with the hypertable options.
34
39
  def create_hypertable(table_name,
@@ -41,6 +46,7 @@ module Timescaledb
41
46
  number_partitions: nil,
42
47
  **hypertable_options)
43
48
 
49
+ original_logger = ActiveRecord::Base.logger
44
50
  ActiveRecord::Base.logger = Logger.new(STDOUT)
45
51
 
46
52
  options = ["chunk_time_interval => INTERVAL '#{chunk_time_interval}'"]
@@ -58,16 +64,18 @@ module Timescaledb
58
64
 
59
65
  if compress_segmentby
60
66
  execute <<~SQL
61
- ALTER TABLE #{table_name} SET (
62
- timescaledb.compress,
63
- timescaledb.compress_orderby = '#{compress_orderby}',
64
- timescaledb.compress_segmentby = '#{compress_segmentby}'
65
- )
67
+ ALTER TABLE #{table_name} SET (
68
+ timescaledb.compress,
69
+ timescaledb.compress_orderby = '#{compress_orderby}',
70
+ timescaledb.compress_segmentby = '#{compress_segmentby}'
71
+ )
66
72
  SQL
67
73
  end
68
74
  if compression_interval
69
75
  execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compression_interval}')"
70
76
  end
77
+ ensure
78
+ ActiveRecord::Base.logger = original_logger if original_logger
71
79
  end
72
80
 
73
81
  # Create a new continuous aggregate
@@ -79,7 +87,11 @@ module Timescaledb
79
87
  # @option refresh_policies [String] start_offset: INTERVAL or integer
80
88
  # @option refresh_policies [String] end_offset: INTERVAL or integer
81
89
  # @option refresh_policies [String] schedule_interval: INTERVAL
90
+ # @option materialized_only [Boolean] Override the WITH clause 'timescaledb.materialized_only'
91
+ # @option create_group_indexes [Boolean] Override the WITH clause 'timescaledb.create_group_indexes'
92
+ # @option finalized [Boolean] Override the WITH clause 'timescaledb.finalized'
82
93
  #
94
+ # @see https://docs.timescale.com/api/latest/continuous-aggregates/create_materialized_view/
83
95
  # @see https://docs.timescale.com/api/latest/continuous-aggregates/add_continuous_aggregate_policy/
84
96
  #
85
97
  # @example
@@ -94,15 +106,19 @@ module Timescaledb
94
106
  def create_continuous_aggregate(table_name, query, **options)
95
107
  execute <<~SQL
96
108
  CREATE MATERIALIZED VIEW #{table_name}
97
- WITH (timescaledb.continuous) AS
109
+ WITH (
110
+ timescaledb.continuous
111
+ #{build_with_clause_option_string(:materialized_only, options)}
112
+ #{build_with_clause_option_string(:create_group_indexes, options)}
113
+ #{build_with_clause_option_string(:finalized, options)}
114
+ ) AS
98
115
  #{query.respond_to?(:to_sql) ? query.to_sql : query}
99
- WITH #{"NO" unless options[:with_data]} DATA;
116
+ WITH #{'NO' unless options[:with_data]} DATA;
100
117
  SQL
101
118
 
102
119
  create_continuous_aggregate_policy(table_name, **(options[:refresh_policies] || {}))
103
120
  end
104
121
 
105
-
106
122
  # Drop a new continuous aggregate.
107
123
  #
108
124
  # It basically DROP MATERIALIZED VIEW for a given @name.
@@ -137,6 +153,18 @@ module Timescaledb
137
153
  def remove_retention_policy(table_name)
138
154
  execute "SELECT remove_retention_policy('#{table_name}')"
139
155
  end
156
+
157
+ private
158
+
159
+ # Build a string for the WITH clause of the CREATE MATERIALIZED VIEW statement.
160
+ # When the option is omitted, this method returns an empty string, which allows this gem to use the
161
+ # defaults provided by TimescaleDB.
162
+ def build_with_clause_option_string(option_key, options)
163
+ return '' unless options.key?(option_key)
164
+
165
+ value = options[option_key] ? 'true' : 'false'
166
+ ",timescaledb.#{option_key}=#{value}"
167
+ end
140
168
  end
141
169
  end
142
170
 
@@ -2,10 +2,14 @@ require 'active_record/connection_adapters/postgresql_adapter'
2
2
  require 'active_support/core_ext/string/indent'
3
3
 
4
4
  module Timescaledb
5
+ # Schema dumper overrides default schema dumper to include:
6
+ # * hypertables
7
+ # * retention policies
8
+ # * continuous aggregates
9
+ # * compression settings
5
10
  module SchemaDumper
6
11
  def tables(stream)
7
12
  super # This will call #table for each table in the database
8
-
9
13
  return unless Timescaledb::Hypertable.table_exists?
10
14
 
11
15
  timescale_hypertables(stream)
@@ -13,6 +17,29 @@ module Timescaledb
13
17
  timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them
14
18
  end
15
19
 
20
+ # Ignores Timescale related schemas when dumping the schema
21
+ IGNORE_SCHEMAS = %w[
22
+ _timescaledb_cache
23
+ _timescaledb_config
24
+ _timescaledb_catalog
25
+ _timescaledb_debug
26
+ _timescaledb_functions
27
+ _timescaledb_internal
28
+ timescaledb_experimental
29
+ timescaledb_information
30
+ toolkit_experimental
31
+ ]
32
+
33
+ def schemas(stream)
34
+ schema_names = @connection.schema_names - ["public", *IGNORE_SCHEMAS]
35
+ if schema_names.any?
36
+ schema_names.sort.each do |name|
37
+ stream.puts " create_schema #{name.inspect}"
38
+ end
39
+ stream.puts
40
+ end
41
+ end
42
+
16
43
  def timescale_hypertables(stream)
17
44
  sorted_hypertables.each do |hypertable|
18
45
  timescale_hypertable(hypertable, stream)
@@ -112,19 +139,20 @@ module Timescaledb
112
139
  def timescale_continuous_aggregates(stream)
113
140
  return unless Timescaledb::ContinuousAggregates.table_exists?
114
141
 
115
- Timescaledb::ContinuousAggregates.all.each do |aggregate|
116
- opts = if (refresh_policy = aggregate.jobs.refresh_continuous_aggregate.first)
117
- interval = timescale_interval(refresh_policy.schedule_interval)
118
- end_offset = timescale_interval(refresh_policy.config["end_offset"])
119
- start_offset = timescale_interval(refresh_policy.config["start_offset"])
120
- %Q[, refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"}]
121
- else
122
- ""
123
- end
142
+ Timescaledb::ContinuousAggregates.all.find_each do |aggregate|
143
+ refresh_policies_opts = if (refresh_policy = aggregate.jobs.refresh_continuous_aggregate.first)
144
+ interval = timescale_interval(refresh_policy.schedule_interval)
145
+ end_offset = timescale_interval(refresh_policy.config["end_offset"])
146
+ start_offset = timescale_interval(refresh_policy.config["start_offset"])
147
+ %(refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"})
148
+ else
149
+ ""
150
+ end
124
151
 
152
+ with_clause_opts = "materialized_only: #{aggregate[:materialized_only]}, finalized: #{aggregate[:finalized]}"
125
153
  stream.puts <<~AGG.indent(2)
126
- create_continuous_aggregate("#{aggregate.view_name}", <<-SQL#{opts})
127
- #{aggregate.view_definition.strip.gsub(/;$/, "")}
154
+ create_continuous_aggregate("#{aggregate.view_name}", <<-SQL, #{refresh_policies_opts}, #{with_clause_opts})
155
+ #{aggregate.view_definition.strip.gsub(/;$/, '')}
128
156
  SQL
129
157
  AGG
130
158
  stream.puts
@@ -0,0 +1,41 @@
1
+ module Timescaledb
2
+ class Stats
3
+ class Chunks
4
+ # @param [Array<String>] hypertables The list of hypertable names.
5
+ # @param [Timescaledb:Connection] connection The PG connection.
6
+ def initialize(hypertables = [], connection = Timescaledb.connection)
7
+ @connection = connection
8
+ @hypertables = hypertables
9
+ end
10
+
11
+ delegate :query_count, to: :@connection
12
+
13
+ # @return [Hash] The chunks stats
14
+ def to_h
15
+ { total: total, compressed: compressed, uncompressed: uncompressed }
16
+ end
17
+
18
+ private
19
+
20
+ def total
21
+ query_count(base_query, [@hypertables])
22
+ end
23
+
24
+ def compressed
25
+ compressed_query = [base_query, 'is_compressed'].join(' AND ')
26
+
27
+ query_count(compressed_query, [@hypertables])
28
+ end
29
+
30
+ def uncompressed
31
+ uncompressed_query = [base_query, 'NOT is_compressed'].join(' AND ')
32
+
33
+ query_count(uncompressed_query, [@hypertables])
34
+ end
35
+
36
+ def base_query
37
+ "SELECT COUNT(1) FROM timescaledb_information.chunks WHERE hypertable_name IN ($1)"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+
2
+ module Timescaledb
3
+ class Stats
4
+ class ContinuousAggregates
5
+ # @param [Timescaledb:Connection] connection The PG connection.
6
+ def initialize(connection = Timescaledb.connection)
7
+ @connection = connection
8
+ end
9
+
10
+ delegate :query_count, to: :@connection
11
+
12
+ # @return [Hash] The continuous_aggregates stats
13
+ def to_h
14
+ { total: total }
15
+ end
16
+
17
+ private
18
+
19
+ def total
20
+ query_count('SELECT COUNT(1) FROM timescaledb_information.continuous_aggregates')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,102 @@
1
+ require_relative './chunks'
2
+
3
+ module Timescaledb
4
+ class Stats
5
+ class Hypertables
6
+ # @param [Timescaledb:Connection] connection The PG connection.
7
+ # @param [Array<String>] hypertables The list of hypertable names.
8
+ def initialize(hypertables = [], connection = Timescaledb.connection)
9
+ @connection = connection
10
+ @hypertables = hypertables.map(&method('hypertable_name_with_schema'))
11
+ end
12
+
13
+ delegate :query, :query_first, :query_count, to: :@connection
14
+
15
+ # @return [Hash] The hypertables stats
16
+ def to_h
17
+ {
18
+ count: @hypertables.count,
19
+ uncompressed_count: uncompressed_count,
20
+ approximate_row_count: approximate_row_count,
21
+ chunks: Timescaledb::Stats::Chunks.new(@hypertables).to_h,
22
+ size: size
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def uncompressed_count
29
+ @hypertables.count do |hypertable|
30
+ query("SELECT * from hypertable_compression_stats('#{hypertable}')").empty?
31
+ end
32
+ end
33
+
34
+ def approximate_row_count
35
+ @hypertables.each_with_object(Hash.new) do |hypertable, summary|
36
+ row_count = query_first("SELECT * FROM approximate_row_count('#{hypertable}')").approximate_row_count.to_i
37
+
38
+ summary[hypertable] = row_count
39
+ end
40
+ end
41
+
42
+ def size
43
+ sum = -> (method_name) { (@hypertables.map(&method(method_name)).inject(:+) || 0) }
44
+
45
+ {
46
+ uncompressed: humanize_bytes(sum[:before_total_bytes]),
47
+ compressed: humanize_bytes(sum[:after_total_bytes])
48
+ }
49
+ end
50
+
51
+ def before_total_bytes(hypertable)
52
+ (compression_stats[hypertable]&.before_compression_total_bytes || detailed_size[hypertable]).to_i
53
+ end
54
+
55
+ def after_total_bytes(hypertable)
56
+ (compression_stats[hypertable]&.after_compression_total_bytes || 0).to_i
57
+ end
58
+
59
+ def compression_stats
60
+ @compression_stats ||=
61
+ @hypertables.each_with_object(Hash.new) do |hypertable, stats|
62
+ stats[hypertable] = query_first(compression_stats_query, [hypertable])
63
+ stats
64
+ end
65
+ end
66
+
67
+ def compression_stats_query
68
+ 'SELECT * FROM hypertable_compression_stats($1)'
69
+ end
70
+
71
+ def detailed_size
72
+ @detailed_size ||=
73
+ @hypertables.each_with_object(Hash.new) do |hypertable, size|
74
+ size[hypertable] = query_first(detailed_size_query, [hypertable]).total_bytes
75
+ size
76
+ end
77
+ end
78
+
79
+ def detailed_size_query
80
+ 'SELECT * FROM hypertable_detailed_size($1)'
81
+ end
82
+
83
+ def hypertable_name_with_schema(hypertable)
84
+ [hypertable.hypertable_schema, hypertable.hypertable_name].compact.join('.')
85
+ end
86
+
87
+ def humanize_bytes(bytes)
88
+ units = %w(B KiB MiB GiB TiB PiB EiB)
89
+
90
+ return '0 B' if bytes == 0
91
+
92
+ exp = (Math.log2(bytes) / 10).floor
93
+ max_exp = units.size - 1
94
+ exp = max_exp if exp > max_exp
95
+
96
+ value = (bytes.to_f / (1 << (exp * 10))).round(1)
97
+
98
+ "#{value} #{units[exp]}"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,29 @@
1
+
2
+ module Timescaledb
3
+ class Stats
4
+ class JobStats
5
+ # @param [Timescaledb:Connection] connection The PG connection.
6
+ def initialize(connection = Timescaledb.connection)
7
+ @connection = connection
8
+ end
9
+
10
+ delegate :query_first, to: :@connection
11
+
12
+ # @return [Hash] The job_stats stats
13
+ def to_h
14
+ query_first(job_stats_query).to_h.transform_values(&:to_i)
15
+ end
16
+
17
+ private
18
+
19
+ def job_stats_query
20
+ <<-SQL
21
+ SELECT SUM(total_successes)::INT AS success,
22
+ SUM(total_runs)::INT AS runs,
23
+ SUM(total_failures)::INT AS failures
24
+ FROM timescaledb_information.job_stats
25
+ SQL
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ require_relative './stats/continuous_aggregates'
2
+ require_relative './stats/hypertables'
3
+ require_relative './stats/job_stats'
4
+
5
+ module Timescaledb
6
+ class Stats
7
+ # @param [Array<OpenStruct>] hypertables The list of hypertables.
8
+ # @param [Timescaledb:Connection] connection The PG connection.
9
+ def initialize(hypertables = [], connection = Timescaledb.connection)
10
+ @hypertables = hypertables
11
+ @connection = connection
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ hypertables: Hypertables.new(@hypertables).to_h,
17
+ continuous_aggregates: ContinuousAggregates.new.to_h,
18
+ jobs_stats: JobStats.new.to_h
19
+ }
20
+ end
21
+ end
22
+ end
@@ -67,7 +67,7 @@ module Timescaledb
67
67
 
68
68
  select( %|time_bucket('#{timeframe}', "#{time}")|,
69
69
  *segment_by,
70
- "toolkit_experimental.candlestick_agg(#{time}, #{value}, #{volume}) as candlestick")
70
+ "candlestick_agg(#{time}, #{value}, #{volume}) as candlestick")
71
71
  .order(1)
72
72
  .group(*(segment_by ? [1,2] : 1))
73
73
  end
@@ -82,49 +82,16 @@ module Timescaledb
82
82
  unscoped
83
83
  .from("(#{raw.to_sql}) AS candlestick")
84
84
  .select("time_bucket",*segment_by,
85
- "toolkit_experimental.open(candlestick),
86
- toolkit_experimental.high(candlestick),
87
- toolkit_experimental.low(candlestick),
88
- toolkit_experimental.close(candlestick),
89
- toolkit_experimental.open_time(candlestick),
90
- toolkit_experimental.high_time(candlestick),
91
- toolkit_experimental.low_time(candlestick),
92
- toolkit_experimental.close_time(candlestick),
93
- toolkit_experimental.volume(candlestick),
94
- toolkit_experimental.vwap(candlestick)")
95
- end
96
-
97
- scope :_ohlc, -> (timeframe: '1h',
98
- segment_by: segment_by_column,
99
- time: time_column,
100
- value: value_column) do
101
-
102
- select( *segment_by,
103
- %|time_bucket('#{timeframe}', #{time}) as "#{time}"|,
104
- "toolkit_experimental.ohlc(#{time}, #{value})")
105
- .order(1)
106
- .group(*(segment_by ? [1,2] : 1))
107
- end
108
-
109
-
110
-
111
- scope :ohlc, -> (timeframe: '1h',
112
- segment_by: segment_by_column,
113
- time: time_column,
114
- value: value_column) do
115
-
116
- raw = _ohlc(timeframe: timeframe, segment_by: segment_by, time: time, value: value)
117
- unscoped
118
- .from("(#{raw.to_sql}) AS ohlc")
119
- .select(*segment_by, time,
120
- "toolkit_experimental.open(ohlc),
121
- toolkit_experimental.high(ohlc),
122
- toolkit_experimental.low(ohlc),
123
- toolkit_experimental.close(ohlc),
124
- toolkit_experimental.open_time(ohlc),
125
- toolkit_experimental.high_time(ohlc),
126
- toolkit_experimental.low_time(ohlc),
127
- toolkit_experimental.close_time(ohlc)")
85
+ "open(candlestick),
86
+ high(candlestick),
87
+ low(candlestick),
88
+ close(candlestick),
89
+ open_time(candlestick),
90
+ high_time(candlestick),
91
+ low_time(candlestick),
92
+ close_time(candlestick),
93
+ volume(candlestick),
94
+ vwap(candlestick)")
128
95
  end
129
96
  end
130
97
  end
@@ -1,3 +1,3 @@
1
1
  module Timescaledb
2
- VERSION = '0.2.7'
2
+ VERSION = '0.2.8'
3
3
  end
data/lib/timescaledb.rb CHANGED
@@ -3,15 +3,18 @@ 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/connection'
6
7
  require_relative 'timescaledb/toolkit'
7
8
  require_relative 'timescaledb/chunk'
8
9
  require_relative 'timescaledb/compression_settings'
10
+ require_relative 'timescaledb/connection_handling'
9
11
  require_relative 'timescaledb/continuous_aggregates'
10
12
  require_relative 'timescaledb/dimensions'
11
13
  require_relative 'timescaledb/hypertable'
12
14
  require_relative 'timescaledb/job'
13
15
  require_relative 'timescaledb/job_stats'
14
16
  require_relative 'timescaledb/schema_dumper'
17
+ require_relative 'timescaledb/stats'
15
18
  require_relative 'timescaledb/stats_report'
16
19
  require_relative 'timescaledb/migration_helpers'
17
20
  require_relative 'timescaledb/version'
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.2.7
4
+ version: 0.2.8
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: 2023-02-06 00:00:00.000000000 Z
11
+ date: 2024-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -153,8 +153,12 @@ files:
153
153
  - lib/timescaledb/application_record.rb
154
154
  - lib/timescaledb/chunk.rb
155
155
  - lib/timescaledb/compression_settings.rb
156
+ - lib/timescaledb/connection.rb
157
+ - lib/timescaledb/connection_handling.rb
156
158
  - lib/timescaledb/continuous_aggregates.rb
157
159
  - lib/timescaledb/database.rb
160
+ - lib/timescaledb/database/chunk_statements.rb
161
+ - lib/timescaledb/database/hypertable_statements.rb
158
162
  - lib/timescaledb/database/quoting.rb
159
163
  - lib/timescaledb/database/schema_statements.rb
160
164
  - lib/timescaledb/database/types.rb
@@ -166,6 +170,11 @@ files:
166
170
  - lib/timescaledb/scenic/adapter.rb
167
171
  - lib/timescaledb/scenic/extension.rb
168
172
  - lib/timescaledb/schema_dumper.rb
173
+ - lib/timescaledb/stats.rb
174
+ - lib/timescaledb/stats/chunks.rb
175
+ - lib/timescaledb/stats/continuous_aggregates.rb
176
+ - lib/timescaledb/stats/hypertables.rb
177
+ - lib/timescaledb/stats/job_stats.rb
169
178
  - lib/timescaledb/stats_report.rb
170
179
  - lib/timescaledb/toolkit.rb
171
180
  - lib/timescaledb/toolkit/helpers.rb