time_range_uniqueness 1.0.0 → 1.0.2
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 +18 -0
- data/README.md +13 -2
- data/lib/time_range_uniqueness/migration_additions.rb +14 -1
- data/lib/time_range_uniqueness/model_additions.rb +11 -7
- data/lib/time_range_uniqueness/version.rb +1 -1
- 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: dc737c44b6dbbf13c6781eef86f089832aaf04cda0a50c0d513f91e19536f600
|
|
4
|
+
data.tar.gz: 1be53b987bdc21f730b2ace9ea9aa9478eef92387ecdc6508a6bfad68f90d4ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6d32eb4c17f95d7d0815e692f09eed56d62aeb259a9c3b05e70fa2c78dfc59ac89fbd8127dc9bf33e29e8a5c5d2382ae21fdf07dd6ae906d9b36be3a878c4e5
|
|
7
|
+
data.tar.gz: 889b032899cbdc7cc3fc2cb73d05b593d438ecf112561dca2926a715c713d26cf92e5224cc85a9470b55c3404426484a5c66633ad8e716f7a13365e17c910bda
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.2] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
- Update the README to match the current behavior: document the Ruby >= 3.2 and
|
|
6
|
+
ActiveRecord >= 7.1 requirements, correct the migration example's version stamp, clarify
|
|
7
|
+
that `validates_time_range_uniqueness` is available on all models, and list the
|
|
8
|
+
validation/constraint behavior added in 1.0.0 and 1.0.1.
|
|
9
|
+
|
|
10
|
+
## [1.0.1] - 2026-05-31
|
|
11
|
+
|
|
12
|
+
- Fix the overlap validation to treat a `NULL` scope value as never-conflicting, matching
|
|
13
|
+
the exclusion constraint (`NULL = NULL` is never true in PostgreSQL). Previously the
|
|
14
|
+
validation reported a false overlap for rows the database would accept.
|
|
15
|
+
- Support models with a composite primary key in the overlap validation. Previously the
|
|
16
|
+
validation raised `ArgumentError` when excluding the current record, so such models
|
|
17
|
+
could not be validated or created.
|
|
18
|
+
- Raise `ArgumentError` when a custom `:name` exceeds PostgreSQL's 63-character identifier
|
|
19
|
+
limit, instead of relying on the database to truncate it silently.
|
|
20
|
+
|
|
3
21
|
## [1.0.0] - 2026-05-31
|
|
4
22
|
|
|
5
23
|
- Require Ruby >= 3.2 and support ActiveRecord 7.1 through 8.x (and pg >= 1.5).
|
data/README.md
CHANGED
|
@@ -11,6 +11,17 @@ It adds support for creating exclusion constraints on PostgreSQL `tstzrange` col
|
|
|
11
11
|
- **Migration Additions**: Adds a custom method for generating exclusion constraints on time range columns in PostgreSQL using `tstzrange`.
|
|
12
12
|
- **Model Additions**: Adds validation to ensure time ranges do not overlap with existing records.
|
|
13
13
|
- Supports optional scoping to ensure time ranges are unique within specified contexts (e.g., unique per event name).
|
|
14
|
+
- Honors the time range's bound inclusivity (`..` vs `...`) so the model validation agrees with the database-level exclusion constraint.
|
|
15
|
+
- Treats a `NULL` scope value as never-conflicting, matching PostgreSQL's exclusion-constraint semantics (`NULL = NULL` is never true).
|
|
16
|
+
- Works with models that use a composite primary key.
|
|
17
|
+
- Keeps generated constraint names within PostgreSQL's 63-character identifier limit, and raises if a custom `:name` exceeds it.
|
|
18
|
+
- Quotes table and column identifiers in the generated migration and validation SQL.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby >= 3.2
|
|
23
|
+
- ActiveRecord >= 7.1, < 9.0
|
|
24
|
+
- PostgreSQL with the `btree_gist` extension available
|
|
14
25
|
|
|
15
26
|
## Installation
|
|
16
27
|
|
|
@@ -46,7 +57,7 @@ In your migrations, you can use the `add_time_range_uniqueness` method to add a
|
|
|
46
57
|
### Example
|
|
47
58
|
|
|
48
59
|
```ruby
|
|
49
|
-
class AddEventTimeRangeUniqueness < ActiveRecord::Migration[
|
|
60
|
+
class AddEventTimeRangeUniqueness < ActiveRecord::Migration[7.1]
|
|
50
61
|
def change
|
|
51
62
|
add_time_range_uniqueness :events,
|
|
52
63
|
with: :event_time_range,
|
|
@@ -60,7 +71,7 @@ This example ensures that the `event_time_range` column in the `events` table is
|
|
|
60
71
|
|
|
61
72
|
### Model Additions
|
|
62
73
|
|
|
63
|
-
The gem also provides model-level validation to ensure time ranges do not overlap.
|
|
74
|
+
The gem also provides model-level validation to ensure time ranges do not overlap. The `validates_time_range_uniqueness` class method is available on all ActiveRecord models, so you can declare it directly in your model like this:
|
|
64
75
|
|
|
65
76
|
#### Options:
|
|
66
77
|
- `with`: **(Required)** The name of the time range column to validate.
|
|
@@ -47,7 +47,7 @@ module TimeRangeUniqueness
|
|
|
47
47
|
# @option options [Array<Symbol>] :scope (Optional) Columns to scope the uniqueness check.
|
|
48
48
|
# @option options [String] :name (Optional) The name of the constraint.
|
|
49
49
|
def add_time_range_uniqueness(table, options = {})
|
|
50
|
-
|
|
50
|
+
validate_options!(options)
|
|
51
51
|
|
|
52
52
|
time_range_column = options[:with]
|
|
53
53
|
scope_columns = Array(options[:scope])
|
|
@@ -61,6 +61,19 @@ module TimeRangeUniqueness
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
+
# Validates the options passed to +add_time_range_uniqueness+.
|
|
65
|
+
#
|
|
66
|
+
# @param options [Hash] The options for the constraint.
|
|
67
|
+
# @raise [ArgumentError] If +:with+ is missing or +:name+ exceeds the identifier limit.
|
|
68
|
+
def validate_options!(options)
|
|
69
|
+
raise ArgumentError, 'You must specify the :with option with the time range column name' unless options[:with]
|
|
70
|
+
|
|
71
|
+
name = options[:name]
|
|
72
|
+
return unless name && name.to_s.length > MAX_IDENTIFIER_LENGTH
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, "The :name option exceeds the #{MAX_IDENTIFIER_LENGTH}-character identifier limit"
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
# Applies the changes for the up migration.
|
|
65
78
|
#
|
|
66
79
|
# @param table [Symbol, String] The name of the table.
|
|
@@ -70,15 +70,16 @@ module TimeRangeUniqueness
|
|
|
70
70
|
time_range = record.public_send(time_range_column)
|
|
71
71
|
return false if time_range.nil?
|
|
72
72
|
|
|
73
|
+
# A NULL scope value can never satisfy the exclusion constraint's `=` comparison,
|
|
74
|
+
# so such a record can never conflict at the database level. Mirror that here.
|
|
75
|
+
return false if scope_columns.any? { |col| record.public_send(col).nil? }
|
|
76
|
+
|
|
73
77
|
column = record.class.connection.quote_column_name(time_range_column)
|
|
74
78
|
bounds = time_range.exclude_end? ? '[)' : '[]'
|
|
75
79
|
|
|
76
|
-
scoped_relation(record, scope_columns)
|
|
77
|
-
"#{column} && tstzrange(?, ?, ?)",
|
|
78
|
-
|
|
79
|
-
time_range.end,
|
|
80
|
-
bounds
|
|
81
|
-
).exists?
|
|
80
|
+
scoped_relation(record, scope_columns)
|
|
81
|
+
.where("#{column} && tstzrange(?, ?, ?)", time_range.begin, time_range.end, bounds)
|
|
82
|
+
.exists?
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
# Builds the set of other records to check against, optionally scoped by the given columns.
|
|
@@ -88,7 +89,10 @@ module TimeRangeUniqueness
|
|
|
88
89
|
# @return [ActiveRecord::Relation] All other records, scoped by the given columns.
|
|
89
90
|
def self.scoped_relation(record, scope_columns)
|
|
90
91
|
klass = record.class
|
|
91
|
-
|
|
92
|
+
# Pair each primary key column with its value so this works for both single
|
|
93
|
+
# and composite primary keys (record.id is an array for composite keys).
|
|
94
|
+
excluded = Array(klass.primary_key).zip(Array(record.id)).to_h
|
|
95
|
+
relation = klass.where.not(excluded)
|
|
92
96
|
|
|
93
97
|
scope_columns.each do |col|
|
|
94
98
|
relation = relation.where(col => record.public_send(col))
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: time_range_uniqueness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- j-boers-13
|
|
@@ -71,7 +71,7 @@ licenses:
|
|
|
71
71
|
metadata:
|
|
72
72
|
source_code_uri: https://github.com/j-boers-13/time_range_uniqueness
|
|
73
73
|
homepage_uri: https://github.com/j-boers-13/time_range_uniqueness
|
|
74
|
-
changelog_uri: https://github.com/j-boers-13/time_range_uniqueness/CHANGELOG.md
|
|
74
|
+
changelog_uri: https://github.com/j-boers-13/time_range_uniqueness/blob/main/CHANGELOG.md
|
|
75
75
|
rubygems_mfa_required: 'true'
|
|
76
76
|
rdoc_options: []
|
|
77
77
|
require_paths:
|