nandi 2.1.1 → 3.0.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: 70ecd10143c43ea10379b4dc7c6d1bbf4d4bc3d654cbf15dde273b49cf229a08
4
- data.tar.gz: 0b64d84387f9e4be7eed24ddadc9f74d58d118cca7d91e0e526f680ee5fcfba8
3
+ metadata.gz: 986532981584b1518a8422a2cc212ed963782fb31554012a0e2b4da50bf8a3db
4
+ data.tar.gz: 282b4f7c95256f6a8af1090b82a2324d0cc12e4570bf22608c219d8486cb5334
5
5
  SHA512:
6
- metadata.gz: 8f057aa2c71d7169d6482a2fbc2fe7fb4721bfeef3b650a74c09e002fe9b7eff4fa3479b5e2c0af7940d91d88ca71dcddab0fbd169077fee1ec00cb26e447f60
7
- data.tar.gz: 112cc9d2d08c4802767d2c481d11f30ff87844c765a0afcb0224dcb1bbfc1c69dc4d7c6dd618a79d744bb7fdff03f2945f216f2eba90511d99b6c4ad58fcdd59
6
+ metadata.gz: 7788cca1a5ca77b8e8e04decc6819b41078936452f3ab2cae00a3b35ca6c338f4fb4870e2d8eb11f4f3109029c41a2ccd327fb980417e1b95c1bf48644bca23e
7
+ data.tar.gz: 2331b4022d381b2863ecbf2429365aa2870fb39386fde1cb0aa27ea8800fc3c7777490896a2d55af95696d8b0457c2f485890f5dcd8878019f1a50b2ea28af09
data/README.md CHANGED
@@ -473,6 +473,16 @@ Register a block to be called on output, for example a code formatter. Whatever
473
473
  config.post_process { |migration| MyFormatter.format(migration) }
