activerecord-multirange 1.1.0 → 1.1.1
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/CHANGELOG.md +7 -1
- data/Gemfile.lock +1 -1
- data/README.md +215 -12
- data/lib/activerecord-multirange/adapter.rb +1 -22
- data/lib/activerecord-multirange/schema_statements.rb +1 -8
- data/lib/activerecord-multirange/version.rb +1 -1
- data/lib/activerecord-multirange.rb +35 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b09cdb0717d19e40572d66ecb2e7774494057658fb3409fefdf04a67db06345
|
4
|
+
data.tar.gz: aa643ce5bf82da7e69b042478ee884668fd1970da0861b91e43e7a5c2e4ae3e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31209bbe807b481f93275650fae69c2c23fa76518fc987890daec2c619f595571467cb55fdd8cc526e8bc4e007af68eb9f239e68d810854dc3e6977c4631d248
|
7
|
+
data.tar.gz: 87c60c9246d43e7b148c02798171df9747b7aa2fb5c21b40969ed77b8461064440d4f9f73840f2b5d8aa11cd47d33017d48fbfb9a9c9ad28da7ce952e713fa26
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [1.1.
|
3
|
+
## [1.1.1] - 2025-06-11
|
4
|
+
|
5
|
+
- Added automatic registration of multirange types in `NATIVE_DATABASE_TYPES` for Rails 8 compatibility
|
6
|
+
- Fixed `rails db:schema:dump` support without requiring manual type registration
|
7
|
+
- Centralized multirange type definitions in `MULTIRANGE_TYPES` constant
|
8
|
+
|
9
|
+
## [1.1.0] - 2024-06-01
|
4
10
|
|
5
11
|
- Minor version bump with improvements and updates
|
6
12
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# Activercord Multirange
|
2
|
-
|
1
|
+
# Activercord Multirange
|
2
|
+
|
3
3
|
This gem adds full suppport of [Postgress Multiranges](https://www.postgresql.org/docs/14/rangetypes.html#RANGETYPES-BUILTIN) types.
|
4
4
|
|
5
5
|
[](https://badge.fury.io/rb/activerecord-multirange)
|
@@ -12,30 +12,233 @@ Install the gem and add to the application's Gemfile by executing:
|
|
12
12
|
|
13
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
14
14
|
|
15
|
-
$ gem install activerecord-multirange
|
15
|
+
$ gem install activerecord-multirange
|
16
16
|
|
17
17
|
## Usage
|
18
18
|
|
19
19
|
### Initialize it
|
20
20
|
|
21
|
-
```
|
22
|
-
# config/initializers/activerecord_multirange
|
21
|
+
```ruby
|
22
|
+
# config/initializers/activerecord_multirange.rb
|
23
23
|
|
24
24
|
Activerecord::Multirange.add_multirange_column_type
|
25
25
|
```
|
26
26
|
|
27
27
|
### Migrations
|
28
28
|
|
29
|
-
All multirange types are available
|
29
|
+
All multirange types are available in migrations. Here are examples for different multirange types:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class CreateSchedules < ActiveRecord::Migration[7.0]
|
33
|
+
def change
|
34
|
+
create_table :schedules do |t|
|
35
|
+
t.string :name
|
36
|
+
t.tsmultirange :available_times # Timestamp multirange
|
37
|
+
t.tstzmultirange :available_times_tz # Timestamp with timezone multirange
|
38
|
+
t.datemultirange :available_dates # Date multirange
|
39
|
+
t.nummultirange :price_ranges # Numeric multirange
|
40
|
+
t.int8multirange :id_ranges # Bigint multirange
|
41
|
+
t.int4multirange :quantity_ranges # Integer multirange
|
42
|
+
|
43
|
+
t.timestamps
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class CreateBookings < ActiveRecord::Migration[7.0]
|
51
|
+
def change
|
52
|
+
create_table :bookings do |t|
|
53
|
+
t.string :title
|
54
|
+
t.tstzmultirange :booked_periods
|
55
|
+
t.datemultirange :blackout_dates
|
56
|
+
|
57
|
+
t.timestamps
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
### Models
|
64
|
+
|
65
|
+
Define your models to work with multirange columns:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class Schedule < ApplicationRecord
|
69
|
+
# Multirange columns are automatically handled by ActiveRecord
|
70
|
+
# No special configuration needed
|
71
|
+
end
|
72
|
+
|
73
|
+
class Booking < ApplicationRecord
|
74
|
+
validates :title, presence: true
|
75
|
+
|
76
|
+
scope :overlapping_with, ->(time_range) { where('booked_periods && ?', time_range) }
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Creating and Working with Multirange Data
|
81
|
+
|
82
|
+
#### Creating Records with Multirange Values
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# Using timestamp multiranges for scheduling
|
86
|
+
schedule =
|
87
|
+
Schedule.create!(
|
88
|
+
name: 'Conference Room A',
|
89
|
+
available_times: [
|
90
|
+
Time.parse('2024-01-15 09:00')..Time.parse('2024-01-15 12:00'),
|
91
|
+
Time.parse('2024-01-15 14:00')..Time.parse('2024-01-15 17:00')
|
92
|
+
]
|
93
|
+
)
|
94
|
+
|
95
|
+
# Using date multiranges for availability periods
|
96
|
+
booking =
|
97
|
+
Booking.create!(
|
98
|
+
title: 'Annual Maintenance',
|
99
|
+
booked_periods: [
|
100
|
+
Time.zone.parse('2024-03-01 00:00')..Time.zone.parse('2024-03-03 23:59'),
|
101
|
+
Time.zone.parse('2024-06-15 00:00')..Time.zone.parse('2024-06-17 23:59')
|
102
|
+
],
|
103
|
+
blackout_dates: [
|
104
|
+
Date.parse('2024-12-24')..Date.parse('2024-12-26'),
|
105
|
+
Date.parse('2024-12-31')..Date.parse('2024-01-01')
|
106
|
+
]
|
107
|
+
)
|
108
|
+
|
109
|
+
# Using numeric multiranges for pricing tiers
|
110
|
+
product = Product.create!(name: 'Premium Service', price_ranges: [10.0..50.0, 100.0..500.0, 1000.0..5000.0])
|
111
|
+
```
|
112
|
+
|
113
|
+
#### Reading and Manipulating Multirange Data
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
schedule = Schedule.find(1)
|
117
|
+
|
118
|
+
# Access multirange values
|
119
|
+
puts schedule.available_times
|
120
|
+
# => [2024-01-15 09:00:00 UTC..2024-01-15 12:00:00 UTC, 2024-01-15 14:00:00 UTC..2024-01-15 17:00:00 UTC]
|
121
|
+
|
122
|
+
# Check if multirange contains a specific value
|
123
|
+
morning_slot = Time.parse('2024-01-15 10:30')
|
124
|
+
puts schedule.available_times.any? { |range| range.cover?(morning_slot) }
|
125
|
+
# => true
|
126
|
+
|
127
|
+
# Add new time ranges
|
128
|
+
schedule.available_times += [Time.parse('2024-01-15 18:00')..Time.parse('2024-01-15 20:00')]
|
129
|
+
schedule.save!
|
30
130
|
|
131
|
+
# Working with individual ranges
|
132
|
+
schedule.available_times.each { |time_range| puts "Available from #{time_range.begin} to #{time_range.end}" }
|
133
|
+
```
|
134
|
+
|
135
|
+
### Querying Multirange Columns
|
136
|
+
|
137
|
+
#### Overlap Queries
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Find schedules that overlap with a specific time range
|
141
|
+
search_range = Time.parse('2024-01-15 10:00')..Time.parse('2024-01-15 11:00')
|
142
|
+
overlapping_schedules = Schedule.where('available_times && ?', search_range)
|
143
|
+
|
144
|
+
# Find bookings that don't overlap with a date range
|
145
|
+
available_dates = Date.parse('2024-03-01')..Date.parse('2024-03-05')
|
146
|
+
non_conflicting_bookings = Booking.where('NOT (blackout_dates && ?)', available_dates)
|
147
|
+
```
|
148
|
+
|
149
|
+
#### Contains Queries
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# Find schedules that contain a specific timestamp
|
153
|
+
specific_time = Time.parse('2024-01-15 10:30')
|
154
|
+
containing_schedules = Schedule.where('available_times @> ?', specific_time)
|
155
|
+
|
156
|
+
# Find products within a specific price range
|
157
|
+
price_point = 75.0
|
158
|
+
products_in_range = Product.where('price_ranges @> ?', price_point)
|
159
|
+
```
|
160
|
+
|
161
|
+
#### Other Useful Queries
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
# Check if multirange is contained within another range
|
165
|
+
broad_range = Time.parse('2024-01-15 08:00')..Time.parse('2024-01-15 18:00')
|
166
|
+
fully_contained = Schedule.where('available_times <@ ?', broad_range)
|
167
|
+
|
168
|
+
# Find records where multiranges are strictly left of a range
|
169
|
+
cutoff_time = Time.parse('2024-01-15 12:00')..Time.parse('2024-01-15 24:00')
|
170
|
+
morning_only = Schedule.where('available_times << ?', cutoff_time)
|
31
171
|
|
172
|
+
# Find records where multiranges are strictly right of a range
|
173
|
+
start_time = Time.parse('2024-01-15 00:00')..Time.parse('2024-01-15 12:00')
|
174
|
+
afternoon_only = Schedule.where('available_times >> ?', start_time)
|
32
175
|
```
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
176
|
+
|
177
|
+
### Practical Examples
|
178
|
+
|
179
|
+
#### Availability Scheduling System
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class Room < ApplicationRecord
|
183
|
+
def available_during?(time_range)
|
184
|
+
available_times.any? { |range| range.cover?(time_range) }
|
185
|
+
end
|
186
|
+
|
187
|
+
def book_time!(time_range)
|
188
|
+
# Remove the booked time from available times
|
189
|
+
new_availability = []
|
190
|
+
available_times.each do |available_range|
|
191
|
+
if available_range.overlaps?(time_range)
|
192
|
+
# Split the range if needed
|
193
|
+
if available_range.begin < time_range.begin
|
194
|
+
new_availability << (available_range.begin...time_range.begin)
|
195
|
+
end
|
196
|
+
if time_range.end < available_range.end
|
197
|
+
new_availability << (time_range.end...available_range.end)
|
198
|
+
end
|
199
|
+
else
|
200
|
+
new_availability << available_range
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
update!(available_times: new_availability)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Usage
|
209
|
+
room = Room.find(1)
|
210
|
+
booking_time = Time.parse('2024-01-15 10:00')..Time.parse('2024-01-15 11:00')
|
211
|
+
|
212
|
+
if room.available_during?(booking_time)
|
213
|
+
room.book_time!(booking_time)
|
214
|
+
puts 'Room booked successfully!'
|
215
|
+
else
|
216
|
+
puts 'Room not available during requested time'
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
#### Price Range Management
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
class Product < ApplicationRecord
|
224
|
+
def price_tier_for(quantity)
|
225
|
+
price_ranges.each_with_index do |range, index|
|
226
|
+
if range.cover?(quantity)
|
227
|
+
return index + 1
|
228
|
+
end
|
229
|
+
end
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
|
233
|
+
def applies_to_quantity?(quantity)
|
234
|
+
quantity_ranges.any? { |range| range.cover?(quantity) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Usage
|
239
|
+
product = Product.find(1)
|
240
|
+
puts "Quantity 25 is in tier: #{product.price_tier_for(25)}"
|
241
|
+
puts "Product applies to quantity 150: #{product.applies_to_quantity?(150)}"
|
39
242
|
```
|
40
243
|
|
41
244
|
## Development
|
@@ -9,28 +9,7 @@ module Activerecord
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.native_database_types
|
12
|
-
super.merge(
|
13
|
-
{
|
14
|
-
datemultirange: {
|
15
|
-
name: 'datemultirange'
|
16
|
-
},
|
17
|
-
nummultirange: {
|
18
|
-
name: 'nummultirange'
|
19
|
-
},
|
20
|
-
tsmultirange: {
|
21
|
-
name: 'tsmultirange'
|
22
|
-
},
|
23
|
-
tstzmultirange: {
|
24
|
-
name: 'tstzmultirange'
|
25
|
-
},
|
26
|
-
int4multirange: {
|
27
|
-
name: 'int4multirange'
|
28
|
-
},
|
29
|
-
int8multirange: {
|
30
|
-
name: 'int8multirange'
|
31
|
-
}
|
32
|
-
}
|
33
|
-
)
|
12
|
+
super.merge(Activerecord::Multirange::MULTIRANGE_TYPES)
|
34
13
|
end
|
35
14
|
|
36
15
|
def load_multirange_types
|
@@ -4,14 +4,7 @@ module Activerecord
|
|
4
4
|
module Multirange
|
5
5
|
module SchemaStatements
|
6
6
|
def native_database_types
|
7
|
-
super.merge(
|
8
|
-
tsmultirange: { name: "tsmultirange" },
|
9
|
-
datemultirange: { name: "datemultirange" },
|
10
|
-
tstzmultirange: { name: "tstzmultirange" },
|
11
|
-
nummultirange: { name: "nummultirange" },
|
12
|
-
int8multirange: { name: "int8multirange" },
|
13
|
-
int4multirange: { name: "int4multirange" }
|
14
|
-
})
|
7
|
+
super.merge(Activerecord::Multirange::MULTIRANGE_TYPES)
|
15
8
|
end
|
16
9
|
end
|
17
10
|
end
|
@@ -13,8 +13,21 @@ module Activerecord
|
|
13
13
|
class Error < StandardError
|
14
14
|
end
|
15
15
|
|
16
|
+
# Multirange types that need to be registered
|
17
|
+
MULTIRANGE_TYPES = {
|
18
|
+
tsmultirange: { name: "tsmultirange" },
|
19
|
+
datemultirange: { name: "datemultirange" },
|
20
|
+
tstzmultirange: { name: "tstzmultirange" },
|
21
|
+
nummultirange: { name: "nummultirange" },
|
22
|
+
int8multirange: { name: "int8multirange" },
|
23
|
+
int4multirange: { name: "int4multirange" }
|
24
|
+
}.freeze
|
25
|
+
|
16
26
|
def self.add_multirange_column_type
|
17
27
|
ActiveSupport.on_load(:active_record) do
|
28
|
+
# Register multirange types in NATIVE_DATABASE_TYPES for Rails 8 compatibility
|
29
|
+
Activerecord::Multirange.register_native_database_types
|
30
|
+
|
18
31
|
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(Adapter)
|
19
32
|
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::TypeMapInitializer
|
20
33
|
.prepend(TypeMap)
|
@@ -27,5 +40,27 @@ module Activerecord
|
|
27
40
|
)
|
28
41
|
end
|
29
42
|
end
|
43
|
+
|
44
|
+
def self.register_native_database_types
|
45
|
+
# Ensure the PostgreSQL adapter is loaded
|
46
|
+
return unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
47
|
+
|
48
|
+
adapter_class = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
49
|
+
|
50
|
+
# Check if NATIVE_DATABASE_TYPES is already defined and modifiable
|
51
|
+
if adapter_class.const_defined?(:NATIVE_DATABASE_TYPES)
|
52
|
+
current_types = adapter_class::NATIVE_DATABASE_TYPES
|
53
|
+
|
54
|
+
# Only modify if our types aren't already registered
|
55
|
+
unless current_types.key?(:tsmultirange)
|
56
|
+
# Create a new hash with existing types plus our multirange types
|
57
|
+
new_types = current_types.merge(MULTIRANGE_TYPES)
|
58
|
+
|
59
|
+
# Replace the constant with the updated hash
|
60
|
+
adapter_class.send(:remove_const, :NATIVE_DATABASE_TYPES)
|
61
|
+
adapter_class.const_set(:NATIVE_DATABASE_TYPES, new_types.freeze)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
30
65
|
end
|
31
66
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-multirange
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gustavo Warmling Teixeira
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-06-
|
10
|
+
date: 2025-06-11 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: pg
|