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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/.tool-versions +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +11 -3
  7. data/Gemfile.scenic +7 -0
  8. data/Gemfile.scenic.lock +121 -0
  9. data/README.md +267 -121
  10. data/Rakefile +16 -1
  11. data/bin/console +2 -2
  12. data/bin/setup +2 -0
  13. data/bin/tsdb +30 -6
  14. data/examples/{Gemfile → all_in_one/Gemfile} +1 -1
  15. data/examples/{Gemfile.lock → all_in_one/Gemfile.lock} +2 -2
  16. data/examples/{all_in_one.rb → all_in_one/all_in_one.rb} +3 -6
  17. data/examples/ranking/.gitattributes +7 -0
  18. data/examples/ranking/.gitignore +29 -0
  19. data/examples/ranking/.ruby-version +1 -0
  20. data/examples/ranking/Gemfile +33 -0
  21. data/examples/ranking/Gemfile.lock +189 -0
  22. data/examples/ranking/README.md +166 -0
  23. data/examples/ranking/Rakefile +6 -0
  24. data/examples/ranking/app/controllers/application_controller.rb +2 -0
  25. data/examples/ranking/app/controllers/concerns/.keep +0 -0
  26. data/examples/ranking/app/jobs/application_job.rb +7 -0
  27. data/examples/ranking/app/models/application_record.rb +3 -0
  28. data/examples/ranking/app/models/concerns/.keep +0 -0
  29. data/examples/ranking/app/models/game.rb +2 -0
  30. data/examples/ranking/app/models/play.rb +7 -0
  31. data/examples/ranking/bin/bundle +114 -0
  32. data/examples/ranking/bin/rails +4 -0
  33. data/examples/ranking/bin/rake +4 -0
  34. data/examples/ranking/bin/setup +33 -0
  35. data/examples/ranking/config/application.rb +39 -0
  36. data/examples/ranking/config/boot.rb +4 -0
  37. data/examples/ranking/config/credentials.yml.enc +1 -0
  38. data/examples/ranking/config/database.yml +86 -0
  39. data/examples/ranking/config/environment.rb +5 -0
  40. data/examples/ranking/config/environments/development.rb +60 -0
  41. data/examples/ranking/config/environments/production.rb +75 -0
  42. data/examples/ranking/config/environments/test.rb +53 -0
  43. data/examples/ranking/config/initializers/cors.rb +16 -0
  44. data/examples/ranking/config/initializers/filter_parameter_logging.rb +8 -0
  45. data/examples/ranking/config/initializers/inflections.rb +16 -0
  46. data/examples/ranking/config/initializers/timescale.rb +3 -0
  47. data/examples/ranking/config/locales/en.yml +33 -0
  48. data/examples/ranking/config/puma.rb +43 -0
  49. data/examples/ranking/config/routes.rb +6 -0
  50. data/examples/ranking/config/storage.yml +34 -0
  51. data/examples/ranking/config.ru +6 -0
  52. data/examples/ranking/db/migrate/20220209120747_create_games.rb +10 -0
  53. data/examples/ranking/db/migrate/20220209120910_create_plays.rb +19 -0
  54. data/examples/ranking/db/migrate/20220209143347_create_score_per_hours.rb +5 -0
  55. data/examples/ranking/db/schema.rb +47 -0
  56. data/examples/ranking/db/seeds.rb +7 -0
  57. data/examples/ranking/db/views/score_per_hours_v01.sql +7 -0
  58. data/examples/ranking/lib/tasks/.keep +0 -0
  59. data/examples/ranking/log/.keep +0 -0
  60. data/examples/ranking/public/robots.txt +1 -0
  61. data/examples/ranking/storage/.keep +0 -0
  62. data/examples/ranking/tmp/.keep +0 -0
  63. data/examples/ranking/tmp/pids/.keep +0 -0
  64. data/examples/ranking/tmp/storage/.keep +0 -0
  65. data/examples/ranking/vendor/.keep +0 -0
  66. data/lib/timescaledb/acts_as_hypertable/core.rb +87 -0
  67. data/lib/timescaledb/acts_as_hypertable.rb +62 -0
  68. data/lib/{timescale → timescaledb}/chunk.rb +9 -1
  69. data/lib/{timescale → timescaledb}/compression_settings.rb +3 -2
  70. data/lib/timescaledb/continuous_aggregates.rb +19 -0
  71. data/lib/timescaledb/dimensions.rb +6 -0
  72. data/lib/{timescale → timescaledb}/hypertable.rb +9 -5
  73. data/lib/timescaledb/job.rb +10 -0
  74. data/lib/{timescale → timescaledb}/job_stats.rb +3 -4
  75. data/lib/{timescale → timescaledb}/migration_helpers.rb +35 -5
  76. data/lib/timescaledb/scenic/adapter.rb +55 -0
  77. data/lib/timescaledb/scenic/extension.rb +72 -0
  78. data/lib/timescaledb/schema_dumper.rb +88 -0
  79. data/lib/timescaledb/stats_report.rb +35 -0
  80. data/lib/timescaledb/version.rb +3 -0
  81. data/lib/timescaledb.rb +64 -0
  82. data/{timescale.gemspec → timescaledb.gemspec} +6 -4
  83. metadata +106 -20
  84. data/lib/timescale/acts_as_hypertable.rb +0 -114
  85. data/lib/timescale/continuous_aggregates.rb +0 -9
  86. data/lib/timescale/job.rb +0 -13
  87. data/lib/timescale/stats_report.rb +0 -28
  88. data/lib/timescale/version.rb +0 -3
  89. 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 Timescale
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 Timescale
2
- class CompressionSettings < ActiveRecord::Base
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
@@ -0,0 +1,6 @@
1
+ module Timescaledb
2
+ class Dimension < ActiveRecord::Base
3
+ self.table_name = "timescaledb_information.dimensions"
4
+ end
5
+ Dimensions = Dimension
6
+ end
@@ -1,7 +1,6 @@
1
- module Timescale
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: "Timescale::CompressionSettings"
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: "Timescale::ContinuousAggregates"
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
- struct_from("SELECT * from hypertable_compression_stats('#{self.hypertable_name}')").first || {}
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 Timescale
2
- class JobStats < ActiveRecord::Base
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 Timescale
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
- setup_hypertable_options(table_name, **options[:hypertable]) if options.key?(:hypertable)
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 setup_hypertable_options(table_name,
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
- def create_continuous_aggregates(name, query, **options)
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(Timescale::MigrationHelpers)
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
@@ -0,0 +1,3 @@
1
+ module Timescaledb
2
+ VERSION = '0.2.0'
3
+ end
@@ -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