474
474
  ```
475
475
 
476
+ #register_migration_modifier(klass)
477
+
478
+ Register a custom migration modifier. Migration modifiers inherit from `Nandi::MigrationModifiers::Base` and implement `.up(instructions)`, `.down(instructions)`, or both. Nandi calls the appropriate method based on migration direction.
479
+
480
+ Nandi ships with one built-in modifier, `Nandi::MigrationModifiers::CreateTableValidatesFks`, which is enabled by default. See the [Migration Modifiers](#migration-modifiers) section for details.
481
+
482
+ ```rb
483
+ config.register_migration_modifier(MyCustomModifier)
484
+ ```
485
+
476
486
  #register_method(name, klass)
477
487
 
478
488
  Register a custom DDL method.
@@ -483,6 +493,56 @@ Parameters:
483
493
 
484
494
  `klass` (Class) — The class to initialise with the arguments to the method. It should define a `template` instance method which will return a subclass of Cell::ViewModel from the Cells templating library and a `procedure` method that returns the name of the method. It may optionally define a `mixins` method, which will return an array of `Module`s to be mixed into any migration that uses this method.
485
495
 
496
+ ## Migration Modifiers
497
+
498
+ Migration modifiers allow instructions to change their behaviour based on other instructions in the same migration. After the direction block runs, Nandi iterates over all configured modifiers and passes the full instruction list to each one.
499
+
500
+ ### Built-in: `CreateTableValidatesFks`
501
+
502
+ When you create a table and add a foreign key on that same table in a single migration, the FK does not need the usual `NOT VALID` deferral — the table is brand new, so there are no existing rows to validate. Nandi detects this automatically and sets `validate: true` on the relevant FK instructions:
503
+
504
+ ```rb
505
+ class CreatePaymentsWithFk < Nandi::Migration
506
+ def up
507
+ create_table :payments do |t|
508
+ t.bigint :mandate_id, null: false
509
+ end
510
+
511
+ add_foreign_key :payments, :mandates
512
+ end
513
+ end
514
+ ```
515
+
516
+ The compiled output will include `validate: true` on the `add_foreign_key` call, skipping the separate validate step that would otherwise be required.
517
+
518
+ This modifier is enabled by default. To disable it, you would need to replace `config.migration_modifiers` entirely — there is no built-in opt-out.
519
+
520
+ ### Writing a custom modifier
521
+
522
+ Inherit from `Nandi::MigrationModifiers::Base` and override `.up`, `.down`, or both. The base class provides no-op defaults so you only implement what you need:
523
+
524
+ ```rb
525
+ class MyModifier < Nandi::MigrationModifiers::Base
526
+ def self.up(instructions)
527
+ # inspect and mutate instructions for the up direction
528
+ end
529
+
530
+ def self.down(instructions)
531
+ # inspect and mutate instructions for the down direction
532
+ end
533
+ end
534
+ ```
535
+
536
+ Register it in your Nandi initializer:
537
+
538
+ ```rb
539
+ Nandi.configure do |config|
540
+ config.register_migration_modifier(MyModifier)
541
+ end
542
+ ```
543
+
544
+ Modifiers are applied in registration order, after all built-in modifiers.
545
+
486
546
  ## Multi-Database Support
487
547
 
488
548
  Nandi 2.0+ supports managing migrations for multiple databases within a single Rails application.
data/lib/nandi/config.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "nandi/migration_modifiers"
3
4
  require "nandi/renderers"
4
5
  require "nandi/lockfile"
5
6
  require "nandi/multi_database"
@@ -30,13 +31,18 @@ module Nandi
30
31
  attr_writer :lockfile_directory
31
32
 
32
33
  # @api private
33
- attr_reader :post_processor, :custom_methods
34
+ attr_reader :post_processor, :custom_methods, :migration_modifiers
34
35
 
35
36
  def initialize(renderer: Renderers::ActiveRecord)
36
37
  @renderer = renderer
37
38
  @custom_methods = {}
38
39
  @compile_files = DEFAULT_COMPILE_FILES
39
40
  @lockfile_directory = DEFAULT_LOCKFILE_DIRECTORY
41
+ @migration_modifiers = [MigrationModifiers::CreateTableValidatesFks]
42
+ end
43
+
44
+ def register_migration_modifier(klass)
45
+ @migration_modifiers << klass
40
46
  end
41
47
 
42
48
  # Register a block to be called on output, for example a code formatter. Whatever is
@@ -73,11 +79,11 @@ module Nandi
73
79
  def migration_directory(database_name = nil) = config(database_name).migration_directory
74
80
  def output_directory(database_name = nil) = config(database_name).output_directory
75
81
  def access_exclusive_lock_timeout(database_name = nil) = config(database_name).access_exclusive_lock_timeout
76
- def access_exclusive_lock_timeout_limit(database_name = nil) = config(database_name).access_exclusive_lock_timeout_limit
82
+ def access_exclusive_lock_timeout_max(database_name = nil) = config(database_name).access_exclusive_lock_timeout_max
77
83
  def access_exclusive_statement_timeout(database_name = nil) = config(database_name).access_exclusive_statement_timeout
78
- def access_exclusive_statement_timeout_limit(database_name = nil) = config(database_name).access_exclusive_statement_timeout_limit
79
- def concurrent_lock_timeout_limit(database_name = nil) = config(database_name).concurrent_lock_timeout_limit
80
- def concurrent_statement_timeout_limit(database_name = nil) = config(database_name).concurrent_statement_timeout_limit
84
+ def access_exclusive_statement_timeout_max(database_name = nil) = config(database_name).access_exclusive_statement_timeout_max
85
+ def concurrent_lock_timeout_min(database_name = nil) = config(database_name).concurrent_lock_timeout_min
86
+ def concurrent_statement_timeout_min(database_name = nil) = config(database_name).concurrent_statement_timeout_min
81
87
  def concurrent_lock_timeout(database_name = nil) = config(database_name).concurrent_lock_timeout
82
88
  def concurrent_statement_timeout(database_name = nil) = config(database_name).concurrent_statement_timeout
83
89
  # rubocop:enable Layout/LineLength
@@ -86,11 +92,11 @@ module Nandi
86
92
  delegate :migration_directory=,
87
93
  :output_directory=,
88
94
  :access_exclusive_lock_timeout=,
89
- :access_exclusive_lock_timeout_limit=,
95
+ :access_exclusive_lock_timeout_max=,
90
96
  :access_exclusive_statement_timeout=,
91
- :access_exclusive_statement_timeout_limit=,
92
- :concurrent_lock_timeout_limit=,
93
- :concurrent_statement_timeout_limit=,
97
+ :access_exclusive_statement_timeout_max=,
98
+ :concurrent_lock_timeout_min=,
99
+ :concurrent_statement_timeout_min=,
94
100
  :concurrent_lock_timeout=,
95
101
  :concurrent_statement_timeout=,
96
102
  to: :default
@@ -5,24 +5,29 @@ require "active_support/inflector"
5
5
  module Nandi
6
6
  module Instructions
7
7
  class AddForeignKey
8
- attr_reader :table, :target
8
+ attr_reader :table, :target, :validate
9
9
 
10
10
  def initialize(table:, target:, name: nil, **extra_args)
11
11
  @table = table
12
12
  @target = target
13
13
  @extra_args = extra_args
14
14
  @name = name
15
+ @validate = false
15
16
  end
16
17
 
17
18
  def procedure
18
19
  :add_foreign_key
19
20
  end
20
21
 
22
+ def validate!
23
+ @validate = true
24
+ end
25
+
21
26
  def extra_args
22
27
  {
23
28
  **@extra_args,
24
29
  name: name,
25
- validate: false,
30
+ validate: validate,
26
31
  }.compact
27
32
  end
28
33
 
@@ -317,6 +317,8 @@ module Nandi
317
317
 
318
318
  public_send(direction) unless current_instructions.any?
319
319
 
320
+ Nandi.config.migration_modifiers.each { |mod| mod.public_send(direction, current_instructions) }
321
+
320
322
  current_instructions
321
323
  end
322
324
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module MigrationModifiers
5
+ class Base
6
+ def self.up(instructions); end
7
+ def self.down(instructions); end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module MigrationModifiers
5
+ class CreateTableValidatesFks < Base
6
+ def self.up(instructions)
7
+ new_tables = instructions.
8
+ select { |i| i.procedure == :create_table }.
9
+ to_set { |i| i.table.to_sym }
10
+
11
+ return if new_tables.empty?
12
+
13
+ instructions.
14
+ grep(Instructions::AddForeignKey).
15
+ select { |i| new_tables.include?(i.table.to_sym) }.
16
+ each(&:validate!)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/migration_modifiers/base"
4
+ require "nandi/migration_modifiers/create_table_validates_fks"
5
+
6
+ module Nandi
7
+ module MigrationModifiers
8
+ end
9
+ end
@@ -9,11 +9,12 @@ module Nandi
9
9
  DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT = 1_500
10
10
  DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT = 5_000
11
11
 
12
- DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT_LIMIT =
12
+ DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT_MAX =
13
13
  DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT
14
- DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT_LIMIT =
14
+ DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT_MAX =
15
15
  DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT
16
- DEFAULT_CONCURRENT_TIMEOUT_LIMIT = 3_600_000
16
+ DEFAULT_CONCURRENT_LOCK_TIMEOUT_MIN = 10_000
17
+ DEFAULT_CONCURRENT_STATEMENT_TIMEOUT_MIN = 30_000
17
18
 
18
19
  DEFAULT_MIGRATION_DIRECTORY = "db/safe_migrations"
19
20
  DEFAULT_OUTPUT_DIRECTORY = "db/migrate"
@@ -33,22 +34,22 @@ module Nandi
33
34
  # The maximum lock timeout for migrations that take an ACCESS EXCLUSIVE
34
35
  # lock and therefore block all reads and writes. Default: 5,000ms.
35
36
  # @return [Integer]
36
- attr_accessor :access_exclusive_statement_timeout_limit
37
+ attr_accessor :access_exclusive_statement_timeout_max
37
38
 
38
39
  # The maximum statement timeout for migrations that take an ACCESS
39
40
  # EXCLUSIVE lock and therefore block all reads and writes. Default: 1500ms.
40
41
  # @return [Integer]
41
- attr_accessor :access_exclusive_lock_timeout_limit
42
+ attr_accessor :access_exclusive_lock_timeout_max
42
43
 
43
44
  # The minimum statement timeout for migrations that take place concurrently.
44
45
  # Default: 3,600,000ms (ie, 3 hours).
45
46
  # @return [Integer]
46
- attr_accessor :concurrent_statement_timeout_limit
47
+ attr_accessor :concurrent_statement_timeout_min
47
48
 
48
49
  # The minimum lock timeout for migrations that take place concurrently.
49
50
  # Default: 3,600,000ms (ie, 3 hours).
50
51
  # @return [Integer]
51
- attr_accessor :concurrent_lock_timeout_limit
52
+ attr_accessor :concurrent_lock_timeout_min
52
53
 
53
54
  # The default lock timeout for migrations that take place concurrently
54
55
  # (eg. add_index, remove_index). When set, concurrent migrations will use
@@ -71,6 +72,13 @@ module Nandi
71
72
  attr_accessor :migration_directory,
72
73
  :lockfile_name
73
74
 
75
+ RENAMED_KEYS = {
76
+ access_exclusive_lock_timeout_limit: :access_exclusive_lock_timeout_max,
77
+ access_exclusive_statement_timeout_limit: :access_exclusive_statement_timeout_max,
78
+ concurrent_lock_timeout_limit: :concurrent_lock_timeout_min,
79
+ concurrent_statement_timeout_limit: :concurrent_statement_timeout_min,
80
+ }.freeze
81
+
74
82
  def initialize(name:, config:)
75
83
  @name = name
76
84
  @raw_config = config
@@ -81,24 +89,36 @@ module Nandi
81
89
  @output_directory = config[:output_directory] || "db/#{path_prefix(name, default)}migrate"
82
90
  @lockfile_name = config[:lockfile_name] || ".#{path_prefix(name, default)}nandilock.yml"
83
91
 
84
- timeout_limits(config)
92
+ timeout_limits(normalize_config(config))
85
93
  end
86
94
 
87
95
  private
88
96
 
97
+ def normalize_config(config)
98
+ RENAMED_KEYS.each_with_object(config.dup) do |(old_key, new_key), normalized|
99
+ next unless normalized.key?(old_key)
100
+
101
+ Nandi::DEPRECATOR.warn(
102
+ "#{old_key} is deprecated and will be removed in a future version. " \
103
+ "Use #{new_key} instead.",
104
+ )
105
+ normalized[new_key] ||= normalized.delete(old_key)
106
+ end
107
+ end
108
+
89
109
  def timeout_limits(config)
90
110
  @access_exclusive_lock_timeout =
91
111
  config[:access_exclusive_lock_timeout] || DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT
92
112
  @access_exclusive_statement_timeout =
93
113
  config[:access_exclusive_statement_timeout] || DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT
94
- @access_exclusive_lock_timeout_limit =
95
- config[:access_exclusive_lock_timeout_limit] || DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT_LIMIT
96
- @access_exclusive_statement_timeout_limit =
97
- config[:access_exclusive_statement_timeout_limit] || DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT_LIMIT
98
- @concurrent_lock_timeout_limit =
99
- config[:concurrent_lock_timeout_limit] || DEFAULT_CONCURRENT_TIMEOUT_LIMIT
100
- @concurrent_statement_timeout_limit =
101
- config[:concurrent_statement_timeout_limit] || DEFAULT_CONCURRENT_TIMEOUT_LIMIT
114
+ @access_exclusive_lock_timeout_max =
115
+ config[:access_exclusive_lock_timeout_max] || DEFAULT_ACCESS_EXCLUSIVE_LOCK_TIMEOUT_MAX
116
+ @access_exclusive_statement_timeout_max =
117
+ config[:access_exclusive_statement_timeout_max] || DEFAULT_ACCESS_EXCLUSIVE_STATEMENT_TIMEOUT_MAX
118
+ @concurrent_lock_timeout_min =
119
+ config[:concurrent_lock_timeout_min] || DEFAULT_CONCURRENT_LOCK_TIMEOUT_MIN
120
+ @concurrent_statement_timeout_min =
121
+ config[:concurrent_statement_timeout_min] || DEFAULT_CONCURRENT_STATEMENT_TIMEOUT_MIN
102
122
  @concurrent_lock_timeout = config[:concurrent_lock_timeout]
103
123
  @concurrent_statement_timeout = config[:concurrent_statement_timeout]
104
124
  end
@@ -43,11 +43,11 @@ module Nandi
43
43
  end
44
44
 
45
45
  def statement_timeout_maximum
46
- Nandi.config.access_exclusive_statement_timeout_limit
46
+ Nandi.config.access_exclusive_statement_timeout_max
47
47
  end
48
48
 
49
49
  def lock_timeout_maximum
50
- Nandi.config.access_exclusive_lock_timeout_limit
50
+ Nandi.config.access_exclusive_lock_timeout_max
51
51
  end
52
52
  end
53
53
  end
@@ -53,11 +53,11 @@ module Nandi
53
53
  end
54
54
 
55
55
  def minimum_lock_timeout
56
- Nandi.config.concurrent_lock_timeout_limit
56
+ Nandi.config.concurrent_lock_timeout_min
57
57
  end
58
58
 
59
59
  def minimum_statement_timeout
60
- Nandi.config.concurrent_statement_timeout_limit
60
+ Nandi.config.concurrent_statement_timeout_min
61
61
  end
62
62
  end
63
63
  end
@@ -74,13 +74,13 @@ module Nandi
74
74
  def statement_timeout_is_within_acceptable_bounds
75
75
  migration.strictest_lock != Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE ||
76
76
  migration.statement_timeout <=
77
- Nandi.config.access_exclusive_statement_timeout_limit
77
+ Nandi.config.access_exclusive_statement_timeout_max
78
78
  end
79
79
 
80
80
  def lock_timeout_is_within_acceptable_bounds
81
81
  migration.strictest_lock != Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE ||
82
82
  migration.lock_timeout <=
83
- Nandi.config.access_exclusive_lock_timeout_limit
83
+ Nandi.config.access_exclusive_lock_timeout_max
84
84
  end
85
85
 
86
86
  def each_instruction_validation
data/lib/nandi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nandi
4
- VERSION = "2.1.1"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/nandi.rb CHANGED
@@ -4,8 +4,11 @@ require "nandi/config"
4
4
  require "nandi/renderers"
5
5
  require "nandi/compiled_migration"
6
6
  require "active_support/core_ext/string/inflections"
7
+ require "active_support/deprecation"
7
8
 
8
9
  module Nandi
10
+ DEPRECATOR = ActiveSupport::Deprecation.new("4.0", "Nandi")
11
+
9
12
  class Error < StandardError; end
10
13
 
11
14
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nandi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless Engineering
@@ -120,6 +120,9 @@ files:
120
120
  - lib/nandi/instructions/validate_constraint.rb
121
121
  - lib/nandi/lockfile.rb
122
122
  - lib/nandi/migration.rb
123
+ - lib/nandi/migration_modifiers.rb
124
+ - lib/nandi/migration_modifiers/base.rb
125
+ - lib/nandi/migration_modifiers/create_table_validates_fks.rb
123
126
  - lib/nandi/migration_violations.rb
124
127
  - lib/nandi/multi_database.rb
125
128
  - lib/nandi/multi_db_generator.rb
@@ -177,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
180
  - !ruby/object:Gem::Version
178
181
  version: '0'
179
182
  requirements: []
180
- rubygems_version: 4.0.3
183
+ rubygems_version: 4.0.10
181
184
  specification_version: 4
182
185
  summary: Fear-free migrations for PostgreSQL
183
186
  test_files: []