sequel-tstzrange-fields 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 35ed468cd261b95f2381b6f90c5e1b8afa2318aa406ce06fbec30e8fdb85b89e
4
+ data.tar.gz: f501a551e2decbcc47ea133a0527921c182049a48248692ae0d0dea205b04471
5
+ SHA512:
6
+ metadata.gz: 9d8fe7ed1837254fa1cc6a183f780d29af01e27367dbbe56e2a7380446ff7aa36808ead45bbf65e562a5e8aff849d8e04f9019a2db40f79f75d709495c1da160
7
+ data.tar.gz: 0b94430bb0c3cc0eec9e37309493f08c76fceda6277d71d9918ff12d0ee1b0aaafd5cae364c2eb6530ac72f1f245f1f2ce9c3f698544f0a58dc3c445c604459b
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require 'sequel/model'
5
+ require 'yajl'
6
+
7
+ # Plugin for adding methods for working with time ranges.
8
+ #
9
+ # == Example
10
+ #
11
+ # Defining a model class with a timestamptz range:
12
+ #
13
+ # class ACME::Lease < Sequel::Model(:leases)
14
+ # plugin :tstzrange_fields, :active_during
15
+ #
16
+ # And in the schema:
17
+ #
18
+ # create_table(:leases) do
19
+ # primary_key :id
20
+ # tstzrange :active_during
21
+ # end
22
+ #
23
+ # You can use it as follows:
24
+ #
25
+ # lease = ACME::Lease.new
26
+ # lease.active_during_begin = Time.now
27
+ # lease.active_during_end = 1.year.from_now
28
+ # lease.active_during = 1.year.ago..1.year.from_now
29
+ # lease.active_during_end = nil # Unbounded end set
30
+ # lease.active_during = nil # Empty set
31
+ #
32
+ module Sequel
33
+ module Plugins
34
+ module TstzrangeFields
35
+ VERSION = '0.1.1'
36
+
37
+ def self.configure(model, *args)
38
+ unless model.db.schema_type_class(:tstzrange)
39
+ msg = 'tstzrange_fields plugin requires pg_range db extension to be installed. ' \
40
+ ' Use db.extension(:pg_range) after the db = Sequel.connect call.'
41
+ raise msg
42
+ end
43
+ args << :period if args.empty?
44
+ args = args.flatten
45
+
46
+ setup_model(model)
47
+
48
+ args.flatten.each do |column|
49
+ create_accessors(model, column)
50
+ end
51
+ end
52
+
53
+ def self.setup_model(model)
54
+ model.class.define_method(:new_tstzrange) do |b, e|
55
+ b = value_to_time(b)
56
+ e = value_to_time(e)
57
+ return Sequel::Postgres::PGRange.empty(:tstzrange) if b.nil? && e.nil?
58
+
59
+ return Sequel::Postgres::PGRange.new(b&.to_time, e&.to_time, db_type: :tstzrange, exclude_end: true)
60
+ end
61
+
62
+ model.class.define_method(:value_to_time) do |v|
63
+ return v if v.nil?
64
+ return v if v.respond_to?(:to_time)
65
+
66
+ return Time.parse(v)
67
+ end
68
+ end
69
+
70
+ def self.create_accessors(model, column)
71
+ get_column_method = column.to_sym
72
+ set_column_method = "#{column}=".to_sym
73
+ get_begin_method = "#{column}_begin".to_sym
74
+ set_begin_method = "#{column}_begin=".to_sym
75
+ get_end_method = "#{column}_end".to_sym
76
+ set_end_method = "#{column}_end=".to_sym
77
+
78
+ model.define_method(get_column_method) do
79
+ self[column]
80
+ end
81
+
82
+ model.define_method(set_column_method) do |value|
83
+ case value
84
+ when Sequel::Postgres::PGRange
85
+ self[column] = value
86
+ when Float::INFINITY
87
+ range = Sequel::Postgres::PGRange.new(nil, nil, empty: false, db_type: :tstzrange)
88
+ self[column] = range
89
+ when 'empty'
90
+ self[column] = Sequel::Postgres::PGRange.empty(:tstzrange)
91
+ else
92
+ beg = value.respond_to?(:begin) ? value.begin : (value[:begin] || value['begin'])
93
+ en = value.respond_to?(:end) ? value.end : (value[:end] || value['end'])
94
+ self[column] = self.class.new_tstzrange(beg, en)
95
+ end
96
+ end
97
+
98
+ model.define_method(get_begin_method) do
99
+ send(get_column_method).begin
100
+ end
101
+
102
+ model.define_method(set_begin_method) do |new_time|
103
+ new_range = self.class.new_tstzrange(new_time, send(get_end_method))
104
+ send(set_column_method, new_range)
105
+ end
106
+
107
+ model.define_method(get_end_method) do
108
+ r = send(get_column_method)
109
+ return r.nil? ? nil : r.end # &.end is invalid syntax
110
+ end
111
+
112
+ model.define_method(set_end_method) do |new_time|
113
+ new_range = self.class.new_tstzrange(send(get_begin_method), new_time)
114
+ send(set_column_method, new_range)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/integer/time'
4
+ require 'sequel'
5
+ require 'sequel/model'
6
+ require 'sequel/extensions/pg_range'
7
+ require 'sequel/plugins/tstzrange_fields'
8
+
9
+ RSpec.describe Sequel::Plugins::TstzrangeFields do
10
+ before(:each) do
11
+ @db = Sequel.connect('postgres://sequel_tstzrange:sequel_tstzrange@localhost:18101/sequel_tstzrange_test')
12
+ @db.extension(:pg_range)
13
+ end
14
+ after(:each) do
15
+ @db.disconnect
16
+ end
17
+
18
+ it 'errors if the db does not have pg_range registered' do
19
+ db = Sequel.connect('postgres://sequel_tstzrange:sequel_tstzrange@localhost:18101/sequel_tstzrange_test')
20
+ db.create_table(:tstzrange_fields_test, temp: true) do
21
+ primary_key :id
22
+ end
23
+ expect do
24
+ mc = Class.new(Sequel::Model(db[:tstzrange_fields_test]))
25
+ mc.plugin(:tstzrange_fields)
26
+ end.to raise_error(/tstzrange_fields plugin requires/)
27
+ end
28
+
29
+ context 'with no fields given' do
30
+ let(:model_class) do
31
+ @db.create_table(:tstzrange_fields_test, temp: true) do
32
+ primary_key :id
33
+ tstzrange :period
34
+ end
35
+ mc = Class.new(Sequel::Model(@db[:tstzrange_fields_test]))
36
+ mc.class_eval do
37
+ def initialize(*)
38
+ super
39
+ self[:period] ||= self.class.new_tstzrange(nil, nil)
40
+ end
41
+ end
42
+ mc.plugin(:tstzrange_fields)
43
+ mc
44
+ end
45
+
46
+ let(:model_object) { model_class.new }
47
+
48
+ it 'uses :period as the high-level accessor' do
49
+ expect(model_object).to respond_to(:period, :period=, :period_begin, :period_begin=, :period_end, :period_end=)
50
+ end
51
+
52
+ it 'sets a default empty range' do
53
+ expect(model_object.period).to be_empty
54
+ end
55
+ end
56
+
57
+ context 'for the given field' do
58
+ let(:model_class) do
59
+ @db.create_table(:tstzrange_fields_test, temp: true) do
60
+ primary_key :id
61
+ tstzrange :range
62
+ end
63
+ mc = Class.new(Sequel::Model(@db[:tstzrange_fields_test]))
64
+ mc.plugin(:tstzrange_fields, :range)
65
+ mc.class_eval do
66
+ def initialize(*)
67
+ super
68
+ self[:range] ||= self.class.new_tstzrange(nil, nil)
69
+ end
70
+ end
71
+ mc
72
+ end
73
+
74
+ let(:model_object) { model_class.new }
75
+
76
+ let(:t) { Time.at(3.years.ago.to_i) }
77
+ let(:ts) { t.to_s }
78
+
79
+ it 'sets a default empty range' do
80
+ expect(model_object.range).to be_empty
81
+ end
82
+
83
+ it 'can set an infinite range by assigning the field to Float::INFINITY' do
84
+ expect(model_object.range).to be_empty
85
+
86
+ model_object.range = Float::INFINITY
87
+ expect(model_object.range_begin).to be_nil
88
+ expect(model_object.range_end).to be_nil
89
+ expect(model_object.range).not_to be_empty
90
+ model_object.save_changes
91
+ expect(model_class.where(Sequel.function(:lower_inf, :range)).count).to eq(1)
92
+ expect(model_class.where(Sequel.function(:upper_inf, :range)).count).to eq(1)
93
+ end
94
+
95
+ it 'can set an empty range by assigning the field to the string "empty"' do
96
+ model_object.range_begin = t
97
+ model_object.range_end = t + 1.day
98
+ expect(model_object.range).not_to be_empty
99
+
100
+ model_object.range = 'empty'
101
+ expect(model_object.range).to be_empty
102
+ expect(model_object.range_begin).to be_nil
103
+ expect(model_object.range_end).to be_nil
104
+ model_object.save_changes
105
+ expect(model_class.where(Sequel.function(:lower_inf, :range)).count).to eq(0)
106
+ expect(model_class.where(Sequel.function(:upper_inf, :range)).count).to eq(0)
107
+ end
108
+
109
+ it 'can get/set the start' do
110
+ model_object.range_begin = t
111
+ expect(model_object.range_begin).to eq(t)
112
+ expect(model_object.save_changes.refresh.range_begin).to eq(t)
113
+
114
+ model_object.range_begin = ts
115
+ expect(model_object.range_begin).to eq(t)
116
+ expect(model_object.save_changes.refresh.range_begin).to eq(t)
117
+
118
+ model_object.range_begin = nil
119
+ expect(model_object.range_begin).to be_nil
120
+ expect(model_object.save_changes.refresh.range_begin).to be_nil
121
+ end
122
+
123
+ it 'can get/set the end' do
124
+ model_object.range_end = t
125
+ expect(model_object.range_end).to eq(t)
126
+ expect(model_object.save_changes.refresh.range_end).to eq(t)
127
+
128
+ model_object.range_end = ts
129
+ expect(model_object.range_end).to eq(t)
130
+ expect(model_object.save_changes.refresh.range_end).to eq(t)
131
+
132
+ model_object.range_end = nil
133
+ expect(model_object.range_end).to be_nil
134
+ expect(model_object.save_changes.refresh.range_end).to be_nil
135
+ end
136
+
137
+ it 'can initialize an instance using accessors' do
138
+ o = model_class.create(range_begin: nil, range_end: nil)
139
+ expect(o.range).to be_empty
140
+
141
+ o = model_class.create(range_begin: Time.now, range_end: 1.hour.from_now)
142
+ expect(o.range).not_to be_cover(30.minutes.ago)
143
+ expect(o.range).to be_cover(30.minutes.from_now)
144
+ expect(o.range).not_to be_cover(90.minutes.from_now)
145
+
146
+ o = model_class.create(range_begin: nil, range_end: Time.now)
147
+ expect(o.range).to be_cover(30.minutes.ago)
148
+ expect(o.range).not_to be_cover(30.minutes.from_now)
149
+
150
+ o = model_class.create(range_begin: Time.now, range_end: nil)
151
+ expect(o.range).not_to be_cover(30.minutes.ago)
152
+ expect(o.range).to be_cover(30.minutes.from_now)
153
+ end
154
+
155
+ it 'can be assigned to directly with an object with begin/end methods or keys' do
156
+ early = 1.day.ago
157
+ late = 2.days.from_now
158
+
159
+ forms = [
160
+ early...late,
161
+ OpenStruct.new(begin: early, end: late),
162
+ { begin: early, end: late },
163
+ { 'begin' => early, 'end' => late }
164
+ ]
165
+
166
+ forms.each do |value|
167
+ model_object.range = value
168
+ model_object.save_changes.refresh
169
+ expect(model_object.range_begin).to be_within(1).of(early)
170
+ expect(model_object.range_end).to be_within(1).of(late)
171
+ end
172
+
173
+ model_object.range = {}
174
+ expect(model_object.range).to be_empty
175
+
176
+ expect { model_object.range = 1 }.to raise_error(TypeError)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel/plugins/tstzrange_fields'
4
+
5
+ RSpec.configure do |config|
6
+ # config.full_backtrace = true
7
+
8
+ # RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 600
9
+
10
+ config.expect_with :rspec do |expectations|
11
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
12
+ end
13
+
14
+ config.mock_with :rspec do |mocks|
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+
18
+ config.order = :random
19
+ Kernel.srand config.seed
20
+
21
+ config.filter_run :focus
22
+ config.run_all_when_everything_filtered = true
23
+ config.disable_monkey_patching!
24
+ config.default_formatter = 'doc' if config.files_to_run.one?
25
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-tstzrange-fields
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Lithic Tech
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yajl-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-sequel
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description:
154
+ email:
155
+ - hello@lithic.tech
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - lib/sequel/plugins/tstzrange_fields.rb
161
+ - spec/sequel/plugins/tstzrange_fields_spec.rb
162
+ - spec/spec_helper.rb
163
+ homepage:
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: 2.4.0
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubygems_version: 3.1.4
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: Gem for enabling time ranges when working with postgres
186
+ test_files: []