nandi 3.1.0 → 3.3.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: ea1c9b758014a99ea83562525b13d80c54ed0dd310ffd64eaebae6009235cebf
4
- data.tar.gz: 77d9ae338cd152b213acaa18c2e6ba6ecbe56f7f1fe955264694d98d8b526030
3
+ metadata.gz: ab45002075955602da710e92a39cc3b9fe8bdc9371a33e97319e9e24d68a3385
4
+ data.tar.gz: 25f3e29b999a37eeb5d46ea272b12e313ae11de5c272dd81120dde4d2dd6ecfe
5
5
  SHA512:
6
- metadata.gz: c3b1935d279ba467262f79d7153ad5cfe1741da7d6c42ba618dd038f7216cc32435403343ad3adfde4e800953658cf5c8db75ab5b2fc434faef54d6871b4081b
7
- data.tar.gz: 0c71da4c6691fe6e3b75acaa32c23af55b32c4b94ce3a7e7afc2021e98f2c64ba1aeb19e7da9a182ba0584c716fec3a725d1dff7ce7c789c49bb18f2a8cf170b
6
+ metadata.gz: a916d9acc8c50060a472e2c6630529ac6b6832380d2198f1d8e19785ce7da5ff211e24e44b33ce51d6f305fab24e98ba7297cba55904f89d394cc939cb202883
7
+ data.tar.gz: 6b1674699aa6e907474c00d31c6a337ae4af7d9e29d1528811e8dcc19e2235a3340fd59ae1f4f1b4176f21c882508e6d20ec992a77b292d7c31a78c05f671315
data/README.md CHANGED
@@ -315,6 +315,10 @@ Drops an existing NOT NULL constraint. Please not that this migration is not saf
315
315
 
316
316
  Changes the default value for this column when new rows are inserted into the table.
317
317
 
318
+ ### `#extend_varchar_column_limit(table, column, from:, to:)`
319
+
320
+ Widens (or removes, with `to: nil`) the character limit on an existing varchar column. Since Postgres 9.2 this is metadata-only; _narrowing_ a limit requires a full-table scan and isn't supported. The generated migration re-checks the column's actual type and limit in the database against `from:` before changing it, so this is safe to recompile even after it has already run. Not safely reversible: `down` should be `irreversible_migration`.
321
+
318
322
  ### `#remove_index(table, target)`
319
323
 
320
324
  Drop an index from the database.
@@ -566,6 +570,9 @@ These options can be set individually for each database. **All are optional** -
566
570
  - `access_exclusive_statement_timeout_limit`: Maximum allowed statement timeout (default: 1,500ms)
567
571
  - `concurrent_lock_timeout_limit`: Minimum timeout for concurrent operations (default: 3,600,000ms / 1 hour)
568
572
  - `concurrent_statement_timeout_limit`: Minimum statement timeout for concurrent operations (default: 3,600,000ms / 1 hour)
573
+ - `concurrent_lock_timeout`: Lock timeout for concurrent operations (`add_index`/`remove_index`). When set, concurrent migrations use `set_lock_timeout` instead of `disable_lock_timeout!`. Default: `nil` (timeout disabled)
574
+ - `concurrent_statement_timeout`: Statement timeout for concurrent operations. When set, concurrent migrations use `set_statement_timeout` instead of `disable_statement_timeout!`. Default: `nil` (timeout disabled)
575
+ - `table_overrides`: Per-table overrides of `concurrent_lock_timeout`/`concurrent_statement_timeout`, keyed by table name. Falls back to the database-level value for any table (or key) not listed
569
576
 
570
577
  **Global options** (set via config accessors):
571
578
 
@@ -621,6 +628,20 @@ Nandi.configure do |config|
621
628
  migration
622
629
  end
623
630
  end
