pg_ha_migrations 1.8.0 → 2.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.
@@ -1,10 +1,6 @@
1
1
  module PgHaMigrations::UnsafeStatements
2
2
  def self.disable_or_delegate_default_method(method_name, error_message, allow_reentry_from_compatibility_module: false)
3
3
  define_method(method_name) do |*args, &block|
4
- if PgHaMigrations.config.check_for_dependent_objects
5
- disallow_migration_method_if_dependent_objects!(method_name, arguments: args)
6
- end
7
-
8
4
  if PgHaMigrations.config.disable_default_migration_methods
9
5
  # Most migration methods are only ever called by a migration and
10
6
  # therefore aren't re-entrant or callable from another migration
@@ -12,7 +8,7 @@ module PgHaMigrations::UnsafeStatements
12
8
  # implementations in `ActiveRecord::Migration::Compatibility` so
13
9
  # we have to explicitly handle that case by allowing execution of
14
10
  # the original implementation by its original name.
15
- unless allow_reentry_from_compatibility_module && caller[0] =~ /lib\/active_record\/migration\/compatibility.rb/
11
+ unless allow_reentry_from_compatibility_module && caller[0] =~ /lib\/active_record\/migration\/compatibility.rb/
16
12
  raise PgHaMigrations::UnsafeMigrationError, error_message
17
13
  end
18
14
  end
@@ -22,51 +18,88 @@ module PgHaMigrations::UnsafeStatements
22
18
  ruby2_keywords method_name
23
19
  end
24
20
 
25
- def self.delegate_unsafe_method_to_migration_base_class(method_name)
21
+ def self.delegate_unsafe_method_to_migration_base_class(method_name, with_lock: true)
26
22
  define_method("unsafe_#{method_name}") do |*args, &block|
27
23
  if PgHaMigrations.config.check_for_dependent_objects
28
24
  disallow_migration_method_if_dependent_objects!(method_name, arguments: args)
29
25
  end
30
26
 
31
- execute_ancestor_statement(method_name, *args, &block)
27
+ if with_lock
28
+ safely_acquire_lock_for_table(args.first) do
29
+ execute_ancestor_statement(method_name, *args, &block)
30
+ end
31
+ else
32
+ execute_ancestor_statement(method_name, *args, &block)
33
+ end
32
34
  end
33
35
  ruby2_keywords "unsafe_#{method_name}"
34
36
  end
35
37
 
38
+ def self.delegate_raw_method_to_migration_base_class(method_name)
39
+ define_method("raw_#{method_name}") do |*args, &block|
40
+ execute_ancestor_statement(method_name, *args, &block)
41
+ end
42
+ ruby2_keywords "raw_#{method_name}"
43
+ end
44
+
45
+ # Direct dispatch to underlying Rails method as unsafe_<method_name> with dependent object check / safe lock acquisition
46
+ delegate_unsafe_method_to_migration_base_class :add_check_constraint
36
47
  delegate_unsafe_method_to_migration_base_class :add_column
37
- delegate_unsafe_method_to_migration_base_class :change_table
38
- delegate_unsafe_method_to_migration_base_class :drop_table
39
- delegate_unsafe_method_to_migration_base_class :rename_table
40
- delegate_unsafe_method_to_migration_base_class :rename_column
41
48
  delegate_unsafe_method_to_migration_base_class :change_column
42
49
  delegate_unsafe_method_to_migration_base_class :change_column_default
43
- delegate_unsafe_method_to_migration_base_class :remove_column
44
- delegate_unsafe_method_to_migration_base_class :execute
45
- delegate_unsafe_method_to_migration_base_class :remove_index
46
- delegate_unsafe_method_to_migration_base_class :add_foreign_key
47
- delegate_unsafe_method_to_migration_base_class :remove_foreign_key
48
- delegate_unsafe_method_to_migration_base_class :add_check_constraint
50
+ delegate_unsafe_method_to_migration_base_class :drop_table
51
+ delegate_unsafe_method_to_migration_base_class :execute, with_lock: false # too generic for locking
49
52
  delegate_unsafe_method_to_migration_base_class :remove_check_constraint
