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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 868b4a177acdae3b835b05cb3b7d18741298b99592cdfb429dd38196287dc9f4
4
- data.tar.gz: 3b2a570fe65b0a08024bd730e1743a15966d0c9bfd2c674d5533e70014a27d69
3
+ metadata.gz: dc737c44b6dbbf13c6781eef86f089832aaf04cda0a50c0d513f91e19536f600
4
+ data.tar.gz: 1be53b987bdc21f730b2ace9ea9aa9478eef92387ecdc6508a6bfad68f90d4ac
5
5
  SHA512:
6
- metadata.gz: 8c01bcca608fc1341a154f3f7b40293f483ca27814fdfac96139edf390eac43b3d791c2b1aa8d9004d17597a91db06bfdd6205030c46d114abdddb2fd441d5ac
7
- data.tar.gz: 5b31ad1001851747cb211231329bdab526750558482d152eaa4fbe4217ef7afc637287d14a6235f1d5d6b8dbdee85ce2286f6dd1babf1bdaa37cb01c29f5e8e5
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[6.1]
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. You can include this validation in your models like this:
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
- raise ArgumentError, 'You must specify the :with option with the time range column name' unless options[:with]
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).where(
77
- "#{column} && tstzrange(?, ?, ?)",
78
- time_range.begin,
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
- relation = klass.where.not(klass.primary_key => record.id)
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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TimeRangeUniqueness
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
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.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: