strong_migrations 0.6.8 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []