timescaledb 0.2.7 → 0.2.9

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: 89553264709636b53e3d56174f3e244561c10f2c4801b922ba25adb8bf32aca7
4
+ data.tar.gz: f254f76133d13d6854f8f7dee865a0f3bb4adc57617c200ee276d1408541475a
5
5
  SHA512:
6
- metadata.gz: 37f96329e32067e2d891f1ea8c40f3b2af4e6b4422f1a6a47593013b70092f95207e9d1b6ddc8708f342fede9395e92951b52a04d57bafb1eb4bdeecd29c481b
7
- data.tar.gz: 4fdb61ef0fae73b217c2f14791f29bfb1dfe0b447c3a26c95f2a78d2ecf9ed97de2f14b8c3af0741dab91dac3746430a5281e0f98c4a97279fd62506fb2ceba7
6
+ metadata.gz: b30bbba9b0da08b3cad42fbc80e73c7aed2e37763d01cfd2b93d569e4bd65768a507fc6e25a757c7b83bd4f9ad3e7e18d98090b7a2f2e8bebcb5d16fb3a63d7c
7
+ data.tar.gz: bb0ba7379e86631d71256cf66cc506eb42511145f17e343e749bc10e28b7c394a482e9d4a3380ff9ee2af511be51d44529556566fe08421aa92b924755d492c8
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,54 @@
1
+ require 'singleton'
2
+
3
+ module Timescaledb
4
+ # Minimal connection setup for Timescaledb directly with the PG.
5
+ # The concept is use a singleton component that can query
6
+ # independently of the ActiveRecord::Base connections.
7
+ # This is useful for the extension and hypertable metadata.
8
+ # It can also #use_connection from active record if needed.
9
+ class Connection
10
+ include Singleton
11
+
12
+ attr_writer :config
13
+
14
+ # @param [String] query The SQL raw query.
15
+ # @param [Array] params The SQL query parameters.
16
+ # @return [Array<OpenStruct>] The SQL result.
17
+ def query(query, params = [])
18
+ query = params.empty? ? connection.exec(query) : connection.exec_params(query, params)
19
+
20
+ query.map(&OpenStruct.method(:new))
21
+ end
22
+
23
+ # @param [String] query The SQL raw query.
24
+ # @param [Array] params The SQL query parameters.
25
+ # @return [OpenStruct] The first SQL result.
26
+ def query_first(query, params = [])
27
+ query(query, params).first
28
+ end
29
+
30
+ # @param [String] query The SQL raw query.
31
+ # @param [Array] params The SQL query parameters.
32
+ # @return [Integr] The count value from SQL result.
33
+ def query_count(query, params = [])
34
+ query_first(query, params).count.to_i
35
+ end
36
+
37
+ # @param [Boolean] True if the connection singleton was configured, otherwise returns false.
38
+ def connected?
39
+ !@config.nil?
40
+ end
41
+
42
+ # Override the connection with a raw PG connection.
43
+ # @param [PG::Connection] connection The raw PG connection.
44
+ def use_connection connection
45
+ @connection = connection
46
+ end
47
+
48
+ private
49
+
50
+ def connection
51
+ @connection ||= PG.connect(@config)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ module Timescaledb
2
+ class ConnectionNotEstablishedError < StandardError; end
3
+
4
+ module_function
5
+
6
+ # @param [String] config with the postgres connection string.
7
+ def establish_connection(config)
8
+ Connection.instance.config = config
9
+ end
10
+
11
+ # @param [PG::Connection] to use it directly from a raw connection
12
+ def use_connection conn
13
+ Connection.instance.use_connection conn
14
+ end
15
+
16
+ def connection
17
+ raise ConnectionNotEstablishedError.new unless Connection.instance.connected?
18
+
19
+ Connection.instance
20
+ end
21
+ 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
@@ -0,0 +1,23 @@
1
+ module Timescaledb
2
+
3
+ # Provides metadata around the extension in the database
4
+ module Extension
5
+ module_function
6
+ # @return String version of the timescaledb extension
7
+ def version
8
+ @version ||= Timescaledb.connection.query_first(<<~SQL)&.version
9
+ SELECT extversion as version
10
+ FROM pg_extension
11
+ WHERE extname = 'timescaledb'
12
+ SQL
13
+ end
14
+
15
+ def installed?
16
+ version.present?
17
+ end
18
+
19
+ def update!
20
+ Timescaledb.connection.execute('ALTER EXTENSION timescaledb UPDATE')
21
+ end
22
+ end
23
+ end
@@ -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
 
@@ -25,7 +25,6 @@ module Timescaledb
25
25
 
26
26
  # @override Scenic::Adapters::Postgres#create_view
27
27
  # to add the `with: ` keyword that can be used for such option.
28
- #
29
28
  def create_view(name, version: nil, with: nil, sql_definition: nil, materialized: false, no_data: false)
30
29
  if version.present? && sql_definition.present?
31
30
  raise(
@@ -69,4 +68,4 @@ end
69
68
 
70
69
 
71
70
  Scenic::Adapters::Postgres.include(Timescaledb::Scenic::Extension)
72
- ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Timescaledb::Scenic::MigrationHelpers)
71
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Timescaledb::Scenic::MigrationHelpers)
@@ -2,15 +2,57 @@ 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
10
+ # It also ignores Timescale related schemas when dumping the schema.
11
+ # It also ignores dumping options as extension is not installed or no hypertables are available.
5
12
  module SchemaDumper
