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
-
#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.
-
#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.
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:
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 |