validates_overlap 1.1.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e43124f32b6ea56070aceb59e6149abff30b32a077150cee473e0142c75dbbcf
4
- data.tar.gz: cd44ffa9dd62741e92cd0447df0da93680527535750b747cc298caeb284fbebb
3
+ metadata.gz: 22d9351114cf974a1843daead1ff7fb57503951d2cfecd9bfe19c2041bd4dd01
4
+ data.tar.gz: 95e4e59f48f05f22ad589a06e77224fd1d065414ddb22a0d68f17e23005c0168
5
5
  SHA512:
6
- metadata.gz: ee62420473abe259fd2259296637b30945de74fe4b3a09d11dbd61dd0042479867d47fca0916f2bbbad3f75335ea51d02ae0b6c283ee9a593e939cc883efa323
7
- data.tar.gz: 16f7b1c95f79023a49e33ed568cfeb7a2ab6af1398129f43a1469d22e07a68b77b570c84676ad2ea72c94606ef8a5abac9d55dcfe436e5a808c36dd7d7f633e7
6
+ metadata.gz: 8483ac32c24e082f364de7e090dd01b6d14183657bbf6e002770b1d256904eed24e56fcf19707bfa586d79ca9be24b1b7e5e5f9d09de9f2f18d404cc0dec840b
7
+ data.tar.gz: 4b2d9a55f399584ba014ca1633bfa4e278ad42a89db9ff5db4828da9c71d1bba731ceb9394ee5cb9903f2ffa51c76f542efea40037a4e99e7ed6bcea2498f73d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # ValidatesOverlap 1.x Change Log
2
2
 