6
13
  def tables(stream)
7
14
  super # This will call #table for each table in the database
8
15
 
9
- return unless Timescaledb::Hypertable.table_exists?
16
+ if exports_timescaledb_metadata?
17
+ timescale_hypertables(stream)
18
+ timescale_retention_policies(stream)
19
+ timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them
20
+ end
21
+ end
22
+
23
+ # Ignore dumps in case DB is not eligible for TimescaleDB metadata.
24
+ # @return [Boolean] true if the extension is installed and hypertables are available, otherwise false.
25
+ private def exports_timescaledb_metadata?
26
+ # Note it's safe to use the raw connection here because we're only reading from the database
27
+ # and not modifying it. We're also on the same connection pool as ActiveRecord::Base.
28
+ # The dump process also runs standalone, so we don't need to worry about the connection being
29
+ # used elsewhere.
30
+ Timescaledb.use_connection @connection.raw_connection
10
31
 
11
- timescale_hypertables(stream)
12
- timescale_retention_policies(stream)
13
- timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them
32
+ Timescaledb.extension.installed? && Timescaledb.hypertables.any?
33
+ end
34
+
35
+ # Ignores Timescale related schemas when dumping the schema
36
+ IGNORE_SCHEMAS = %w[
37
+ _timescaledb_cache
38
+ _timescaledb_config
39
+ _timescaledb_catalog
40
+ _timescaledb_debug
41
+ _timescaledb_functions
42
+ _timescaledb_internal
43
+ timescaledb_experimental
44
+ timescaledb_information
45
+ toolkit_experimental
46
+ ]
47
+
48
+ def schemas(stream)
49
+ schema_names = @connection.schema_names - ["public", *IGNORE_SCHEMAS]
50
+ if schema_names.any?
51
+ schema_names.sort.each do |name|
52
+ stream.puts " create_schema #{name.inspect}"
53
+ end
54
+ stream.puts
55
+ end
14
56
  end
15
57
 
16
58
  def timescale_hypertables(stream)
@@ -112,19 +154,20 @@ module Timescaledb
112
154
  def timescale_continuous_aggregates(stream)
113
155
  return unless Timescaledb::ContinuousAggregates.table_exists?
114
156
 
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
157
+ Timescaledb::ContinuousAggregates.all.find_each do |aggregate|
158
+ refresh_policies_opts = if (refresh_policy = aggregate.jobs.refresh_continuous_aggregate.first)
159
+ interval = timescale_interval(refresh_policy.schedule_interval)
160
+ end_offset = timescale_interval(refresh_policy.config["end_offset"])
161
+ start_offset = timescale_interval(refresh_policy.config["start_offset"])
162
+ %(refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"})
163
+ else
164
+ ""
165
+ end
124
166
 
167
+ with_clause_opts = "materialized_only: #{aggregate[:materialized_only]}, finalized: #{aggregate[:finalized]}"
125
168
  stream.puts <<~AGG.indent(2)
126
- create_continuous_aggregate("#{aggregate.view_name}", <<-SQL#{opts})
127
- #{aggregate.view_definition.strip.gsub(/;$/, "")}
169
+ create_continuous_aggregate("#{aggregate.view_name}", <<-SQL, #{refresh_policies_opts}, #{with_clause_opts})
170
+ #{aggregate.view_definition.strip.gsub(/;$/, '')}
128
171
  SQL
129
172
  AGG
130
173
  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.9'
3
3
  end
data/lib/timescaledb.rb CHANGED
@@ -3,22 +3,34 @@ 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'
20
+ require_relative 'timescaledb/extension'
17
21
  require_relative 'timescaledb/version'
18
22
 
19
23
  module Timescaledb
20
24
  module_function
21
25
 
26
+ def connection
27
+ Connection.instance
28
+ end
29
+
30
+ def extension
31
+ Extension
32
+ end
33
+
22
34
  def chunks
23
35
  Chunk.all
24
36
  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.2.7
4
+ version: 0.2.9
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-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -153,12 +153,17 @@ 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
161
165
  - lib/timescaledb/dimensions.rb
166
+ - lib/timescaledb/extension.rb
162
167
  - lib/timescaledb/hypertable.rb
163
168
  - lib/timescaledb/job.rb
164
169
  - lib/timescaledb/job_stats.rb
@@ -166,6 +171,11 @@ files:
166
171
  - lib/timescaledb/scenic/adapter.rb
167
172
  - lib/timescaledb/scenic/extension.rb
168
173
  - lib/timescaledb/schema_dumper.rb
174
+ - lib/timescaledb/stats.rb
175
+ - lib/timescaledb/stats/chunks.rb
176
+ - lib/timescaledb/stats/continuous_aggregates.rb
177
+ - lib/timescaledb/stats/hypertables.rb
178
+ - lib/timescaledb/stats/job_stats.rb
169
179
  - lib/timescaledb/stats_report.rb
170
180
  - lib/timescaledb/toolkit.rb
171
181
  - lib/timescaledb/toolkit/helpers.rb