active_record-postgres-constraints 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: