active_record-postgres-constraints 0.1.5 → 0.2.0

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: 5271c66bc4b63fe16a80b9777e4cd2c208408664b441752579a5faf5882fb439
4
- data.tar.gz: 54609bd96e07eaba2bb66ad2ec4fed1c0ba0a21de4fc93b40db65941556ba916
3
+ metadata.gz: b3315c2077c81d530ad63720aea7fda16d01298233aec01e5f47990c2aa43969
4
+ data.tar.gz: '048e1f4a77a7839d9eec3a75b8417dcfcd72390c960eeee3e75f2599369113a4'
5
5
  SHA512:
6
- metadata.gz: 12eef31e5ea5b276c312497ec44b9b6f18eed9bc6f74dd41d5d9667abfd0952b816c2e74e1081e27e1cb4edddb5920da8747ced197be6327cb41b612f6b2b9d6
7
- data.tar.gz: f0dc14c943ce9e0ed4369d100c6cbc6e392d2f8ffcc2fc1a4dd541f263994814ec403e293a27d2eb22d33b6dbf6fece66553ef0f707ad6223730a090ce698285
6
+ metadata.gz: 3cdba026319a96312e707fb97efa408b1394c71812ed4fde78dd086f7916dc5cf0e854d8d86570e96e7ad8a69eb8fc01b02ff14f42cbf0aa3afe868abf454e61
7
+ data.tar.gz: 3ccba16e546aa92e2d0d436cc66df0c84acd0605ede6b7ad40a12158879e223424ab0c1f03c53569f7b5e870c1cc9bdbcf73c17cdbd519f17a423ac8db221ad8
data/README.md CHANGED
@@ -14,7 +14,7 @@ using features like this, then you should set the schema format to :sql.
14
14
  No longer is this the case. You can now use the default schema format
15
15
  (:ruby) and still preserve your check constraints.
16
16
 
17
- At this time, this only supports check constraints for the postgresql ActiveRecord database adapter.
17
+ At this time, this only supports check and exclude constraints for the postgresql ActiveRecord database adapter.
18
18
 
19
19
  ## Usage
20
20
 
@@ -44,6 +44,34 @@ remove_check_constraint :people
44
44
  remove_check_constraint :people, title: ['Mr.', 'Mrs.', 'Dr.']
45
45
  ```
46
46
 
47
+ #### Add an exclude constraint
48
+ Add exclude constraints to a table in a migration:
49
+
50
+ ```ruby
51
+ create_table :booking do |t|
52
+ t.integer :room_id
53
+ t.datetime :from
54
+ t.datetime :to
55
+ t.exclude_constraint using: :gist, 'tsrange("from", "to")' => :overlaps, room_id: :equals
56
+ end
57
+ ```
58
+
59
+ OR
60
+
61
+ ```ruby
62
+ add_exclude_constraint :booking, using: :gist, 'tsrange("from", "to")' => :overlaps, room_id: :equals
63
+ ```
64
+
65
+ #### Remove an exclude constraint
66
+
67
+ ```ruby
68
+ # If you don't need it to be reversible:
69
+ remove_exclude_constraint :booking
70
+
71
+ # If you need it to be reversible (Recommended):
72
+ remove_exclude_constraint :booking, using: :gist, 'tsrange("from", "to")' => :overlaps, room_id: :equals
73
+ ```
74
+
47
75
  ## Installation
48
76
  Add this line to your application's Gemfile:
49
77
 
data/Rakefile CHANGED
@@ -7,4 +7,5 @@ rescue LoadError
7
7
  end
8
8
 
9
9
  require 'bundler/gem_tasks'
10
+ require 'parallel_tests/tasks'
10
11
  task default: :spec
@@ -1,51 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'constraints/command_recorder'
4
- require_relative 'constraints/postgresql_adapter'
5
- require_relative 'constraints/railtie'
6
- require_relative 'constraints/schema_creation'
7
- require_relative 'constraints/schema_dumper'
8
- require_relative 'constraints/table_definition'
9
- require_relative 'constraints/version'
10
-
11
3
  module ActiveRecord
12
4
  module Postgres
13
5
  module Constraints
14
- class << self
15
- def normalize_conditions(conditions)
16
- conditions = [conditions] unless conditions.is_a?(Array)
17
- conditions = conditions.map do |condition|
18
- if condition.is_a?(Hash)
19
- normalize_conditions_hash(condition)
20
- else
21
- condition
22
- end
23
- end
24
-
25
- return conditions.first if 1 == conditions.length
26
-
27
- "(#{conditions.join(') AND (')})"
28
- end
6
+ CONSTRAINT_TYPES = {
7
+ check: 'c',
8
+ exclude: 'x',
9
+ }.freeze
29
10
 
30
- def normalize_conditions_hash(hash)
31
- hash = hash.reduce([]) do |array, (column, predicate)|
32
- predicate = predicate.join("', '") if predicate.is_a?(Array)
33
- array << "#{column} IN ('#{predicate}')"
34
- end
35
- "(#{hash.join(') AND (')})"
36
- end
11
+ def self.class_for_constraint_type(type)
12
+ 'ActiveRecord::Postgres::Constraints::Types::'\
13
+ "#{type.to_s.classify}".constantize
14
+ end
37
15
 
38
- def to_sql(table, name_or_conditions, conditions = nil)
39
- if conditions
40
- name = name_or_conditions
41
- else
42
- name = "#{table}_#{Time.zone.now.nsec}"
43
- conditions = name_or_conditions
44
- end
16
+ def self.normalize_name_and_conditions(table, name_or_conditions, conditions)
17
+ return [name_or_conditions, conditions] if conditions
45
18
 
46
- "CONSTRAINT #{name} CHECK (#{normalize_conditions(conditions)})"
47
- end
19
+ ["#{table}_#{Time.zone.now.nsec}", name_or_conditions]
48
20
  end
49
21
  end
50
22
  end
51
23
  end
24
+
25
+ require_relative 'constraints/command_recorder'
26
+ require_relative 'constraints/postgresql_adapter'
27
+ require_relative 'constraints/railtie'
28
+ require_relative 'constraints/schema_creation'
29
+ require_relative 'constraints/schema_dumper'
30
+ require_relative 'constraints/table_definition'
31
+ require_relative 'constraints/types/check'
32
+ require_relative 'constraints/types/exclude'
33
+ require_relative 'constraints/version'
@@ -4,26 +4,32 @@ module ActiveRecord
4
4
  module Postgres
5
5
  module Constraints
6
6
  module CommandRecorder
7
- def add_check_constraint(*args, &block)
8
- record(:add_check_constraint, args, &block)
9
- end
7
+ CONSTRAINT_TYPES.keys.each do |type|
8
+ define_method("add_#{type}_constraint") do |*args, &block|
9
+ record(:"add_#{type}_constraint", args, &block)
10
+ end
10
11
 
11
- def invert_add_check_constraint(args, &block)
12
- [:remove_check_constraint, args, block]
13
- end
12
+ define_method("invert_add_#{type}_constraint") do |args, &block|
13
+ [:"remove_#{type}_constraint", args, block]
14
+ end
14
15
 
15
- def remove_check_constraint(*args, &block)
16
- if args.length < 3
17
- raise ActiveRecord::IrreversibleMigration,
18
- 'To make this migration reversible, pass the constraint to '\
19
- 'remove_check_constraint, i.e. `remove_check_constraint, '\
20
- "#{args[0].inspect}, #{args[1].inspect}, 'price > 999'`"
16
+ define_method("remove_#{type}_constraint") do |*args, &block|
17
+ if args.length < 3
18
+ example_constraint = ActiveRecord::Postgres::Constraints.
19
+ class_for_constraint_type(type).
20
+ example_constraint
21
+
22
+ raise ActiveRecord::IrreversibleMigration,
23
+ 'To make this migration reversible, pass the constraint to '\
24
+ "remove_#{type}_constraint, i.e. `remove_#{type}_constraint "\
25
+ "#{args[0].inspect}, #{args[1].inspect}, #{example_constraint}`"
26
+ end
27
+ record(:"remove_#{type}_constraint", args, &block)
21
28
  end
22
- record(:remove_check_constraint, args, &block)
23
- end
24
29
 
25
- def invert_remove_check_constraint(args, &block)
26
- [:add_check_constraint, args, block]
30
+ define_method("invert_remove_#{type}_constraint") do |args, &block|
31
+ [:"add_#{type}_constraint", args, block]
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -4,20 +4,36 @@ module ActiveRecord
4
4
  module Postgres
5
5
  module Constraints
6
6
  module PostgreSQLAdapter