53
+ delegate_unsafe_method_to_migration_base_class :remove_column
54
+ delegate_unsafe_method_to_migration_base_class :rename_column
55
+ delegate_unsafe_method_to_migration_base_class :rename_table
50
56
 
51
- disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead"
57
+ # Direct dispatch to underlying Rails method as raw_<method_name> without dependent object check / locking
58
+ delegate_raw_method_to_migration_base_class :add_check_constraint
59
+ delegate_raw_method_to_migration_base_class :add_column
60
+ delegate_raw_method_to_migration_base_class :add_foreign_key
61
+ delegate_raw_method_to_migration_base_class :add_index
62
+ delegate_raw_method_to_migration_base_class :change_column
63
+ delegate_raw_method_to_migration_base_class :change_column_default
64
+ delegate_raw_method_to_migration_base_class :change_column_null
65
+ delegate_raw_method_to_migration_base_class :change_table
66
+ delegate_raw_method_to_migration_base_class :create_table
67
+ delegate_raw_method_to_migration_base_class :drop_table
68
+ delegate_raw_method_to_migration_base_class :execute
69
+ delegate_raw_method_to_migration_base_class :remove_check_constraint
70
+ delegate_raw_method_to_migration_base_class :remove_column
71
+ delegate_raw_method_to_migration_base_class :remove_foreign_key
72
+ delegate_raw_method_to_migration_base_class :remove_index
73
+ delegate_raw_method_to_migration_base_class :rename_column
74
+ delegate_raw_method_to_migration_base_class :rename_table
75
+
76
+ # Raises error if disable_default_migration_methods is true
77
+ # Otherwise, direct dispatch to underlying Rails method without dependent object check / locking
78
+ disable_or_delegate_default_method :add_check_constraint, ":add_check_constraint is NOT SAFE! Use :safe_add_unvalidated_check_constraint and then :safe_validate_check_constraint instead"
52
79
  disable_or_delegate_default_method :add_column, ":add_column is NOT SAFE! Use safe_add_column instead"
53
- disable_or_delegate_default_method :change_table, ":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead"
54
- disable_or_delegate_default_method :drop_table, ":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed"
55
- disable_or_delegate_default_method :rename_table, ":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed"
56
- disable_or_delegate_default_method :rename_column, ":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed"
80
+ disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key"
81
+ disable_or_delegate_default_method :add_index, ":add_index is NOT SAFE! Use safe_add_concurrent_index instead"
57
82
  disable_or_delegate_default_method :change_column, ":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead"
58
83
  disable_or_delegate_default_method :change_column_default, ":change_column_default is NOT SAFE! Use safe_change_column_default instead"
59
84
  disable_or_delegate_default_method :change_column_null, ":change_column_null is NOT (guaranteed to be) SAFE! Either use :safe_make_column_nullable or explicitly call :unsafe_make_column_not_nullable to proceed"
60
- disable_or_delegate_default_method :remove_column, ":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed"
61
- disable_or_delegate_default_method :add_index, ":add_index is NOT SAFE! Use safe_add_concurrent_index instead"
85
+ disable_or_delegate_default_method :change_table, ":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead"
86
+ disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead"
87
+ disable_or_delegate_default_method :drop_table, ":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed"
62
88
  disable_or_delegate_default_method :execute, ":execute is NOT SAFE! Explicitly call :unsafe_execute to proceed", allow_reentry_from_compatibility_module: true
63
- disable_or_delegate_default_method :remove_index, ":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1"
64
- disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key"
65
- disable_or_delegate_default_method :remove_foreign_key, ":remove_foreign_key is NOT SAFE! Explicitly call :unsafe_remove_foreign_key"
66
- disable_or_delegate_default_method :add_check_constraint, ":add_check_constraint is NOT SAFE! Use :safe_add_unvalidated_check_constraint and then :safe_validate_check_constraint instead"
67
89
  disable_or_delegate_default_method :remove_check_constraint, ":remove_check_constraint is NOT SAFE! Explicitly call :unsafe_remove_check_constraint to proceed"
90
+ disable_or_delegate_default_method :remove_column, ":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed"
91
+ disable_or_delegate_default_method :remove_foreign_key, ":remove_foreign_key is NOT SAFE! Explicitly call :unsafe_remove_foreign_key"
92
+ disable_or_delegate_default_method :remove_index, ":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1"
93
+ disable_or_delegate_default_method :rename_column, ":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed"
94
+ disable_or_delegate_default_method :rename_table, ":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed"
95
+
96
+ # Note that unsafe_* methods defined below do not run dependent object checks
68
97
 
69
- def unsafe_create_table(table, options={}, &block)
98
+ def unsafe_change_table(*args, &block)
99
+ raise PgHaMigrations::UnsafeMigrationError.new(":change_table is too generic to even allow an unsafe variant. Use a combination of safe and explicit unsafe migration methods instead")
100
+ end
101
+
102
+ def unsafe_create_table(table, **options, &block)
70
103
  if options[:force] && !PgHaMigrations.config.allow_force_create_table
71
104
  raise PgHaMigrations::UnsafeMigrationError.new(":force is NOT SAFE! Explicitly call unsafe_drop_table first if you want to recreate an existing table")
72
105
  end
@@ -74,7 +107,7 @@ module PgHaMigrations::UnsafeStatements
74
107
  execute_ancestor_statement(:create_table, table, **options, &block)
75
108
  end
76
109
 
77
- def unsafe_add_index(table, column_names, options = {})
110
+ def unsafe_add_index(table, column_names, **options)
78
111
  if ((ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 2) || ActiveRecord::VERSION::MAJOR > 5) &&
79
112
  column_names.is_a?(String) && /\W/.match?(column_names) && options.key?(:opclass)
80
113
  raise PgHaMigrations::InvalidMigrationError, "ActiveRecord drops the :opclass option when supplying a string containing an expression or list of columns; instead either supply an array of columns or include the opclass in the string for each column"
@@ -90,7 +123,92 @@ module PgHaMigrations::UnsafeStatements
90
123
 
91
124
  options[:name] = validated_index.name
92
125
 
93
- execute_ancestor_statement(:add_index, table, column_names, **options)
126
+ if options[:algorithm] == :concurrently
127
+ execute_ancestor_statement(:add_index, table, column_names, **options)
128
+ else
129
+ safely_acquire_lock_for_table(table, mode: :share) do
130
+ execute_ancestor_statement(:add_index, table, column_names, **options)
131
+ end
132
+ end
133
+ end
134
+
135
+ def unsafe_remove_index(table, column = nil, **options)
136
+ if options[:algorithm]== :concurrently
137
+ execute_ancestor_statement(:remove_index, table, column, **options)
138
+ else
139
+ safely_acquire_lock_for_table(table) do
140
+ execute_ancestor_statement(:remove_index, table, column, **options)
141
+ end
142
+ end
143
+ end
144
+
145
+ def unsafe_add_foreign_key(from_table, to_table, **options)
146
+ safely_acquire_lock_for_table(from_table, to_table, mode: :share_row_exclusive) do
147
+ execute_ancestor_statement(:add_foreign_key, from_table, to_table, **options)
148
+ end
149
+ end
150
+
151
+ def unsafe_remove_foreign_key(from_table, to_table = nil, **options)
152
+ calculated_to_table = options.fetch(:to_table, to_table)
153
+
154
+ if calculated_to_table.nil?
155
+ raise PgHaMigrations::InvalidMigrationError.new("The :to_table positional arg / kwarg is required for lock acquisition")
156
+ end
157
+
158
+ safely_acquire_lock_for_table(from_table, calculated_to_table) do
159
+ execute_ancestor_statement(:remove_foreign_key, from_table, to_table, **options)
160
+ end
161
+ end
162
+
163
+ def unsafe_rename_enum_value(name, old_value, new_value)
164
+ if ActiveRecord::Base.connection.postgresql_version < 10_00_00
165
+ raise PgHaMigrations::InvalidMigrationError, "Renaming an enum value is not supported on Postgres databases before version 10"
166
+ end
167
+
168
+ raw_execute("ALTER TYPE #{PG::Connection.quote_ident(name.to_s)} RENAME VALUE '#{PG::Connection.escape_string(old_value)}' TO '#{PG::Connection.escape_string(new_value)}'")
169
+ end
170
+
171
+ def unsafe_make_column_not_nullable(table, column, **options) # options arg is only present for backwards compatiblity
172
+ quoted_table_name = connection.quote_table_name(table)
173
+ quoted_column_name = connection.quote_column_name(column)
174
+
175
+ safely_acquire_lock_for_table(table) do
176
+ raw_execute("ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} SET NOT NULL")
177
+ end
178
+ end
179
+
180
+ def unsafe_remove_constraint(table, name:)
181
+ raise ArgumentError, "Expected <name> to be present" unless name.present?
182
+
183
+ quoted_table_name = connection.quote_table_name(table)
184
+ quoted_constraint_name = connection.quote_table_name(name)
185
+ sql = "ALTER TABLE #{quoted_table_name} DROP CONSTRAINT #{quoted_constraint_name}"
186
+
187
+ safely_acquire_lock_for_table(table) do
188
+ say_with_time "remove_constraint(#{table.inspect}, name: #{name.inspect})" do
189
+ connection.execute(sql)
190
+ end
191
+ end
192
+ end
193
+
194
+ def unsafe_partman_update_config(table, **options)
195
+ invalid_options = options.keys - PgHaMigrations::PARTMAN_UPDATE_CONFIG_OPTIONS
196
+
197
+ raise ArgumentError, "Unrecognized argument(s): #{invalid_options}" unless invalid_options.empty?
198
+
199
+ PgHaMigrations::PartmanConfig.schema = _quoted_partman_schema
200
+
201
+ config = PgHaMigrations::PartmanConfig.find(_fully_qualified_table_name_for_partman(table))
202
+
203
+ config.assign_attributes(**options)
204
+
205
+ inherit_privileges_changed = config.inherit_privileges_changed?
206
+
207
+ say_with_time "partman_update_config(#{table.inspect}, #{options.map { |k,v| "#{k}: #{v.inspect}" }.join(", ")})" do
208
+ config.save!
209
+ end
210
+
211
+ safe_partman_reapply_privileges(table) if inherit_privileges_changed
94
212
  end
