timescaledb 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +11 -3
- data/Gemfile.scenic +7 -0
- data/Gemfile.scenic.lock +121 -0
- data/README.md +267 -121
- data/Rakefile +16 -1
- data/bin/console +2 -2
- data/bin/setup +2 -0
- data/bin/tsdb +30 -6
- data/examples/{Gemfile → all_in_one/Gemfile} +1 -1
- data/examples/{Gemfile.lock → all_in_one/Gemfile.lock} +2 -2
- data/examples/{all_in_one.rb → all_in_one/all_in_one.rb} +3 -6
- data/examples/ranking/.gitattributes +7 -0
- data/examples/ranking/.gitignore +29 -0
- data/examples/ranking/.ruby-version +1 -0
- data/examples/ranking/Gemfile +33 -0
- data/examples/ranking/Gemfile.lock +189 -0
- data/examples/ranking/README.md +166 -0
- data/examples/ranking/Rakefile +6 -0
- data/examples/ranking/app/controllers/application_controller.rb +2 -0
- data/examples/ranking/app/controllers/concerns/.keep +0 -0
- data/examples/ranking/app/jobs/application_job.rb +7 -0
- data/examples/ranking/app/models/application_record.rb +3 -0
- data/examples/ranking/app/models/concerns/.keep +0 -0
- data/examples/ranking/app/models/game.rb +2 -0
- data/examples/ranking/app/models/play.rb +7 -0
- data/examples/ranking/bin/bundle +114 -0
- data/examples/ranking/bin/rails +4 -0
- data/examples/ranking/bin/rake +4 -0
- data/examples/ranking/bin/setup +33 -0
- data/examples/ranking/config/application.rb +39 -0
- data/examples/ranking/config/boot.rb +4 -0
- data/examples/ranking/config/credentials.yml.enc +1 -0
- data/examples/ranking/config/database.yml +86 -0
- data/examples/ranking/config/environment.rb +5 -0
- data/examples/ranking/config/environments/development.rb +60 -0
- data/examples/ranking/config/environments/production.rb +75 -0
- data/examples/ranking/config/environments/test.rb +53 -0
- data/examples/ranking/config/initializers/cors.rb +16 -0
- data/examples/ranking/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/ranking/config/initializers/inflections.rb +16 -0
- data/examples/ranking/config/initializers/timescale.rb +3 -0
- data/examples/ranking/config/locales/en.yml +33 -0
- data/examples/ranking/config/puma.rb +43 -0
- data/examples/ranking/config/routes.rb +6 -0
- data/examples/ranking/config/storage.yml +34 -0
- data/examples/ranking/config.ru +6 -0
- data/examples/ranking/db/migrate/20220209120747_create_games.rb +10 -0
- data/examples/ranking/db/migrate/20220209120910_create_plays.rb +19 -0
- data/examples/ranking/db/migrate/20220209143347_create_score_per_hours.rb +5 -0
- data/examples/ranking/db/schema.rb +47 -0
- data/examples/ranking/db/seeds.rb +7 -0
- data/examples/ranking/db/views/score_per_hours_v01.sql +7 -0
- data/examples/ranking/lib/tasks/.keep +0 -0
- data/examples/ranking/log/.keep +0 -0
- data/examples/ranking/public/robots.txt +1 -0
- data/examples/ranking/storage/.keep +0 -0
- data/examples/ranking/tmp/.keep +0 -0
- data/examples/ranking/tmp/pids/.keep +0 -0
- data/examples/ranking/tmp/storage/.keep +0 -0
- data/examples/ranking/vendor/.keep +0 -0
- data/lib/timescaledb/acts_as_hypertable/core.rb +87 -0
- data/lib/timescaledb/acts_as_hypertable.rb +62 -0
- data/lib/{timescale → timescaledb}/chunk.rb +9 -1
- data/lib/{timescale → timescaledb}/compression_settings.rb +3 -2
- data/lib/timescaledb/continuous_aggregates.rb +19 -0
- data/lib/timescaledb/dimensions.rb +6 -0
- data/lib/{timescale → timescaledb}/hypertable.rb +9 -5
- data/lib/timescaledb/job.rb +10 -0
- data/lib/{timescale → timescaledb}/job_stats.rb +3 -4
- data/lib/{timescale → timescaledb}/migration_helpers.rb +35 -5
- data/lib/timescaledb/scenic/adapter.rb +55 -0
- data/lib/timescaledb/scenic/extension.rb +72 -0
- data/lib/timescaledb/schema_dumper.rb +88 -0
- data/lib/timescaledb/stats_report.rb +35 -0
- data/lib/timescaledb/version.rb +3 -0
- data/lib/timescaledb.rb +64 -0
- data/{timescale.gemspec → timescaledb.gemspec} +6 -4
- metadata +106 -20
- data/lib/timescale/acts_as_hypertable.rb +0 -114
- data/lib/timescale/continuous_aggregates.rb +0 -9
- data/lib/timescale/job.rb +0 -13
- data/lib/timescale/stats_report.rb +0 -28
- data/lib/timescale/version.rb +0 -3
- data/lib/timescale.rb +0 -52
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module ActsAsHypertable
|
5
|
+
module Core
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def time_column
|
12
|
+
@time_column ||= hypertable_options[:time_column] || :created_at
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def define_association_scopes
|
18
|
+
scope :chunks, -> do
|
19
|
+
Chunk.where(hypertable_name: table_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
scope :hypertable, -> do
|
23
|
+
Hypertable.find_by(hypertable_name: table_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
scope :jobs, -> do
|
27
|
+
Job.where(hypertable_name: table_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
scope :job_stats, -> do
|
31
|
+
JobStats.where(hypertable_name: table_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
scope :compression_settings, -> do
|
35
|
+
CompressionSettings.where(hypertable_name: table_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
scope :continuous_aggregates, -> do
|
39
|
+
ContinuousAggregates.where(hypertable_name: table_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def define_default_scopes
|
44
|
+
scope :previous_month, -> do
|
45
|
+
where(
|
46
|
+
"DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
|
47
|
+
start_time: Date.today.last_month.in_time_zone.beginning_of_month.to_date,
|
48
|
+
end_time: Date.today.last_month.in_time_zone.end_of_month.to_date
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
scope :previous_week, -> do
|
53
|
+
where(
|
54
|
+
"DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
|
55
|
+
start_time: Date.today.last_week.in_time_zone.beginning_of_week.to_date,
|
56
|
+
end_time: Date.today.last_week.in_time_zone.end_of_week.to_date
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
scope :this_month, -> do
|
61
|
+
where(
|
62
|
+
"DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
|
63
|
+
start_time: Date.today.in_time_zone.beginning_of_month.to_date,
|
64
|
+
end_time: Date.today.in_time_zone.end_of_month.to_date
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
scope :this_week, -> do
|
69
|
+
where(
|
70
|
+
"DATE(#{time_column}) >= :start_time AND DATE(#{time_column}) <= :end_time",
|
71
|
+
start_time: Date.today.in_time_zone.beginning_of_week.to_date,
|
72
|
+
end_time: Date.today.in_time_zone.end_of_week.to_date
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
scope :yesterday, -> { where("DATE(#{time_column}) = ?", Date.yesterday.in_time_zone.to_date) }
|
77
|
+
scope :today, -> { where("DATE(#{time_column}) = ?", Date.today.in_time_zone.to_date) }
|
78
|
+
scope :last_hour, -> { where("#{time_column} > ?", 1.hour.ago.in_time_zone) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def normalize_hypertable_options
|
82
|
+
hypertable_options[:time_column] = hypertable_options[:time_column].to_sym
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
# If you want your model to hook into its underlying hypertable
|
5
|
+
# as well as have access to TimescaleDB specific data, methods, and more,
|
6
|
+
# specify this macro in your model.
|
7
|
+
#
|
8
|
+
# @note Your model's table needs to have already been converted to a hypertable
|
9
|
+
# via the TimescaleDB `create_hypertable` function for this to work.
|
10
|
+
#
|
11
|
+
# @see https://docs.timescale.com/api/latest/hypertable/create_hypertable/ for
|
12
|
+
# how to use the SQL `create_hypertable` function.
|
13
|
+
# @see Timescaledb::MigrationHelpers#create_table for how to create a new hypertable
|
14
|
+
# via a Rails migration utilizing the standard `create_table` method.
|
15
|
+
#
|
16
|
+
# @example Enabling the macro on your model
|
17
|
+
# class Event < ActiveRecord::Base
|
18
|
+
# acts_as_hypertable
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @see Timescaledb::ActsAsHypertable#acts_as_hypertable
|
22
|
+
# for configuration options
|
23
|
+
module ActsAsHypertable
|
24
|
+
DEFAULT_OPTIONS = {
|
25
|
+
time_column: :created_at
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
def acts_as_hypertable?
|
29
|
+
included_modules.include?(Timescaledb::ActsAsHypertable::Core)
|
30
|
+
end
|
31
|
+
|
32
|
+
# == Configuration options
|
33
|
+
#
|
34
|
+
# @param [Hash] options The options to initialize your macro with.
|
35
|
+
# @option options [Symbol] :time_column The name of the column in your
|
36
|
+
# model's table containing time values. The name provided should be
|
37
|
+
# the same name as the `time_column_name` you passed to the
|
38
|
+
# TimescaleDB `create_hypertable` function.
|
39
|
+
#
|
40
|
+
# @example Enabling the macro on your model with options
|
41
|
+
# class Event < ActiveRecord::Base
|
42
|
+
# acts_as_hypertable time_column: :timestamp
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
def acts_as_hypertable(options = {})
|
46
|
+
return if acts_as_hypertable?
|
47
|
+
|
48
|
+
include Timescaledb::ActsAsHypertable::Core
|
49
|
+
|
50
|
+
class_attribute :hypertable_options, instance_writer: false
|
51
|
+
|
52
|
+
self.hypertable_options = DEFAULT_OPTIONS.dup
|
53
|
+
hypertable_options.merge!(options)
|
54
|
+
normalize_hypertable_options
|
55
|
+
|
56
|
+
define_association_scopes
|
57
|
+
define_default_scopes
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveRecord::Base.extend Timescaledb::ActsAsHypertable
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
1
|
+
module Timescaledb
|
2
2
|
class Chunk < ActiveRecord::Base
|
3
3
|
self.table_name = "timescaledb_information.chunks"
|
4
4
|
self.primary_key = "chunk_name"
|
@@ -8,6 +8,14 @@ module Timescale
|
|
8
8
|
scope :compressed, -> { where(is_compressed: true) }
|
9
9
|
scope :uncompressed, -> { where(is_compressed: false) }
|
10
10
|
|
11
|
+
scope :resume, -> do
|
12
|
+
{
|
13
|
+
total: count,
|
14
|
+
compressed: compressed.count,
|
15
|
+
uncompressed: uncompressed.count
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
11
19
|
def compress!
|
12
20
|
execute("SELECT compress_chunk(#{chunk_relation})")
|
13
21
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
-
module
|
2
|
-
class
|
1
|
+
module Timescaledb
|
2
|
+
class CompressionSetting < ActiveRecord::Base
|
3
3
|
self.table_name = "timescaledb_information.compression_settings"
|
4
4
|
belongs_to :hypertable, foreign_key: :hypertable_name
|
5
5
|
end
|
6
|
+
CompressionSettings = CompressionSetting
|
6
7
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Timescaledb
|
2
|
+
class ContinuousAggregate < ActiveRecord::Base
|
3
|
+
self.table_name = "timescaledb_information.continuous_aggregates"
|
4
|
+
self.primary_key = 'materialization_hypertable_name'
|
5
|
+
|
6
|
+
has_many :jobs, foreign_key: "hypertable_name",
|
7
|
+
class_name: "Timescaledb::Job"
|
8
|
+
|
9
|
+
has_many :chunks, foreign_key: "hypertable_name",
|
10
|
+
class_name: "Timescaledb::Chunk"
|
11
|
+
|
12
|
+
scope :resume, -> do
|
13
|
+
{
|
14
|
+
total: count
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
ContinuousAggregates = ContinuousAggregate
|
19
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
module
|
1
|
+
module Timescaledb
|
2
2
|
class Hypertable < ActiveRecord::Base
|
3
3
|
self.table_name = "timescaledb_information.hypertables"
|
4
|
-
|
5
4
|
self.primary_key = "hypertable_name"
|
6
5
|
|
7
6
|
has_many :jobs, foreign_key: "hypertable_name"
|
@@ -9,11 +8,15 @@ module Timescale
|
|
9
8
|
|
10
9
|
has_many :compression_settings,
|
11
10
|
foreign_key: "hypertable_name",
|
12
|
-
class_name: "
|
11
|
+
class_name: "Timescaledb::CompressionSetting"
|
12
|
+
|
13
|
+
has_one :dimensions,
|
14
|
+
foreign_key: "hypertable_name",
|
15
|
+
class_name: "Timescaledb::Dimension"
|
13
16
|
|
14
17
|
has_many :continuous_aggregates,
|
15
18
|
foreign_key: "hypertable_name",
|
16
|
-
class_name: "
|
19
|
+
class_name: "Timescaledb::ContinuousAggregate"
|
17
20
|
|
18
21
|
def chunks_detailed_size
|
19
22
|
struct_from "SELECT * from chunks_detailed_size('#{self.hypertable_name}')"
|
@@ -24,7 +27,8 @@ module Timescale
|
|
24
27
|
end
|
25
28
|
|
26
29
|
def compression_stats
|
27
|
-
|
30
|
+
@compression_stats ||=
|
31
|
+
struct_from("SELECT * from hypertable_compression_stats('#{self.hypertable_name}')").first || {}
|
28
32
|
end
|
29
33
|
|
30
34
|
def detailed_size
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Timescaledb
|
2
|
+
class Job < ActiveRecord::Base
|
3
|
+
self.table_name = "timescaledb_information.jobs"
|
4
|
+
self.primary_key = "job_id"
|
5
|
+
|
6
|
+
scope :compression, -> { where(proc_name: [:tsbs_compress_chunks, :policy_compression]) }
|
7
|
+
scope :refresh_continuous_aggregate, -> { where(proc_name: :policy_refresh_continuous_aggregate) }
|
8
|
+
scope :scheduled, -> { where(scheduled: true) }
|
9
|
+
end
|
10
|
+
end
|
@@ -1,11 +1,9 @@
|
|
1
|
-
module
|
2
|
-
class
|
1
|
+
module Timescaledb
|
2
|
+
class JobStat < ActiveRecord::Base
|
3
3
|
self.table_name = "timescaledb_information.job_stats"
|
4
4
|
|
5
5
|
belongs_to :job
|
6
6
|
|
7
|
-
attribute :last_run_duration, :interval
|
8
|
-
|
9
7
|
scope :success, -> { where(last_run_status: "Success") }
|
10
8
|
scope :scheduled, -> { where(job_status: "Scheduled") }
|
11
9
|
scope :resume, -> do
|
@@ -15,4 +13,5 @@ module Timescale
|
|
15
13
|
.to_a.map{|e|e.attributes.transform_keys(&:to_sym) }
|
16
14
|
end
|
17
15
|
end
|
16
|
+
JobStats = JobStat
|
18
17
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'active_record/connection_adapters/postgresql_adapter'
|
2
2
|
|
3
3
|
# Useful methods to run TimescaleDB in you Ruby app.
|
4
|
-
module
|
4
|
+
module Timescaledb
|
5
5
|
# Migration helpers can help you to setup hypertables by default.
|
6
6
|
module MigrationHelpers
|
7
7
|
# create_table can receive `hypertable` argument
|
@@ -21,12 +21,12 @@ module Timescale
|
|
21
21
|
# end
|
22
22
|
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
|
23
23
|
super
|
24
|
-
|
24
|
+
create_hypertable(table_name, **options[:hypertable]) if options.key?(:hypertable)
|
25
25
|
end
|
26
26
|
|
27
27
|
# Setup hypertable from options
|
28
28
|
# @see create_table with the hypertable options.
|
29
|
-
def
|
29
|
+
def create_hypertable(table_name,
|
30
30
|
time_column: 'created_at',
|
31
31
|
chunk_time_interval: '1 week',
|
32
32
|
compress_segmentby: nil,
|
@@ -51,7 +51,28 @@ module Timescale
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
|
54
|
+
# Create a new continuous aggregate
|
55
|
+
#
|
56
|
+
# @param name [String, Symbol] The name of the continuous aggregate.
|
57
|
+
# @param query [String] The SQL query for the aggregate view definition.
|
58
|
+
# @param with_data [Boolean] Set to true to create the aggregate WITH DATA
|
59
|
+
# @param refresh_policies [Hash] Set to create a refresh policy
|
60
|
+
# @option refresh_policies [String] start_offset: INTERVAL or integer
|
61
|
+
# @option refresh_policies [String] end_offset: INTERVAL or integer
|
62
|
+
# @option refresh_policies [String] schedule_interval: INTERVAL
|
63
|
+
#
|
64
|
+
# @see https://docs.timescale.com/api/latest/continuous-aggregates/add_continuous_aggregate_policy/
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# create_continuous_aggregate(:activity_counts, query: <<-SQL, refresh_policies: { schedule_interval: "INTERVAL '1 hour'" })
|
68
|
+
# SELECT
|
69
|
+
# time_bucket(INTERVAL '1 day', activity.created_at) AS bucket,
|
70
|
+
# count(*)
|
71
|
+
# FROM activity
|
72
|
+
# GROUP BY bucket
|
73
|
+
# SQL
|
74
|
+
#
|
75
|
+
def create_continuous_aggregate(name, query, **options)
|
55
76
|
execute <<~SQL
|
56
77
|
CREATE MATERIALIZED VIEW #{name}
|
57
78
|
WITH (timescaledb.continuous) AS
|
@@ -69,7 +90,16 @@ module Timescale
|
|
69
90
|
SQL
|
70
91
|
end
|
71
92
|
end
|
93
|
+
alias_method :create_continuous_aggregates, :create_continuous_aggregate
|
94
|
+
|
95
|
+
def create_retention_policy(table_name, interval:)
|
96
|
+
execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{interval}')"
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove_retention_policy(table_name)
|
100
|
+
execute "SELECT remove_retention_policy('#{table_name}')"
|
101
|
+
end
|
72
102
|
end
|
73
103
|
end
|
74
104
|
|
75
|
-
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(
|
105
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Timescaledb::MigrationHelpers)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'scenic/adapters/postgres'
|
2
|
+
require 'scenic/adapters/postgres/views'
|
3
|
+
|
4
|
+
module Timescaledb
|
5
|
+
module Scenic
|
6
|
+
class Views < ::Scenic::Adapters::Postgres::Views
|
7
|
+
# All of the views that this connection has defined, excluding any
|
8
|
+
# Timescale continuous aggregates. Those should be defined using
|
9
|
+
# +create_continuous_aggregate+ rather than +create_view+.
|
10
|
+
#
|
11
|
+
# @return [Array<Scenic::View>]
|
12
|
+
def all
|
13
|
+
ts_views = views_from_timescale.map { |v| to_scenic_view(v) }
|
14
|
+
pg_views = views_from_postgres.map { |v| to_scenic_view(v) }
|
15
|
+
ts_view_names = ts_views.map(&:name)
|
16
|
+
# Skip records with matching names (includes the schema name
|
17
|
+
# for records not in the public schema)
|
18
|
+
pg_views.reject { |v| v.name.in?(ts_view_names) }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def views_from_timescale
|
24
|
+
connection.execute(<<-SQL.squish)
|
25
|
+
SELECT
|
26
|
+
view_name as viewname,
|
27
|
+
view_definition AS definition,
|
28
|
+
'm' AS kind,
|
29
|
+
view_schema AS namespace
|
30
|
+
FROM timescaledb_information.continuous_aggregates
|
31
|
+
SQL
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Adapter < ::Scenic::Adapters::Postgres
|
36
|
+
# Timescale does some funky stuff under the hood with continuous
|
37
|
+
# aggregates. A continuous aggregate is made up of:
|
38
|
+
#
|
39
|
+
# 1. A hypertable to store the materialized data
|
40
|
+
# 2. An entry in the jobs table to refresh the data
|
41
|
+
# 3. A view definition that union's the hypertable and any recent data
|
42
|
+
# not included in the hypertable
|
43
|
+
#
|
44
|
+
# That doesn't dump well, even to structure.sql (we lose the job
|
45
|
+
# definition, since it's not part of the DDL).
|
46
|
+
#
|
47
|
+
# Our schema dumper implementation will handle dumping the continuous
|
48
|
+
# aggregate definitions, but we need to override Scenic's schema dumping
|
49
|
+
# to exclude those continuous aggregates.
|
50
|
+
def views
|
51
|
+
Views.new(connection).all
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Scenic does not include `WITH` option that is used with continuous aggregates.
|
2
|
+
module Timescaledb
|
3
|
+
module Scenic
|
4
|
+
module Extension
|
5
|
+
# @override Scenic::Adapters::Postgres#create_materialized_view
|
6
|
+
# Creates a materialized view in the database
|
7
|
+
#
|
8
|
+
# @param name The name of the materialized view to create
|
9
|
+
# @param sql_definition The SQL schema that defines the materialized view.
|
10
|
+
# @param with [String] Default: nil. Set with: "..." to add "WITH (...)".
|
11
|
+
# @param no_data [Boolean] Default: false. Set to true to not create data.
|
12
|
+
# materialized view without running the associated query. You will need
|
13
|
+
# to perform a non-concurrent refresh to populate with data.
|
14
|
+
#
|
15
|
+
# This is typically called in a migration via {Statements#create_view}.
|
16
|
+
# @return [void]
|
17
|
+
def create_materialized_view(name, sql_definition, with: nil, no_data: false)
|
18
|
+
execute <<-SQL
|
19
|
+
CREATE MATERIALIZED VIEW #{quote_table_name(name)}
|
20
|
+
#{"WITH (#{with})" if with} AS
|
21
|
+
#{sql_definition.rstrip.chomp(';')}
|
22
|
+
#{'WITH NO DATA' if no_data};
|
23
|
+
SQL
|
24
|
+
end
|
25
|
+
|
26
|
+
# @override Scenic::Adapters::Postgres#create_view
|
27
|
+
# to add the `with: ` keyword that can be used for such option.
|
28
|
+
#
|
29
|
+
def create_view(name, version: nil, with: nil, sql_definition: nil, materialized: false, no_data: false)
|
30
|
+
if version.present? && sql_definition.present?
|
31
|
+
raise(
|
32
|
+
ArgumentError,
|
33
|
+
"sql_definition and version cannot both be set",
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
if version.blank? && sql_definition.blank?
|
38
|
+
version = 1
|
39
|
+
end
|
40
|
+
|
41
|
+
sql_definition ||= definition(name, version)
|
42
|
+
|
43
|
+
if materialized
|
44
|
+
::Scenic.database.create_materialized_view(
|
45
|
+
name,
|
46
|
+
sql_definition,
|
47
|
+
no_data: no_data,
|
48
|
+
with: with
|
49
|
+
)
|
50
|
+
else
|
51
|
+
::Scenic.database.create_view(name, sql_definition, with: with)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def definition(name, version)
|
58
|
+
::Scenic::Definition.new(name, version).to_sql
|
59
|
+
end
|
60
|
+
end
|
61
|
+
module MigrationHelpers
|
62
|
+
# Create a timescale continuous aggregate view
|
63
|
+
def create_scenic_continuous_aggregate(name)
|
64
|
+
::Scenic.database.create_view(name, materialized: true, no_data: true, with: "timescaledb.continuous")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Timescaledb::Scenic::MigrationHelpers)
|
72
|
+
Scenic::Adapters::Postgres.prepend(Timescaledb::Scenic::Extension)
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
2
|
+
require 'active_support/core_ext/string/indent'
|
3
|
+
|
4
|
+
module Timescaledb
|
5
|
+
module SchemaDumper
|
6
|
+
def tables(stream)
|
7
|
+
super # This will call #table for each table in the database
|
8
|
+
views(stream) unless defined?(Scenic) # Don't call this twice if we're using Scenic
|
9
|
+
end
|
10
|
+
|
11
|
+
def table(table_name, stream)
|
12
|
+
super(table_name, stream)
|
13
|
+
if hypertable = Timescaledb::Hypertable.find_by(hypertable_name: table_name)
|
14
|
+
timescale_hypertable(hypertable, stream)
|
15
|
+
timescale_retention_policy(hypertable, stream)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def views(stream)
|
20
|
+
timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them
|
21
|
+
super if defined?(super)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def timescale_hypertable(hypertable, stream)
|
27
|
+
dim = hypertable.dimensions
|
28
|
+
extra_settings = {
|
29
|
+
time_column: "#{dim.column_name}",
|
30
|
+
chunk_time_interval: "#{dim.time_interval.inspect}"
|
31
|
+
}.merge(timescale_compression_settings_for(hypertable)).map {|k, v| %Q[#{k}: "#{v}"]}.join(", ")
|
32
|
+
|
33
|
+
stream.puts %Q[ create_hypertable "#{hypertable.hypertable_name}", #{extra_settings}]
|
34
|
+
stream.puts
|
35
|
+
end
|
36
|
+
|
37
|
+
def timescale_retention_policy(hypertable, stream)
|
38
|
+
hypertable.jobs.where(proc_name: "policy_retention").each do |job|
|
39
|
+
stream.puts %Q[ create_retention_policy "#{job.hypertable_name}", interval: "#{job.config["drop_after"]}"]
|
40
|
+
stream.puts
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def timescale_compression_settings_for(hypertable)
|
45
|
+
compression_settings = hypertable.compression_settings.each_with_object({}) do |setting, compression_settings|
|
46
|
+
compression_settings[:compress_segmentby] = setting.attname if setting.segmentby_column_index
|
47
|
+
|
48
|
+
if setting.orderby_column_index
|
49
|
+
direction = setting.orderby_asc ? "ASC" : "DESC"
|
50
|
+
compression_settings[:compress_orderby] = "#{setting.attname} #{direction}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
hypertable.jobs.compression.each do |job|
|
55
|
+
compression_settings[:compression_interval] = job.config["compress_after"]
|
56
|
+
end
|
57
|
+
compression_settings
|
58
|
+
end
|
59
|
+
|
60
|
+
def timescale_continuous_aggregates(stream)
|
61
|
+
Timescaledb::ContinuousAggregates.all.each do |aggregate|
|
62
|
+
opts = if (refresh_policy = aggregate.jobs.refresh_continuous_aggregate.first)
|
63
|
+
interval = timescale_interval(refresh_policy.schedule_interval)
|
64
|
+
end_offset = timescale_interval(refresh_policy.config["end_offset"])
|
65
|
+
start_offset = timescale_interval(refresh_policy.config["start_offset"])
|
66
|
+
%Q[, refresh_policies: { start_offset: "#{start_offset}", end_offset: "#{end_offset}", schedule_interval: "#{interval}"}]
|
67
|
+
else
|
68
|
+
""
|
69
|
+
end
|
70
|
+
|
71
|
+
stream.puts <<~AGG.indent(2)
|
72
|
+
create_continuous_aggregate("#{aggregate.view_name}", <<-SQL#{opts})
|
73
|
+
#{aggregate.view_definition.strip.gsub(/;$/, "")}
|
74
|
+
SQL
|
75
|
+
AGG
|
76
|
+
stream.puts
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def timescale_interval(value)
|
81
|
+
return "NULL" if value.nil? || value.to_s.downcase == "null"
|
82
|
+
|
83
|
+
"INTERVAL '#{value}'"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(Timescaledb::SchemaDumper)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "active_support/core_ext/numeric/conversions"
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module StatsReport
|
5
|
+
module_function
|
6
|
+
def resume(scope=Hypertable.all)
|
7
|
+
base_filter = {hypertable_name: scope.pluck(:hypertable_name)}
|
8
|
+
{
|
9
|
+
hypertables: {
|
10
|
+
count: scope.count,
|
11
|
+
uncompressed: scope.to_a.count { |h| h.compression_stats.empty? },
|
12
|
+
approximate_row_count: approximate_row_count(scope),
|
13
|
+
chunks: Chunk.where(base_filter).resume,
|
14
|
+
size: compression_resume(scope)
|
15
|
+
},
|
16
|
+
continuous_aggregates: ContinuousAggregates.where(base_filter).resume,
|
17
|
+
jobs_stats: JobStats.where(base_filter).resume
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def compression_resume(scope)
|
22
|
+
sum = -> (method) { (scope.map(&method).inject(:+) || 0).to_formatted_s(:human_size)}
|
23
|
+
{
|
24
|
+
uncompressed: sum[:before_total_bytes],
|
25
|
+
compressed: sum[:after_total_bytes]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def approximate_row_count(scope)
|
30
|
+
scope.to_a.map do |hypertable|
|
31
|
+
{ hypertable.hypertable_name => hypertable.approximate_row_count }
|
32
|
+
end.inject(&:merge!)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/timescaledb.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
require_relative 'timescaledb/acts_as_hypertable'
|
4
|
+
require_relative 'timescaledb/acts_as_hypertable/core'
|
5
|
+
require_relative 'timescaledb/chunk'
|
6
|
+
require_relative 'timescaledb/compression_settings'
|
7
|
+
require_relative 'timescaledb/continuous_aggregates'
|
8
|
+
require_relative 'timescaledb/dimensions'
|
9
|
+
require_relative 'timescaledb/hypertable'
|
10
|
+
require_relative 'timescaledb/job'
|
11
|
+
require_relative 'timescaledb/job_stats'
|
12
|
+
require_relative 'timescaledb/schema_dumper'
|
13
|
+
require_relative 'timescaledb/stats_report'
|
14
|
+
require_relative 'timescaledb/migration_helpers'
|
15
|
+
require_relative 'timescaledb/version'
|
16
|
+
|
17
|
+
module Timescaledb
|
18
|
+
module_function
|
19
|
+
|
20
|
+
def chunks
|
21
|
+
Chunk.all
|
22
|
+
end
|
23
|
+
|
24
|
+
def hypertables
|
25
|
+
Hypertable.all
|
26
|
+
end
|
27
|
+
|
28
|
+
def continuous_aggregates
|
29
|
+
ContinuousAggregates.all
|
30
|
+
end
|
31
|
+
|
32
|
+
def compression_settings
|
33
|
+
CompressionSettings.all
|
34
|
+
end
|
35
|
+
|
36
|
+
def jobs
|
37
|
+
Job.all
|
38
|
+
end
|
39
|
+
|
40
|
+
def job_stats
|
41
|
+
JobStats.all
|
42
|
+
end
|
43
|
+
|
44
|
+
def stats(scope=Hypertable.all)
|
45
|
+
StatsReport.resume(scope)
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_hypertable_options
|
49
|
+
Timescaledb::ActsAsHypertable::DEFAULT_OPTIONS
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
require 'scenic'
|
55
|
+
require_relative 'timescaledb/scenic/adapter'
|
56
|
+
require_relative 'timescaledb/scenic/extension'
|
57
|
+
|
58
|
+
Scenic.configure do |config|
|
59
|
+
config.database = Timescaledb::Scenic::Adapter.new
|
60
|
+
end
|
61
|
+
|
62
|
+
rescue LoadError
|
63
|
+
# This is expected when the scenic gem is not being used
|
64
|
+
end
|