timescaledb-rails 0.1.2 → 0.1.4
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/README.md +120 -11
- data/lib/timescaledb/rails/extensions/active_record/base.rb +20 -0
- data/lib/timescaledb/rails/extensions/active_record/command_recorder.rb +82 -0
- data/lib/timescaledb/rails/extensions/active_record/postgresql_database_tasks.rb +43 -11
- data/lib/timescaledb/rails/extensions/active_record/schema_dumper.rb +35 -9
- data/lib/timescaledb/rails/extensions/active_record/schema_statements.rb +59 -3
- data/lib/timescaledb/rails/model/scopes.rb +59 -0
- data/lib/timescaledb/rails/model.rb +74 -0
- data/lib/timescaledb/rails/models/chunk.rb +30 -0
- data/lib/timescaledb/rails/models/hypertable.rb +52 -4
- data/lib/timescaledb/rails/models/job.rb +4 -0
- data/lib/timescaledb/rails/models.rb +2 -1
- data/lib/timescaledb/rails/railtie.rb +1 -20
- data/lib/timescaledb/rails/version.rb +1 -1
- data/lib/timescaledb/rails.rb +16 -0
- metadata +21 -3
- data/lib/timescaledb/rails/extensions/active_record/database_tasks.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12170e03389f6ac4f3bba7816dfb20dc75a47103032ec3e3799f3c78dca25a4b
|
4
|
+
data.tar.gz: f26796af01f755648e952f9e851f24d05d7adb3439e3653d793beef624ea8044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22c4c72512895fadb2a7701222e6b890b60a15948dc9c6b5c01ded24afff9865d6ebc954673452b55b95ac68b18c933dc2bf7aec6ac135285ecfa950d346e8e3
|
7
|
+
data.tar.gz: f67c442f8d2eb837566f8cc3a450d809a769343799997f3979a222bd0413c2ce89fad70b9b8f3e96267ab61e8bcf3cc2c167c15a60db1f1144ea17daf9cfee36
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
# TimescaleDB extension for Rails [](https://github.com/crunchloop/timescaledb-rails/actions?query=workflow%3ACI)
|
2
|
-
|
3
|
-
`timescaledb-rails` extends ActiveRecord PostgreSQL adapter and provides features from [`TimescaleDB`](https://www.timescale.com). It provides support for hypertables and other features added by TimescaleDB PostgreSQL extension.
|
1
|
+
# TimescaleDB extension for Rails [](https://badge.fury.io/rb/timescaledb-rails) [](https://github.com/crunchloop/timescaledb-rails/actions?query=workflow%3ACI)
|
4
2
|
|
3
|
+
`timescaledb-rails` extends ActiveRecord PostgreSQL adapter and provides features from [TimescaleDB](https://www.timescale.com). It provides support for hypertables and other features added by TimescaleDB PostgreSQL extension.
|
5
4
|
|
6
5
|
## Installation
|
7
6
|
|
@@ -17,16 +16,18 @@ Or include it in your project's `Gemfile` with Bundler:
|
|
17
16
|
gem 'timescaledb-rails', '~> 0.1'
|
18
17
|
```
|
19
18
|
|
20
|
-
##
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Migrations
|
21
22
|
|
22
|
-
Create a hypertable from a PostgreSQL table
|
23
|
+
Create a hypertable from a PostgreSQL table
|
23
24
|
|
24
25
|
```ruby
|
25
26
|
class CreateEvent < ActiveRecord::Migration[7.0]
|
26
27
|
def change
|
27
28
|
create_table :events, id: false do |t|
|
28
29
|
t.string :name, null: false
|
29
|
-
t.time :
|
30
|
+
t.time :occurred_at, null: false
|
30
31
|
|
31
32
|
t.timestamps
|
32
33
|
end
|
@@ -36,7 +37,7 @@ class CreateEvent < ActiveRecord::Migration[7.0]
|
|
36
37
|
end
|
37
38
|
```
|
38
39
|
|
39
|
-
Create a hypertable without a PostgreSQL table
|
40
|
+
Create a hypertable without a PostgreSQL table
|
40
41
|
|
41
42
|
```ruby
|
42
43
|
class CreatePayloadHypertable < ActiveRecord::Migration[7.0]
|
@@ -50,19 +51,127 @@ class CreatePayloadHypertable < ActiveRecord::Migration[7.0]
|
|
50
51
|
end
|
51
52
|
```
|
52
53
|
|
53
|
-
|
54
|
+
Add hypertable compression
|
54
55
|
|
55
56
|
```ruby
|
56
57
|
class AddEventCompression < ActiveRecord::Migration[7.0]
|
57
|
-
def
|
58
|
-
add_hypertable_compression :events, 20.days, segment_by: :name, order_by: '
|
58
|
+
def up
|
59
|
+
add_hypertable_compression :events, 20.days, segment_by: :name, order_by: 'occurred_at DESC'
|
60
|
+
end
|
61
|
+
|
62
|
+
def down
|
63
|
+
remove_hypertable_compression :events
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
Add hypertable retention policy
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class AddEventRetentionPolicy < ActiveRecord::Migration[7.0]
|
72
|
+
def up
|
73
|
+
add_hypertable_retention_policy :events, 1.year
|
74
|
+
end
|
75
|
+
|
76
|
+
def down
|
77
|
+
remove_hypertable_retention_policy :events
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Add hypertable reorder policy
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class AddEventReorderPolicy < ActiveRecord::Migration[7.0]
|
86
|
+
def up
|
87
|
+
add_hypertable_reorder_policy :events, :index_events_on_created_at_and_name
|
88
|
+
end
|
89
|
+
|
90
|
+
def down
|
91
|
+
remove_hypertable_reorder_policy :events
|
59
92
|
end
|
60
93
|
end
|
61
94
|
```
|
62
95
|
|
96
|
+
### Models
|
97
|
+
|
98
|
+
If one of your models need TimescaleDB support, just include `Timescaledb::Rails::Model`
|
99
|
+
```ruby
|
100
|
+
class Event < ActiveRecord::Base
|
101
|
+
include Timescaledb::Rails::Model
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
If the hypertable does not belong to the default schema, don't forget to override `table_name`
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class Event < ActiveRecord::Base
|
109
|
+
include Timescaledb::Rails::Model
|
110
|
+
|
111
|
+
self.table_name = 'v1.events'
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
If you need to query data for a specific time period, `Timescaledb::Rails::Model` incluldes useful scopes
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# If you want to get all records from last year
|
119
|
+
Event.last_year #=> [#<Event name...>, ...]
|
120
|
+
|
121
|
+
# Or if you want to get records from this year
|
122
|
+
Event.this_year #=> [#<Event name...>, ...]
|
123
|
+
|
124
|
+
# Or even getting records from today
|
125
|
+
Event.today #=> [#<Event name...>, ...]
|
126
|
+
```
|
127
|
+
|
128
|
+
Here the list of all available scopes
|
129
|
+
|
130
|
+
* last_year
|
131
|
+
* last_month
|
132
|
+
* last_week
|
133
|
+
* this_year
|
134
|
+
* this_month
|
135
|
+
* this_week
|
136
|
+
* yesterday
|
137
|
+
* today
|
138
|
+
|
139
|
+
If you need information about your hypertable, use the following helper methods to get useful information
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# Hypertable metadata
|
143
|
+
Event.hypertable #=> #<Timescaledb::Rails::Hypertable ...>
|
144
|
+
|
145
|
+
# Hypertable chunks metadata
|
146
|
+
Event.hypertable_chunks #=> [#<Timescaledb::Rails::Chunk ...>, ...]
|
147
|
+
|
148
|
+
# Hypertable jobs, it includes jobs like compression, retention or reorder policies, etc.
|
149
|
+
Event.hypertable_jobs #=> [#<Timescaledb::Rails::Job ...>, ...]
|
150
|
+
|
151
|
+
# Hypertable dimensions, like time or space dimensions
|
152
|
+
Event.hypertable_dimensions #=> [#<Timescaledb::Rails::Dimension ...>, ...]
|
153
|
+
|
154
|
+
# Hypertable compression settings
|
155
|
+
Event.hypertable_compression_settings #=> [#<Timescaledb::Rails::CompressionSetting ...>, ...]
|
156
|
+
```
|
157
|
+
|
158
|
+
If you need to compress or decompress a specific chunk
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
chunk = Event.hypertable_chunks.first
|
162
|
+
|
163
|
+
chunk.compress! unless chunk.is_compressed?
|
164
|
+
|
165
|
+
chunk.decompress! if chunk.is_compressed?
|
166
|
+
```
|
167
|
+
|
168
|
+
## Contributing
|
169
|
+
|
170
|
+
Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests.
|
171
|
+
|
63
172
|
## Supported Ruby/Rails versions
|
64
173
|
|
65
|
-
Supported Ruby/Rails versions are listed in [`.github/workflows/ci.yaml`](
|
174
|
+
Supported Ruby/Rails versions are listed in [`.github/workflows/ci.yaml`](./.github/workflows/ci.yaml)
|
66
175
|
|
67
176
|
## License
|
68
177
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module ActiveRecord
|
6
|
+
# :nodoc:
|
7
|
+
module Base
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# :nodoc:
|
11
|
+
module ClassMethods
|
12
|
+
# Returns if the current active record model is a hypertable.
|
13
|
+
def hypertable?
|
14
|
+
connection.hypertable_exists?(table_name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module ActiveRecord
|
6
|
+
# :nodoc:
|
7
|
+
module CommandRecorder
|
8
|
+
def create_hypertable(*args, &block)
|
9
|
+
record(:create_hypertable, args, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_hypertable_compression(*args, &block)
|
13
|
+
record(:add_hypertable_compression, args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove_hypertable_compression(*args, &block)
|
17
|
+
record(:remove_hypertable_compression, args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_hypertable_reorder_policy(*args, &block)
|
21
|
+
record(:add_hypertable_reorder_policy, args, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_hypertable_reorder_policy(*args, &block)
|
25
|
+
record(:remove_hypertable_reorder_policy, args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_hypertable_retention_policy(*args, &block)
|
29
|
+
record(:add_hypertable_retention_policy, args, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove_hypertable_retention_policy(*args, &block)
|
33
|
+
record(:remove_hypertable_retention_policy, args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def invert_create_hypertable(args, &block)
|
37
|
+
if block.nil?
|
38
|
+
raise ::ActiveRecord::IrreversibleMigration, 'create_hypertable is only reversible if given a block (can be empty).' # rubocop:disable Layout/LineLength
|
39
|
+
end
|
40
|
+
|
41
|
+
[:drop_table, args.first, block]
|
42
|
+
end
|
43
|
+
|
44
|
+
def invert_add_hypertable_compression(args, &block)
|
45
|
+
[:remove_hypertable_compression, args, block]
|
46
|
+
end
|
47
|
+
|
48
|
+
def invert_remove_hypertable_compression(args, &block)
|
49
|
+
if args.size < 2
|
50
|
+
raise ::ActiveRecord::IrreversibleMigration, 'remove_hypertable_compression is only reversible if given table name and compress period.' # rubocop:disable Layout/LineLength
|
51
|
+
end
|
52
|
+
|
53
|
+
[:add_hypertable_compression, args, block]
|
54
|
+
end
|
55
|
+
|
56
|
+
def invert_add_hypertable_retention_policy(args, &block)
|
57
|
+
[:remove_hypertable_retention_policy, args, block]
|
58
|
+
end
|
59
|
+
|
60
|
+
def invert_remove_hypertable_retention_policy(args, &block)
|
61
|
+
if args.size < 2
|
62
|
+
raise ::ActiveRecord::IrreversibleMigration, 'remove_hypertable_retention_policy is only reversible if given table name and drop after period.' # rubocop:disable Layout/LineLength
|
63
|
+
end
|
64
|
+
|
65
|
+
[:add_hypertable_retention_policy, args, block]
|
66
|
+
end
|
67
|
+
|
68
|
+
def invert_add_hypertable_reorder_policy(args, &block)
|
69
|
+
[:remove_hypertable_reorder_policy, args, block]
|
70
|
+
end
|
71
|
+
|
72
|
+
def invert_remove_hypertable_reorder_policy(args, &block)
|
73
|
+
if args.size < 2
|
74
|
+
raise ::ActiveRecord::IrreversibleMigration, 'remove_hypertable_reorder_policy is only reversible if given table name and index name.' # rubocop:disable Layout/LineLength
|
75
|
+
end
|
76
|
+
|
77
|
+
[:add_hypertable_reorder_policy, args, block]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -9,8 +9,11 @@ module Timescaledb
|
|
9
9
|
# :nodoc:
|
10
10
|
module PostgreSQLDatabaseTasks
|
11
11
|
# @override
|
12
|
-
def structure_dump(filename, extra_flags)
|
13
|
-
|
12
|
+
def structure_dump(filename, extra_flags) # rubocop:disable Metrics/MethodLength
|
13
|
+
extra_flags = Array(extra_flags)
|
14
|
+
extra_flags << timescale_structure_dump_default_flags if timescale_enabled?
|
15
|
+
|
16
|
+
super(filename, extra_flags)
|
14
17
|
|
15
18
|
return unless timescale_enabled?
|
16
19
|
|
@@ -19,6 +22,8 @@ module Timescaledb
|
|
19
22
|
drop_ts_insert_trigger_statment(hypertable, file)
|
20
23
|
create_hypertable_statement(hypertable, file)
|
21
24
|
add_hypertable_compression_statement(hypertable, file)
|
25
|
+
add_hypertable_reorder_policy_statement(hypertable, file)
|
26
|
+
add_hypertable_retention_policy_statement(hypertable, file)
|
22
27
|
end
|
23
28
|
end
|
24
29
|
end
|
@@ -45,30 +50,57 @@ module Timescaledb
|
|
45
50
|
file << "SELECT add_compression_policy('#{hypertable.hypertable_name}', INTERVAL '#{hypertable.compression_policy_interval}');\n\n" # rubocop:disable Layout/LineLength
|
46
51
|
end
|
47
52
|
|
53
|
+
def add_hypertable_reorder_policy_statement(hypertable, file)
|
54
|
+
return unless hypertable.reorder?
|
55
|
+
|
56
|
+
file << "SELECT add_reorder_policy('#{hypertable.hypertable_name}', '#{hypertable.reorder_policy_index_name}');\n\n" # rubocop:disable Layout/LineLength
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_hypertable_retention_policy_statement(hypertable, file)
|
60
|
+
return unless hypertable.retention?
|
61
|
+
|
62
|
+
file << "SELECT add_retention_policy('#{hypertable.hypertable_name}', INTERVAL '#{hypertable.retention_policy_interval}');\n\n" # rubocop:disable Layout/LineLength
|
63
|
+
end
|
64
|
+
|
48
65
|
def hypertable_options(hypertable)
|
49
66
|
sql_statements = ["if_not_exists => 'TRUE'"]
|
50
|
-
sql_statements << "chunk_time_interval => INTERVAL '#{hypertable.chunk_time_interval
|
67
|
+
sql_statements << "chunk_time_interval => INTERVAL '#{hypertable.chunk_time_interval}'"
|
51
68
|
|
52
69
|
sql_statements.compact.join(', ')
|
53
70
|
end
|
54
71
|
|
55
72
|
def hypertable_compression_options(hypertable)
|
56
|
-
segmentby_setting = hypertable.compression_settings.segmentby_setting.first
|
57
|
-
orderby_setting = hypertable.compression_settings.orderby_setting.first
|
58
|
-
|
59
73
|
sql_statements = ['timescaledb.compress']
|
60
|
-
sql_statements << "timescaledb.compress_segmentby = '#{segmentby_setting.attname}'" if segmentby_setting
|
61
74
|
|
62
|
-
if
|
63
|
-
|
64
|
-
|
75
|
+
if (segments = compression_segment_settings(hypertable)).present?
|
76
|
+
sql_statements << "timescaledb.compress_segmentby = '#{segments.join(', ')}'"
|
77
|
+
end
|
65
78
|
|
66
|
-
|
79
|
+
if (orders = compression_order_settings(hypertable)).present?
|
80
|
+
sql_statements << "timescaledb.compress_orderby = '#{orders.join(', ')}'"
|
67
81
|
end
|
68
82
|
|
69
83
|
sql_statements.join(', ')
|
70
84
|
end
|
71
85
|
|
86
|
+
def compression_order_settings(hypertable)
|
87
|
+
hypertable.compression_order_settings.map do |os|
|
88
|
+
Timescaledb::Rails::OrderbyCompression.new(os.attname, os.orderby_asc).to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def compression_segment_settings(hypertable)
|
93
|
+
hypertable.compression_segment_settings.map(&:attname)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns `pg_dump` flag to exclude `_timescaledb_internal` schema tables.
|
97
|
+
#
|
98
|
+
# @return [String]
|
99
|
+
def timescale_structure_dump_default_flags
|
100
|
+
'--exclude-schema=_timescaledb_internal'
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Boolean]
|
72
104
|
def timescale_enabled?
|
73
105
|
Timescaledb::Rails::Hypertable.table_exists?
|
74
106
|
end
|
@@ -16,6 +16,8 @@ module Timescaledb
|
|
16
16
|
|
17
17
|
hypertable(hypertable, stream)
|
18
18
|
hypertable_compression(hypertable, stream)
|
19
|
+
hypertable_reorder(hypertable, stream)
|
20
|
+
hypertable_retention(hypertable, stream)
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
@@ -38,6 +40,24 @@ module Timescaledb
|
|
38
40
|
stream.puts
|
39
41
|
end
|
40
42
|
|
43
|
+
def hypertable_reorder(hypertable, stream)
|
44
|
+
return unless hypertable.reorder?
|
45
|
+
|
46
|
+
options = [hypertable.hypertable_name.inspect, hypertable.reorder_policy_index_name.inspect]
|
47
|
+
|
48
|
+
stream.puts " add_hypertable_reorder_policy #{options.join(', ')}"
|
49
|
+
stream.puts
|
50
|
+
end
|
51
|
+
|
52
|
+
def hypertable_retention(hypertable, stream)
|
53
|
+
return unless hypertable.retention?
|
54
|
+
|
55
|
+
options = [hypertable.hypertable_name.inspect, hypertable.retention_policy_interval.inspect]
|
56
|
+
|
57
|
+
stream.puts " add_hypertable_retention_policy #{options.join(', ')}"
|
58
|
+
stream.puts
|
59
|
+
end
|
60
|
+
|
41
61
|
def hypertable_options(hypertable)
|
42
62
|
options = {
|
43
63
|
chunk_time_interval: hypertable.chunk_time_interval
|
@@ -50,21 +70,27 @@ module Timescaledb
|
|
50
70
|
end
|
51
71
|
|
52
72
|
def hypertable_compression_options(hypertable)
|
53
|
-
segmentby_setting = hypertable.compression_settings.segmentby_setting.first
|
54
|
-
orderby_setting = hypertable.compression_settings.orderby_setting.first
|
55
|
-
|
56
73
|
[].tap do |result|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
orderby = Timescaledb::Rails::OrderbyCompression.new(orderby_setting.attname,
|
61
|
-
orderby_setting.orderby_asc).to_s
|
74
|
+
if (segments = compression_segment_settings(hypertable)).present?
|
75
|
+
result << "segment_by: #{segments.join(', ').inspect}"
|
76
|
+
end
|
62
77
|
|
63
|
-
|
78
|
+
if (orders = compression_order_settings(hypertable)).present?
|
79
|
+
result << "order_by: #{orders.join(', ').inspect}"
|
64
80
|
end
|
65
81
|
end
|
66
82
|
end
|
67
83
|
|
84
|
+
def compression_order_settings(hypertable)
|
85
|
+
hypertable.compression_order_settings.map do |os|
|
86
|
+
Timescaledb::Rails::OrderbyCompression.new(os.attname, os.orderby_asc).to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def compression_segment_settings(hypertable)
|
91
|
+
hypertable.compression_segment_settings.map(&:attname)
|
92
|
+
end
|
93
|
+
|
68
94
|
def format_hypertable_option_value(value)
|
69
95
|
case value
|
70
96
|
when String then value.inspect
|
@@ -1,12 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
4
|
-
|
5
3
|
module Timescaledb
|
6
4
|
module Rails
|
7
5
|
module ActiveRecord
|
8
6
|
# :nodoc:
|
9
7
|
module SchemaStatements
|
8
|
+
# Returns an array of hypertable names defined in the database.
|
9
|
+
def hypertables
|
10
|
+
query_values('SELECT hypertable_name FROM timescaledb_information.hypertables')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Checks to see if the hypertable exists on the database.
|
14
|
+
#
|
15
|
+
# hypertable_exists?(:developers)
|
16
|
+
#
|
17
|
+
def hypertable_exists?(hypertable)
|
18
|
+
query_value(
|
19
|
+
<<-SQL.squish
|
20
|
+
SELECT COUNT(*) FROM timescaledb_information.hypertables WHERE hypertable_name = #{quote(hypertable)}
|
21
|
+
SQL
|
22
|
+
).to_i.positive?
|
23
|
+
end
|
24
|
+
|
10
25
|
# Converts given standard PG table into a hypertable.
|
11
26
|
#
|
12
27
|
# create_hypertable('readings', 'created_at', chunk_time_interval: '7 days')
|
@@ -38,9 +53,50 @@ module Timescaledb
|
|
38
53
|
|
39
54
|
execute "ALTER TABLE #{table_name} SET (#{options.join(', ')})"
|
40
55
|
|
41
|
-
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compress_after}')"
|
56
|
+
execute "SELECT add_compression_policy('#{table_name}', INTERVAL '#{compress_after.inspect}')"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Disables compression from given table.
|
60
|
+
#
|
61
|
+
# remove_hypertable_compression('events')
|
62
|
+
#
|
63
|
+
def remove_hypertable_compression(table_name, compress_after = nil, segment_by: nil, order_by: nil) # rubocop:disable Lint/UnusedMethodArgument
|
64
|
+
execute "SELECT remove_compression_policy('#{table_name.inspect}');"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add a data retention policy to given hypertable.
|
68
|
+
#
|
69
|
+
# add_hypertable_retention_policy('events', 7.days)
|
70
|
+
#
|
71
|
+
def add_hypertable_retention_policy(table_name, drop_after)
|
72
|
+
execute "SELECT add_retention_policy('#{table_name}', INTERVAL '#{drop_after.inspect}')"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Removes data retention policy from given hypertable.
|
76
|
+
#
|
77
|
+
# remove_hypertable_retention_policy('events')
|
78
|
+
#
|
79
|
+
def remove_hypertable_retention_policy(table_name, _drop_after = nil)
|
80
|
+
execute "SELECT remove_retention_policy('#{table_name}')"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Adds a policy to reorder chunks on a given hypertable index in the background.
|
84
|
+
#
|
85
|
+
# add_hypertable_reorder_policy('events', 'index_events_on_created_at_and_name')
|
86
|
+
#
|
87
|
+
def add_hypertable_reorder_policy(table_name, index_name)
|
88
|
+
execute "SELECT add_reorder_policy('#{table_name}', '#{index_name}')"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Removes a policy to reorder a particular hypertable.
|
92
|
+
#
|
93
|
+
# remove_hypertable_reorder_policy('events')
|
94
|
+
#
|
95
|
+
def remove_hypertable_reorder_policy(table_name, _index_name = nil)
|
96
|
+
execute "SELECT remove_reorder_policy('#{table_name}')"
|
42
97
|
end
|
43
98
|
|
99
|
+
# @return [String]
|
44
100
|
def hypertable_options_to_sql(options)
|
45
101
|
sql_statements = options.map do |option, value|
|
46
102
|
case option
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
module Model
|
6
|
+
# :nodoc:
|
7
|
+
module Scopes
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# rubocop:disable Metrics/BlockLength
|
11
|
+
included do
|
12
|
+
scope :last_year, lambda {
|
13
|
+
date = Date.current - 1.year
|
14
|
+
|
15
|
+
between_time_column(date.beginning_of_year, date.end_of_year)
|
16
|
+
}
|
17
|
+
|
18
|
+
scope :last_month, lambda {
|
19
|
+
date = Date.current - 1.month
|
20
|
+
|
21
|
+
between_time_column(date.beginning_of_month, date.end_of_month)
|
22
|
+
}
|
23
|
+
|
24
|
+
scope :last_week, lambda {
|
25
|
+
date = Date.current - 1.week
|
26
|
+
|
27
|
+
between_time_column(date.beginning_of_week, date.end_of_week)
|
28
|
+
}
|
29
|
+
|
30
|
+
scope :yesterday, lambda {
|
31
|
+
where("DATE(#{hypertable_time_column_name}) = ?", Date.current - 1.day)
|
32
|
+
}
|
33
|
+
|
34
|
+
scope :this_year, lambda {
|
35
|
+
between_time_column(Date.current.beginning_of_year, Date.current.end_of_year)
|
36
|
+
}
|
37
|
+
|
38
|
+
scope :this_month, lambda {
|
39
|
+
between_time_column(Date.current.beginning_of_month, Date.current.end_of_month)
|
40
|
+
}
|
41
|
+
|
42
|
+
scope :this_week, lambda {
|
43
|
+
between_time_column(Date.current.beginning_of_week, Date.current.end_of_week)
|
44
|
+
}
|
45
|
+
|
46
|
+
scope :today, lambda {
|
47
|
+
where("DATE(#{hypertable_time_column_name}) = ?", Date.current)
|
48
|
+
}
|
49
|
+
|
50
|
+
# @!visibility private
|
51
|
+
scope :between_time_column, lambda { |from, to|
|
52
|
+
where("DATE(#{hypertable_time_column_name}) BETWEEN ? AND ?", from, to)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
# rubocop:enable Metrics/BlockLength
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timescaledb/rails/model/scopes'
|
4
|
+
|
5
|
+
module Timescaledb
|
6
|
+
module Rails
|
7
|
+
# :nodoc:
|
8
|
+
module Model
|
9
|
+
PUBLIC_SCHEMA_NAME = 'public'
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
include Scopes
|
14
|
+
|
15
|
+
# :nodoc:
|
16
|
+
module ClassMethods
|
17
|
+
delegate :time_column_name, to: :hypertable, prefix: true
|
18
|
+
|
19
|
+
# Returns only the name of the hypertable, table_name could include
|
20
|
+
# the schema path, we need to remove it.
|
21
|
+
#
|
22
|
+
# @return [String]
|
23
|
+
def hypertable_name
|
24
|
+
table_name.split('.').last
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the schema where hypertable is stored.
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
def hypertable_schema
|
31
|
+
if table_name.split('.').size > 1
|
32
|
+
table_name.split('.')[0..-2].join('.')
|
33
|
+
else
|
34
|
+
PUBLIC_SCHEMA_NAME
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Timescaledb::Rails::Hypertable]
|
39
|
+
def hypertable
|
40
|
+
Timescaledb::Rails::Hypertable.find_by(hypertable_where_options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Chunk>]
|
44
|
+
def hypertable_chunks
|
45
|
+
Timescaledb::Rails::Chunk.where(hypertable_where_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Job>]
|
49
|
+
def hypertable_jobs
|
50
|
+
Timescaledb::Rails::Job.where(hypertable_where_options)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::Dimension>]
|
54
|
+
def hypertable_dimensions
|
55
|
+
Timescaledb::Rails::Dimension.where(hypertable_where_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [ActiveRecord::Relation<Timescaledb::Rails::CompressionSetting>]
|
59
|
+
def hypertable_compression_settings
|
60
|
+
Timescaledb::Rails::CompressionSetting.where(hypertable_where_options)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Returns hypertable name and schema.
|
66
|
+
#
|
67
|
+
# @return [Hash]
|
68
|
+
def hypertable_where_options
|
69
|
+
{ hypertable_name: hypertable_name, hypertable_schema: hypertable_schema }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Timescaledb
|
4
|
+
module Rails
|
5
|
+
# :nodoc:
|
6
|
+
class Chunk < ::ActiveRecord::Base
|
7
|
+
self.table_name = 'timescaledb_information.chunks'
|
8
|
+
self.primary_key = 'hypertable_name'
|
9
|
+
|
10
|
+
scope :compressed, -> { where(is_compressed: true) }
|
11
|
+
scope :decompressed, -> { where(is_compressed: false) }
|
12
|
+
|
13
|
+
def chunk_full_name
|
14
|
+
"#{chunk_schema}.#{chunk_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def compress!
|
18
|
+
::ActiveRecord::Base.connection.execute(
|
19
|
+
"SELECT compress_chunk('#{chunk_full_name}')"
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def decompress!
|
24
|
+
::ActiveRecord::Base.connection.execute(
|
25
|
+
"SELECT decompress_chunk('#{chunk_full_name}')"
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -11,6 +11,7 @@ module Timescaledb
|
|
11
11
|
class_name: 'Timescaledb::Rails::CompressionSetting'
|
12
12
|
has_many :dimensions, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Dimension'
|
13
13
|
has_many :jobs, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Job'
|
14
|
+
has_many :chunks, foreign_key: 'hypertable_name', class_name: 'Timescaledb::Rails::Chunk'
|
14
15
|
|
15
16
|
# @return [String]
|
16
17
|
def time_column_name
|
@@ -19,14 +20,34 @@ module Timescaledb
|
|
19
20
|
|
20
21
|
# @return [String]
|
21
22
|
def chunk_time_interval
|
22
|
-
time_dimension.time_interval
|
23
|
+
interval = time_dimension.time_interval
|
24
|
+
|
25
|
+
interval.is_a?(String) ? interval : interval.inspect
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [ActiveRecord::Relation<CompressionSetting>]
|
29
|
+
def compression_segment_settings
|
30
|
+
compression_settings.segmentby_setting
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [ActiveRecord::Relation<CompressionSetting>]
|
34
|
+
def compression_order_settings
|
35
|
+
compression_settings.orderby_setting.where.not(attname: time_column_name)
|
23
36
|
end
|
24
37
|
|
25
38
|
# @return [String]
|
26
39
|
def compression_policy_interval
|
27
|
-
|
28
|
-
|
29
|
-
|
40
|
+
parse_duration(compression_job.config['compress_after'])
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String]
|
44
|
+
def reorder_policy_index_name
|
45
|
+
reorder_job.config['index_name']
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [String]
|
49
|
+
def retention_policy_interval
|
50
|
+
parse_duration(retention_job.config['drop_after'])
|
30
51
|
end
|
31
52
|
|
32
53
|
# @return [Boolean]
|
@@ -34,8 +55,28 @@ module Timescaledb
|
|
34
55
|
compression_job.present?
|
35
56
|
end
|
36
57
|
|
58
|
+
# @return [Boolean]
|
59
|
+
def reorder?
|
60
|
+
reorder_job.present?
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Boolean]
|
64
|
+
def retention?
|
65
|
+
retention_job.present?
|
66
|
+
end
|
67
|
+
|
37
68
|
private
|
38
69
|
|
70
|
+
# @return [Job]
|
71
|
+
def reorder_job
|
72
|
+
@reorder_job ||= jobs.policy_reorder.first
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Job]
|
76
|
+
def retention_job
|
77
|
+
@retention_job ||= jobs.policy_retention.first
|
78
|
+
end
|
79
|
+
|
39
80
|
# @return [Job]
|
40
81
|
def compression_job
|
41
82
|
@compression_job ||= jobs.policy_compression.first
|
@@ -45,6 +86,13 @@ module Timescaledb
|
|
45
86
|
def time_dimension
|
46
87
|
@time_dimension ||= dimensions.time.first
|
47
88
|
end
|
89
|
+
|
90
|
+
# @return [String]
|
91
|
+
def parse_duration(duration)
|
92
|
+
ActiveSupport::Duration.parse(duration).inspect
|
93
|
+
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
|
94
|
+
duration
|
95
|
+
end
|
48
96
|
end
|
49
97
|
end
|
50
98
|
end
|
@@ -8,8 +8,12 @@ module Timescaledb
|
|
8
8
|
self.primary_key = 'hypertable_name'
|
9
9
|
|
10
10
|
POLICY_COMPRESSION = 'policy_compression'
|
11
|
+
POLICY_REORDER = 'policy_reorder'
|
12
|
+
POLICY_RETENTION = 'policy_retention'
|
11
13
|
|
12
14
|
scope :policy_compression, -> { where(proc_name: POLICY_COMPRESSION) }
|
15
|
+
scope :policy_reorder, -> { where(proc_name: POLICY_REORDER) }
|
16
|
+
scope :policy_retention, -> { where(proc_name: POLICY_RETENTION) }
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './models/chunk'
|
3
4
|
require_relative './models/compression_setting'
|
4
5
|
require_relative './models/dimension'
|
5
|
-
require_relative './models/job'
|
6
6
|
require_relative './models/hypertable'
|
7
|
+
require_relative './models/job'
|
@@ -2,11 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'rails'
|
4
4
|
|
5
|
-
require_relative 'extensions/active_record/database_tasks'
|
6
|
-
require_relative 'extensions/active_record/postgresql_database_tasks'
|
7
|
-
require_relative 'extensions/active_record/schema_dumper'
|
8
|
-
require_relative 'extensions/active_record/schema_statements'
|
9
|
-
|
10
5
|
module Timescaledb
|
11
6
|
module Rails
|
12
7
|
# :nodoc:
|
@@ -19,21 +14,7 @@ module Timescaledb
|
|
19
14
|
|
20
15
|
initializer 'timescaledb-rails.add_timescale_support_to_active_record' do
|
21
16
|
ActiveSupport.on_load(:active_record) do
|
22
|
-
::
|
23
|
-
Timescaledb::Rails::ActiveRecord::DatabaseTasks
|
24
|
-
)
|
25
|
-
|
26
|
-
::ActiveRecord::Tasks::PostgreSQLDatabaseTasks.prepend(
|
27
|
-
Timescaledb::Rails::ActiveRecord::PostgreSQLDatabaseTasks
|
28
|
-
)
|
29
|
-
|
30
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(
|
31
|
-
Timescaledb::Rails::ActiveRecord::SchemaStatements
|
32
|
-
)
|
33
|
-
|
34
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(
|
35
|
-
Timescaledb::Rails::ActiveRecord::SchemaDumper
|
36
|
-
)
|
17
|
+
Timescaledb::Rails.load
|
37
18
|
end
|
38
19
|
end
|
39
20
|
end
|
data/lib/timescaledb/rails.rb
CHANGED
@@ -1,7 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './rails/model'
|
4
|
+
|
5
|
+
require_relative './rails/extensions/active_record/base'
|
6
|
+
require_relative './rails/extensions/active_record/command_recorder'
|
7
|
+
require_relative './rails/extensions/active_record/postgresql_database_tasks'
|
8
|
+
require_relative './rails/extensions/active_record/schema_dumper'
|
9
|
+
require_relative './rails/extensions/active_record/schema_statements'
|
10
|
+
|
3
11
|
module Timescaledb
|
4
12
|
# :nodoc:
|
5
13
|
module Rails
|
14
|
+
# Adds TimescaleDB support to ActiveRecord.
|
15
|
+
def self.load
|
16
|
+
::ActiveRecord::Migration::CommandRecorder.prepend(ActiveRecord::CommandRecorder)
|
17
|
+
::ActiveRecord::Tasks::PostgreSQLDatabaseTasks.prepend(ActiveRecord::PostgreSQLDatabaseTasks)
|
18
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::SchemaStatements)
|
19
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(ActiveRecord::SchemaDumper)
|
20
|
+
::ActiveRecord::Base.include(ActiveRecord::Base) # rubocop:disable Rails/ActiveSupportOnLoad
|
21
|
+
end
|
6
22
|
end
|
7
23
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timescaledb-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Iván Etchart
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-12-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -25,6 +25,20 @@ dependencies:
|
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '6.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: debug
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1.0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1.0'
|
28
42
|
- !ruby/object:Gem::Dependency
|
29
43
|
name: pg
|
30
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -119,11 +133,15 @@ files:
|
|
119
133
|
- README.md
|
120
134
|
- lib/timescaledb-rails.rb
|
121
135
|
- lib/timescaledb/rails.rb
|
122
|
-
- lib/timescaledb/rails/extensions/active_record/
|
136
|
+
- lib/timescaledb/rails/extensions/active_record/base.rb
|
137
|
+
- lib/timescaledb/rails/extensions/active_record/command_recorder.rb
|
123
138
|
- lib/timescaledb/rails/extensions/active_record/postgresql_database_tasks.rb
|
124
139
|
- lib/timescaledb/rails/extensions/active_record/schema_dumper.rb
|
125
140
|
- lib/timescaledb/rails/extensions/active_record/schema_statements.rb
|
141
|
+
- lib/timescaledb/rails/model.rb
|
142
|
+
- lib/timescaledb/rails/model/scopes.rb
|
126
143
|
- lib/timescaledb/rails/models.rb
|
144
|
+
- lib/timescaledb/rails/models/chunk.rb
|
127
145
|
- lib/timescaledb/rails/models/compression_setting.rb
|
128
146
|
- lib/timescaledb/rails/models/dimension.rb
|
129
147
|
- lib/timescaledb/rails/models/hypertable.rb
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Timescaledb
|
4
|
-
module Rails
|
5
|
-
module ActiveRecord
|
6
|
-
# :nodoc:
|
7
|
-
module DatabaseTasks
|
8
|
-
# @override
|
9
|
-
def structure_dump_flags_for(adapter)
|
10
|
-
return (flags = super) if adapter != 'postgresql'
|
11
|
-
|
12
|
-
if flags.nil?
|
13
|
-
timescaledb_structure_dump_default_flags
|
14
|
-
elsif flags.is_a?(Array)
|
15
|
-
flags << timescaledb_structure_dump_default_flags
|
16
|
-
elsif flags.is_a?(String)
|
17
|
-
"#{flags} #{timescaledb_structure_dump_default_flags}"
|
18
|
-
else
|
19
|
-
flags
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns `pg_dump` flag to exclude `_timescaledb_internal` schema tables.
|
24
|
-
#
|
25
|
-
# @return [String]
|
26
|
-
def timescaledb_structure_dump_default_flags
|
27
|
-
'--exclude-schema=_timescaledb_internal'
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|