95
213
 
96
214
  ruby2_keywords def execute_ancestor_statement(method_name, *args, &block)
@@ -1,3 +1,3 @@
1
1
  module PgHaMigrations
2
- VERSION = "1.8.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -19,9 +19,9 @@ module PgHaMigrations
19
19
  def self.config
20
20
  @config ||= Config.new(
21
21
  true,
22
- false,
23
22
  true,
24
23
  false,
24
+ true,
25
25
  true
26
26
  )
27
27
  end
@@ -71,6 +71,7 @@ module PgHaMigrations
71
71
  UndefinedTableError = Class.new(StandardError)
72
72
  end
73
73
 
74
+ require "pg_ha_migrations/constraint"
74
75
  require "pg_ha_migrations/relation"
75
76
  require "pg_ha_migrations/blocking_database_transactions"
76
77
  require "pg_ha_migrations/blocking_database_transactions_reporter"
@@ -32,12 +32,12 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "rake", ">= 12.3.3"
33
33
  spec.add_development_dependency "rspec", "~> 3.0"
34
34
  spec.add_development_dependency "pg"
35
- spec.add_development_dependency "db-query-matchers", "~> 0.12.0"
35
+ spec.add_development_dependency "db-query-matchers", "~> 0.14"
36
36
  spec.add_development_dependency "pry"
37
37
  spec.add_development_dependency "pry-byebug"
38
- spec.add_development_dependency "appraisal", "~> 2.2.0"
38
+ spec.add_development_dependency "appraisal", "~> 2.5"
39
39
 
40
- spec.add_dependency "rails", ">= 6.1", "< 7.2"
40
+ spec.add_dependency "rails", ">= 7.1", "< 8.1"
41
41
  spec.add_dependency "relation_to_struct", ">= 1.5.1"
42
42
  spec.add_dependency "ruby2_keywords"
43
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_ha_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - celeen
@@ -11,10 +11,9 @@ authors:
11
11
  - kexline4710
12
12
  - mgates
13
13
  - redneckbeard
14
- autorequire:
15
14
  bindir: exe
16
15
  cert_chain: []
