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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -16
- data/LICENSE.txt +1 -1
- data/README.md +656 -111
- data/lib/generators/strong_migrations/install_generator.rb +46 -0
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +25 -0
- data/lib/strong_migrations.rb +150 -33
- data/lib/strong_migrations/checker.rb +599 -0
- data/lib/strong_migrations/database_tasks.rb +1 -1
- data/lib/strong_migrations/migration.rb +15 -176
- data/lib/strong_migrations/railtie.rb +0 -4
- data/lib/strong_migrations/safe_methods.rb +125 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/tasks/strong_migrations.rake +0 -6
- metadata +33 -17
- data/lib/strong_migrations/unsafe_migration.rb +0 -4
@@ -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
|
-
|
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
|
17
|
-
|
18
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
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,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.
|
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:
|
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:
|
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:
|
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: '
|
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: '
|
85
|
-
|
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/
|
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.
|
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
|
-
|
125
|
-
|
126
|
-
signing_key:
|
141
|
+
rubygems_version: 3.2.3
|
142
|
+
signing_key:
|
127
143
|
specification_version: 4
|
128
|
-
summary: Catch unsafe migrations
|
144
|
+
summary: Catch unsafe migrations in development
|
129
145
|
test_files: []
|