timescaledb 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +41 -9
  4. data/bin/console +2 -2
  5. data/bin/tsdb +3 -3
  6. data/docs/command_line.md +178 -0
  7. data/docs/img/lttb_example.png +0 -0
  8. data/docs/img/lttb_sql_vs_ruby.gif +0 -0
  9. data/docs/img/lttb_zoom.gif +0 -0
  10. data/docs/index.md +61 -0
  11. data/docs/migrations.md +69 -0
  12. data/docs/models.md +78 -0
  13. data/docs/toolkit.md +394 -0
  14. data/docs/toolkit_lttb_tutorial.md +557 -0
  15. data/docs/toolkit_lttb_zoom.md +357 -0
  16. data/docs/videos.md +16 -0
  17. data/examples/all_in_one/all_in_one.rb +40 -6
  18. data/examples/all_in_one/benchmark_comparison.rb +108 -0
  19. data/examples/all_in_one/caggs.rb +93 -0
  20. data/examples/all_in_one/query_data.rb +78 -0
  21. data/examples/ranking/config/initializers/timescale.rb +1 -2
  22. data/examples/toolkit-demo/compare_volatility.rb +64 -0
  23. data/examples/toolkit-demo/lttb/README.md +15 -0
  24. data/examples/toolkit-demo/lttb/lttb.rb +92 -0
  25. data/examples/toolkit-demo/lttb/lttb_sinatra.rb +139 -0
  26. data/examples/toolkit-demo/lttb/lttb_test.rb +21 -0
  27. data/examples/toolkit-demo/lttb/views/index.erb +27 -0
  28. data/examples/toolkit-demo/lttb-zoom/README.md +13 -0
  29. data/examples/toolkit-demo/lttb-zoom/lttb_zoomable.rb +90 -0
  30. data/examples/toolkit-demo/lttb-zoom/views/index.erb +33 -0
  31. data/lib/timescaledb/acts_as_time_vector.rb +18 -0
  32. data/lib/timescaledb/dimensions.rb +1 -0
  33. data/lib/timescaledb/hypertable.rb +5 -1
  34. data/lib/timescaledb/migration_helpers.rb +30 -11
  35. data/lib/timescaledb/schema_dumper.rb +4 -1
  36. data/lib/timescaledb/stats_report.rb +1 -1
  37. data/lib/timescaledb/toolkit/helpers.rb +20 -0
  38. data/lib/timescaledb/toolkit/time_vector.rb +66 -0
  39. data/lib/timescaledb/toolkit.rb +3 -0
  40. data/lib/timescaledb/version.rb +1 -1
  41. data/lib/timescaledb.rb +1 -0
  42. data/mkdocs.yml +33 -0
  43. metadata +30 -4
  44. data/examples/all_in_one/Gemfile +0 -11
  45. data/examples/all_in_one/Gemfile.lock +0 -51
@@ -0,0 +1,78 @@
1
+ require 'bundler/inline' #require only what you need
2
+
3
+ gemfile(true) do
4
+ gem 'timescaledb', path: '../..'
5
+ gem 'pry'
6
+ gem 'faker'
7
+ end
8
+
9
+ require 'pp'
10
+ # ruby all_in_one.rb postgres://user:pass@host:port/db_name
11
+ ActiveRecord::Base.establish_connection( ARGV.last)
12
+
13
+ # Simple example
14
+ class Event < ActiveRecord::Base
15
+ self.primary_key = nil
16
+ acts_as_hypertable
17
+
18
+ # If you want to override the automatic assingment of the `created_at ` time series column
19
+ def self.timestamp_attributes_for_create_in_model
20
+ []
21
+ end
22
+ def self.timestamp_attributes_for_update_in_model
23
+ []
24
+ end
25
+ end
26
+
27
+ # Setup Hypertable as in a migration
28
+ ActiveRecord::Base.connection.instance_exec do
29
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
30
+
31
+ drop_table(Event.table_name) if Event.table_exists?
32
+
33
+ hypertable_options = {
34
+ time_column: 'created_at',
35
+ chunk_time_interval: '7 day',
36
+ compress_segmentby: 'identifier',
37
+ compression_interval: '7 days'
38
+ }
39
+
40
+ create_table(:events, id: false, hypertable: hypertable_options) do |t|
41
+ t.string :identifier, null: false
42
+ t.jsonb :payload
43
+ t.timestamps
44
+ end
45
+ end
46
+
47
+ def generate_fake_data(total: 100_000)
48
+ time = 1.month.ago
49
+ total.times.flat_map do
50
+ identifier = %w[sign_up login click scroll logout view]
51
+ time = time + rand(60).seconds
52
+ {
53
+ created_at: time,
54
+ updated_at: time,
55
+ identifier: identifier.sample,
56
+ payload: {
57
+ "name" => Faker::Name.name,
58
+ "email" => Faker::Internet.email
59
+ }
60
+ }
61
+ end
62
+ end
63
+
64
+
65
+ batch = generate_fake_data total: 10_000
66
+ ActiveRecord::Base.logger = nil
67
+ Event.insert_all(batch, returning: false)
68
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
69
+
70
+ pp Event.previous_month.count
71
+ pp Event.previous_week.count
72
+ pp Event.previous_month.group('identifier').count
73
+ pp Event.previous_week.group('identifier').count
74
+
75
+ pp Event
76
+ .previous_month
77
+ .select("time_bucket('1 day', created_at) as time, identifier, count(*)")
78
+ .group("1,2").map(&:attributes)
@@ -1,3 +1,2 @@
1
- require 'timescale'
1
+ require 'timescaledb'
2
2
  require 'scenic'
