strong_migrations 0.6.0 → 2.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.
@@ -1,234 +1,69 @@
1
1
  # dependencies
2
2
  require "active_support"
3
3
 
4
+ # adapters
5
+ require_relative "strong_migrations/adapters/abstract_adapter"
6
+ require_relative "strong_migrations/adapters/mysql_adapter"
7
+ require_relative "strong_migrations/adapters/mariadb_adapter"
8
+ require_relative "strong_migrations/adapters/postgresql_adapter"
9
+
4
10
  # modules
5
- require "strong_migrations/checker"
6
- require "strong_migrations/database_tasks"
7
- require "strong_migrations/migration"
8
- require "strong_migrations/migration_helpers"
9
- require "strong_migrations/version"
11
+ require_relative "strong_migrations/checks"
12
+ require_relative "strong_migrations/safe_methods"
13
+ require_relative "strong_migrations/checker"
14
+ require_relative "strong_migrations/migration"
15
+ require_relative "strong_migrations/migration_context"
16
+ require_relative "strong_migrations/migrator"
17
+ require_relative "strong_migrations/version"
10
18
 
11
19
  # integrations
12
- require "strong_migrations/railtie" if defined?(Rails)
20
+ require_relative "strong_migrations/railtie" if defined?(Rails)
13
21
 
14
22
  module StrongMigrations
15
23
  class Error < StandardError; end
16
24
  class UnsafeMigration < Error; end
25
+ class UnsupportedVersion < Error; end
17
26
 
18
27
  class << self
19
28
  attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
20
29
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
21
- :enabled_checks, :lock_timeout, :statement_timeout, :helpers
30
+ :enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
31
+ :safe_by_default, :target_sql_mode, :lock_timeout_retries, :lock_timeout_retry_delay,
32
+ :alphabetize_schema, :skipped_databases, :remove_invalid_indexes
33
+ attr_writer :lock_timeout_limit
22
34
  end
23
35
  self.auto_analyze = false
24
36
  self.start_after = 0
37
+ self.lock_timeout_retries = 0
38
+ self.lock_timeout_retry_delay = 10 # seconds
25
39
  self.checks = []