631
+
632
+ # Give a specific, huge table a longer concurrent statement/lock timeout than
633
+ # the database-wide default, without loosening it for every other table.
634
+ Nandi.configure do |config|
635
+ config.register_database(:primary,
636
+ concurrent_lock_timeout: 120_000, # 2 minutes, database-wide default
637
+ concurrent_statement_timeout: 600_000, # 10 minutes, database-wide default
638
+ table_overrides: {
639
+ payments: {
640
+ concurrent_lock_timeout: 300_000, # 5 minutes
641
+ concurrent_statement_timeout: 1_800_000, # 30 minutes
642
+ },
643
+ })
644
+ end
624
645
  ```
625
646
 
626
647
  ### Directory Structure
data/lib/nandi/config.rb CHANGED
@@ -84,8 +84,8 @@ module Nandi
84
84
  def access_exclusive_statement_timeout_max(database_name = nil) = config(database_name).access_exclusive_statement_timeout_max
85
85
  def concurrent_lock_timeout_min(database_name = nil) = config(database_name).concurrent_lock_timeout_min
86
86
  def concurrent_statement_timeout_min(database_name = nil) = config(database_name).concurrent_statement_timeout_min
87
- def concurrent_lock_timeout(database_name = nil) = config(database_name).concurrent_lock_timeout
88
- def concurrent_statement_timeout(database_name = nil) = config(database_name).concurrent_statement_timeout
87
+ def concurrent_lock_timeout(database_name = nil, table_name = nil) = config(database_name).concurrent_lock_timeout(table_name)
88
+ def concurrent_statement_timeout(database_name = nil, table_name = nil) = config(database_name).concurrent_statement_timeout(table_name)
89
89
  # rubocop:enable Layout/LineLength
90
90
 
91
91
  # Delegate setter methods to the default database for backwards compatibility
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nandi
4
+ module Instructions
5
+ class ExtendVarcharColumnLimit
6
+ attr_reader :table, :column, :from, :to
7
+
8
+ def initialize(table:, column:, from:, to:)
9
+ unless from.is_a?(Integer)
10
+ raise ArgumentError, "extend_varchar_column_limit requires an integer `from:` limit"
11
+ end
12
+
13
+ unless to.nil? || (to.is_a?(Integer) && to > from)
14
+ raise ArgumentError,
15
+ "extend_varchar_column_limit only supports widening a character limit, or " \
16
+ "removing it entirely (to: nil), but #{table}.#{column} would go from #{from} " \
17
+ "to #{to.inspect}. Narrowing a limit requires a full-table scan under an " \
18
+ "ACCESS EXCLUSIVE lock and isn't supported here."
19
+ end
20
+
21
+ @table = table
22
+ @column = column
23
+ @from = from
24
+ @to = to
25
+ end
26
+
27
+ def procedure
28
+ :extend_varchar_column_limit
29
+ end
30
+
31
+ def lock
32
+ Nandi::Migration::LockWeights::ACCESS_EXCLUSIVE
33
+ end
34
+ end
35
+ end
36
+ end
@@ -12,6 +12,7 @@ require "nandi/instructions/add_foreign_key"
12
12
  require "nandi/instructions/drop_constraint"
13
13
  require "nandi/instructions/remove_not_null_constraint"
14
14
  require "nandi/instructions/change_column_default"
15
+ require "nandi/instructions/extend_varchar_column_limit"
15
16
  require "nandi/instructions/validate_constraint"
16
17
  require "nandi/instructions/add_check_constraint"
17
18
  require "nandi/instructions/irreversible_migration"
@@ -311,6 +311,19 @@ module Nandi
311
311
  )
312
312
  end
313
313
 
314
+ # Widens (or removes) the character limit on an existing varchar column.
315
+ # Since Postgres 9.2 this is metadata-only (a brief ACCESS EXCLUSIVE lock).
316
+ # Narrowing remains a different, unsafe operation and is not supported.
317
+ #
318
+ # Not safely reversible: `down` should be `irreversible_migration`.
319
+ # @param table [Symbol, String] The name of the table with the column
320
+ # @param column [Symbol, String] The name of the column to change
321
+ # @param from [Integer] The column's current character limit
322
+ # @param to [Integer, nil] The new character limit, or nil to remove it entirely
323
+ def extend_varchar_column_limit(table, column, from:, to:)
324
+ current_instructions << Instructions::ExtendVarcharColumnLimit.new(table:, column:, from:, to:)
325
+ end
326
+
314
327
  # Raises an `ActiveRecord::IrreversibleMigration` error for use in
315
328
  # irreversible migrations
316
329
  def irreversible_migration
@@ -337,7 +350,7 @@ module Nandi
337
350
 
338
351
  def disable_lock_timeout?
339
352
  if self.class.lock_timeout.nil?
340
- strictest_lock == LockWeights::SHARE && Nandi.config.concurrent_lock_timeout(database_name).nil?
353
+ strictest_lock == LockWeights::SHARE && Nandi.config.concurrent_lock_timeout(database_name, table).nil?
341
354
  else
342
355
  false
343
356
  end
@@ -345,7 +358,7 @@ module Nandi
345
358
 
346
359
  def disable_statement_timeout?
347
360
  if self.class.statement_timeout.nil?
348
- strictest_lock == LockWeights::SHARE && Nandi.config.concurrent_statement_timeout(database_name).nil?
361
+ strictest_lock == LockWeights::SHARE && Nandi.config.concurrent_statement_timeout(database_name, table).nil?
349
362
  else
350
363
  false
351
364
  end
@@ -360,7 +373,7 @@ module Nandi
360
373
  end
361
374
 
362
375
  def mixins
363
- (up_instructions + down_instructions).inject([]) do |mixins, i|
376
+ all_instructions.inject([]) do |mixins, i|
364
377
  i.respond_to?(:mixins) ? [*mixins, *i.mixins] : mixins
365
378
  end.uniq
366
379
  end
@@ -383,7 +396,7 @@ module Nandi
383
396
 
384
397
  def default_statement_timeout
385
398
  if strictest_lock == LockWeights::SHARE
386
- Nandi.config.concurrent_statement_timeout(database_name) ||
399
+ Nandi.config.concurrent_statement_timeout(database_name, table) ||
387
400
  Nandi.config.access_exclusive_statement_timeout(database_name)
388
401
  else
389
402
  Nandi.config.access_exclusive_statement_timeout(database_name)
@@ -392,12 +405,24 @@ module Nandi
392
405
 
393
406
  def default_lock_timeout
394
407
  if strictest_lock == LockWeights::SHARE
395
- Nandi.config.concurrent_lock_timeout(database_name) || Nandi.config.access_exclusive_lock_timeout(database_name)
408
+ Nandi.config.concurrent_lock_timeout(database_name, table) ||
409
+ Nandi.config.access_exclusive_lock_timeout(database_name)
396
410
  else
397
411
  Nandi.config.access_exclusive_lock_timeout(database_name)
398
412
  end
399
413
  end
400
414
 
415
+ # The table this migration modifies, if any. Validator guarantees a migration
416
+ # modifies at most one table, so this is unambiguous.
417
+ def table
418
+ instruction_with_table = all_instructions.find { |i| i.respond_to?(:table) }
419
+ instruction_with_table&.table&.to_sym
420
+ end
421
+
422
+ def all_instructions
423
+ up_instructions + down_instructions
424
+ end
425
+
401
426
  def invoke_custom_method(name, ...)
402
427
  klass = Nandi.config.custom_methods[name]
403
428
  current_instructions << klass.new(...)
@@ -54,14 +54,24 @@ module Nandi
54
54
  # The default lock timeout for migrations that take place concurrently
55
55
  # (eg. add_index, remove_index). When set, concurrent migrations will use
56
56
  # set_lock_timeout instead of disable_lock_timeout!. Default: nil (disabled).
57
+ # Can be overridden for a specific table via `table_overrides`.
58
+ # @param table_name [Symbol, String, nil]
57
59
  # @return [Integer, nil]
58
- attr_accessor :concurrent_lock_timeout
60
+ def concurrent_lock_timeout(table_name = nil)
61
+ table_override(table_name, :concurrent_lock_timeout) || @concurrent_lock_timeout
62
+ end
63
+ attr_writer :concurrent_lock_timeout
59
64
 
60
65
  # The default statement timeout for migrations that take place concurrently
61
66
  # (eg. add_index, remove_index). When set, concurrent migrations will use
62
67
  # set_statement_timeout instead of disable_statement_timeout!. Default: nil (disabled).
68
+ # Can be overridden for a specific table via `table_overrides`.
69
+ # @param table_name [Symbol, String, nil]
63
70
  # @return [Integer, nil]
64
- attr_accessor :concurrent_statement_timeout
71
+ def concurrent_statement_timeout(table_name = nil)
72
+ table_override(table_name, :concurrent_statement_timeout) || @concurrent_statement_timeout
73
+ end
74
+ attr_writer :concurrent_statement_timeout
65
75
 
66
76
  # The directory for output files. Default: `db/migrate`
67
77
  # @return [String]
@@ -121,6 +131,17 @@ module Nandi
121
131
  config[:concurrent_statement_timeout_min] || DEFAULT_CONCURRENT_STATEMENT_TIMEOUT_MIN
122
132
  @concurrent_lock_timeout = config[:concurrent_lock_timeout]
123
133
  @concurrent_statement_timeout = config[:concurrent_statement_timeout]
134
+ @table_overrides = normalize_table_overrides(config[:table_overrides])
135
+ end
136
+
137
+ def normalize_table_overrides(table_overrides)
138
+ (table_overrides || {}).transform_keys(&:to_sym)
139
+ end
140
+
141
+ def table_override(table_name, key)
142
+ return nil if table_name.nil?
143
+
144
+ @table_overrides.dig(table_name.to_sym, key)
124
145
  end
125
146
 
126
147
  def path_prefix(name, default)
@@ -105,6 +105,16 @@ module Nandi
105
105
  formatted_property :value
106
106
  end
107
107
 
108
+ class ExtendVarcharColumnLimitCell < Base
109
+ property :table
110
+ property :column
111
+ property :from
112
+
113
+ def to_type_sql
114
+ model.to.nil? ? "character varying" : "character varying(#{model.to})"
115
+ end
116
+ end
117
+
108
118
  class RemoveNotNullConstraintCell < Base
109
119
  formatted_property :table
110
120
  formatted_property :column
data/lib/nandi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nandi
4
- VERSION = "3.1.0"
4
+ VERSION = "3.3.0"
5
5
  end
@@ -0,0 +1,16 @@
1
+ execute <<-SQL
2
+ DO $$
3
+ DECLARE
4
+ actual_type text;
5
+ BEGIN
6
+ SELECT format_type(atttypid, atttypmod) INTO actual_type
7
+ FROM pg_attribute
8
+ WHERE attrelid = '<%= table %>'::regclass AND attname = '<%= column %>' AND NOT attisdropped;
9
+
10
+ IF actual_type IS DISTINCT FROM 'character varying(<%= from %>)' THEN
11
+ RAISE EXCEPTION 'expected <%= table %>.<%= column %> to be character varying(<%= from %>), found %', actual_type;
12
+ END IF;
13
+ END $$;
14
+
15
+ ALTER TABLE <%= table %> ALTER COLUMN "<%= column %>" TYPE <%= to_type_sql %>;
16
+ SQL
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: 3.1.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless Engineering
@@ -112,6 +112,7 @@ files:
112
112
  - lib/nandi/instructions/create_table.rb
113
113
  - lib/nandi/instructions/drop_constraint.rb
114
114
  - lib/nandi/instructions/drop_table.rb
115
+ - lib/nandi/instructions/extend_varchar_column_limit.rb
115
116
  - lib/nandi/instructions/irreversible_migration.rb
116
117
  - lib/nandi/instructions/remove_column.rb
117
118
  - lib/nandi/instructions/remove_index.rb
@@ -155,6 +156,7 @@ files:
155
156
  - lib/templates/nandi/renderers/active_record/instructions/create_table/show.rb.erb
156
157
  - lib/templates/nandi/renderers/active_record/instructions/drop_constraint/show.rb.erb
157
158
  - lib/templates/nandi/renderers/active_record/instructions/drop_table/show.rb.erb
159
+ - lib/templates/nandi/renderers/active_record/instructions/extend_varchar_column_limit/show.rb.erb
158
160
  - lib/templates/nandi/renderers/active_record/instructions/irreversible_migration/show.rb.erb
159
161
  - lib/templates/nandi/renderers/active_record/instructions/remove_column/show.rb.erb
160
162
  - lib/templates/nandi/renderers/active_record/instructions/remove_index/show.rb.erb
@@ -180,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
182
  - !ruby/object:Gem::Version
181
183
  version: '0'
182
184
  requirements: []
183
- rubygems_version: 4.0.10
185
+ rubygems_version: 4.0.20
184
186
  specification_version: 4
185
187
  summary: Fear-free migrations for PostgreSQL
186
188
  test_files: []