3
-
@@ -0,0 +1,64 @@
1
+ require 'bundler/setup'
2
+ require 'timescaledb'
3
+
4
+
5
+ # Compare volatility processing in Ruby vs SQL.
6
+ class Measurement < ActiveRecord::Base
7
+ acts_as_hypertable time_column: "ts"
8
+ acts_as_time_vector segment_by: "device_id", value_column: "val"
9
+
10
+ scope :volatility_sql, -> do
11
+ select("device_id, timevector(#{time_column}, #{value_column}) -> sort() -> delta() -> abs() -> sum() as volatility")
12
+ .group("device_id")
13
+ end
14
+
15
+ scope :volatility_ruby, -> {
16
+ volatility = Hash.new(0)
17
+ previous = Hash.new
18
+ find_all do |measurement|
19
+ device_id = measurement.device_id
20
+ if previous[device_id]
21
+ delta = (measurement.val - previous[device_id]).abs
22
+ volatility[device_id] += delta
23
+ end
24
+ previous[device_id] = measurement.val
25
+ end
26
+ volatility
27
+ }
28
+ end
29
+
30
+ ActiveRecord::Base.establish_connection ENV["PG_URI"]
31
+ ActiveRecord::Base.connection.add_toolkit_to_search_path!
32
+
33
+
34
+ ActiveRecord::Base.connection.instance_exec do
35
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
36
+
37
+ unless Measurement.table_exists?
38
+ hypertable_options = {
39
+ time_column: 'ts',
40
+ chunk_time_interval: '1 day',
41
+ }
42
+ create_table :measurements, hypertable: hypertable_options, id: false do |t|
43
+ t.integer :device_id
44
+ t.decimal :val
45
+ t.timestamp :ts
46
+ end
47
+ end
48
+ end
49
+
50
+ if Measurement.count.zero?
51
+ ActiveRecord::Base.connection.execute(<<~SQL)
52
+ INSERT INTO measurements (ts, device_id, val)
53
+ SELECT ts, device_id, random()*80
54
+ FROM generate_series(TIMESTAMP '2022-01-01 00:00:00',
55
+ TIMESTAMP '2022-02-01 00:00:00',
56
+ INTERVAL '5 minutes') AS g1(ts),
57
+ generate_series(0, 5) AS g2(device_id);
58
+ SQL
59
+ end
60
+
61
+ Benchmark.bm do |x|
62
+ x.report("ruby") { Measurement.volatility_ruby }
63
+ x.report("sql") { Measurement.volatility_sql.map(&:attributes) }
64
+ end
@@ -0,0 +1,15 @@
1
+ # LTTB examples
2
+
3
+ This folder contains a few ideas to explore and learn more about the lttb algorithm.
4
+
5
+ There is a [./lttb.rb](./lttb.rb) file that is the Ruby implementation of lttb
6
+ and also contains the related [./lttb_test.rb](./lttb_test.rb) file that
7
+ verifies the same example from the Timescale Toolkit [implementation](https://github.com/timescale/timescaledb-toolkit/blob/6ee2ea1e8ff64bab10b90bdf4cd4b0f7ed763934/extension/src/lttb.rs#L512-L530).
8
+
9
+ The [./lttb_sinatra.rb](./lttb_sinatra.rb) is a small webserver that compares
10
+ the SQL vs Ruby implementation. It also uses the [./views](./views) folder which
11
+ contains the view rendering part.
12
+
13
+ You can learn more by reading the [LTTB tutorial](https://jonatas.github.io/timescaledb/toolkit_lttb_tutorial/).
14
+
15
+
@@ -0,0 +1,92 @@
1
+ module Triangle
2
+ module_function
3
+ def area(a, b, c)
4
+ (ax, ay),(bx,by),(cx,cy) = a,b,c
5
+ (
6
+ (ax - cx).to_f * (by - ay) -
7
+ (ax - bx).to_f * (cy - ay)
8
+ ).abs * 0.5
9
+ end
10
+ end
11
+ class Lttb
12
+ class << self
13
+ def avg(array)
14
+ array.sum.to_f / array.size
15
+ end
16
+
17
+ def downsample(data, threshold)
18
+ new(data, threshold).downsample
19
+ end
20
+ end
21
+
22
+ attr_reader :data, :threshold
23
+ def initialize(data, threshold)
24
+ fail 'data is not an array' unless data.is_a? Array
25
+ fail "threshold should be >= 2. It's #{threshold}." if threshold < 2
26
+ @data = data
27
+ @threshold = threshold
28
+ end
29
+
30
+ def downsample
31
+ case @data.first.first
32
+ when Time, DateTime, Date
33
+ transformed_dates = true
34
+ dates_to_numbers()
35
+ end
36
+ process.tap do |downsampled|
37
+ numbers_to_dates(downsampled) if transformed_dates
38
+ end
39
+ end
40
+ private
41
+
42
+ def process
43
+ return data if threshold >= data.size || threshold == 0
44
+
45
+ sampled = [data.first, data.last] # Keep first and last point. append in the middle.
46
+ point_index = 0
47
+
48
+ (threshold - 2).times do |i|
49
+ step = [((i+1.0) * bucket_size).to_i, data.size].min
50
+ next_point = (i * bucket_size).to_i + 1
51
+
52
+ break if next_point > data.size - 2
53
+
54
+ points = data[step, slice]
55
+ avg_x = Lttb.avg(points.map(&:first)).to_i
56
+ avg_y = Lttb.avg(points.map(&:last))
57
+
58
+ max_area = -1.0
59
+
60
+ (next_point...(step + 1)).each do |idx|
61
+ area = Triangle.area(data[point_index], data[idx], [avg_x, avg_y])
62
+
63
+ if area > max_area
64
+ max_area = area
65
+ next_point = idx
66
+ end
67
+ end
68
+
69
+ sampled.insert(-2, data[next_point])
70
+ point_index = next_point
71
+ end
72
+
73
+ sampled
74
+ end
75
+
76
+ def bucket_size
77
+ @bucket_size ||= ((data.size - 2.0) / (threshold - 2.0))
78
+ end
79
+
80
+ def slice
81
+ @slice ||= bucket_size.to_i
82
+ end
83
+
84
+ def dates_to_numbers
85
+ @start_date = data[0][0].dup
86
+ data.each{|d| d[0] = d[0] - @start_date }
87
+ end
88
+
89
+ def numbers_to_dates(downsampled)
90
+ downsampled.each{|d| d[0] = @start_date + d[0]}
91
+ end
92
+ end
@@ -0,0 +1,139 @@
1
+ # ruby lttb.rb postgres://user:pass@host:port/db_name
2
+ require 'bundler/inline' #require only what you need
3
+
4
+ gemfile(true) do
5
+ gem 'timescaledb', path: '../../..'
6
+ gem 'pry'
7
+ gem 'sinatra', require: false
8
+ gem 'sinatra-reloader', require: false
9
+ gem 'sinatra-cross_origin', require: false
10
+ gem 'chartkick'
11
+ end
12
+
13
+ require 'timescaledb/toolkit'
14
+ require 'sinatra'
15
+ require 'sinatra/json'
16
+ require 'sinatra/cross_origin'
17
+ require 'chartkick'
18
+ require_relative 'lttb'
19
+
20
+ PG_URI = ARGV.last
21
+
22
+ VALID_SIZES = %i[small med big]
23
+ def download_weather_dataset size: :small
24
+ unless VALID_SIZES.include?(size)
25
+ fail "Invalid size: #{size}. Valids are #{VALID_SIZES}"
26
+ end
27
+ url = "https://timescaledata.blob.core.windows.net/datasets/weather_#{size}.tar.gz"
28
+ puts "fetching #{size} weather dataset..."
29
+ system "wget \"#{url}\""
30
+ puts "done!"
31
+ end
32
+
33
+ def setup size: :small
34
+ file = "weather_#{size}.tar.gz"
35
+ download_weather_dataset(size: size) unless File.exists? file
36
+ puts "extracting #{file}"
37
+ system "tar -xvzf #{file} "
38
+ puts "creating data structures"
39
+ system "psql #{PG_URI} < weather.sql"
40
+ system %|psql #{PG_URI} -c "\\COPY locations FROM weather_#{size}_locations.csv CSV"|
41
+ system %|psql #{PG_URI} -c "\\COPY conditions FROM weather_#{size}_conditions.csv CSV"|
42
+ end
43
+
44
+ ActiveRecord::Base.establish_connection(PG_URI)
45
+ class Location < ActiveRecord::Base
46
+ self.primary_key = "device_id"
47
+
48
+ has_many :conditions, foreign_key: "device_id"
49
+ end
50
+
51
+ class Condition < ActiveRecord::Base
52
+ acts_as_hypertable time_column: "time"
53
+ acts_as_time_vector value_column: "temperature", segment_by: "device_id"
54
+
55
+ belongs_to :location, foreign_key: "device_id"
56
+ end
57
+
58
+ # Setup Hypertable as in a migration
59
+ ActiveRecord::Base.connection.instance_exec do
60
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
61
+
62
+ unless Condition.table_exists?
63
+ setup size: :big
64
+ end
65
+ end
66
+
67
+ require 'sinatra/reloader'
68
+ require 'sinatra/contrib'
69
+ register Sinatra::Reloader
70
+ register Sinatra::Contrib
71
+ include Chartkick::Helper
72
+
73
+ set :bind, '0.0.0.0'
74
+ set :port, 9999
75
+
76
+ def conditions
77
+ device_ids = (1..9).map{|i|"weather-pro-00000#{i}"}
78
+ Condition
79
+ .where(device_id: device_ids.first)
80
+ .order('time')
81
+ end
82
+
83
+ def threshold
84
+ params[:threshold]&.to_i || 50
85
+ end
86
+
87
+ configure do
88
+ enable :cross_origin
89
+ end
90
+ before do
91
+ response.headers['Access-Control-Allow-Origin'] = '*'
92
+ end
93
+
94
+ # routes...
95
+ options "*" do
96
+ response.headers["Allow"] = "GET, PUT, POST, DELETE, OPTIONS"
97
+ response.headers["Access-Control-Allow-Headers"] = "Authorization,
98
+ Content-Type, Accept, X-User-Email, X-Auth-Token"
99
+ response.headers["Access-Control-Allow-Origin"] = "*"
100
+ 200
101
+ end
102
+
103
+ get '/' do
104
+ headers 'Access-Control-Allow-Origin' => 'https://cdn.jsdelivr.net/'
105
+
106
+ erb :index
107
+ end
108
+
109
+ get '/lttb_ruby' do
110
+ payload = conditions
111
+ .pluck(:device_id, :time, :temperature)
112
+ .group_by(&:first)
113
+ .map do |device_id, data|
114
+ data.each(&:shift)
115
+ {
116
+ name: device_id,
117
+ data: Lttb.downsample(data, threshold)
118
+ }
119
+ end
120
+ json payload
121
+ end
122
+
123
+ get "/lttb_sql" do
124
+ downsampled = conditions
125
+ .lttb(threshold: threshold)
126
+ .map do |device_id, data|
127
+ {
128
+ name: device_id,
129
+ data: data.sort_by(&:first)
130
+ }
131
+ end
132
+ json downsampled
133
+ end
134
+
135
+
136
+ get '/all_data' do
137
+ data = conditions.pluck(:time, :temperature)
138
+ json [ { name: "All data", data: data} ]
139
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'lttb'
2
+ require 'pp'
3
+ require 'date'
4
+
5
+ data = [
6
+ ['2020-1-1', 10],
7
+ ['2020-1-2', 21],
8
+ ['2020-1-3', 19],
9
+ ['2020-1-4', 32],
10
+ ['2020-1-5', 12],
11
+ ['2020-1-6', 14],
12
+ ['2020-1-7', 18],
13
+ ['2020-1-8', 29],
14
+ ['2020-1-9', 23],
15
+ ['2020-1-10', 27],
16
+ ['2020-1-11', 14]]
17
+ data.each do |e|
18
+ e[0] = Time.mktime(*e[0].split('-'))
19
+ end
20
+
21
+ pp Lttb.downsample(data, 5)
@@ -0,0 +1,27 @@
1
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>
2
+ <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8hammerjs@2.0.8"></script>
3
+ <script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
4
+ <script src="https://cdn.jsdelivr.net/npm/highcharts@10.2.1/highcharts.min.js"></script>
5
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.0/dist/chartjs-adapter-moment.min.js"></script>
6
+ <script src="https://cdn.jsdelivr.net/npm/chartkick@4.2.0/dist/chartkick.min.js"></script>
7
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@1.2.1/dist/chartjs-plugin-zoom.min.js"></script>
8
+ <h3>Downsampling <%= conditions.count %> records to
9
+ <select value="<%= threshold %>" onchange="location.href=`/?threshold=${this.value}`">
10
+ <option><%= threshold %></option>
11
+ <option value="50">50</option>
12
+ <option value="100">100</option>
13
+ <option value="500">500</option>
14
+ <option value="1000">1000</option>
15
+ <option value="5000">5000</option>
16
+ </select> points.
17
+ </h3>
18
+
19
+ <h3>SQL</h3>
20
+ <%= line_chart("/lttb_sql?threshold=#{threshold}",
21
+ loading: "dowsampled data from SQL") %>
22
+ <h3>Ruby</h3>
23
+ <%= line_chart("/lttb_ruby?threshold=#{threshold}",
24
+ library: {chart: {zoomType: 'x'}},
25
+ points: true, loading: "downsampled data from Ruby") %>
26
+ <!--%= line_chart("/all_data", loading: "Loading all data") %-->
27
+
@@ -0,0 +1,13 @@
1
+ # LTTB examples
2
+
3
+ This folder contains an example to explore the a dynamic reloading of downsampled data.
4
+
5
+ It keeps the same amount of data and refresh the data with a higher resolution
6
+ as you keep zooming in.
7
+ There is a [./lttb_zoomable.rb](./lttb_zoomable.rb) file is a small webserver that compares
8
+ the SQL vs Ruby implementation. It also uses the [./views](./views) folder which
9
+ contains the view with the rendering and javascript part.
10
+
11
+ You can learn more by reading the [LTTB Zoom tutorial](https://jonatas.github.io/timescaledb/toolkit_lttb_zoom/).
12
+
13
+
@@ -0,0 +1,90 @@
1
+ # ruby lttb_zoomable.rb postgres://user:pass@host:port/db_name
2
+ require 'bundler/inline' #require only what you need
3
+
4
+ gemfile(true) do
5
+ gem 'timescaledb', path: '../../..'
6
+ gem 'pry'
7
+ gem 'sinatra', require: false
8
+ gem 'sinatra-reloader'
9
+ gem 'sinatra-cross_origin'
10
+ end
11
+
12
+ require 'timescaledb/toolkit'
13
+ require 'sinatra'
14
+ require 'sinatra/json'
15
+ require 'sinatra/contrib'
16
+
17
+ register Sinatra::Reloader
18
+ register Sinatra::Contrib
19
+
20
+ PG_URI = ARGV.last
21
+
22
+ VALID_SIZES = %i[small med big]
23
+ def download_weather_dataset size: :small
24
+ unless VALID_SIZES.include?(size)
25
+ fail "Invalid size: #{size}. Valids are #{VALID_SIZES}"
26
+ end
27
+ url = "https://timescaledata.blob.core.windows.net/datasets/weather_#{size}.tar.gz"
28
+ puts "fetching #{size} weather dataset..."
29
+ system "wget \"#{url}\""
30
+ puts "done!"
31
+ end
32
+
33
+ def setup size: :small
34
+ file = "weather_#{size}.tar.gz"
35
+ download_weather_dataset(size: size) unless File.exists? file
36
+ puts "extracting #{file}"
37
+ system "tar -xvzf #{file} "
38
+ puts "creating data structures"
39
+ system "psql #{PG_URI} < weather.sql"
40
+ system %|psql #{PG_URI} -c "\\COPY locations FROM weather_#{size}_locations.csv CSV"|
41
+ system %|psql #{PG_URI} -c "\\COPY conditions FROM weather_#{size}_conditions.csv CSV"|
42
+ end
43
+
44
+ ActiveRecord::Base.establish_connection(PG_URI)
45
+
46
+ class Condition < ActiveRecord::Base
47
+ acts_as_hypertable time_column: "time"
48
+ acts_as_time_vector value_column: "temperature", segment_by: "device_id"
49
+ end
50
+
51
+ # Setup Hypertable as in a migration
52
+ ActiveRecord::Base.connection.instance_exec do
53
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
54
+
55
+ if !Condition.table_exists? || Condition.count.zero?
56
+
57
+ setup size: :big
58
+ end
59
+ end
60
+
61
+
62
+ def filter_by_request_params
63
+ filter= {device_id: "weather-pro-000001"}
64
+ if params[:filter] && params[:filter] != "null"
65
+ from, to = params[:filter].split(",").map(&Time.method(:parse))
66
+ filter[:time] = from..to
67
+ end
68
+ filter
69
+ end
70
+
71
+ def conditions
72
+ Condition.where(filter_by_request_params).order('time')
73
+ end
74
+
75
+ def threshold
76
+ params[:threshold]&.to_i || 50
77
+ end
78
+
79
+ configure do
80
+ enable :cross_origin
81
+ end
82
+
83
+ get '/' do
84
+ erb :index
85
+ end
86
+
87
+ get "/lttb_sql" do
88
+ downsampled = conditions.lttb(threshold: threshold, segment_by: nil)
89
+ json downsampled
90
+ end
@@ -0,0 +1,33 @@
1
+ <head>
2
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>
3
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
4
+ </head>
5
+
6
+ <h3>Downsampling <%= conditions.count %> records to
7
+ <select value="<%= threshold %>" onchange="location.href=`/?threshold=${this.value}`">
8
+ <option><%= threshold %></option>
9
+ <option value="50">50</option>
10
+ <option value="100">100</option>
11
+ <option value="500">500</option>
12
+ <option value="1000">1000</option>
13
+ <option value="5000">5000</option>
14
+ </select> points.
15
+ </h3>
16
+ <div id='container'></div>
17
+ <script>
18
+ let chart = document.getElementById('container');
19
+ function fetch(filter) {
20
+ $.ajax({
21
+ url: `/lttb_sql?threshold=<%= threshold %>&filter=${filter}`,
22
+ success: function(result) {
23
+ let x = result.map((e) => e[0]);
24
+ let y = result.map((e) => parseFloat(e[1]));
25
+ Plotly.newPlot(chart, [{x, y,"mode": "markers", "type": "scatter"}]);
26
+ chart.on('plotly_relayout',
27
+ function(eventdata){
28
+ fetch([eventdata['xaxis.range[0]'],eventdata['xaxis.range[1]']]);
29
+ });
30
+ }});
31
+ }
32
+ fetch(null);
33
+ </script>
@@ -0,0 +1,18 @@
1
+ module Timescaledb
2
+ module ActsAsTimeVector
3
+ def acts_as_time_vector(options = {})
4
+ return if acts_as_time_vector?
5
+
6
+ include Timescaledb::Toolkit::TimeVector
7
+
8
+ class_attribute :time_vector_options, instance_writer: false
9
+ define_default_scopes
10
+ self.time_vector_options = options
11
+ end
12
+
13
+ def acts_as_time_vector?
14
+ included_modules.include?(Timescaledb::ActsAsTimeVector)
15
+ end
16
+ end
17
+ end
18
+ ActiveRecord::Base.extend Timescaledb::ActsAsTimeVector
@@ -1,6 +1,7 @@
1
1
  module Timescaledb
2
2
  class Dimension < ActiveRecord::Base
3
3
  self.table_name = "timescaledb_information.dimensions"
4
+ attribute :time_interval, :interval
4
5
  end
5
6
  Dimensions = Dimension
6
7
  end
@@ -10,7 +10,7 @@ module Timescaledb
10
10
  foreign_key: "hypertable_name",
11
11
  class_name: "Timescaledb::CompressionSetting"
12
12
 
13
- has_one :dimensions,
13
+ has_many :dimensions,
14
14
  foreign_key: "hypertable_name",
15
15
  class_name: "Timescaledb::Dimension"
16
16
 
@@ -18,6 +18,10 @@ module Timescaledb
18
18
  foreign_key: "hypertable_name",
19
19
  class_name: "Timescaledb::ContinuousAggregate"
20
20
 
21
+ def main_dimension
22
+ dimensions.find_by dimension_number: 1
23
+ end
24
+
21
25
  def chunks_detailed_size
22
26
  struct_from "SELECT * from chunks_detailed_size('#{self.hypertable_name}')"
23
27
  end
@@ -72,26 +72,45 @@ module Timescaledb
72
72
  # GROUP BY bucket
73
73
  # SQL
74
74
  #
75
- def create_continuous_aggregate(name, query, **options)
75
+ def create_continuous_aggregate(table_name, query, **options)
76
76
  execute <<~SQL
77
- CREATE MATERIALIZED VIEW #{name}
77
+ CREATE MATERIALIZED VIEW #{table_name}
78
78
  WITH (timescaledb.continuous) AS
79
79
  #{query.respond_to?(:to_sql) ? query.to_sql : query}
80
80
  WITH #{"NO" unless options[:with_data]} DATA;
81
81
  SQL
82
82
 
83
- if (policy = options[:refresh_policies])
84
- # TODO: assert valid keys
85
- execute <<~SQL
86
- SELECT add_continuous_aggregate_policy('#{name}',
87
- start_offset => #{policy[:start_offset]},
88
- end_offset => #{policy[:end_offset]},
89
- schedule_interval => #{policy[:schedule_interval]});
90
- SQL
91
- end
83
+ create_continuous_aggregate_policy(table_name, options[:refresh_policies] || {})
84
+ end
85
+
86
+
87
+ # Drop a new continuous aggregate.
88
+ #
89
+ # It basically DROP MATERIALIZED VIEW for a given @name.
90
+ #
91
+ # @param name [String, Symbol] The name of the continuous aggregate view.
92
+ def drop_continuous_aggregates view_name
93
+ execute "DROP MATERIALIZED VIEW #{view_name}"
92
94
  end
95
+
93
96
  alias_method :create_continuous_aggregates, :create_continuous_aggregate
94
97
 
98
+ def create_continuous_aggregate_policy(table_name, **options)
99
+ return if options.empty?
100
+
101
+ # TODO: assert valid keys
102
+ execute <<~SQL
103
+ SELECT add_continuous_aggregate_policy('#{table_name}',
104
+ start_offset => #{options[:start_offset]},
105
+ end_offset => #{options[:end_offset]},
106
+ schedule_interval => #{options[:schedule_interval]});
107
+ SQL
108
+ end
109
+
110
+ def remove_continuous_aggregate_policy(table_name)
111
+ execute "SELECT remove_continuous_aggregate_policy('#{table_name}')"
112
+ end
113
+
95
114
  def create_retention_policy(table_name, interval:)
96
115
  execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{interval}')"
97
116
  end