3
+ ## 1.2.0 (2026-08-11)
4
+
5
+ RSpec tests: **85 → 126** (+41 tests)
6
+
7
+ ### Bug Fixes
8
+
9
+ - 🎉 the validator is now stateless and thread-safe 🎉 — fixes [Issue #50](https://github.com/tilo/validates_overlap/issues/50): concurrent validations of the same model class could corrupt each other's query, because Rails shares one validator instance per class and the query lived on it as instance state (intermittent `ActiveRecord::PreparedStatementInvalid`, or silently wrong validation results). Thanks to [Jorge Santos](https://github.com/jsantos) for the report
10
+ - string range columns raised `TypeError` because a default shift of `0` was added even when no shift was configured — shifts are now only applied when set
11
+ - open-ended (nil) endpoints produced wrong results in several cases: an endless range failed to conflict with records after January 2038 (the nil endpoint was substituted with a Unix-time sentinel), and open-ended integer or string ranges could silently never conflict at all. A nil endpoint now simply drops its comparison from the query — type-independent and exact
12
+ - the record's primary key is now passed to the database as a bind value when a persisted record is excluded from the comparison — it was interpolated into the SQL, which broke string keys containing a quote
13
+
14
+ ### Improvements
15
+
16
+ - documented in the README that `start_shift` / `end_shift` work in both directions: widening the range enforces a minimum gap, shrinking it tolerates a specified amount of overlap — now locked in by specs
17
+ - the overlap check works on any linearly orderable column type — now covered by specs for date, datetime, timestamp, integer, decimal, and string range columns (including open-ended ranges and integer gap/tolerance shifts) and documented in the README ("non-date ranges")
18
+ - `:time` range columns now raise `OverlapValidator::UnsupportedColumnType` — time-of-day is a cyclic domain, where a wraparound window is indistinguishable from accidentally swapped fields; the validator refuses loudly instead of answering wrong (see the README note for cyclic domains)
19
+ - test coverage: real UUID/string primary key test restored (lost in a 2019 refactor), new tests for `:scoped_model`, literal scope values, and the two-attributes requirement; the long-disabled endless-objects test was fixed and re-enabled — the suite has no pending tests
20
+
21
+ ### Internal
22
+
23
+ - removed the accessors `sql_conditions`, `sql_values`, and `scoped_model` from `OverlapValidator` — they were the shared state causing thread-safety issues; the query-building methods now take and return their inputs
24
+ - removed the constants `OverlapValidator::BEGIN_OF_UNIX_TIME` and `END_OF_UNIX_TIME` — the sentinel substitution is gone; an open-ended boundary simply contributes no comparison to the query
25
+
3
26
  ## 1.1.0 (2026-08-07)
4
27
 
5
28
  RSpec tests: **81 → 85** (+4 tests)
data/README.md CHANGED
@@ -1,17 +1,41 @@
1
1
  # ValidatesOverlap
2
2
 
3
- ![Gem Version](https://img.shields.io/gem/v/validates_overlap) [![Ruby](https://github.com/tilo/validates_overlap/actions/workflows/ruby.yml/badge.svg)](https://github.com/tilo/validates_overlap/actions/workflows/ruby.yml) [![codecov](https://codecov.io/gh/tilo/validates_overlap/branch/main/graph/badge.svg)](https://codecov.io/gh/tilo/validates_overlap) [![Downloads](https://img.shields.io/gem/dt/validates_overlap)](https://rubygems.org/gems/validates_overlap) [![RubyGems](https://img.shields.io/badge/RubyGems-validates__overlap-brightgreen?logo=rubygems&logoColor=white)](https://rubygems.org/gems/validates_overlap) [![Ruby Toolbox](https://img.shields.io/badge/Ruby%20Toolbox-validates__overlap-brightgreen)](https://www.ruby-toolbox.com/projects/validates_overlap)
3
+ ![Gem Version](https://img.shields.io/gem/v/validates_overlap) [![RSpec](https://github.com/tilo/validates_overlap/actions/workflows/ruby.yml/badge.svg)](https://github.com/tilo/validates_overlap/actions/workflows/ruby.yml) [![codecov](https://codecov.io/gh/tilo/validates_overlap/branch/main/graph/badge.svg)](https://app.codecov.io/gh/tilo/validates_overlap/tree/main) [![Downloads](https://img.shields.io/gem/dt/validates_overlap)](https://rubygems.org/gems/validates_overlap) [![RubyGems](https://img.shields.io/badge/RubyGems-validates__overlap-brightgreen?logo=rubygems&logoColor=white)](https://rubygems.org/gems/validates_overlap) [![Ruby Toolbox](https://img.shields.io/badge/Ruby%20Toolbox-validates__overlap-brightgreen)](https://www.ruby-toolbox.com/projects/validates_overlap)
4
4
 
5
- `validates_overlap` adds an overlap validation to ActiveRecord models.
6
- Ideal solution for booking applications where you want to make sure, that one place can be booked only once in specific time period.
5
+ `validates_overlap` provides an ActiveRecord validator for resources that must not overlap, e.g. in datetime. Think rentals, meetings, bookings, work shifts, or assignments where the same resource cannot be assigned to multiple people or entities during overlapping time periods. But it also works for other domains than datetime (see below).
7
6
 
8
- You name the two attributes that define a time range for example starts_at and ends_at and the validator checks with a single SQL query that no other record's range overlaps it. If one does, the record gets a normal validation error.
7
+ You specify two attributes defining a datetime range, such as `starts_at` and `ends_at`, and the validator checks with a single SQL query whether another record overlaps that range — no records are loaded for the comparison. If one does, the record receives a normal validation error.
9
8
 
10
- Typical uses: bookings, reservations, meetings, work shifts, rentals anywhere a resource must not be double-booked for the same period.
9
+ It also supports scoped validation (per user, room, resource, etc.), open-ended ranges (a nil start or end counts as extending forever), ranges that may touch at their boundaries (`exclude_edges`), required gaps between ranges or a tolerated amount of overlap (`start_shift` / `end_shift`), associations, and retrieving the conflicting records.
11
10
 
12
- The check runs entirely in the database, so no records are loaded to compare against. It supports scoping the comparison (per user, per room, …), open-ended ranges (a nil start or end counts as extending forever), ranges that may touch at the edges, required gaps between ranges, validating through associations, and loading the conflicting records when you want to show them to the user.
11
+ The range columns don't have to be dates or times: any linearly orderable column type works, such as integer ranges (ticket number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions).
13
12
 
14
- ## Compatibility
13
+ ## Note: Other Domains
14
+
15
+ Other domains / types can be checked for overlap, as long as they can be compared linearly.
16
+ e.g. The overlap check runs on plain SQL comparisons, so any linearly orderable column type works — for example integer ranges (no two records may claim overlapping number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions). A nil endpoint means the range is open-ended on that side, for these types too, and the shifts work for numeric ranges as well (e.g. an integer gap or overlap tolerance). The test suite covers `date`, `datetime`, `timestamp`, `integer`, `decimal`, and `string` range columns.
17
+
18
+ ## ⚠️ Note: Cyclic Domains can NOT be validated for overlap
19
+
20
+ Overlap validation requires a linear domain: every range must satisfy `start <= end`. On a cyclic (wrap-around) domain, like time, every pair of values denotes *some* valid range (`11:00..10:00` is simply the 23-hour complement of `10:00..11:00`), so a wraparound range is indistinguishable from accidentally swapped fields — no validation can tell intent from typo. This is a mathematical property of circular domains, not an implementation gap.
21
+
22
+ The validator therefore refuses `:time` range columns and raises `OverlapValidator::UnsupportedColumnType` — use datetime columns instead, or split windows that cross midnight into two records.
23
+
24
+ But cyclicity is a property of the domain, not the column type — ⚠️ user-encoded cyclic domains hide inside perfectly linear columns, where no guard can see them:
25
+
26
+ - day-of-week as integer (0..6): a Friday-to-Monday shift range `5..1` wraps — same pathology as `22:00..02:00`, stored in an innocent `:integer` column
27
+ - month numbers (1..12): a November-to-February season range `11..2`
28
+ - ISO week numbers: a range from week 52 to week 2 across New Year
29
+ - angles / compass headings (0..360): a heading sector `350..10`
30
+ - longitude (−180°..+180°)
31
+ - hour-of-day as integer — people re-implement `:time` in an int column all the time
32
+ - time-of-day (24-hour clock values without a date component)
33
+
34
+ If your domain is cyclic, the validator will silently give wrong answers for wrapping ranges. Restructure the data instead: split wrapping ranges into two linear records, or lift the values into a linear domain (e.g. datetime instead of time-of-day).
35
+
36
+ To catch inverted ranges loudly instead of silently (for any column type), pair the overlap validation with an order check on your model, e.g. `validates :ends_at, comparison: { greater_than: :starts_at }`.
37
+
38
+ ## Ruby / Rails Compatibility
15
39
 
16
40
  Every combination below is verified on every push by the [CI matrix](https://github.com/tilo/validates_overlap/actions):
17
41
 
@@ -26,6 +50,8 @@ Every combination below is verified on every push by the [CI matrix](https://git
26
50
 
27
51
  The gemspec requires `activerecord >= 6.0`. Rails 6.0 is not part of the test matrix, but no incompatibilities are known. The previous version 0.8.6 was compatible with Rails 3, 4, and 5.
28
52
 
53
+ Note for MySQL users: use `DATETIME` (not `TIMESTAMP`) columns for your range attributes — MySQL's `TIMESTAMP` type cannot store dates after January 2038, which matters for long-running or far-future ranges. PostgreSQL and SQLite date/time types have no such limit.
54
+
29
55
  ## Usage
30
56
 
31
57
  Add to your gemfile
@@ -57,8 +83,29 @@ validates :starts_at, :ends_at, :overlap => {:exclude_edges => ["starts_at", "en
57
83
 
58
84
  #### shift edges
59
85
 
86
+ The shifts move the record's own range edges before the overlap check, so you can require a gap between records — or tolerate a bounded overlap:
87
+
60
88
  ```ruby
89
+ # widen the range: records must be at least 1 day apart (gap enforced)
61
90
  validates :starts_at, :ends_at, :overlap => {:start_shift => -1.day, :end_shift => 1.day}
91
+
92
+ # shrink the range: up to 2 days of overlap are accepted
93
+ validates :starts_at, :ends_at, :overlap => {:start_shift => 2.days, :end_shift => -2.days}
94
+ ```
95
+
96
+ #### non-date ranges
97
+
98
+ The overlap check runs on plain SQL comparisons, so any orderable column type works — for example integer ranges (no two records may claim overlapping number blocks), decimal ranges (price bands), or string ranges (alphabetical partitions). A nil endpoint means the range is open-ended on that side, for these types too, and the shifts work for numeric ranges as well (e.g. an integer gap or overlap tolerance). The test suite covers `date`, `datetime`, `timestamp`, `integer`, `decimal`, and `string` range columns.
99
+
100
+ ```ruby
101
+ class TicketBlock < ActiveRecord::Base
102
+ validates :number_start, :number_end, :overlap => true
103
+ end
104
+
105
+ TicketBlock.create!(number_start: 100, number_end: 199)
106
+ TicketBlock.new(number_start: 150, number_end: 250).valid? # => false (overlaps)
107
+ TicketBlock.new(number_start: 150, number_end: nil).valid? # => false (open-ended, overlaps)
108
+ TicketBlock.new(number_start: 200, number_end: 299).valid? # => true
62
109
  ```
63
110
 
64
111
  #### define custom validation key(s) and message
@@ -70,7 +117,7 @@ validates :starts_at, :ends_at, :overlap => {:message_title => [:start_at, :end_
70
117
 
71
118
  #### with complicated relations
72
119
 
73
- Example describes valildatation of user, positions and time slots.
120
+ Example describes validation of user, positions and time slots.
74
121
  User can't be assigned 2 times on position which is under time slot with time overlap.
75
122
 
76
123
  ```ruby
@@ -3,12 +3,9 @@ require 'active_support/i18n'
3
3
  I18n.load_path << File.dirname(__FILE__) + '/locale/en.yml'
4
4
 
5
5
  class OverlapValidator < ActiveModel::EachValidator
6
- BEGIN_OF_UNIX_TIME = Time.at(-2_147_483_648).to_datetime
7
- END_OF_UNIX_TIME = Time.at(2_147_483_648).to_datetime
8
-
9
- attr_accessor :sql_conditions
10
- attr_accessor :sql_values
11
- attr_accessor :scoped_model
6
+ # Raised when a range attribute uses a column type the validator cannot
7
+ # operate on (e.g. :time — a cyclic domain, see README)
8
+ class UnsupportedColumnType < ArgumentError; end
12
9
 
13
10
  def initialize(args)
14
11
  attributes_are_range(args[:attributes])
@@ -16,11 +13,15 @@ class OverlapValidator < ActiveModel::EachValidator
16
13
  super
17
14
  end
18
15
 
16
+ # NOTE: Rails registers ONE validator instance per model class, shared by every
17
+ # validation of that class (including concurrent ones) — so the query being
18
+ # built must never be stored on the validator itself (issue #50)
19
19
  def validate(record)
20
- initialize_query(record, options)
21
- if overlapped_exists?
20
+ reject_unsupported_column_types(record)
21
+ relation, sql_conditions, sql_values = initialize_query(record, options)
22
+ if overlapped_exists?(relation, sql_conditions, sql_values)
22
23
  if options[:load_overlapped]
23
- record.instance_variable_set(:@overlapped_records, get_overlapped)
24
+ record.instance_variable_set(:@overlapped_records, get_overlapped(relation, sql_conditions, sql_values))
24
25
  end
25
26
 
26
27
  if record.respond_to? attributes.first
@@ -39,22 +40,41 @@ class OverlapValidator < ActiveModel::EachValidator
39
40
 
40
41
  protected
41
42
 
43
+ # Time-of-day is a cyclic domain: every pair of values denotes some valid
44
+ # range there, so wraparound intent is indistinguishable from accidentally
45
+ # swapped fields — refuse loudly instead of answering wrong (see README).
46
+ # Checked at validate time, not at class-definition time, because column
47
+ # metadata must not be touched while migrations may still be pending.
48
+ def reject_unsupported_column_types(record)
49
+ return unless record.class.respond_to?(:columns_hash)
50
+ attributes.each do |attr|
51
+ next if attr.to_s.include?('.')
52
+ column = record.class.columns_hash[attr.to_s]
53
+ next unless column && column.type == :time
54
+ raise UnsupportedColumnType, "#{record.class.name}##{attr} is a :time column; time-of-day is a cyclic domain and cannot be validated for overlap — use datetime columns, or split ranges that cross midnight (see README)"
55
+ end
56
+ end
57
+
58
+ # Build the complete overlap query for this record.
59
+ # return array in form [relation, sql_conditions, sql_values]
42
60
  def initialize_query(record, options = {})
43
61
  scoped_model = options[:scoped_model].present? ? options[:scoped_model].constantize : record.class
44
- self.scoped_model = scoped_model.default_scoped
45
- generate_overlap_sql_values(record)
46
- generate_overlap_sql_conditions(record)
47
- add_attributes(record, options[:scope]) if options && options[:scope].present?
48
- add_query_options(options[:query_options]) if options && options[:query_options].present?
62
+ relation = scoped_model.default_scoped
63
+ sql_values = generate_overlap_sql_values(record)
64
+ sql_conditions, primary_key_values = generate_overlap_sql_conditions(record, sql_values)
65
+ sql_values = sql_values.merge(primary_key_values)
66
+ sql_conditions, sql_values = add_attributes(record, options[:scope], sql_conditions, sql_values) if options && options[:scope].present?
67
+ relation = add_query_options(relation, options[:query_options]) if options && options[:query_options].present?
68
+ [relation, sql_conditions, sql_values]
49
69
  end
50
70
 
51
71
  # Check if exists at least one record in DB which is overlapped with current record
52
- def overlapped_exists?
53
- scoped_model.exists?([sql_conditions, sql_values])
72
+ def overlapped_exists?(relation, sql_conditions, sql_values)
73
+ relation.exists?([sql_conditions, sql_values])
54
74
  end
55
75
 
56
- def get_overlapped
57
- scoped_model.where([sql_conditions, sql_values])
76
+ def get_overlapped(relation, sql_conditions, sql_values)
77
+ relation.where([sql_conditions, sql_values])
58
78
  end
59
79
 
60
80
  # Resolve attributes values from record to use in sql conditions
@@ -108,55 +128,70 @@ class OverlapValidator < ActiveModel::EachValidator
108
128
  record.send(primary_key_name)
109
129
  end
110
130
 
111
- # Generate sql condition for time range cross
112
- def generate_overlap_sql_conditions(record)
131
+ # Generate sql condition for time range cross; a persisted record is excluded
132
+ # from the comparison by its primary key, passed as a bind value
133
+ # return array in form [sql_conditions, sql_values]
134
+ def generate_overlap_sql_conditions(record, sql_values)
113
135
  starts_at_attr, ends_at_attr = attributes_to_sql(record)
114
- main_condition = condition_string(starts_at_attr, ends_at_attr)
115
- primary_key_name = primary_key(record)
116
- key = primary_key_value(primary_key_name, record)
136
+ main_condition = condition_string(starts_at_attr, ends_at_attr, sql_values)
117
137
  if record.new_record?
118
- self.sql_conditions = main_condition
138
+ [main_condition, {}]
119
139
  else
120
- self.sql_conditions = "#{main_condition} AND #{record_table_name(record)}.#{primary_key(record)} !="
121
- self.sql_conditions += key.is_a?(String) ? "'#{key}'" : key.to_s
140
+ key = primary_key_value(primary_key(record), record)
141
+ ["#{main_condition} AND #{record_table_name(record)}.#{primary_key(record)} != :record_primary_key_value", { record_primary_key_value: key }]
122
142
  end
123
143
  end
124
144
 
125
- # Return hash of values for overlap sql condition
145
+ # Return hash of values for overlap sql condition; a nil endpoint means the
146
+ # record's range is open-ended on that side — no value is emitted for it and
147
+ # condition_string drops the corresponding comparison
148
+ # NOTE: shifts are only applied when configured — unconditionally adding a
149
+ # default of 0 would raise a TypeError for non-numeric types such as String
126
150
  def generate_overlap_sql_values(record)
127
151
  starts_at_value, ends_at_value = resolve_values_from_attributes(record)
128
- starts_at_value += options.fetch(:start_shift) { 0 } if starts_at_value && options
129
- ends_at_value += options.fetch(:end_shift) { 0 } if ends_at_value && options
130
- self.sql_values = { starts_at_value: starts_at_value || BEGIN_OF_UNIX_TIME, ends_at_value: ends_at_value || END_OF_UNIX_TIME }
152
+ start_shift = options && options[:start_shift]
153
+ end_shift = options && options[:end_shift]
154
+ starts_at_value += start_shift if starts_at_value && start_shift
155
+ ends_at_value += end_shift if ends_at_value && end_shift
156
+ sql_values = {}
157
+ sql_values[:starts_at_value] = starts_at_value if starts_at_value
158
+ sql_values[:ends_at_value] = ends_at_value if ends_at_value
159
+ sql_values
131
160
  end
132
161
 
133
162
  # Return the condition string depend on exclude_edges option.
134
- def condition_string(starts_at_attr, ends_at_attr)
163
+ # A comparison is only emitted for endpoints the record actually has: an
164
+ # open-ended side matches every other record by definition, so its clause is
165
+ # dropped (a record with both endpoints nil overlaps everything)
166
+ def condition_string(starts_at_attr, ends_at_attr, sql_values)
135
167
  except_option = Array(options[:exclude_edges]).map(&:to_s)
136
168
  starts_at_sign = except_option.include?(starts_at_attr.to_s.split('.').last) ? '<' : '<='
137
169
  ends_at_sign = except_option.include?(ends_at_attr.to_s.split('.').last) ? '>' : '>='
138
170
  query = []
139
- query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)"
140
- query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)"
141
- query.join(' AND ')
171
+ query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)" if sql_values.key?(:starts_at_value)
172
+ query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)" if sql_values.key?(:ends_at_value)
173
+ query.empty? ? '1 = 1' : query.join(' AND ')
142
174
  end
143
175
 
144
176
  # Add attributes and values to sql conditions.
145
177
  # helps to use with scope options, so scope can be added as this forms :scope => "user_id" or :scope => ["user_id", "place_id"]
146
- def add_attributes(record, attrs)
178
+ # return array in form [sql_conditions, sql_values]
179
+ def add_attributes(record, attrs, sql_conditions, sql_values)
147
180
  if attrs.is_a?(Array)
148
- attrs.each { |attr| add_attribute(record, attr) }
181
+ attrs.each { |attr| sql_conditions, sql_values = add_attribute(record, attr, sql_conditions, sql_values) }
149
182
  elsif attrs.is_a?(Hash)
150
183
  attrs.each do |attr_name, value|
151
- add_attribute(record, attr_name, value)
184
+ sql_conditions, sql_values = add_attribute(record, attr_name, sql_conditions, sql_values, value)
152
185
  end
153
186
  else
154
- add_attribute(record, attrs)
187
+ sql_conditions, sql_values = add_attribute(record, attrs, sql_conditions, sql_values)
155
188
  end
189
+ [sql_conditions, sql_values]
156
190
  end
157
191
 
158
192
  # Add attribute and his value to sql condition
159
- def add_attribute(record, attr_name, value = nil)
193
+ # return array in form [sql_conditions, sql_values]
194
+ def add_attribute(record, attr_name, sql_conditions, sql_values, value = nil)
160
195
  _value = resolve_attribute_value(record, attr_name, value)
161
196
  operator = if _value.nil?
162
197
  ' IS NULL'
@@ -166,8 +201,9 @@ class OverlapValidator < ActiveModel::EachValidator
166
201
  ' = :%s'
167
202
  end
168
203
 
169
- self.sql_conditions += " AND #{attribute_to_sql(attr_name, record)} #{operator}" % value_attribute_name(attr_name)
170
- sql_values.merge!(:"#{value_attribute_name(attr_name)}" => _value)
204
+ sql_conditions += " AND #{attribute_to_sql(attr_name, record)} #{operator}" % value_attribute_name(attr_name)
205
+ sql_values = sql_values.merge(:"#{value_attribute_name(attr_name)}" => _value)
206
+ [sql_conditions, sql_values]
171
207
  end
172
208
 
173
209
  def value_attribute_name(attr_name)
@@ -200,9 +236,11 @@ class OverlapValidator < ActiveModel::EachValidator
200
236
  # Allow to use scope, joins, includes methods before querying
201
237
  # == Example:
202
238
  # validates_overlap :date_from, :date_to, :query_options => {:includes => "visits"}
203
- def add_query_options(methods)
239
+ # return the relation with the query options applied
240
+ def add_query_options(relation, methods)
204
241
  methods.each do |method_name, params|
205
- self.scoped_model = scoped_model.send(method_name.to_sym, *params)
242
+ relation = relation.send(method_name.to_sym, *params)
206
243
  end
244
+ relation
207
245
  end
208
246
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ValidatesOverlap
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validates_overlap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Bortlik
@@ -164,8 +164,9 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description: It can be useful when you are developing some app where you will work
168
- with meetings, events etc.
167
+ description: Adds ActiveRecord validations that prevent overlapping date/time ranges
168
+ bookings, reservations, meetings, shifts. One SQL query; supports scoping and
169
+ open-ended ranges
169
170
  email:
170
171
  - robinbortlik@gmail.com
171
172
  - tilo.sloboda@gmail.com
@@ -189,7 +190,7 @@ homepage: https://github.com/tilo/validates_overlap
189
190
  licenses:
190
191
  - MIT
191
192
  metadata:
192
- source_code_uri: https://github.com/tilo/validates_overlap/tree/v1.1.0
193
+ source_code_uri: https://github.com/tilo/validates_overlap/tree/v1.2.0
193
194
  bug_tracker_uri: https://github.com/tilo/validates_overlap/issues
194
195
  changelog_uri: https://github.com/tilo/validates_overlap/blob/main/CHANGELOG.md
195
196
  rubygems_mfa_required: 'true'