timescaledb 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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