timescaledb 0.1.0 → 0.1.5
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 +3 -5
- data/Gemfile.lock +11 -3
- data/Gemfile.scenic +7 -0
- data/Gemfile.scenic.lock +121 -0
- data/README.md +273 -112
- data/Rakefile +16 -1
- data/bin/console +4 -1
- data/bin/setup +2 -0
- data/bin/tsdb +48 -0
- data/examples/Gemfile +1 -1
- data/examples/Gemfile.lock +2 -2
- data/examples/all_in_one.rb +3 -6
- data/lib/timescale/acts_as_hypertable/core.rb +87 -0
- data/lib/timescale/acts_as_hypertable.rb +62 -0
- data/lib/timescale/chunk.rb +8 -0
- data/lib/timescale/compression_settings.rb +2 -1
- data/lib/timescale/continuous_aggregates.rb +11 -1
- data/lib/timescale/dimensions.rb +6 -0
- data/lib/timescale/hypertable.rb +25 -5
- data/lib/timescale/job.rb +2 -5
- data/lib/timescale/job_stats.rb +8 -3
- data/lib/timescale/migration_helpers.rb +33 -3
- data/lib/timescale/scenic/adapter.rb +55 -0
- data/lib/timescale/schema_dumper.rb +88 -0
- data/lib/timescale/stats_report.rb +35 -0
- data/lib/timescale/version.rb +1 -1
- data/lib/timescale.rb +29 -4
- data/timescale.gemspec +13 -5
- metadata +116 -7
- data/lib/timescale/hypertable_helpers.rb +0 -39
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescale
|
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 Timescale
|
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 Timescale::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 Timescale::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?(Timescale::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 Timescale::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 Timescale::ActsAsHypertable
|
data/lib/timescale/chunk.rb
CHANGED
@@ -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
1
|
module Timescale
|
2
|
-
class
|
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
|
@@ -1,9 +1,19 @@
|
|
1
1
|
module Timescale
|
2
|
-
class
|
2
|
+
class ContinuousAggregate < ActiveRecord::Base
|
3
3
|
self.table_name = "timescaledb_information.continuous_aggregates"
|
4
4
|
self.primary_key = 'materialization_hypertable_name'
|
5
5
|
|
6
6
|
has_many :jobs, foreign_key: "hypertable_name",
|
7
7
|
class_name: "Timescale::Job"
|
8
|
+
|
9
|
+
has_many :chunks, foreign_key: "hypertable_name",
|
10
|
+
class_name: "Timescale::Chunk"
|
11
|
+
|
12
|
+
scope :resume, -> do
|
13
|
+
{
|
14
|
+
total: count
|
15
|
+
}
|
16
|
+
end
|
8
17
|
end
|
18
|
+
ContinuousAggregates = ContinuousAggregate
|
9
19
|
end
|
data/lib/timescale/hypertable.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module Timescale
|
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,18 +8,39 @@ module Timescale
|
|
9
8
|
|
10
9
|
has_many :compression_settings,
|
11
10
|
foreign_key: "hypertable_name",
|
12
|
-
class_name: "Timescale::
|
11
|
+
class_name: "Timescale::CompressionSetting"
|
12
|
+
|
13
|
+
has_one :dimensions,
|
14
|
+
foreign_key: "hypertable_name",
|
15
|
+
class_name: "Timescale::Dimension"
|
13
16
|
|
14
17
|
has_many :continuous_aggregates,
|
15
18
|
foreign_key: "hypertable_name",
|
16
|
-
class_name: "Timescale::
|
19
|
+
class_name: "Timescale::ContinuousAggregate"
|
17
20
|
|
18
|
-
def
|
21
|
+
def chunks_detailed_size
|
19
22
|
struct_from "SELECT * from chunks_detailed_size('#{self.hypertable_name}')"
|
20
23
|
end
|
21
24
|
|
25
|
+
def approximate_row_count
|
26
|
+
struct_from("SELECT * FROM approximate_row_count('#{self.hypertable_name}')").first.approximate_row_count
|
27
|
+
end
|
28
|
+
|
22
29
|
def compression_stats
|
23
|
-
|
30
|
+
@compression_stats ||=
|
31
|
+
struct_from("SELECT * from hypertable_compression_stats('#{self.hypertable_name}')").first || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def detailed_size
|
35
|
+
struct_from("SELECT * FROM hypertable_detailed_size('#{self.hypertable_name}')").first
|
36
|
+
end
|
37
|
+
|
38
|
+
def before_total_bytes
|
39
|
+
compression_stats["before_compression_total_bytes"] || detailed_size.total_bytes
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_total_bytes
|
43
|
+
compression_stats["after_compression_total_bytes"] || 0
|
24
44
|
end
|
25
45
|
|
26
46
|
private
|
data/lib/timescale/job.rb
CHANGED
@@ -3,11 +3,8 @@ module Timescale
|
|
3
3
|
self.table_name = "timescaledb_information.jobs"
|
4
4
|
self.primary_key = "job_id"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
attribute :retry_period, :interval
|
9
|
-
|
10
|
-
scope :compression, -> { where(proc_name: "tsbs_compress_chunks") }
|
6
|
+
scope :compression, -> { where(proc_name: [:tsbs_compress_chunks, :policy_compression]) }
|
7
|
+
scope :refresh_continuous_aggregate, -> { where(proc_name: :policy_refresh_continuous_aggregate) }
|
11
8
|
scope :scheduled, -> { where(scheduled: true) }
|
12
9
|
end
|
13
10
|
end
|
data/lib/timescale/job_stats.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
module Timescale
|
2
|
-
class
|
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") }
|
9
|
+
scope :resume, -> do
|
10
|
+
select("sum(total_successes)::int as success,
|
11
|
+
sum(total_runs)::int as runs,
|
12
|
+
sum(total_failures)::int as failures")
|
13
|
+
.to_a.map{|e|e.attributes.transform_keys(&:to_sym) }
|
14
|
+
end
|
11
15
|
end
|
16
|
+
JobStats = JobStat
|
12
17
|
end
|
@@ -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,6 +90,15 @@ 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
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'scenic/adapters/postgres'
|
2
|
+
require 'scenic/adapters/postgres/views'
|
3
|
+
|
4
|
+
module Timescale
|
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 definintions, 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,88 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
2
|
+
require 'active_support/core_ext/string/indent'
|
3
|
+
|
4
|
+
module Timescale
|
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 = Timescale::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
|
+
Timescale::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(Timescale::SchemaDumper)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "active_support/core_ext/numeric/conversions"
|
2
|
+
|
3
|
+
module Timescale
|
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_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/timescale/version.rb
CHANGED
data/lib/timescale.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
|
-
require "timescale/version"
|
2
1
|
require 'active_record'
|
2
|
+
|
3
|
+
require_relative 'timescale/acts_as_hypertable'
|
4
|
+
require_relative 'timescale/acts_as_hypertable/core'
|
3
5
|
require_relative 'timescale/chunk'
|
6
|
+
require_relative 'timescale/compression_settings'
|
7
|
+
require_relative 'timescale/continuous_aggregates'
|
8
|
+
require_relative 'timescale/dimensions'
|
4
9
|
require_relative 'timescale/hypertable'
|
5
10
|
require_relative 'timescale/job'
|
6
11
|
require_relative 'timescale/job_stats'
|
7
|
-
require_relative 'timescale/
|
8
|
-
require_relative 'timescale/
|
9
|
-
require_relative 'timescale/hypertable_helpers'
|
12
|
+
require_relative 'timescale/schema_dumper'
|
13
|
+
require_relative 'timescale/stats_report'
|
10
14
|
require_relative 'timescale/migration_helpers'
|
15
|
+
require_relative 'timescale/version'
|
11
16
|
|
12
17
|
module Timescale
|
13
18
|
module_function
|
19
|
+
|
14
20
|
def chunks
|
15
21
|
Chunk.all
|
16
22
|
end
|
@@ -34,4 +40,23 @@ module Timescale
|
|
34
40
|
def job_stats
|
35
41
|
JobStats.all
|
36
42
|
end
|
43
|
+
|
44
|
+
def stats(scope=Hypertable.all)
|
45
|
+
StatsReport.resume(scope)
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_hypertable_options
|
49
|
+
Timescale::ActsAsHypertable::DEFAULT_OPTIONS
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
require 'scenic'
|
55
|
+
require_relative 'timescale/scenic/adapter'
|
56
|
+
|
57
|
+
Scenic.configure do |config|
|
58
|
+
config.database = Timescale::Scenic::Adapter.new
|
59
|
+
end
|
60
|
+
rescue LoadError
|
61
|
+
# This is expected when the scenic gem is not being used
|
37
62
|
end
|
data/timescale.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Jônatas Davi Paganini"]
|
7
7
|
spec.email = ["jonatasdp@gmail.com"]
|
8
8
|
|
9
|
-
spec.summary = %q{
|
9
|
+
spec.summary = %q{TimescaleDB helpers for Ruby ecosystem.}
|
10
10
|
spec.description = %q{Functions from timescaledb available in the ActiveRecord models.}
|
11
11
|
spec.homepage = "https://github.com/jonatas/timescale"
|
12
12
|
spec.license = "MIT"
|
@@ -23,10 +23,18 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
24
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
25
|
end
|
26
|
-
spec.bindir = "
|
27
|
-
spec.executables = spec.files.grep(%r{^
|
26
|
+
spec.bindir = "bin"
|
27
|
+
spec.executables = spec.files.grep(%r{^bin/tsdb}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
spec.add_dependency
|
31
|
-
spec.add_dependency
|
30
|
+
spec.add_dependency "pg", "~> 1.2"
|
31
|
+
spec.add_dependency "activerecord"
|
32
|
+
spec.add_dependency "activesupport"
|
33
|
+
|
34
|
+
spec.add_development_dependency "pry"
|
35
|
+
spec.add_development_dependency "rspec-its"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
spec.add_development_dependency "dotenv"
|
38
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
39
|
+
spec.add_development_dependency "database_cleaner-active_record"
|
32
40
|
end
|