17
- date: 2024-01-12 00:00:00.000000000 Z
16
+ date: 2025-04-17 00:00:00.000000000 Z
18
17
  dependencies:
19
18
  - !ruby/object:Gem::Dependency
20
19
  name: rake
@@ -64,14 +63,14 @@ dependencies:
64
63
  requirements:
65
64
  - - "~>"
66
65
  - !ruby/object:Gem::Version
67
- version: 0.12.0
66
+ version: '0.14'
68
67
  type: :development
69
68
  prerelease: false
70
69
  version_requirements: !ruby/object:Gem::Requirement
71
70
  requirements:
72
71
  - - "~>"
73
72
  - !ruby/object:Gem::Version
74
- version: 0.12.0
73
+ version: '0.14'
75
74
  - !ruby/object:Gem::Dependency
76
75
  name: pry
77
76
  requirement: !ruby/object:Gem::Requirement
@@ -106,34 +105,34 @@ dependencies:
106
105
  requirements:
107
106
  - - "~>"
108
107
  - !ruby/object:Gem::Version
109
- version: 2.2.0
108
+ version: '2.5'
110
109
  type: :development
111
110
  prerelease: false
112
111
  version_requirements: !ruby/object:Gem::Requirement
113
112
  requirements:
114
113
  - - "~>"
115
114
  - !ruby/object:Gem::Version
116
- version: 2.2.0
115
+ version: '2.5'
117
116
  - !ruby/object:Gem::Dependency
118
117
  name: rails
119
118
  requirement: !ruby/object:Gem::Requirement
120
119
  requirements:
121
120
  - - ">="
122
121
  - !ruby/object:Gem::Version
123
- version: '6.1'
122
+ version: '7.1'
124
123
  - - "<"
125
124
  - !ruby/object:Gem::Version
126
- version: '7.2'
125
+ version: '8.1'
127
126
  type: :runtime
128
127
  prerelease: false
129
128
  version_requirements: !ruby/object:Gem::Requirement
130
129
  requirements:
131
130
  - - ">="
132
131
  - !ruby/object:Gem::Version
133
- version: '6.1'
132
+ version: '7.1'
134
133
  - - "<"
135
134
  - !ruby/object:Gem::Version
136
- version: '7.2'
135
+ version: '8.1'
137
136
  - !ruby/object:Gem::Dependency
138
137
  name: relation_to_struct
139
138
  requirement: !ruby/object:Gem::Requirement
@@ -186,13 +185,14 @@ files:
186
185
  - bin/setup
187
186
  - docker-compose.yml
188
187
  - gemfiles/.bundle/config
189
- - gemfiles/rails_6.1.gemfile
190
- - gemfiles/rails_7.0.gemfile
191
188
  - gemfiles/rails_7.1.gemfile
189
+ - gemfiles/rails_7.2.gemfile
190
+ - gemfiles/rails_8.0.gemfile
192
191
  - lib/pg_ha_migrations.rb
193
192
  - lib/pg_ha_migrations/allowed_versions.rb
194
193
  - lib/pg_ha_migrations/blocking_database_transactions.rb
195
194
  - lib/pg_ha_migrations/blocking_database_transactions_reporter.rb
195
+ - lib/pg_ha_migrations/constraint.rb
196
196
  - lib/pg_ha_migrations/dependent_objects_checks.rb
197
197
  - lib/pg_ha_migrations/hacks/add_index_on_only.rb
198
198
  - lib/pg_ha_migrations/hacks/cleanup_unnecessary_output.rb
@@ -210,7 +210,6 @@ homepage: ''
210
210
  licenses:
211
211
  - MIT
212
212
  metadata: {}
213
- post_install_message:
214
213
  rdoc_options: []
215
214
  require_paths:
216
215
  - lib
@@ -225,8 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
224
  - !ruby/object:Gem::Version
226
225
  version: '0'
227
226
  requirements: []
228
- rubygems_version: 3.2.3
229
- signing_key:
227
+ rubygems_version: 3.6.2
230
228
  specification_version: 4
231
229
  summary: Enforces DDL/migration safety in Ruby on Rails project with an emphasis on
232
230
  explicitly choosing trade-offs and avoiding unnecessary magic.