26
- self.error_messages = {
27
- add_column_default:
28
- "Adding a column with a non-null default causes the entire table to be rewritten.
29
- Instead, add the column without a default value, then change the default.
30
-
31
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
32
- def up
33
- %{add_command}
34
- %{change_command}
35
- end
36
-
37
- def down
38
- %{remove_command}
39
- end
40
- end
41
-
42
- Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
43
-
44
- class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
45
- disable_ddl_transaction!
46
-
47
- def up
48
- %{code}
49
- end
50
- end%{append}",
51
-
52
- add_column_json:
53
- "There's no equality operator for the json column type, which can
54
- cause errors for existing SELECT DISTINCT queries. Use jsonb instead.",
55
-
56
- change_column:
57
- "Changing the type of an existing column requires the entire
58
- table and indexes to be rewritten. A safer approach is to:
59
-
60
- 1. Create a new column
61
- 2. Write to both columns
62
- 3. Backfill data from the old column to the new column
63
- 4. Move reads from the old column to the new column
64
- 5. Stop writing to the old column
65
- 6. Drop the old column",
66
-
67
- remove_column: "Active Record caches attributes which causes problems
68
- when removing columns. Be sure to ignore the column%{column_suffix}:
69
-
70
- class %{model} < %{base_model}
71
- %{code}
72
- end
73
-
74
- Deploy the code, then wrap this step in a safety_assured { ... } block.
75
-
76
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
77
- def change
78
- safety_assured { %{command} }
79
- end
80
- end",
81
-
82
- rename_column:
83
- "Renaming a column is dangerous. A safer approach is to:
84
-
85
- 1. Create a new column
86
- 2. Write to both columns
87
- 3. Backfill data from the old column to new column
88
- 4. Move reads from the old column to the new column
89
- 5. Stop writing to the old column
90
- 6. Drop the old column",
91
-
92
- rename_table:
93
- "Renaming a table is dangerous. A safer approach is to:
94
-
95
- 1. Create a new table. Don't forget to recreate indexes from the old table
96
- 2. Write to both tables
97
- 3. Backfill data from the old table to new table
98
- 4. Move reads from the old table to the new table
99
- 5. Stop writing to the old table
100
- 6. Drop the old table",
101
-
102
- add_reference:
103
- "Adding an index non-concurrently locks the table. Instead, use:
104
-
105
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
106
- disable_ddl_transaction!
107
-
108
- def change
109
- %{command}
110
- end
111
- end",
112
-
113
- add_index:
114
- "Adding an index non-concurrently locks the table. Instead, use:
115
-
116
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
117
- disable_ddl_transaction!
118
-
119
- def change
120
- %{command}
121
- end
122
- end",
123
-
124
- remove_index:
125
- "Removing an index non-concurrently locks the table. Instead, use:
40
+ self.safe_by_default = false
41
+ self.check_down = false
42
+ self.alphabetize_schema = false
43
+ self.skipped_databases = []
44
+ self.remove_invalid_indexes = false
126
45
 
127
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
128
- disable_ddl_transaction!
129
-
130
- def change
131
- %{command}
132
- end
133
- end",
134
-
135
- add_index_columns:
136
- "Adding a non-unique index with more than three columns rarely improves performance.
137
- Instead, start an index with columns that narrow down the results the most.",
138
-
139
- change_table:
140
- "Strong Migrations does not support inspecting what happens inside a
141
- change_table block, so cannot help you here. Please make really sure that what
142
- you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
143
-
144
- create_table:
145
- "The force option will destroy existing tables.
146
- If this is intended, drop the existing table first.
147
- Otherwise, remove the force option.",
148
-
149
- execute:
150
- "Strong Migrations does not support inspecting what happens inside an
151
- execute call, so cannot help you here. Please make really sure that what
152
- you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
153
-
154
- change_column_null:
155
- "Passing a default value to change_column_null runs a single UPDATE query,
156
- which can cause downtime. Instead, backfill the existing rows in the
157
- Rails console or a separate migration with disable_ddl_transaction!.
158
-
159
- class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
160
- disable_ddl_transaction!
161
-
162
- def up
163
- %{code}
164
- end
165
- end",
166
-
167
- change_column_null_postgresql:
168
- "Setting NOT NULL on a column requires an AccessExclusiveLock,
169
- which is expensive on large tables. Instead, use a constraint and
170
- validate it in a separate migration with a more agreeable RowShareLock.
171
-
172
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
173
- def change
174
- %{add_constraint_code}
175
- end
176
- end
177
-
178
- class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
179
- def change
180
- %{validate_constraint_code}
181
- end
182
- end",
183
-
184
- change_column_null_postgresql_helper:
185
- "Setting NOT NULL on a column requires an AccessExclusiveLock,
186
- which is expensive on large tables. Instead, we can use a constraint and
187
- validate it in a separate step with a more agreeable RowShareLock.
188
-
189
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
190
- disable_ddl_transaction!
191
-
192
- def change
193
- %{command}
194
- end
195
- end",
196
-
197
- change_column_null_mysql:
198
- "Setting NOT NULL on an existing column is not safe with your database engine.",
199
-
200
- add_foreign_key:
201
- "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
202
- which is expensive on large tables. Instead, validate it in a separate migration
203
- with a more agreeable RowShareLock.
204
-
205
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
206
- def change
207
- %{add_foreign_key_code}
46
+ # private
47
+ def self.developer_env?
48
+ env == "development" || env == "test"
208
49
  end
209
- end
210
50
 
211
- class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
212
- def change
213
- %{validate_foreign_key_code}
51
+ # private
52
+ def self.env
53
+ if defined?(Rails.env)
54
+ Rails.env
55
+ else
56
+ # default to production for safety
57
+ ENV["RACK_ENV"] || "production"
58
+ end
214
59
  end
215
- end",
216
-
217
- add_foreign_key_helper:
218
- "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
219
- which is expensive on large tables. Instead, we can validate it in a separate step
220
- with a more agreeable RowShareLock.
221
-
222
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
223
- disable_ddl_transaction!
224
60
 
225
- def change
226
- %{command}
61
+ def self.lock_timeout_limit
62
+ unless defined?(@lock_timeout_limit)
63
+ @lock_timeout_limit = developer_env? ? false : 10
64
+ end
65
+ @lock_timeout_limit
227
66
  end
228
- end",
229
- }
230
- self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
231
- self.helpers = false
232
67
 
