rein 5.0.0 → 5.1.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: 12dbfe85b21d4fe3a16982a86d784701a74ed611046b9eaa0dcddea86afb8bd3
4
- data.tar.gz: be0b34a4ecaa0098217e6bb8327fbc1cf09adf329a3ceb30d26afb4be3fb071b
3
+ metadata.gz: 4688e25fef8600352e4ba1cd54a5ace2eeca7f827a7ca3ad023de66b93f46d7f
4
+ data.tar.gz: 918d01449019e40a5d01f858755dbc3f10d4673f936ce0c9f2578de081f1f26c
5
5
  SHA512:
6
- metadata.gz: a9bce885142492aa55ef15346ef19d1d631a1f85cd9ba8bc7c60ce977bd481a3d2962c3507bccde0ed9c5306f8a8f6d76b30e8e83a721f030f04df999a03ac47
7
- data.tar.gz: 9e8c8ef912098fc12d944c1198783bf2b0308fa3a241ca937ae03d4ee9532c9acd391da045bb778a002c2cf528c81112008309614254aaf01a1f1863e9385add
6
+ metadata.gz: 8a27510401ee3dbb708f41cda16b2c697a7e2de643923c35c4657eb3b783885ffeb09186a6fd6c0683b78f27c667d82b15893b2d1d051b9656042ab9a7f1fe85
7
+ data.tar.gz: e82ea6c4ed180a76471419d99316ea75b80f040c8576bacfb6ed671a6ba12bdd3adc4d5cc36a15f3632bd77a16459b93e0872e081e12f07c6ab352a8b28a8615
@@ -1,3 +1,9 @@
1
+ ## Unreleased
2
+
3
+ ## 5.1.0 (2020-01-08)
4
+
5
+ * Add validate option to foreign key and check constraints
6
+
1
7
  ## 5.0.0 (2019-08-23)
2
8
 
3
9
  * Upgrade development deps
data/README.md CHANGED
@@ -19,6 +19,7 @@ advantage of reversible Rails migrations.
19
19
 
20
20
  * [Getting Started](#getting-started)
21
21
  * [Constraint Types](#constraint-types)
22
+ * [Summary](#summary)
22
23
  * [Foreign Key Constraints](#foreign-key-constraints)
23
24
  * [Unique Constraints](#unique-constraints)
24
25
  * [Exclusion Constraints](#exclusion-constraints)
@@ -29,6 +30,7 @@ advantage of reversible Rails migrations.
29
30
  * [Presence Constraints](#presence-constraints)
30
31
  * [Null Constraints](#null-constraints)
31
32
  * [Check Constraints](#check-constraints)
33
+ * [Validate Constraints](#validate-constraints)
32
34
  * [Data Types](#data-types)
33
35
  * [Enumerated Types](#enumerated-types)
34
36
  * [Views](#views)
@@ -62,6 +64,23 @@ end
62
64
 
63
65
  ## Constraint Types
64
66
 
67
+ ### Summary
68
+
69
+ The table below summarises the constraint operations provided by Rein and whether they support [validation](#validate-constraints).
70
+
71
+ | Rein name | Rein method | SQL | Supports `NOT VALID`? |
72
+ | ----------- | ----------- | --- | --------------------- |
73
+ | Foreign Key | `add_foreign_key_constraint` | `FOREIGN KEY` | yes |
74
+ | Unique | `add_unique_constraint` | `UNIQUE` | no |
75
+ | Exclusion | `add_exclusion_constraint` | `EXCLUDE` | no |
76
+ | Inclusion | `add_inclusion_constraint` | `CHECK` | yes |
77
+ | Length | `add_length_constraint` | `CHECK` | yes |
78
+ | Match | `add_match_constraint` | `CHECK` | yes |
79
+ | Numericality | `add_numericality_constraint` | `CHECK` | yes |
80
+ | Presence | `add_presence_constraint` | `CHECK` | yes |
81
+ | Null | `add_null_constraint` | `CHECK` | yes |
82
+ | Check | `add_check_constraint` | `CHECK` | yes |
83
+
65
84
  ### Foreign Key Constraints
66
85
 
67
86
  A foreign key constraint specifies that the values in a column must match the
@@ -461,6 +480,32 @@ To remove a check constraint:
461
480
  remove_check_constraint :books, "substring(title FROM 1 FOR 1) IS DISTINCT FROM 'r'", name: 'no_r_titles'
462
481
  ```
463
482
 
483
+ ### Validate Constraints
484
+
485
+ Adding a constraint can be a very costly operation, especially on larger tables, as the database has to scan all rows in the table to check for violations of the new constraint. During this time, concurrent writes are blocked as an [`ACCESS EXCLUSIVE`](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES) table lock is taken. In addition, adding a foreign key constraint obtains a `SHARE ROW EXCLUSIVE` lock on the referenced table. See the [docs](https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES) for more details.
486
+
487
+ In order to allow constraints to be added concurrently on larger tables, and to allow the addition of constraints on tables containing rows with existing violations, Postgres supports adding constraints using the `NOT VALID` option (currently only for `CHECK` and foreign key constraints).
488
+
489
+ This allows the constraint to be added immediately, without validating existing rows, but enforcing the constraint for any new rows and updates. After that, a `VALIDATE CONSTRAINT` command can be issued to verify that existing rows satisfy the constraint, which is done in a way that does not lock out concurrent updates and "with the least impact on other work".
490
+
491
+ Rein supports adding `CHECK` and foreign key constraints with the `NOT VALID` option by passing `validate: false` to the options of the supported Rein DSL methods, [summarised above](#summary).
492
+
493
+ ```ruby
494
+ add_null_constraint :books, :due_date, if: "state = 'on_loan'", validate: false
495
+ ```
496
+
497
+ With Rails 5.2 or later, you can use [`validate_constraint`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_constraint) in a subsequent migration to validate a `NOT VALID` constraint. If you are using versions of Rails below 5.2, you can use Rein's `validate_table_constraint` method:
498
+
499
+ ```ruby
500
+ validate_table_constraint :books, "no_r_titles"
501
+ ```
502
+
503
+ It's safe (a no-op) to validate a constraint that is already marked as valid.
504
+
505
+ ### Side note on `lock_timeout`
506
+
507
+ It's advisable to set a [sensibly low `lock_timeout`](https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/) in your database migrations, otherwise existing long-running transactions can prevent your migration from acquiring the required locks, resulting in a lock queue that prevents even selects on the target table, potentially brining your production database grinding to a halt.
508
+
464
509
  ## Data Types
465
510
 
466
511
  ### Enumerated Types
@@ -10,6 +10,7 @@ require 'rein/constraint/presence'
10
10
  require 'rein/constraint/primary_key'
11
11
  require 'rein/constraint/unique'
12
12
  require 'rein/constraint/exclusion'
13
+ require 'rein/constraint/validate'
13
14
  require 'rein/schema'
14
15
  require 'rein/type/enum'
15
16
  require 'rein/view'
@@ -27,6 +28,7 @@ module ActiveRecord
27
28
  include Rein::Constraint::PrimaryKey
28
29
  include Rein::Constraint::Unique
29
30
  include Rein::Constraint::Exclusion
31
+ include Rein::Constraint::Validate
30
32
  include Rein::Schema
31
33
  include Rein::Type::Enum
32
34
  include Rein::View
@@ -27,7 +27,7 @@ module Rein
27
27
  sql = "ALTER TABLE #{Util.wrap_identifier(table_name)}"
28
28
  sql << " ADD CONSTRAINT #{name}"
29
29
  sql << " CHECK (#{predicate})"
30
- execute(sql)
30
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
31
31
  end
32
32
 
33
33
  def _remove_check_constraint(table_name, _predicate, options = {})
@@ -31,7 +31,7 @@ module Rein
31
31
  sql << " REFERENCES #{referenced_table} (#{Util.wrap_identifier(referenced_attribute)})"
32
32
  sql << " ON DELETE #{referential_action(options[:on_delete])}" if options[:on_delete].present?
33
33
  sql << " ON UPDATE #{referential_action(options[:on_update])}" if options[:on_update].present?
34
- execute(sql)
34
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
35
35
  add_index(referencing_table, referencing_attribute) if options[:index] == true
36
36
  end
37
37
 
@@ -28,7 +28,8 @@ module Rein
28
28
  values = options[:in].map { |value| quote(value) }.join(', ')
29
29
  attribute = Util.wrap_identifier(attribute)
30
30
  conditions = Util.conditions_with_if("#{attribute} IN (#{values})", options)
31
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
31
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
32
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
32
33
  end
33
34
 
34
35
  def _remove_inclusion_constraint(table, attribute, options = {})
@@ -39,7 +39,8 @@ module Rein
39
39
  [attribute_length, operator, value].join(' ')
40
40
  end.join(' AND ')
41
41
  conditions = Util.conditions_with_if(conditions, options)
42
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
42
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
43
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
43
44
  end
44
45
 
45
46
  def _remove_length_constraint(table, attribute, options = {})
@@ -36,7 +36,8 @@ module Rein
36
36
  [attribute, operator, "'#{value}'"].join(' ')
37
37
  end.join(' AND ')
38
38
  conditions = Util.conditions_with_if(conditions, options)
39
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
39
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
40
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
40
41
  end
41
42
 
42
43
  def _remove_match_constraint(table, attribute, options = {})
@@ -27,7 +27,8 @@ module Rein
27
27
  table = Util.wrap_identifier(table)
28
28
  attribute = Util.wrap_identifier(attribute)
29
29
  conditions = Util.conditions_with_if("#{attribute} IS NOT NULL", options)
30
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
30
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
31
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
31
32
  end
32
33
 
33
34
  def _remove_null_constraint(table, attribute, options = {})
@@ -38,7 +38,8 @@ module Rein
38
38
  [attribute, operator, value].join(' ')
39
39
  end.join(' AND ')
40
40
  conditions = Util.conditions_with_if(conditions, options)
41
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
41
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
42
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
42
43
  end
43
44
 
44
45
  def _remove_numericality_constraint(table, attribute, options = {})
@@ -30,7 +30,8 @@ module Rein
30
30
  "(#{attribute} IS NOT NULL) AND (#{attribute} !~ '^\\s*$')",
31
31
  options
32
32
  )
33
- execute("ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})")
33
+ sql = "ALTER TABLE #{table} ADD CONSTRAINT #{name} CHECK (#{conditions})"
34
+ execute(Util.add_not_valid_suffix_if_required(sql, options))
34
35
  end
35
36
 
36
37
  def _remove_presence_constraint(table, attribute, options = {})
@@ -0,0 +1,25 @@
1
+ require 'rein/util'
2
+
3
+ module Rein
4
+ module Constraint
5
+ # This module contains methods for validating constraints.
6
+ module Validate
7
+ def validate_table_constraint(*args)
8
+ reversible do |dir|
9
+ dir.up { _validate_table_constraint(*args) }
10
+ dir.down do
11
+ # No-op - it's safe to validate an already validated constraint
12
+ # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
13
+ # "Nothing happens if the constraint is already marked valid."
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def _validate_table_constraint(table, constraint_name)
21
+ execute("ALTER TABLE #{Util.wrap_identifier(table)} VALIDATE CONSTRAINT #{constraint_name}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,12 @@
1
1
  module Rein
2
2
  # The {Util} module provides utility methods for handling options.
3
3
  module Util
4
+ # Returns a new string with the suffix appended if required
5
+ def self.add_not_valid_suffix_if_required(sql, options)
6
+ suffix = options[:validate] == false ? ' NOT VALID' : ''
7
+ "#{sql}#{suffix}"
8
+ end
9
+
4
10
  def self.conditions_with_if(conditions, options = {})
5
11
  if options[:if].present?
6
12
  "NOT (#{options[:if]}) OR (#{conditions})"
@@ -1,3 +1,3 @@
1
1
  module Rein
2
- VERSION = '5.0.0'.freeze
2
+ VERSION = '5.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rein
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Bassett
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-23 00:00:00.000000000 Z
11
+ date: 2020-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -144,6 +144,7 @@ files:
144
144
  - lib/rein/constraint/presence.rb
145
145
  - lib/rein/constraint/primary_key.rb
146
146
  - lib/rein/constraint/unique.rb
147
+ - lib/rein/constraint/validate.rb
147
148
  - lib/rein/schema.rb
148
149
  - lib/rein/type/enum.rb
149
150
  - lib/rein/util.rb