7
- def add_check_constraint(table, name_or_conditions, conditions = nil)
8
- constraint = Constraints.to_sql(table, name_or_conditions, conditions)
7
+ CONSTRAINT_TYPES.keys.each do |type|
8
+ define_method "add_#{type}_constraint" do |table, name_or_conditions, conditions = nil|
9
+ add_constraint(type, table, name_or_conditions, conditions)
10
+ end
11
+
12
+ define_method "remove_#{type}_constraint" do |table, name, conditions = nil|
13
+ remove_constraint(type, table, name, conditions)
14
+ end
15
+ end
16
+
17
+ def add_constraint(type, table, name_or_conditions, conditions)
18
+ constraint =
19
+ ActiveRecord::Postgres::Constraints.
20
+ class_for_constraint_type(type).
21
+ to_sql(table, name_or_conditions, conditions)
9
22
  execute("ALTER TABLE #{table} ADD #{constraint}")
10
23
  end
11
24
 
12
- def remove_check_constraint(table, name, _conditions = nil)
25
+ def remove_constraint(_type, table, name, _conditions)
13
26
  execute("ALTER TABLE #{table} DROP CONSTRAINT #{name}")
14
27
  end
15
28
 
16
29
  def constraints(table)
17
- sql = "SELECT conname, consrc FROM pg_constraint
30
+ types = CONSTRAINT_TYPES.values.map { |v| "'#{v}'" }.join(', ')
31
+ sql = "SELECT conname, consrc, contype,
32
+ pg_get_constraintdef(pg_constraint.oid) AS definition
33
+ FROM pg_constraint
18
34
  JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
19
35
  WHERE
