time_range_uniqueness 1.0.0 → 1.0.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1856dd82ab1e6aaddf8cd3c332ecbe84250218b2fae914426eba695d377c8513
|
|
4
|
+
data.tar.gz: f3d66c621fc3089986f9452bfcc02dedadd47c6b75fc914cc8b127af58dde021
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60e9c6b37fa3f0cc8ae51903006a182970900b9e17b37afcd4a7ff57fb84f58d7d84c7657eb661da6055e81ed07328941b2ee885c2c6e74e3fbccb942ae8528e
|
|
7
|
+
data.tar.gz: b2a21bd9c93329bf320ebe9f3b36f79912ab41adfd9a9bbef057d98f1bedbf700a5e058dfbb9ac08c01b3721def1cd71589af17850e9971524954cecef12ba83
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.1] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
- Fix the overlap validation to treat a `NULL` scope value as never-conflicting, matching
|
|
6
|
+
the exclusion constraint (`NULL = NULL` is never true in PostgreSQL). Previously the
|
|
7
|
+
validation reported a false overlap for rows the database would accept.
|
|
8
|
+
- Support models with a composite primary key in the overlap validation. Previously the
|
|
9
|
+
validation raised `ArgumentError` when excluding the current record, so such models
|
|
10
|
+
could not be validated or created.
|
|
11
|
+
- Raise `ArgumentError` when a custom `:name` exceeds PostgreSQL's 63-character identifier
|
|
12
|
+
limit, instead of relying on the database to truncate it silently.
|
|
13
|
+
|
|
3
14
|
## [1.0.0] - 2026-05-31
|
|
4
15
|
|
|
5
16
|
- Require Ruby >= 3.2 and support ActiveRecord 7.1 through 8.x (and pg >= 1.5).
|
|
@@ -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))
|