nandi 0.13.0 → 1.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: 6e12cc00673e41d4494ea444a6a44524cca908c7bda4a315bd681dbec9cf7563
4
- data.tar.gz: 29f0803224e74092b1bce4eaa673cc811bde033e0b9cd72359390094e34a62f9
3
+ metadata.gz: d3c09dc76de1e4d072270551a10f9f244ea4e9b79f4cdc2bb21069764e722724
4
+ data.tar.gz: a6f9af91f0fd1012d8734c75ba1289d2afea7217dab6b1c4d0e0fbba6c6be052
5
5
  SHA512:
6
- metadata.gz: 9987773dcba43ad2750a7c2ea9fcb2640d9ced6ede820ef43d49bd4ee155b55d2b23bddaf0a88569067d6ab4b2730f40d95e5c1e1589786a71bb355f4ef4d682
7
- data.tar.gz: 13804be5be06737df766ee92f7afa52da870553183566c6fdfa25522883accbcbfe624013cf4411aca1da0f199b549650e17e638796a7b4f2dfc53ec1c8a0080
6
+ metadata.gz: 75284ae3b4f85e75fe0293e16ecef39db7cbd68098044066a1dd5c2e9222518871b2f194d9d4bc936663ae11bb76d5d4e5c13931bf3d530fb196efe6d0d88bb4
7
+ data.tar.gz: 0a4bba3b7bce0c6888f21d327522789ad5ccff683aa34356fc5733d18d9b3ad61512a7955dd310ebad48e652497b2310c199def599fb1d469cd01a2124733f79
data/README.md CHANGED
@@ -4,8 +4,8 @@ Friendly Postgres migrations for people who don't want to take down their databa
4
4
 
5
5
  ## Supported
6
6
 
7
- - Ruby 2.6 or above
8
- - Rails 5.2 or above
7
+ - Ruby 3.2 or above
8
+ - Rails 7.1 or above
9
9
  - Postgres 11 or above
10
10
 
11
11
  ## What does it do?
@@ -15,7 +15,7 @@ Nandi provides an alternative API to ActiveRecord's built-in Migration DSL for d
15
15
  ActiveRecord makes many changes easy. Unfortunately, that includes things that should be done with great care. Consider this migration, for example:
16
16
 
17
17
  ```rb
18
- class AddBarIDToFoos < ActiveRecord::Migration[5.2]
18
+ class AddBarIDToFoos < ActiveRecord::Migration[8.0]
19
19
  def change
20
20
  add_reference :foos, :bars, foreign_key: true
21
21
  end
@@ -40,6 +40,7 @@ gem 'activerecord-safer_migrations' # Also required
40
40
  ```
41
41
 
42
42
  Generate a new migration:
43
+
43
44
  ```sh
44
45
  rails generate nandi:migration add_widgets
45
46
  ```
@@ -76,7 +77,7 @@ The result will sort of look like this:
76
77
  ```rb
77
78
  # db/migrate/20190606060606_add_widgets.rb
78
79
 
79
- class AddWidgets < ActiveRecord::Migration[5.2]
80
+ class AddWidgets < ActiveRecord::Migration[8.0]
80
81
  set_lock_timeout(750)
81
82
  set_statement_timeout(1500)
82
83
 
@@ -121,7 +122,7 @@ end
121
122
 
122
123
  # db/migrate/20190606060606_add_widgets_index_on_name_and_price.rb
123
124
 
124
- class AddWidgetsIndexOnNameAndPrice < ActiveRecord::Migration[5.2]
125
+ class AddWidgetsIndexOnNameAndPrice < ActiveRecord::Migration[8.0]
125
126
  set_lock_timeout(750)
126
127
  set_statement_timeout(1500)
127
128
 
@@ -197,7 +198,7 @@ Which, when compiled, takes care of things in the right order:
197
198
  ```rb
198
199
  # db/migrate/20190611124816_add_reference_on_foos_to_bars.rb
199
200
 
200
- class AddReferenceOnFoosToBars < ActiveRecord::Migration[5.2]
201
+ class AddReferenceOnFoosToBars < ActiveRecord::Migration[8.0]
201
202
  set_lock_timeout(5_000)
202
203
  set_statement_timeout(1_500)
203
204
 
@@ -211,7 +212,7 @@ end
211
212
 
212
213
  # db/migrate/20190611124817_add_foreign_key_on_foos_to_bars.rb
213
214
 
214
- class AddForeignKeyOnFoosToBars < ActiveRecord::Migration[5.2]
215
+ class AddForeignKeyOnFoosToBars < ActiveRecord::Migration[8.0]
215
216
  set_lock_timeout(750)
216
217
  set_statement_timeout(1500)
217
218
 
@@ -234,7 +235,7 @@ end
234
235
 
235
236
  # frozen_string_literal: true
236
237
 
237
- class ValidateForeignKeyOnFoosToBars < ActiveRecord::Migration[5.2]
238
+ class ValidateForeignKeyOnFoosToBars < ActiveRecord::Migration[8.0]
238
239
  set_lock_timeout(750)
239
240
  set_statement_timeout(1500)
240
241
 
@@ -260,21 +261,26 @@ Override the default statement timeout for the duration of the migration. For mi
260
261
  ## Migration methods
261
262
 
262
263
  ### `#add_column(table, name, type, **kwargs)`
264
+
263
265
  Adds a new column. Nandi will explicitly set the column to be NULL, as validating a new NOT NULL constraint can be very expensive on large tables and cause availability issues.
264
266
 
265
267
  ### `#add_foreign_key(table, target, column: nil, name: nil)`
268
+
266
269
  Add a foreign key constraint. The generated SQL will include the NOT VALID parameter, which will prevent immediate validation of the constraint, which locks the target table for writes potentially for a long time. Use the separate #validate_constraint method, in a separate migration; this only takes a row-level lock as it scans through.
267
270
 
268
271
  ### `#add_index(table, fields, **kwargs)`
272
+
269
273
  Adds a new index to the database.
270
274
 
271
275
  Nandi will
276
+
272
277
  - add the `CONCURRENTLY` option, which means the change takes a less restrictive lock at the cost of not running in a DDL transaction
273
278
  - default to the `BTREE` index type, as it is commonly a good fit.
274
279
 
275
280
  Because index creation is particularly failure-prone, and because we cannot run in a transaction and therefore risk partially applied migrations that (in a Rails environment) require manual intervention, Nandi Validates that, if there is a add_index statement in the migration, it must be the only statement.
276
281
 
277
282
  ### `#create_table(table) {|columns_reader| ... }`
283
+
278
284
  Creates a new table. Yields a ColumnsReader object as a block, to allow adding columns.
279
285
 
280
286
  Examples:
@@ -286,33 +292,42 @@ end
286
292
  ```
287
293
 
288
294
  ### `#add_reference(table, ref_name, **extra_args)`
295
+
289
296
  Adds a new reference column. Nandi will validate that the foreign key flag is not set to true; use `add_foreign_key` and `validate_foreign_key` instead! Nandi will also set the `index: false` flag, as index creation is unsafe unless done concurrently in a separate migration.
290
297
 
291
298
  ### `#remove_reference(table, ref_name, **extra_args)`
299
+
292
300
  Removes a reference column.
293
301
 
294
302
  ### `#remove_column(table, name, **extra_args)`
303
+
295
304
  Remove an existing column.
296
305
 
297
306
  ### `#drop_constraint(table, name)`
307
+
298
308
  Drops an existing constraint.
299
309
 
300
310
  ### `#remove_not_null_constraint(table, column)`
311
+
301
312
  Drops an existing NOT NULL constraint. Please not that this migration is not safely reversible; to enforce NOT NULL like behaviour, use a CHECK constraint and validate it in a separate migration.
302
313
 
303
314
  ### `#change_column_default(table, column, value)`
315
+
304
316
  Changes the default value for this column when new rows are inserted into the table.
305
317
 
306
318
  ### `#remove_index(table, target)`
319
+
307
320
  Drop an index from the database.
308
321
 
309
322
  Nandi will add the `CONCURRENTLY` option, which means the change takes a less restrictive lock at the cost of not running in a DDL transaction.
310
323
  Because we cannot run in a transaction and therefore risk partially applied migrations that (in a Rails environment) require manual intervention, Nandi Validates that, if there is a remove_index statement in the migration, it must be the only statement.
311
324
 
312
325
  ### `#drop_table(table)`
326
+
313
327
  Drops an existing table.
314
328
 
315
329
  ### `#irreversible_migration`
330
+
316
331
  Raises `ActiveRecord::IrreversibleMigration` error.
317
332
 
318
333
  ## Generators
@@ -370,7 +385,6 @@ rails generate nandi:foreign_key foos bar --no-create-column --column special_ba
370
385
 
371
386
  We generate the name of your foreign key for you. If you want or need to override it (e.g. if it exceeds the max length of a constraint name in Postgres), you can use the `--name` flag:
372
387
 
373
-
374
388
  ```
375
389
  rails generate nandi:foreign_key foos bar --name my_fk
376
390
  ```
@@ -424,15 +438,18 @@ The default statement timeout for migrations that take permissive locks. Can be
424
438
  The default statement timeout for migrations that take ACCESS EXCLUSIVE locks. Can be overridden by way of the `set_statement_timeout` class method in a given migration. Default: 1500ms.
425
439
 
426
440
  ### `compile_files` (String)
441
+
427
442
  The files to compile when the compile generator is run. Default: `all`
428
443
 
429
444
  May be one of the following:
445
+
430
446
  - 'all' compiles all files
431
447
  - 'git-diff' only files changed since last commit
432
448
  - a full or partial version timestamp, eg '20190101010101', '20190101'
433
449
  - a timestamp range , eg '>=20190101010101'
434
450
 
435
451
  ### `lockfile_directory` (String)
452
+
436
453
  The directory where .nandilock.yml will be stored. Default: `db/` in working directory.
437
454
 
438
455
  #post_process {|migration| ... }
@@ -65,7 +65,7 @@ module Nandi
65
65
  end
66
66
 
67
67
  def reference_name
68
- "#{target.singularize}_id".to_sym
68
+ :"#{target.singularize}_id"
69
69
  end
70
70
 
71
71
  def base_path
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Nandi
4
4
  class FileMatcher
5
- TIMESTAMP_REGEX = /\A(?<operator>>|>=)?(?<timestamp>\d+)\z/.freeze
5
+ TIMESTAMP_REGEX = /\A(?<operator>>|>=)?(?<timestamp>\d+)\z/
6
6
 
7
7
  def self.call(*args, **kwargs)
8
8
  new(*args, **kwargs).call
@@ -73,7 +73,7 @@ module Nandi
73
73
  def symbol_key(key)
74
74
  canonical = key.inspect
75
75
 
76
- "#{canonical[1..-1]}:"
76
+ "#{canonical[1..]}:"
77
77
  end
78
78
  end
79
79
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  module Nandi
6
4
  module Instructions
7
5
  class CreateTable
6
+ Column = Struct.new(:name, :type, :args, keyword_init: true)
7
+
8
8
  attr_reader :table, :columns, :timestamps_args, :extra_args
9
9
 
10
10
  def initialize(table:, columns_block:, **kwargs)
@@ -65,7 +65,7 @@ module Nandi
65
65
  end
66
66
 
67
67
  def column(name, type, **args)
68
- @columns << OpenStruct.new(name: name, type: type, args: args)
68
+ @columns << Column.new(name: name, type: type, args: args)
69
69
  end
70
70
 
71
71
  def timestamps(**args)
@@ -39,7 +39,7 @@ module Nandi
39
39
 
40
40
  Nandi::Lockfile.create! unless Nandi::Lockfile.file_present?
41
41
 
42
- @lockfile = YAML.safe_load(File.read(path)).with_indifferent_access
42
+ @lockfile = YAML.safe_load_file(path).with_indifferent_access
43
43
  end
44
44
 
45
45
  def persist!
@@ -357,9 +357,9 @@ module Nandi
357
357
  end.uniq
358
358
  end
359
359
 
360
- def method_missing(name, *args, **kwargs, &block)
360
+ def method_missing(name, ...)
361
361
  if Nandi.config.custom_methods.key?(name)
362
- invoke_custom_method(name, *args, **kwargs, &block)
362
+ invoke_custom_method(name, ...)
363
363
  else
364
364
  super
365
365
  end
@@ -381,9 +381,9 @@ module Nandi
381
381
  Nandi.config.access_exclusive_lock_timeout
382
382
  end
383
383
 
384
- def invoke_custom_method(name, *args, **kwargs, &block)
384
+ def invoke_custom_method(name, ...)
385
385
  klass = Nandi.config.custom_methods[name]
386
- current_instructions << klass.new(*args, **kwargs, &block)
386
+ current_instructions << klass.new(...)
387
387
  end
388
388
  end
389
389
  end
@@ -30,7 +30,7 @@ module Nandi
30
30
 
31
31
  def should_disable_ddl_transaction?
32
32
  [*up_instructions, *down_instructions].
33
- select { |i| i.procedure.to_s.include?("index") }.any?
33
+ any? { |i| i.procedure.to_s.include?("index") }
34
34
  end
35
35
 
36
36
  def activerecord_version
@@ -3,7 +3,6 @@
3
3
  require "cells"
4
4
  require "tilt"
5
5
  require "nandi/formatting"
6
- require "ostruct"
7
6
 
8
7
  module Nandi
9
8
  module Renderers
@@ -61,7 +60,7 @@ module Nandi
61
60
 
62
61
  def columns
63
62
  model.columns.map do |c|
64
- OpenStruct.new(
63
+ ::Nandi::Instructions::CreateTable::Column.new(
65
64
  name: format_value(c.name),
66
65
  type: format_value(c.type),
67
66
  ).tap do |col|
@@ -28,8 +28,8 @@ module Nandi
28
28
  assert(
29
29
  !migration.disable_statement_timeout? &&
30
30
  migration.statement_timeout <= statement_timeout_maximum,
31
- "statement timeout must be at most #{statement_timeout_maximum}ms" \
32
- " as it takes an ACCESS EXCLUSIVE lock",
31
+ "statement timeout must be at most #{statement_timeout_maximum}ms " \
32
+ "as it takes an ACCESS EXCLUSIVE lock",
33
33
  )
34
34
  end
35
35
 
@@ -37,8 +37,8 @@ module Nandi
37
37
  assert(
38
38
  !migration.disable_lock_timeout? &&
39
39
  migration.lock_timeout <= lock_timeout_maximum,
40
- "lock timeout must be at most #{lock_timeout_maximum}ms" \
41
- " as it takes an ACCESS EXCLUSIVE lock",
40
+ "lock timeout must be at most #{lock_timeout_maximum}ms " \
41
+ "as it takes an ACCESS EXCLUSIVE lock",
42
42
  )
43
43
  end
44
44
 
@@ -31,7 +31,7 @@ module Nandi
31
31
  def validate_statement_timeout
32
32
  assert(
33
33
  migration.disable_statement_timeout? || statement_timeout_high_enough,
34
- "statement timeout for concurrent operations "\
34
+ "statement timeout for concurrent operations " \
35
35
  "must be at least #{minimum_statement_timeout}",
36
36
  )
37
37
  end
@@ -39,7 +39,7 @@ module Nandi
39
39
  def validate_lock_timeout
40
40
  assert(
41
41
  migration.disable_lock_timeout? || lock_timeout_high_enough,
42
- "lock timeout for concurrent operations "\
42
+ "lock timeout for concurrent operations " \
43
43
  "must be at least #{minimum_lock_timeout}",
44
44
  )
45
45
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nandi/validation/failure_helpers"
4
+
5
+ module Nandi
6
+ module Validation
7
+ class AddIndexValidator
8
+ include Nandi::Validation::FailureHelpers
9
+
10
+ def self.call(instruction)
11
+ new(instruction).call
12
+ end
13
+
14
+ def initialize(instruction)
15
+ @instruction = instruction
16
+ end
17
+
18
+ def call
19
+ assert(
20
+ not_using_hash_index?,
21
+ "add_index: Nandi does not support hash indexes. Hash indexes typically have " \
22
+ "very specialized use cases. Please revert to using a btree index, or proceed " \
23
+ "with the creation of this index without using Nandi.",
24
+ )
25
+ end
26
+
27
+ attr_reader :instruction
28
+
29
+ private
30
+
31
+ def not_using_hash_index?
32
+ instruction.extra_args[:using] != :hash
33
+ end
34
+ end
35
+ end
36
+ end
@@ -17,6 +17,8 @@ module Nandi
17
17
 
18
18
  def call
19
19
  case instruction.procedure
20
+ when :add_index
21
+ AddIndexValidator.call(instruction)
20
22
  when :remove_index
21
23
  RemoveIndexValidator.call(instruction)
22
24
  when :add_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 = "0.13.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nandi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless Engineering
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2022-11-28 00:00:00.000000000 Z
10
+ date: 2025-03-21 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -66,153 +65,6 @@ dependencies:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '2.0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '2.0'
83
- - !ruby/object:Gem::Dependency
84
- name: byebug
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '11.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '11.0'
97
- - !ruby/object:Gem::Dependency
98
- name: gc_ruboconfig
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 3.3.0
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: 3.3.0
111
- - !ruby/object:Gem::Dependency
112
- name: pry-byebug
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: 3.9.0
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: 3.9.0
125
- - !ruby/object:Gem::Dependency
126
- name: rails
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: 5.2.3
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: 5.2.3
139
- - !ruby/object:Gem::Dependency
140
- name: rake
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: 12.3.3
146
- - - "~>"
147
- - !ruby/object:Gem::Version
148
- version: '13.0'
149
- type: :development
150
- prerelease: false
151
- version_requirements: !ruby/object:Gem::Requirement
152
- requirements:
153
- - - ">="
154
- - !ruby/object:Gem::Version
155
- version: 12.3.3
156
- - - "~>"
157
- - !ruby/object:Gem::Version
158
- version: '13.0'
159
- - !ruby/object:Gem::Dependency
160
- name: rspec
161
- requirement: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - "~>"
164
- - !ruby/object:Gem::Version
165
- version: '3.0'
166
- type: :development
167
- prerelease: false
168
- version_requirements: !ruby/object:Gem::Requirement
169
- requirements:
170
- - - "~>"
171
- - !ruby/object:Gem::Version
172
- version: '3.0'
173
- - !ruby/object:Gem::Dependency
174
- name: rspec_junit_formatter
175
- requirement: !ruby/object:Gem::Requirement
176
- requirements:
177
- - - "~>"
178
- - !ruby/object:Gem::Version
179
- version: '0.4'
180
- type: :development
181
- prerelease: false
182
- version_requirements: !ruby/object:Gem::Requirement
183
- requirements:
184
- - - "~>"
185
- - !ruby/object:Gem::Version
186
- version: '0.4'
187
- - !ruby/object:Gem::Dependency
188
- name: rubocop
189
- requirement: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - "~>"
192
- - !ruby/object:Gem::Version
193
- version: '1.0'
194
- type: :development
195
- prerelease: false
196
- version_requirements: !ruby/object:Gem::Requirement
197
- requirements:
198
- - - "~>"
199
- - !ruby/object:Gem::Version
200
- version: '1.0'
201
- - !ruby/object:Gem::Dependency
202
- name: yard
203
- requirement: !ruby/object:Gem::Requirement
204
- requirements:
205
- - - "~>"
206
- - !ruby/object:Gem::Version
207
- version: '0.9'
208
- type: :development
209
- prerelease: false
210
- version_requirements: !ruby/object:Gem::Requirement
211
- requirements:
212
- - - "~>"
213
- - !ruby/object:Gem::Version
214
- version: '0.9'
215
- description:
216
68
  email:
217
69
  - engineering@gocardless.com
218
70
  executables:
@@ -278,6 +130,7 @@ files:
278
130
  - lib/nandi/timeout_policies/concurrent.rb
279
131
  - lib/nandi/validation.rb
280
132
  - lib/nandi/validation/add_column_validator.rb
133
+ - lib/nandi/validation/add_index_validator.rb
281
134
  - lib/nandi/validation/add_reference_validator.rb
282
135
  - lib/nandi/validation/each_validator.rb
283
136
  - lib/nandi/validation/failure_helpers.rb
@@ -307,7 +160,6 @@ licenses:
307
160
  - MIT
308
161
  metadata:
309
162
  rubygems_mfa_required: 'true'
310
- post_install_message:
311
163
  rdoc_options: []
312
164
  require_paths:
313
165
  - lib
@@ -315,15 +167,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
315
167
  requirements:
316
168
  - - ">="
317
169
  - !ruby/object:Gem::Version
318
- version: 2.6.0
170
+ version: '3.2'
319
171
  required_rubygems_version: !ruby/object:Gem::Requirement
320
172
  requirements:
321
173
  - - ">="
322
174
  - !ruby/object:Gem::Version
323
175
  version: '0'
324
176
  requirements: []
325
- rubygems_version: 3.3.3
326
- signing_key:
177
+ rubygems_version: 3.6.2
327
178
  specification_version: 4
328
179
  summary: Fear-free migrations for PostgreSQL
329
180
  test_files: []