Class: StrongMigrations::Checker
- Inherits:
-
Object
- Object
- StrongMigrations::Checker
- Includes:
- SafeMethods
- Defined in:
- lib/strong_migrations/checker.rb
Instance Attribute Summary collapse
-
#direction ⇒ Object
Returns the value of attribute direction.
-
#transaction_disabled ⇒ Object
Returns the value of attribute transaction_disabled.
Instance Method Summary collapse
-
#initialize(migration) ⇒ Checker
constructor
A new instance of Checker.
- #perform(method, *args) ⇒ Object
- #safety_assured ⇒ Object
Methods included from SafeMethods
#disable_transaction, #in_transaction?, #safe_add_check_constraint, #safe_add_foreign_key, #safe_add_foreign_key_code, #safe_add_index, #safe_add_reference, #safe_by_default_method?, #safe_change_column_null, #safe_remove_index
Constructor Details
#initialize(migration) ⇒ Checker
Returns a new instance of Checker.
7 8 9 10 11 12 13 |
# File 'lib/strong_migrations/checker.rb', line 7 def initialize(migration) @migration = migration @new_tables = [] @safe = false @timeouts_set = false @lock_timeout_checked = false end |
Instance Attribute Details
#direction ⇒ Object
Returns the value of attribute direction.
5 6 7 |
# File 'lib/strong_migrations/checker.rb', line 5 def direction @direction end |
#transaction_disabled ⇒ Object
Returns the value of attribute transaction_disabled.
5 6 7 |
# File 'lib/strong_migrations/checker.rb', line 5 def transaction_disabled @transaction_disabled end |
Instance Method Details
#perform(method, *args) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/strong_migrations/checker.rb', line 25 def perform(method, *args) set_timeouts check_lock_timeout if !safe? || safe_by_default_method?(method) case method when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to columns = case method when :remove_timestamps ["created_at", "updated_at"] when :remove_column [args[1].to_s] when :remove_columns args[1..-1].map(&:to_s) else = args[2] || {} reference = args[1] cols = [] cols << "#{reference}_type" if [:polymorphic] cols << "#{reference}_id" cols end code = "self.ignored_columns = #{columns.inspect}" raise_error :remove_column, model: args[0].to_s.classify, code: code, command: command_str(method, args), column_suffix: columns.size > 1 ? "s" : "" when :change_table raise_error :change_table, header: "Possibly dangerous operation" when :rename_table raise_error :rename_table when :rename_column raise_error :rename_column when :add_index table, columns, = args ||= {} if columns.is_a?(Array) && columns.size > 3 && ![:unique] raise_error :add_index_columns, header: "Best practice" end if postgresql? && [:algorithm] != :concurrently && !new_table?(table) return safe_add_index(table, columns, ) if StrongMigrations.safe_by_default raise_error :add_index, command: command_str("add_index", [table, columns, .merge(algorithm: :concurrently)]) end when :remove_index table, = args unless .is_a?(Hash) = {column: } end ||= {} if postgresql? && [:algorithm] != :concurrently && !new_table?(table) return safe_remove_index(table, ) if StrongMigrations.safe_by_default raise_error :remove_index, command: command_str("remove_index", [table, .merge(algorithm: :concurrently)]) end when :add_column table, column, type, = args ||= {} default = [:default] if !default.nil? && !((postgresql? && postgresql_version >= Gem::Version.new("11")) || (mysql? && mysql_version >= Gem::Version.new("8.0.12")) || (mariadb? && mariadb_version >= Gem::Version.new("10.3.2"))) if [:null] == false = .except(:null) append = " Then add the NOT NULL constraint in separate migrations." end raise_error :add_column_default, add_command: command_str("add_column", [table, column, type, .except(:default)]), change_command: command_str("change_column_default", [table, column, default]), remove_command: command_str("remove_column", [table, column]), code: backfill_code(table, column, default), append: append, rewrite_blocks: rewrite_blocks end if type.to_s == "json" && postgresql? raise_error :add_column_json, command: command_str("add_column", [table, column, :jsonb, ]) end when :change_column table, column, type, = args ||= {} safe = false existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s } if existing_column existing_type = existing_column.sql_type.split("(").first if postgresql? case type.to_s when "string" # safe to increase limit or remove it # not safe to decrease limit or add a limit case existing_type when "character varying" safe = ![:limit] || (existing_column.limit && [:limit] >= existing_column.limit) when "text" safe = ![:limit] end when "text" # safe to change varchar to text (and text to text) safe = ["character varying", "text"].include?(existing_type) when "numeric", "decimal" # numeric and decimal are equivalent and can be used interchangably safe = ["numeric", "decimal"].include?(existing_type) && ( ( # unconstrained ![:precision] && ![:scale] ) || ( # increased precision, same scale [:precision] && existing_column.precision && [:precision] >= existing_column.precision && [:scale] == existing_column.scale ) ) when "datetime", "timestamp", "timestamptz" safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) && postgresql_version >= Gem::Version.new("12") && connection.select_all("SHOW timezone").first["TimeZone"] == "UTC" end elsif mysql? || mariadb? case type.to_s when "string" # https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html # https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column # increased limit, but doesn't change number of length bytes # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar limit = [:limit] || 255 safe = ["varchar"].include?(existing_type) && limit >= existing_column.limit && (limit <= 255 || existing_column.limit > 255) end end end # unsafe to set NOT NULL for safe types if safe && existing_column.null && [:null] == false raise_error :change_column_with_not_null end raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe when :create_table table, = args ||= {} raise_error :create_table if [:force] # keep track of new tables of add_index check @new_tables << table.to_s when :add_reference, :add_belongs_to table, reference, = args ||= {} if postgresql? index_value = .fetch(:index, true) concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently bad_index = index_value && !concurrently_set if bad_index || [:foreign_key] if index_value.is_a?(Hash) [:index] = [:index].merge(algorithm: :concurrently) else = .merge(index: {algorithm: :concurrently}) end return safe_add_reference(table, reference, ) if StrongMigrations.safe_by_default if .delete(:foreign_key) headline = "Adding a foreign key blocks writes on both tables." append = " Then add the foreign key in separate migrations." else headline = "Adding an index non-concurrently locks the table." end raise_error :add_reference, headline: headline, command: command_str(method, [table, reference, ]), append: append end end when :execute raise_error :execute, header: "Possibly dangerous operation" when :change_column_null table, column, null, default = args if !null if postgresql? safe = false if postgresql_version >= Gem::Version.new("12") # TODO likely need to quote the column in some situations safe = constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" } end unless safe # match https://github.com/nullobject/rein constraint_name = "#{table}_#{column}_null" add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]) validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]) remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name]) validate_constraint_code = if ar_version >= 6.1 String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}])) else String.new(safety_assured_str(validate_code)) end if postgresql_version >= Gem::Version.new("12") change_args = [table, column, null] validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}" if ar_version >= 6.1 validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}" else validate_constraint_code << "\n #{safety_assured_str(remove_code)}" end end return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default add_constraint_code = if ar_version >= 6.1 # only quote when needed expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column) command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}]) else safety_assured_str(add_code) end raise_error :change_column_null_postgresql, add_constraint_code: add_constraint_code, validate_constraint_code: validate_constraint_code end elsif mysql? || mariadb? raise_error :change_column_null_mysql elsif !default.nil? raise_error :change_column_null, code: backfill_code(table, column, default) end end when :add_foreign_key from_table, to_table, = args ||= {} # always validated before 5.2 validate = .fetch(:validate, true) || ar_version < 5.2 if postgresql? && validate if ar_version < 5.2 # fk name logic from rails primary_key = [:primary_key] || "id" column = [:column] || "#{to_table.to_s.singularize}_id" hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10) fk_name = [:name] || "fk_rails_#{hashed_identifier}" add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]) validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name]) return safe_add_foreign_key_code(from_table, to_table, add_code, validate_code) if StrongMigrations.safe_by_default raise_error :add_foreign_key, add_foreign_key_code: safety_assured_str(add_code), validate_foreign_key_code: safety_assured_str(validate_code) else return safe_add_foreign_key(from_table, to_table, ) if StrongMigrations.safe_by_default raise_error :add_foreign_key, add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, .merge(validate: false)]), validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table]) end end when :validate_foreign_key if postgresql? && writes_blocked? raise_error :validate_foreign_key end when :add_check_constraint table, expression, = args ||= {} if !new_table?(table) if postgresql? && [:validate] != false = .merge(validate: false) name = [:name] || @migration.(table, expression, )[:name] = {name: name} return safe_add_check_constraint(table, expression, , ) if StrongMigrations.safe_by_default raise_error :add_check_constraint, add_check_constraint_code: command_str("add_check_constraint", [table, expression, ]), validate_check_constraint_code: command_str("validate_check_constraint", [table, ]) elsif mysql? || mariadb? raise_error :add_check_constraint_mysql end end when :validate_check_constraint if postgresql? && writes_blocked? raise_error :validate_check_constraint end end StrongMigrations.checks.each do |check| @migration.instance_exec(method, args, &check) end end result = yield # outdated statistics + a new index can hurt performance of existing queries if StrongMigrations.auto_analyze && direction == :up && method == :add_index if postgresql? connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}" elsif mariadb? || mysql? connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}" end end result end |
#safety_assured ⇒ Object
15 16 17 18 19 20 21 22 23 |
# File 'lib/strong_migrations/checker.rb', line 15 def safety_assured previous_value = @safe begin @safe = true yield ensure @safe = previous_value end end |