strong_migrations 0.3.1 → 0.7.6

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.
@@ -3,7 +3,7 @@ module StrongMigrations
3
3
  def migrate
4
4
  super
5
5
  rescue => e
6
- if e.cause.is_a?(StrongMigrations::UnsafeMigration)
6
+ if e.cause.is_a?(StrongMigrations::Error)
7
7
  # strip cause and clean backtrace
8
8
  def e.cause
9
9
  nil
@@ -1,193 +1,32 @@
1
1
  module StrongMigrations
2
2
  module Migration
3
- def safety_assured
4
- previous_value = @safe
5
- @safe = true
6
- yield
7
- ensure
8
- @safe = previous_value
9
- end
10
-
11
3
  def migrate(direction)
12
- @direction = direction
4
+ strong_migrations_checker.direction = direction
13
5
  super
6
+ connection.begin_db_transaction if strong_migrations_checker.transaction_disabled
14
7
  end
15
8
 
16
- def method_missing(method, *args, &block)
17
- unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe?
18
- ar5 = ActiveRecord::VERSION::MAJOR >= 5
19
-
20
- case method
21
- when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
22
- columns =
23
- case method
24
- when :remove_timestamps
25
- ["created_at", "updated_at"]
26
- when :remove_column
27
- [args[1].to_s]
28
- when :remove_columns
29
- args[1..-1].map(&:to_s)
30
- else
31
- options = args[2] || {}
32
- reference = args[1]
33
- cols = []
34
- cols << "#{reference}_type" if options[:polymorphic]
35
- cols << "#{reference}_id"
36
- cols
37
- end
38
-
39
- code = ar5 ? "self.ignored_columns = #{columns.inspect}" : "def self.columns\n super.reject { |c| #{columns.inspect}.include?(c.name) }\n end"
40
-
41
- raise_error :remove_column,
42
- model: args[0].to_s.classify,
43
- code: code,
44
- command: command_str(method, args),
45
- column_suffix: columns.size > 1 ? "s" : ""
46
- when :change_table
47
- raise_error :change_table, header: "Possibly dangerous operation"
48
- when :rename_table
49
- raise_error :rename_table
50
- when :rename_column
51
- raise_error :rename_column
52
- when :add_index
53
- table, columns, options = args
54
- options ||= {}
55
-
56
- if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
57
- raise_error :add_index_columns, header: "Best practice"
58
- end
59
- if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(table.to_s)
60
- raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
61
- end
62
- when :add_column
63
- table, column, type, options = args
64
- options ||= {}
65
- default = options[:default]
66
-
67
- if !default.nil? && !(postgresql? && postgresql_version >= 110000)
68
- raise_error :add_column_default,
69
- add_command: command_str("add_column", [table, column, type, options.except(:default)]),
70
- change_command: command_str("change_column_default", [table, column, default]),
71
- remove_command: command_str("remove_column", [table, column]),
72
- code: backfill_code(table, column, default)
73
- end
74
-
75
- if type.to_s == "json" && postgresql?
76
- if postgresql_version >= 90400
77
- raise_error :add_column_json
78
- else
79
- raise_error :add_column_json_legacy,
80
- model: table.to_s.classify,
81
- table: connection.quote_table_name(table.to_s)
82
- end
83
- end
84
- when :change_column
85
- table, column, type = args
86
-
87
- safe = false
88
- # assume Postgres 9.1+ since previous versions are EOL
89
- if postgresql? && type.to_s == "text"
90
- found_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
91
- safe = found_column && found_column.type == :string
92
- end
93
- raise_error :change_column unless safe
94
- when :create_table
95
- table, options = args
96
- options ||= {}
97
-
98
- raise_error :create_table if options[:force]
99
-
100
- # keep track of new tables of add_index check
101
- (@new_tables ||= []) << table.to_s
102
- when :add_reference, :add_belongs_to
103
- table, reference, options = args
104
- options ||= {}
105
-
106
- index_value = options.fetch(:index, ar5)
107
- if postgresql? && index_value
108
- columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
109
-
110
- raise_error :add_reference,
111
- reference_command: command_str(method, [table, reference, options.merge(index: false)]),
112
- index_command: command_str("add_index", [table, columns, {algorithm: :concurrently}])
113
- end
114
- when :execute
115
- raise_error :execute, header: "Possibly dangerous operation"
116
- when :change_column_null
117
- table, column, null, default = args
118
- if !null && !default.nil?
119
- raise_error :change_column_null,
120
- code: backfill_code(table, column, default)
121
- end
122
- end
123
-
124
- StrongMigrations.checks.each do |check|
125
- instance_exec(method, args, &check)
126
- end
127
- end
128
-
129
- result = super
130
-
131
- if StrongMigrations.auto_analyze && @direction == :up && postgresql? && method == :add_index
132
- connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
9
+ def method_missing(method, *args)
10
+ strong_migrations_checker.perform(method, *args) do
11
+ super
133
12
  end
134
-
135
- result
136
13
  end
14
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
137
15
 
138
- private
139
-
140
- def postgresql?
141
- %w(PostgreSQL PostGIS).include?(connection.adapter_name)
142
- end
143
-
144
- def postgresql_version
145
- @postgresql_version ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i
146
- end
147
-
148
- def version_safe?
149
- version && version <= StrongMigrations.start_after
150
- end
151
-
152
- def raise_error(message_key, header: nil, **vars)
153
- message = StrongMigrations.error_messages[message_key] || "Missing message"
154
-
155
- ar5 = ActiveRecord::VERSION::MAJOR >= 5
156
- vars[:migration_name] = self.class.name
157
- vars[:migration_suffix] = ar5 ? "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" : ""
158
- vars[:base_model] = ar5 ? "ApplicationRecord" : "ActiveRecord::Base"
159
-
160
- # escape % not followed by {
161
- stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected")
162
- end
163
-
164
- def command_str(command, args)
165
- str_args = args[0..-2].map { |a| a.inspect }
166
-
167
- # prettier last arg
168
- last_arg = args[-1]
169
- if last_arg.is_a?(Hash)
170
- if last_arg.any?
171
- str_args << last_arg.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
172
- end
173
- else
174
- str_args << last_arg.inspect
175
- end
176
-
177
- "#{command} #{str_args.join(", ")}"
178
- end
179
-
180
- def backfill_code(table, column, default)
181
- model = table.to_s.classify
182
- if ActiveRecord::VERSION::MAJOR >= 5
183
- "#{model}.in_batches.update_all #{column}: #{default.inspect}"
184
- else
185
- "#{model}.find_in_batches do |records|\n #{model}.where(id: records.map(&:id)).update_all #{column}: #{default.inspect}\n end"
16
+ def safety_assured
17
+ strong_migrations_checker.safety_assured do
18
+ yield
186
19
  end
187
20
  end
188
21
 
189
22
  def stop!(message, header: "Custom check")
190
23
  raise StrongMigrations::UnsafeMigration, "\n=== #{header} #strong_migrations ===\n\n#{message}\n"
191
24
  end
25
+
26
+ private
27
+
28
+ def strong_migrations_checker
29
+ @strong_migrations_checker ||= StrongMigrations::Checker.new(self)
30
+ end
192
31
  end
193
32
  end
@@ -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,125 @@
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(table, columns, options)
9
+ disable_transaction
10
+ @migration.add_index(table, columns, **options.merge(algorithm: :concurrently))
11
+ end
12
+
13
+ def safe_remove_index(table, options)
14
+ disable_transaction
15
+ @migration.remove_index(table, **options.merge(algorithm: :concurrently))
16
+ end
17
+
18
+ def safe_add_reference(table, reference, 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, **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
+ @migration.add_foreign_key(table, name)
34
+ end
35
+ end
36
+ dir.down do
37
+ @migration.remove_reference(table, reference)
38
+ end
39
+ end
40
+ end
41
+
42
+ def safe_add_foreign_key(from_table, to_table, options)
43
+ @migration.reversible do |dir|
44
+ dir.up do
45
+ @migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
46
+ disable_transaction
47
+ @migration.validate_foreign_key(from_table, to_table)
48
+ end
49
+ dir.down do
50
+ @migration.remove_foreign_key(from_table, to_table)
51
+ end
52
+ end
53
+ end
54
+
55
+ def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
56
+ @migration.reversible do |dir|
57
+ dir.up do
58
+ @migration.safety_assured do
59
+ @migration.execute(add_code)
60
+ disable_transaction
61
+ @migration.execute(validate_code)
62
+ end
63
+ end
64
+ dir.down do
65
+ @migration.remove_foreign_key(from_table, to_table)
66
+ end
67
+ end
68
+ end
69
+
70
+ def safe_add_check_constraint(table, expression, add_options, validate_options)
71
+ @migration.reversible do |dir|
72
+ dir.up do
73
+ @migration.add_check_constraint(table, expression, **add_options)
74
+ disable_transaction
75
+ @migration.validate_check_constraint(table, **validate_options)
76
+ end
77
+ dir.down do
78
+ @migration.remove_check_constraint(table, expression, **add_options)
79
+ end
80
+ end
81
+ end
82
+
83
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code)
84
+ @migration.reversible do |dir|
85
+ dir.up do
86
+ @migration.safety_assured do
87
+ @migration.execute(add_code)
88
+ disable_transaction
89
+ @migration.execute(validate_code)
90
+ end
91
+ if change_args
92
+ @migration.change_column_null(*change_args)
93
+ @migration.safety_assured do
94
+ @migration.execute(remove_code)
95
+ end
96
+ end
97
+ end
98
+ dir.down do
99
+ if change_args
100
+ down_args = change_args.dup
101
+ down_args[2] = true
102
+ @migration.change_column_null(*down_args)
103
+ else
104
+ @migration.safety_assured do
105
+ @migration.execute(remove_code)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # hard to commit at right time when reverting
113
+ # so just commit at start
114
+ def disable_transaction
115
+ if in_transaction? && !transaction_disabled
116
+ @migration.connection.commit_db_transaction
117
+ self.transaction_disabled = true
118
+ end
119
+ end
120
+
121
+ def in_transaction?
122
+ @migration.connection.open_transactions > 0
123
+ end
124
+ end
125
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.3.1"
2
+ VERSION = "0.7.6"
3
3
  end
@@ -1,10 +1,4 @@
1
- # http://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.3.1
4
+ version: 0.7.6
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: 2018-10-19 00:00:00.000000000 Z
13
+ date: 2021-01-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,14 +18,14 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 3.2.0
21
+ version: '5'
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: 3.2.0
28
+ version: '5'
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: bundler
31
31
  requirement: !ruby/object:Gem::Requirement
@@ -72,17 +72,31 @@ dependencies:
72
72
  name: pg
73
73
  requirement: !ruby/object:Gem::Requirement
74
74
  requirements:
75
- - - "<"
75
+ - - ">="
76
76
  - !ruby/object:Gem::Version
77
- version: '1'
77
+ version: '0'
78
78
  type: :development
79
79
  prerelease: false
80
80
  version_requirements: !ruby/object:Gem::Requirement
81
81
  requirements:
82
- - - "<"
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
+ - - ">="
83
90
  - !ruby/object:Gem::Version
84
- version: '1'
85
- description:
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:
86
100
  email:
87
101
  - andrew@chartkick.com
88
102
  - bob.remeika@gmail.com
@@ -94,19 +108,22 @@ files:
94
108
  - CONTRIBUTING.md
95
109
  - LICENSE.txt
96
110
  - README.md
111
+ - lib/generators/strong_migrations/install_generator.rb
112
+ - lib/generators/strong_migrations/templates/initializer.rb.tt
97
113
  - lib/strong_migrations.rb
98
114
  - lib/strong_migrations/alphabetize_columns.rb
115
+ - lib/strong_migrations/checker.rb
99
116
  - lib/strong_migrations/database_tasks.rb
100
117
  - lib/strong_migrations/migration.rb
101
118
  - lib/strong_migrations/railtie.rb
102
- - lib/strong_migrations/unsafe_migration.rb
119
+ - lib/strong_migrations/safe_methods.rb
103
120
  - lib/strong_migrations/version.rb
104
121
  - lib/tasks/strong_migrations.rake
105
122
  homepage: https://github.com/ankane/strong_migrations
106
123
  licenses:
107
124
  - MIT
108
125
  metadata: {}
109
- post_install_message:
126
+ post_install_message:
110
127
  rdoc_options: []
111
128
  require_paths:
112
129
  - lib
@@ -114,16 +131,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
131
  requirements:
115
132
  - - ">="
116
133
  - !ruby/object:Gem::Version
117
- version: '2.2'
134
+ version: '2.4'
118
135
  required_rubygems_version: !ruby/object:Gem::Requirement
119
136
  requirements:
120
137
  - - ">="
121
138
  - !ruby/object:Gem::Version
122
139
  version: '0'
123
140
  requirements: []
124
- rubyforge_project:
125
- rubygems_version: 2.7.7
126
- signing_key:
141
+ rubygems_version: 3.2.3
142
+ signing_key:
127
143
  specification_version: 4
128
- summary: Catch unsafe migrations at dev time
144
+ summary: Catch unsafe migrations in development
129
145
  test_files: []