strong_migrations 0.6.8 → 1.2.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.
@@ -5,10 +5,6 @@ module StrongMigrations
5
5
  class Railtie < Rails::Railtie
6
6
  rake_tasks do
7
7
  load "tasks/strong_migrations.rake"
8
-
9
- ["db:drop", "db:reset", "db:schema:load", "db:structure:load"].each do |t|
10
- Rake::Task[t].enhance ["strong_migrations:safety_assured"]
11
- end
12
8
  end
13
9
  end
14
10
  end
@@ -0,0 +1,118 @@
1
+ module StrongMigrations
2
+ module SafeMethods
3
+ def safe_by_default_method?(method)
4
+ StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
5
+ end
6
+
7
+ # TODO check if invalid index with expected name exists and remove if needed
8
+ def safe_add_index(*args, **options)
9
+ disable_transaction
10
+ @migration.add_index(*args, **options.merge(algorithm: :concurrently))
11
+ end
12
+
13
+ def safe_remove_index(*args, **options)
14
+ disable_transaction
15
+ @migration.remove_index(*args, **options.merge(algorithm: :concurrently))
16
+ end
17
+
18
+ def safe_add_reference(table, reference, *args, **options)
19
+ @migration.reversible do |dir|
20
+ dir.up do
21
+ disable_transaction
22
+ foreign_key = options.delete(:foreign_key)
23
+ @migration.add_reference(table, reference, *args, **options)
24
+ if foreign_key
25
+ # same as Active Record
26
+ name =
27
+ if foreign_key.is_a?(Hash) && foreign_key[:to_table]
28
+ foreign_key[:to_table]
29
+ else
30
+ (ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
31
+ end
32
+
33
+ if reference
34
+ @migration.add_foreign_key(table, name, column: "#{reference}_id")
35
+ else
36
+ @migration.add_foreign_key(table, name)
37
+ end
38
+ end
39
+ end
40
+ dir.down do
41
+ @migration.remove_reference(table, reference)
42
+ end
43
+ end
44
+ end
45
+
46
+ def safe_add_foreign_key(from_table, to_table, *args, **options)
47
+ @migration.reversible do |dir|
48
+ dir.up do
49
+ @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
50
+ disable_transaction
51
+ @migration.validate_foreign_key(from_table, to_table)
52
+ end
53
+ dir.down do
54
+ @migration.remove_foreign_key(from_table, to_table)
55
+ end
56
+ end
57
+ end
58
+
59
+ def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
60
+ @migration.reversible do |dir|
61
+ dir.up do
62
+ @migration.add_check_constraint(table, expression, *args, **add_options)
63
+ disable_transaction
64
+ @migration.validate_check_constraint(table, **validate_options)
65
+ end
66
+ dir.down do
67
+ @migration.remove_check_constraint(table, expression, **add_options)
68
+ end
69
+ end
70
+ end
71
+
72
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
73
+ @migration.reversible do |dir|
74
+ dir.up do
75
+ unless default.nil?
76
+ raise Error, "default value not supported yet with safe_by_default"
77
+ end
78
+
79
+ @migration.safety_assured do
80
+ @migration.execute(add_code)
81
+ disable_transaction
82
+ @migration.execute(validate_code)
83
+ end
84
+ if change_args
85
+ @migration.change_column_null(*change_args)
86
+ @migration.safety_assured do
87
+ @migration.execute(remove_code)
88
+ end
89
+ end
90
+ end
91
+ dir.down do
92
+ if change_args
93
+ down_args = change_args.dup
94
+ down_args[2] = true
95
+ @migration.change_column_null(*down_args)
96
+ else
97
+ @migration.safety_assured do
98
+ @migration.execute(remove_code)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # hard to commit at right time when reverting
106
+ # so just commit at start
107
+ def disable_transaction
108
+ if in_transaction? && !transaction_disabled
109
+ @migration.connection.commit_db_transaction
110
+ self.transaction_disabled = true
111
+ end
112
+ end
113
+
114
+ def in_transaction?
115
+ @migration.connection.open_transactions > 0
116
+ end
117
+ end
118
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.6.8"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -1,10 +1,19 @@
1
1
  # dependencies
2
2
  require "active_support"
3
3
 
4
+ # adapters
5
+ require "strong_migrations/adapters/abstract_adapter"
6
+ require "strong_migrations/adapters/mysql_adapter"
7
+ require "strong_migrations/adapters/mariadb_adapter"
8
+ require "strong_migrations/adapters/postgresql_adapter"
9
+
4
10
  # modules
11
+ require "strong_migrations/checks"
12
+ require "strong_migrations/safe_methods"
5
13
  require "strong_migrations/checker"
6
14
  require "strong_migrations/database_tasks"
7
15
  require "strong_migrations/migration"
16
+ require "strong_migrations/migrator"
8
17
  require "strong_migrations/version"
9
18
 
10
19
  # integrations
@@ -13,195 +22,22 @@ require "strong_migrations/railtie" if defined?(Rails)
13
22
  module StrongMigrations
14
23
  class Error < StandardError; end
15
24
  class UnsafeMigration < Error; end
25
+ class UnsupportedVersion < Error; end
16
26
 
17
27
  class << self
18
28
  attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
19
29
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
20
- :enabled_checks, :lock_timeout, :statement_timeout
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
21
32
  attr_writer :lock_timeout_limit
22
33
  end
23
34
  self.auto_analyze = false
24
35
  self.start_after = 0
36
+ self.lock_timeout_retries = 0
37
+ self.lock_timeout_retry_delay = 10 # seconds
25
38
  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",
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
- "%{headline} 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:
126
-
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_mysql:
185
- "Setting NOT NULL on an existing column is not safe with your database engine.",
186
-
187
- add_foreign_key:
188
- "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
189
- which is expensive on large tables. Instead, validate it in a separate migration
190
- with a more agreeable RowShareLock.
191
-
192
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
193
- def change
194
- %{add_foreign_key_code}
195
- end
196
- end
197
-
198
- class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
199
- def change
200
- %{validate_foreign_key_code}
201
- end
202
- end"
203
- }
204
- self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
39
+ self.safe_by_default = false
40
+ self.check_down = false
205
41
 
206
42
  # private
207
43
  def self.developer_env?
@@ -237,8 +73,12 @@ end"
237
73
  end
238
74
  end
239
75
 
76
+ # load error messages
77
+ require "strong_migrations/error_messages"
78
+
240
79
  ActiveSupport.on_load(:active_record) do
241
80
  ActiveRecord::Migration.prepend(StrongMigrations::Migration)
81
+ ActiveRecord::Migrator.prepend(StrongMigrations::Migrator)
242
82
 
243
83
  if defined?(ActiveRecord::Tasks::DatabaseTasks)
244
84
  ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
@@ -1,10 +1,4 @@
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
2
  # https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/
9
3
  task :alphabetize_columns do
10
4
  $stderr.puts "Dumping schema"
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.8
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  - Bob Remeika
9
9
  - David Waller
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-05-14 00:00:00.000000000 Z
13
+ date: 2022-06-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,87 +18,17 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '5'
21
+ version: '5.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - ">="
27
27
  - !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
- - !ruby/object:Gem::Dependency
86
- name: mysql2
87
- requirement: !ruby/object:Gem::Requirement
88
- requirements:
89
- - - ">="
90
- - !ruby/object:Gem::Version
91
- version: '0'
92
- type: :development
93
- prerelease: false
94
- version_requirements: !ruby/object:Gem::Requirement
95
- requirements:
96
- - - ">="
97
- - !ruby/object:Gem::Version
98
- version: '0'
99
- description:
28
+ version: '5.2'
29
+ description:
100
30
  email:
101
- - andrew@chartkick.com
31
+ - andrew@ankane.org
102
32
  - bob.remeika@gmail.com
103
33
  executables: []
104
34
  extensions: []
@@ -111,18 +41,26 @@ files:
111
41
  - lib/generators/strong_migrations/install_generator.rb
112
42
  - lib/generators/strong_migrations/templates/initializer.rb.tt
113
43
  - lib/strong_migrations.rb
44
+ - lib/strong_migrations/adapters/abstract_adapter.rb
45
+ - lib/strong_migrations/adapters/mariadb_adapter.rb
46
+ - lib/strong_migrations/adapters/mysql_adapter.rb
47
+ - lib/strong_migrations/adapters/postgresql_adapter.rb
114
48
  - lib/strong_migrations/alphabetize_columns.rb
115
49
  - lib/strong_migrations/checker.rb
50
+ - lib/strong_migrations/checks.rb
116
51
  - lib/strong_migrations/database_tasks.rb
52
+ - lib/strong_migrations/error_messages.rb
117
53
  - lib/strong_migrations/migration.rb
54
+ - lib/strong_migrations/migrator.rb
118
55
  - lib/strong_migrations/railtie.rb
56
+ - lib/strong_migrations/safe_methods.rb
119
57
  - lib/strong_migrations/version.rb
120
58
  - lib/tasks/strong_migrations.rake
121
59
  homepage: https://github.com/ankane/strong_migrations
122
60
  licenses:
123
61
  - MIT
124
62
  metadata: {}
125
- post_install_message:
63
+ post_install_message:
126
64
  rdoc_options: []
127
65
  require_paths:
128
66
  - lib
@@ -130,15 +68,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
130
68
  requirements:
131
69
  - - ">="
132
70
  - !ruby/object:Gem::Version
133
- version: '2.4'
71
+ version: '2.6'
134
72
  required_rubygems_version: !ruby/object:Gem::Requirement
135
73
  requirements:
136
74
  - - ">="
137
75
  - !ruby/object:Gem::Version
138
76
  version: '0'
139
77
  requirements: []
140
- rubygems_version: 3.1.2
141
- signing_key:
78
+ rubygems_version: 3.3.7
79
+ signing_key:
142
80
  specification_version: 4
143
81
  summary: Catch unsafe migrations in development
144
82
  test_files: []