20
- pg_constraint.contype = 'c'
36
+ pg_constraint.contype IN (#{types})
21
37
  AND
22
38
  pg_class.relname = '#{table}'".tr("\n", ' ').squeeze(' ')
23
39
  execute sql
@@ -1,39 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord
4
- module Postgres
5
- module Constraints
6
- class Railtie < Rails::Railtie
7
- initializer 'active_record.postgres.constraints.patch_active_record' do |*_args|
8
- engine = self
9
- ActiveSupport.on_load(:active_record) do
10
- AR_CAS = ::ActiveRecord::ConnectionAdapters
3
+ if defined?(::Rails::Railtie)
4
+ module ActiveRecord
5
+ module Postgres
6
+ module Constraints
7
+ class Railtie < ::Rails::Railtie
8
+ initializer 'active_record.postgres.constraints.patch_active_record' do |*_args|
9
+ engine = self
10
+ ActiveSupport.on_load(:active_record) do
11
+ AR_CAS = ::ActiveRecord::ConnectionAdapters
11
12
 
12
- engine.apply_patch! if engine.pg?
13
+ engine.apply_patch! if engine.pg?
14
+ end
13
15
  end
14
- end
15
16
 
16
- def apply_patch!
17
- Rails.logger.info do
18
- 'Applying Postgres Constraints patches to ActiveRecord'
19
- end
20
- AR_CAS::TableDefinition.include TableDefinition
21
- AR_CAS::PostgreSQLAdapter.include PostgreSQLAdapter
22
- AR_CAS::AbstractAdapter::SchemaCreation.prepend SchemaCreation
17
+ def apply_patch!
18
+ Rails.logger.info do
19
+ 'Applying Postgres Constraints patches to ActiveRecord'
20
+ end
21
+ AR_CAS::TableDefinition.include TableDefinition
22
+ AR_CAS::PostgreSQLAdapter.include PostgreSQLAdapter
23
+ AR_CAS::AbstractAdapter::SchemaCreation.prepend SchemaCreation
23
24
 
24
- ::ActiveRecord::Migration::CommandRecorder.include CommandRecorder
25
- ::ActiveRecord::SchemaDumper.prepend SchemaDumper
26
- end
25
+ ::ActiveRecord::Migration::CommandRecorder.include CommandRecorder
26
+ ::ActiveRecord::SchemaDumper.prepend SchemaDumper
27
+ end
27
28
 
28
- def pg?
29
- config = ActiveRecord::Base.connection_config
30
- return true if config && 'postgresql' == config[:adapter]
29
+ def pg?
30
+ config = ActiveRecord::Base.connection_config
31
+ return true if config && 'postgresql' == config[:adapter]
31
32
 
32
- Rails.logger.warn do
33
- 'Not applying Postgres Constraints patches to ActiveRecord ' \
34
- 'since the database is not postgres'
33
+ Rails.logger.warn do
34
+ 'Not applying Postgres Constraints patches to ActiveRecord ' \
35
+ 'since the database is not postgres'
36
+ end
37
+ false
35
38
  end
36
- false
37
39
  end
38
40
  end
39
41
  end
@@ -8,7 +8,7 @@ module ActiveRecord
8
8
  def visit_TableDefinition(table_definition)
9
9
  # rubocop:enable Naming/MethodName
10
10
  result = super
11
- return result unless table_definition.check_constraints
11
+ return result unless table_definition.constraints
12
12
 
13
13
  nesting = 0
14
14
  # Find the closing paren of the "CREATE TABLE ( ... )" clause
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  nesting, should_break = adjust_nesting(nesting, token)
18
18
  break i if should_break
19
19
  end
20
- result[index] = ", #{table_definition.check_constraints.join(', ')})"
20
+ result[index] = ", #{table_definition.constraints.join(', ')})"
21
21
  result
22
22
  end
23
23
 
@@ -5,17 +5,44 @@ module ActiveRecord
5
5
  module Constraints
6
6
  module SchemaDumper
7
7
  def indexes_in_create(table, stream)
8
- super
9
8
  constraints = @connection.constraints(table)
9
+ indexes = @connection.indexes(table).reject do |index|
10
+ constraints.pluck('conname').include?(index_name(index))
11
+ end
12
+ dump_indexes(indexes, stream)
13
+ dump_constraints(constraints, stream)
14
+ end
15
+
16
+ private
17
+
18
+ def dump_indexes(indexes, stream)
19
+ return unless indexes.any?
20
+
21
+ index_statements = indexes.map do |index|
22
+ " t.index #{index_parts(index).join(', ')}"
23
+ end
24
+ stream.puts index_statements.sort.join("\n")
25
+ end
26
+
27
+ def dump_constraints(constraints, stream)
10
28
  return unless constraints.any?
11
29
 
12
30
  constraint_statements = constraints.map do |constraint|
13
- name = constraint['conname']
14
- conditions = constraint['consrc']
15
- " t.check_constraint :#{name}, #{conditions.inspect}"
31
+ type = CONSTRAINT_TYPES.key(constraint['contype'])
32
+ ActiveRecord::Postgres::Constraints.
33
+ class_for_constraint_type(type).
34
+ to_schema_dump(constraint)
16
35
  end
17
36
  stream.puts constraint_statements.sort.join("\n")
18
37
  end
38
+
39
+ def index_name(index)
40
+ if index.is_a?(ActiveRecord::ConnectionAdapters::IndexDefinition)
41
+ index.name
42
+ else
43
+ index['name']
44
+ end
45
+ end
19
46
  end
20
47
  end
21
48
  end
@@ -4,12 +4,21 @@ module ActiveRecord
4
4
  module Postgres
5
5
  module Constraints
6
6
  module TableDefinition
7
- attr_reader :check_constraints
7
+ attr_reader :constraints
8
8
 
9
- def check_constraint(name_or_conditions, conditions = nil)
10
- @check_constraints ||= []
11
- constraint = Constraints.to_sql(name, name_or_conditions, conditions)
12
- check_constraints << constraint
9
+ CONSTRAINT_TYPES.keys.each do |type|
10
+ define_method "#{type}_constraint" do |name_or_conditions, conditions = nil|
11
+ add_constraint(type, name_or_conditions, conditions)
12
+ end
13
+ end
14
+
15
+ def add_constraint(type, name_or_conditions, conditions)
16
+ @constraints ||= []
17
+ constraint =
18
+ ActiveRecord::Postgres::Constraints.
19
+ class_for_constraint_type(type).
20
+ to_sql(name, name_or_conditions, conditions)
21
+ constraints << constraint
13
22
  end
14
23
  end
15
24
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Postgres
5
+ module Constraints
6
+ module Types
7
+ module Check
8
+ class << self
9
+ def to_sql(table, name_or_conditions, conditions = nil)
10
+ name, conditions = ActiveRecord::Postgres::Constraints.
11
+ normalize_name_and_conditions(table, name_or_conditions, conditions)
12
+ "CONSTRAINT #{name} CHECK (#{normalize_conditions(conditions)})"
13
+ end
14
+
15
+ def to_schema_dump(constraint)
16
+ name = constraint['conname']
17
+ conditions = constraint['consrc']
18
+ " t.check_constraint :#{name}, #{conditions.inspect}"
19
+ end
20
+
21
+ def example_constraint
22
+ "'price > 999'"
23
+ end
24
+
25
+ private
26
+
27
+ def normalize_conditions(conditions)
28
+ conditions = [conditions] unless conditions.is_a?(Array)
29
+ conditions = conditions.map do |condition|
30
+ if condition.is_a?(Hash)
31
+ normalize_conditions_hash(condition)
32
+ else
33
+ condition
34
+ end
35
+ end
36
+
37
+ return conditions.first if 1 == conditions.length
38
+
39
+ "(#{conditions.join(') AND (')})"
40
+ end
41
+
42
+ def normalize_conditions_hash(hash)
43
+ hash = hash.reduce([]) do |array, (column, predicate)|
44
+ predicate = predicate.join("', '") if predicate.is_a?(Array)
45
+ array << "#{column} IN ('#{predicate}')"
46
+ end
47
+ "(#{hash.join(') AND (')})"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Postgres
5
+ module Constraints
6
+ module Types
7
+ module Exclude
8
+ OPERATOR_SYMBOLS = {
9
+ equals: '=',
10
+ overlaps: '&&',
11
+ }.freeze
12
+
13
+ class << self
14
+ def to_sql(table, name_or_conditions, conditions = nil)
15
+ name, conditions = ActiveRecord::Postgres::Constraints.
16
+ normalize_name_and_conditions(table, name_or_conditions, conditions)
17
+
18
+ using = conditions.delete(:using)
19
+ using = " USING #{using}" if using
20
+
21
+ where = conditions.delete(:where)
22
+ where = " WHERE (#{where})" if where
23
+
24
+ conditions = normalize_conditions(conditions).join(', ')
25
+
26
+ "CONSTRAINT #{name} EXCLUDE#{using} (#{conditions})#{where}"
27
+ end
28
+
29
+ def to_schema_dump(constraint)
30
+ name = constraint['conname']
31
+ definition = constraint['definition']
32
+
33
+ using = definition.match(/USING (\w*)/).try(:[], 1)
34
+ using = "using: :#{using}, " if using
35
+
36
+ where = definition.match(/WHERE \((.*)\)/).try(:[], 1)
37
+ where = "where: '#{where}'" if where
38
+
39
+ exclusions = definition_to_exclusions(definition).join(', ')
40
+ conditions = "#{using}#{exclusions}#{", #{where}" if where}"
41
+
42
+ " t.exclude_constraint :#{name}, #{conditions}"
43
+ end
44
+
45
+ def example_constraint
46
+ %(using: :gist, 'tsrange("from", "to")' => :overlaps, project_id: :equals)
47
+ end
48
+
49
+ private
50
+
51
+ def definition_to_exclusions(definition)
52
+ definition.
53
+ split(' WHERE')[0].
54
+ match(/\((.*)/)[1].
55
+ chomp(')').
56
+ scan(/((?:[^,(]+|(\((?>[^()]+|\g<-1>)*)\))+)/).
57
+ map!(&:first).
58
+ map!(&:strip).
59
+ flatten.
60
+ map! { |exclusion| element_and_operator(exclusion) }
61
+ end
62
+
63
+ def element_and_operator(exclusion)
64
+ element, operator = exclusion.strip.split(' WITH ')
65
+ "#{normalize_element(element)} #{normalize_operator(operator)}"
66
+ end
67
+
68
+ def normalize_conditions(conditions)
69
+ conditions.map do |element, operator|
70
+ "#{element} WITH #{OPERATOR_SYMBOLS[operator.to_sym]}"
71
+ end
72
+ end
73
+
74
+ def normalize_element(element)
75
+ element.include?('(') ? "'#{element}' =>" : "#{element}:"
76
+ end
77
+
78
+ def normalize_operator(operator)
79
+ ":#{OPERATOR_SYMBOLS.invert[operator]}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module Postgres
5
5
  module Constraints
6
- VERSION = '0.1.5'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record-postgres-constraints
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Isaac Betesh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-13 00:00:00.000000000 Z
11
+ date: 2020-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg
@@ -48,16 +48,16 @@ dependencies:
48
48
  name: osm-rubocop
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: 0.1.15
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - ">="
58
+ - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: 0.1.15
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: rspec
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -110,6 +110,8 @@ files:
110
110
  - lib/active_record/postgres/constraints/schema_creation.rb
111
111
  - lib/active_record/postgres/constraints/schema_dumper.rb
112
112
  - lib/active_record/postgres/constraints/table_definition.rb
113
+ - lib/active_record/postgres/constraints/types/check.rb
114
+ - lib/active_record/postgres/constraints/types/exclude.rb
113
115
  - lib/active_record/postgres/constraints/version.rb
114
116
  homepage: https://github.com/on-site/active_record-postgres-constraints
115
117
  licenses: