Module: ValidatesOverlap::MigrationHelpers

Defined in:
lib/validates_overlap/migration_helpers.rb

Overview

Migration helpers for a database-level overlap guarantee (PostgreSQL only). The validation is check-then-act and cannot prevent double-booking under concurrent writes; an exclusion constraint can (see the README).

Example:

def up
add_overlap_constraint :meetings, :starts_at, :ends_at, scope: :user_id
end
def down
remove_overlap_constraint :meetings
end

Instance Method Summary collapse

Instance Method Details

#add_overlap_constraint(table, starts_at, ends_at = nil, scope: [], name: nil, range_type: nil, exclude_edges: false, without_overlaps: false) ⇒ Object

Two scalar columns: add_overlap_constraint :meetings, :starts_at, :ends_at, scope: :user_id One range column: add_overlap_constraint :meetings, :period, scope: :user_id

scope: column(s) compared with equality, mirroring the validator's :scope name: constraint name (default:

_no_overlap) range_type: PostgreSQL range type; inferred from the column types when omitted exclude_edges: false (default) matches the validator's default — touching edges conflict; true builds half-open ranges, matching the validator's exclude_edges: [start, end] where touching is allowed. Not applicable to a range column (bounds live in the value) without_overlaps: PostgreSQL 18+, single range column with scope only — generate the standard-SQL temporal unique constraint UNIQUE (scope, range WITHOUT OVERLAPS) instead of the EXCLUDE clause



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/validates_overlap/migration_helpers.rb', line 27

def add_overlap_constraint(table, starts_at, ends_at = nil, scope: [], name: nil, range_type: nil, exclude_edges: false, without_overlaps: false)
  assert_postgresql!('add_overlap_constraint')
  scope_columns = Array(scope)
  warn_nullable_scope_columns(table, scope_columns)
  enable_extension 'btree_gist' if !scope_columns.empty? && !connection.extension_enabled?('btree_gist')
  return add_without_overlaps_constraint(table, starts_at, ends_at, scope_columns, name, exclude_edges) if without_overlaps
  elements = scope_columns.map { |column| "#{connection.quote_column_name(column)} WITH =" }
  if ends_at.nil?
    raise ArgumentError, 'validates_overlap: exclude_edges is not applicable to a range column — bound inclusivity is part of the range value itself' if exclude_edges
    assert_range_column!(table, starts_at)
    elements << "#{connection.quote_column_name(starts_at)} WITH &&"
  else
    range_type ||= overlap_range_type(table, starts_at, ends_at)
    bounds = exclude_edges ? '[)' : '[]'
    elements << "#{range_type}(#{connection.quote_column_name(starts_at)}, #{connection.quote_column_name(ends_at)}, '#{bounds}') WITH &&"
  end
  if connection.respond_to?(:add_exclusion_constraint)
    # Rails 7.1+: the recorded command is invertible, so the helper works in def change
    add_exclusion_constraint table, elements.join(', '), using: :gist, name: overlap_constraint_name(table, name)
  else
    execute <<~SQL
      ALTER TABLE #{connection.quote_table_name(table)}
        ADD CONSTRAINT #{connection.quote_column_name(overlap_constraint_name(table, name))}
        EXCLUDE USING gist (#{elements.join(', ')})
    SQL
  end
end

#remove_overlap_constraint(table, name: nil) ⇒ Object

A plain DROP CONSTRAINT on purpose: it drops both constraint forms, while Rails' remove_exclusion_constraint refuses a temporal unique constraint (without_overlaps) — and delegating would not make removal invertible in def change anyway, since the inversion needs the original expression



59
60
61
62
# File 'lib/validates_overlap/migration_helpers.rb', line 59

def remove_overlap_constraint(table, name: nil)
  assert_postgresql!('remove_overlap_constraint')
  execute "ALTER TABLE #{connection.quote_table_name(table)} DROP CONSTRAINT #{connection.quote_column_name(overlap_constraint_name(table, name))}"
end