233
68
  def self.add_check(&block)
234
69
  checks << block
@@ -251,18 +86,19 @@ end",
251
86
  end
252
87
  end
253
88
 
254
- # def self.enable_helpers
255
- # unless helpers
256
- # ActiveRecord::Migration.include(StrongMigrations::MigrationHelpers)
257
- # self.helpers = true
258
- # end
259
- # end
89
+ def self.skip_database(database)
90
+ self.skipped_databases << database
91
+ end
260
92
  end
261
93
 
94
+ # load error messages
95
+ require_relative "strong_migrations/error_messages"
96
+
262
97
  ActiveSupport.on_load(:active_record) do
263
98
  ActiveRecord::Migration.prepend(StrongMigrations::Migration)
99
+ ActiveRecord::MigrationContext.prepend(StrongMigrations::MigrationContext)
100
+ ActiveRecord::Migrator.prepend(StrongMigrations::Migrator)
264
101
 
265
- if defined?(ActiveRecord::Tasks::DatabaseTasks)
266
- ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
267
- end
102
+ require_relative "strong_migrations/schema_dumper"
103
+ ActiveRecord::SchemaDumper.prepend(StrongMigrations::SchemaDumper)
268
104
  end
@@ -1,20 +1,9 @@
1
- # https://nithinbekal.com/posts/safe-rake-tasks
2
-
3
1
  namespace :strong_migrations do
4
- task safety_assured: :environment do
5
- raise "Set SAFETY_ASSURED=1 to run this task in production" if Rails.env.production? && !ENV["SAFETY_ASSURED"]
6
- end
7
-
8
- # https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/
2
+ # https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/
9
3
  task :alphabetize_columns do
10
4
  $stderr.puts "Dumping schema"
11
5
  ActiveRecord::Base.logger.level = Logger::INFO
12
6
 
13
- require "strong_migrations/alphabetize_columns"
14
- ActiveRecord::Base.connection.class.prepend StrongMigrations::AlphabetizeColumns
15
- if ActiveRecord::ConnectionAdapters.const_defined?('PostGISAdapter')
16
- ActiveRecord::ConnectionAdapters::PostGISAdapter.prepend StrongMigrations::AlphabetizeColumns
17
- end
18
- ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend StrongMigrations::AlphabetizeColumns
7
+ StrongMigrations.alphabetize_schema = true
19
8
  end
20
9
  end
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  - Bob Remeika
9
9
  - David Waller
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2020-01-24 00:00:00.000000000 Z
12
+ date: 2025-04-03 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: activerecord
@@ -18,73 +17,16 @@ dependencies:
18
17
  requirements:
19
18
  - - ">="
20
19
  - !ruby/object:Gem::Version
21
- version: '5'
20
+ version: '7'
22
21
  type: :runtime
23
22
  prerelease: false
24
23
  version_requirements: !ruby/object:Gem::Requirement
25
24
  requirements:
26
25
  - - ">="
27
26
  - !ruby/object:Gem::Version
28
- version: '5'
29
- - !ruby/object:Gem::Dependency
30
- name: bundler
31
- requirement: !ruby/object:Gem::Requirement
32
- requirements:
33
- - - ">="
34
- - !ruby/object:Gem::Version
35
- version: '0'
36
- type: :development
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: '0'
43
- - !ruby/object:Gem::Dependency
44
- name: rake
45
- requirement: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: '0'
50
- type: :development
51
- prerelease: false
52
- version_requirements: !ruby/object:Gem::Requirement
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: '0'
57
- - !ruby/object:Gem::Dependency
58
- name: minitest
59
- requirement: !ruby/object:Gem::Requirement
60
- requirements:
61
- - - ">="
62
- - !ruby/object:Gem::Version
63
- version: '0'
64
- type: :development
65
- prerelease: false
66
- version_requirements: !ruby/object:Gem::Requirement
67
- requirements:
68
- - - ">="
69
- - !ruby/object:Gem::Version
70
- version: '0'
71
- - !ruby/object:Gem::Dependency
72
- name: pg
73
- requirement: !ruby/object:Gem::Requirement
74
- requirements:
75
- - - ">="
76
- - !ruby/object:Gem::Version
77
- version: '0'
78
- type: :development
79
- prerelease: false
80
- version_requirements: !ruby/object:Gem::Requirement
81
- requirements:
82
- - - ">="
83
- - !ruby/object:Gem::Version
84
- version: '0'
85
- description:
27
+ version: '7'
86
28
  email:
87
- - andrew@chartkick.com
29
+ - andrew@ankane.org
88
30
  - bob.remeika@gmail.com
89
31
  executables: []
90
32
  extensions: []
@@ -94,20 +36,28 @@ files:
94
36
  - CONTRIBUTING.md
95
37
  - LICENSE.txt
96
38
  - README.md
39
+ - lib/generators/strong_migrations/install_generator.rb
40
+ - lib/generators/strong_migrations/templates/initializer.rb.tt
97
41
  - lib/strong_migrations.rb
98
- - lib/strong_migrations/alphabetize_columns.rb
42
+ - lib/strong_migrations/adapters/abstract_adapter.rb
43
+ - lib/strong_migrations/adapters/mariadb_adapter.rb
44
+ - lib/strong_migrations/adapters/mysql_adapter.rb
45
+ - lib/strong_migrations/adapters/postgresql_adapter.rb
99
46
  - lib/strong_migrations/checker.rb
100
- - lib/strong_migrations/database_tasks.rb
47
+ - lib/strong_migrations/checks.rb
48
+ - lib/strong_migrations/error_messages.rb
101
49
  - lib/strong_migrations/migration.rb
102
- - lib/strong_migrations/migration_helpers.rb
50
+ - lib/strong_migrations/migration_context.rb
51
+ - lib/strong_migrations/migrator.rb
103
52
  - lib/strong_migrations/railtie.rb
53
+ - lib/strong_migrations/safe_methods.rb
54
+ - lib/strong_migrations/schema_dumper.rb
104
55
  - lib/strong_migrations/version.rb
105
56
  - lib/tasks/strong_migrations.rake
106
57
  homepage: https://github.com/ankane/strong_migrations
107
58
  licenses:
108
59
  - MIT
109
60
  metadata: {}
110
- post_install_message:
111
61
  rdoc_options: []
112
62
  require_paths:
113
63
  - lib
@@ -115,15 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
115
65
  requirements:
116
66
  - - ">="
117
67
  - !ruby/object:Gem::Version
118
- version: '2.4'
68
+ version: '3.1'
119
69
  required_rubygems_version: !ruby/object:Gem::Requirement
120
70
  requirements:
121
71
  - - ">="
122
72
  - !ruby/object:Gem::Version
123
73
  version: '0'
124
74
  requirements: []
125
- rubygems_version: 3.1.2
126
- signing_key:
75
+ rubygems_version: 3.6.2
127
76
  specification_version: 4
128
77
  summary: Catch unsafe migrations in development
129
78
  test_files: []
@@ -1,11 +0,0 @@
1
- module StrongMigrations
2
- module AlphabetizeColumns
3
- def columns(*args)
4
- super.sort_by(&:name)
5
- end
6
-
7
- def extensions(*args)
8
- super.sort
9
- end
10
- end
11
- end
@@ -1,117 +0,0 @@
1
- module StrongMigrations
2
- module MigrationHelpers
3
- def add_foreign_key_safely(from_table, to_table, **options)
4
- ensure_postgresql(__method__)
5
- ensure_not_in_transaction(__method__)
6
-
7
- reversible do |dir|
8
- dir.up do
9
- if ActiveRecord::VERSION::STRING >= "5.2"
10
- add_foreign_key(from_table, to_table, options.merge(validate: false))
11
- validate_foreign_key(from_table, to_table)
12
- else
13
- options = connection.foreign_key_options(from_table, to_table, options)
14
- fk_name, column, primary_key = options.values_at(:name, :column, :primary_key)
15
- primary_key ||= "id"
16
-
17
- statement = ["ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)"]
18
- statement << on_delete_update_statement(:delete, options[:on_delete]) if options[:on_delete]
19
- statement << on_delete_update_statement(:update, options[:on_update]) if options[:on_update]
20
- statement << "NOT VALID"
21
-
22
- safety_assured do
23
- execute quote_identifiers(statement.join(" "), [from_table, fk_name, column, to_table, primary_key])
24
- execute quote_identifiers("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
25
- end
26
- end
27
- end
28
-
29
- dir.down do
30
- remove_foreign_key(from_table, to_table)
31
- end
32
- end
33
- end
34
-
35
- def add_null_constraint_safely(table_name, column_name, name: nil)
36
- ensure_postgresql(__method__)
37
- ensure_not_in_transaction(__method__)
38
-
39
- reversible do |dir|
40
- dir.up do
41
- name ||= null_constraint_name(table_name, column_name)
42
-
43
- safety_assured do
44
- execute quote_identifiers("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table_name, name, column_name])
45
- execute quote_identifiers("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table_name, name])
46
- end
47
- end
48
-
49
- dir.down do
50
- remove_null_constraint_safely(table_name, column_name)
51
- end
52
- end
53
- end
54
-
55
- # removing constraints is safe, but this method is safe to reverse as well
56
- def remove_null_constraint_safely(table_name, column_name, name: nil)
57
- # could also ensure in transaction so it can be reversed
58
- # but that's more of a concern for a reversible migrations check
59
- ensure_postgresql(__method__)
60
-
61
- reversible do |dir|
62
- dir.up do
63
- name ||= null_constraint_name(table_name, column_name)
64
-
65
- safety_assured do
66
- execute quote_identifiers("ALTER TABLE %s DROP CONSTRAINT %s", [table_name, name])
67
- end
68
- end
69
-
70
- dir.down do
71
- add_null_constraint_safely(table_name, column_name)
72
- end
73
- end
74
- end
75
-
76
- private
77
-
78
- def ensure_postgresql(method_name)
79
- raise StrongMigrations::Error, "`#{method_name}` is intended for Postgres only" unless postgresql?
80
- end
81
-
82
- def postgresql?
83
- %w(PostgreSQL PostGIS).include?(connection.adapter_name)
84
- end
85
-
86
- def ensure_not_in_transaction(method_name)
87
- if connection.transaction_open?
88
- raise StrongMigrations::Error, "Cannot run `#{method_name}` inside a transaction. Use `disable_ddl_transaction` to disable the transaction."
89
- end
90
- end
91
-
92
- # match https://github.com/nullobject/rein
93
- def null_constraint_name(table_name, column_name)
94
- "#{table_name}_#{column_name}_null"
95
- end
96
-
97
- def on_delete_update_statement(delete_or_update, action)
98
- on = delete_or_update.to_s.upcase
99
-
100
- case action
101
- when :nullify
102
- "ON #{on} SET NULL"
103
- when :cascade
104
- "ON #{on} CASCADE"
105
- when :restrict
106
- "ON #{on} RESTRICT"
107
- else
108
- # same error message as Active Record
109
- raise "'#{action}' is not supported for :on_update or :on_delete.\nSupported values are: :nullify, :cascade, :restrict"
110
- end
111
- end
112
-
113
- def quote_identifiers(statement, identifiers)
114
- statement % identifiers.map { |v| connection.quote_table_name(v) }
115
- end
116
- end
117
- end