Module: OnlineMigrations::SchemaStatements
- Includes:
- BackgroundMigrations::MigrationHelpers, BackgroundSchemaMigrations::MigrationHelpers, ChangeColumnTypeHelpers
- Defined in:
- lib/online_migrations/schema_statements.rb
Instance Method Summary collapse
- #__set_statement_timeout(timeout) ⇒ Object
-
#add_check_constraint(table_name, expression, **options) ⇒ Object
Extends default method to be idempotent.
-
#add_column_with_default(table_name, column_name, type, **options) ⇒ void
Adds a column with a default value without durable locks of the entire table.
- #add_exclusion_constraint(table_name, expression, **options) ⇒ Object
-
#add_foreign_key(from_table, to_table, **options) ⇒ Object
Extends default method to be idempotent.
-
#add_index(table_name, column_name, **options) ⇒ Object
Extends default method to be idempotent and automatically recreate invalid indexes.
-
#add_not_null_constraint(table_name, column_name, name: nil, validate: true) ⇒ void
Adds a NOT NULL constraint to the column.
-
#add_reference_concurrently(table_name, ref_name, **options) ⇒ void
Adds a reference to the table with minimal locking.
-
#add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true) ⇒ void
Adds a limit constraint to the text column.
- #disable_statement_timeout ⇒ Object
-
#finalize_column_rename(table_name, column_name, new_column_name) ⇒ void
Finishes the process of column rename.
-
#finalize_columns_rename(table_name, old_new_column_hash) ⇒ void
Same as ‘finalize_column_rename` but for multiple columns.
-
#finalize_table_rename(table_name, _new_name = nil) ⇒ void
Finishes the process of table rename.
-
#index_name(table_name, options) ⇒ Object
From ActiveRecord.
-
#initialize_column_rename(table_name, column_name, new_column_name) ⇒ void
Renames a column without requiring downtime.
-
#initialize_columns_rename(table_name, old_new_column_hash) ⇒ Object
Same as ‘initialize_column_rename` but for multiple columns.
-
#initialize_table_rename(table_name, new_name) ⇒ void
Renames a table without requiring downtime.
- #pk_and_sequence_for(table) ⇒ Object
-
#remove_index(table_name, column_name = nil, **options) ⇒ Object
Extends default method to be idempotent.
-
#remove_not_null_constraint(table_name, column_name, name: nil) ⇒ void
Removes a NOT NULL constraint from the column.
-
#remove_text_limit_constraint(table_name, column_name, _limit = nil, name: nil) ⇒ void
Removes a limit constraint from the text column.
-
#revert_finalize_column_rename(table_name, column_name, new_column_name) ⇒ void
Reverts operations performed by finalize_column_rename.
-
#revert_finalize_columns_rename(table_name, old_new_column_hash) ⇒ void
Same as ‘revert_finalize_column_rename` but for multiple columns.
-
#revert_finalize_table_rename(table_name, new_name) ⇒ void
Reverts operations performed by finalize_table_rename.
-
#revert_initialize_column_rename(table_name, column_name = nil, new_column_name = nil) ⇒ void
Reverts operations performed by initialize_column_rename.
-
#revert_initialize_columns_rename(table_name, _old_new_column_hash = nil) ⇒ void
Same as ‘revert_initialize_column_rename` but for multiple columns.
-
#revert_initialize_table_rename(table_name, new_name) ⇒ void
Reverts operations performed by initialize_table_rename.
-
#swap_column_names(table_name, column1, column2) ⇒ void
Swaps two column names in a table.
-
#update_column_in_batches(table_name, column_name, value, **options) {|relation| ... } ⇒ void
Updates the value of a column in batches.
-
#update_columns_in_batches(table_name, columns_and_values, batch_size: 1000, batch_column_name: primary_key(table_name), progress: false, pause_ms: 50) ⇒ Object
Same as ‘update_column_in_batches`, but for multiple columns.
-
#validate_check_constraint(table_name, **options) ⇒ Object
Extends default method with disabled statement timeout while validation is run.
-
#validate_foreign_key(from_table, to_table = nil, **options) ⇒ Object
Extends default method with disabled statement timeout while validation is run.
-
#validate_not_null_constraint(table_name, column_name, name: nil) ⇒ void
Validates a NOT NULL constraint on the column.
-
#validate_text_limit_constraint(table_name, column_name, name: nil) ⇒ void
Validates a limit constraint on the text column.
-
#with_lock_retries(&block) ⇒ Object
Executes the block with a retry mechanism that alters the ‘lock_timeout` and sleep time between attempts.
Methods included from BackgroundSchemaMigrations::MigrationHelpers
#add_index_in_background, #create_background_schema_migration, #enqueue_background_schema_migration, #ensure_background_schema_migration_succeeded, #remove_index_in_background
Methods included from BackgroundMigrations::MigrationHelpers
#backfill_column_for_type_change_in_background, #backfill_column_in_background, #backfill_columns_for_type_change_in_background, #backfill_columns_in_background, #copy_column_in_background, #copy_columns_in_background, #create_background_data_migration, #delete_associated_records_in_background, #delete_orphaned_records_in_background, #enqueue_background_data_migration, #ensure_background_data_migration_succeeded, #perform_action_on_relation_in_background, #remove_background_data_migration, #reset_counters_in_background
Methods included from ChangeColumnTypeHelpers
#backfill_column_for_type_change, #backfill_columns_for_type_change, #cleanup_column_type_change, #cleanup_columns_type_change, #finalize_column_type_change, #finalize_columns_type_change, #initialize_column_type_change, #initialize_columns_type_change, #revert_finalize_column_type_change, #revert_finalize_columns_type_change, #revert_initialize_column_type_change, #revert_initialize_columns_type_change
Instance Method Details
#__set_statement_timeout(timeout) ⇒ Object
944 945 946 947 948 |
# File 'lib/online_migrations/schema_statements.rb', line 944 def __set_statement_timeout(timeout) # use ceil to prevent no timeout for values under 1 ms timeout = (timeout.to_f * 1000).ceil if !timeout.is_a?(String) execute("SET statement_timeout TO #{quote(timeout)}") end |
#add_check_constraint(table_name, expression, **options) ⇒ Object
Extends default method to be idempotent
851 852 853 854 855 856 857 858 859 860 |
# File 'lib/online_migrations/schema_statements.rb', line 851 def add_check_constraint(table_name, expression, **) if __check_constraint_exists?(table_name, expression: expression, **) Utils.say(<<~MSG.squish) Check constraint was not created because it already exists (this may be due to an aborted migration or similar). table_name: #{table_name}, expression: #{expression} MSG else super end end |
#add_column_with_default(table_name, column_name, type, **options) ⇒ void
This method should not be run within a transaction
For PostgreSQL 11+ you can use ‘add_column` instead
This method returns an undefined value.
Adds a column with a default value without durable locks of the entire table
This method runs the following steps:
-
Add the column allowing NULLs
-
Change the default value of the column to the specified value
-
Backfill all existing rows in batches
-
Set a ‘NOT NULL` constraint on the column if desired (the default).
These steps ensure a column can be added to a large and commonly used table without locking the entire table for the duration of the table modification.
For large tables (10/100s of millions of records) you may consider implementing
the steps from this helper method yourself as a separate migrations, replacing step #3
with the help of background migrations (see `backfill_column_in_background`).
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 |
# File 'lib/online_migrations/schema_statements.rb', line 439 def add_column_with_default(table_name, column_name, type, **) default = .fetch(:default) if database_version >= 11_00_00 && !Utils.volatile_default?(self, type, default) add_column(table_name, column_name, type, **) else __ensure_not_in_transaction! = .extract!(:batch_size, :batch_column_name, :progress, :pause_ms) if column_exists?(table_name, column_name) Utils.say("Column was not created because it already exists (this may be due to an aborted migration " \ "or similar) table_name: #{table_name}, column_name: #{column_name}") else transaction do add_column(table_name, column_name, type, **, default: nil, null: true) change_column_default(table_name, column_name, default) end end update_column_in_batches(table_name, column_name, default, **) allow_null = .delete(:null) != false if !allow_null # A `NOT NULL` constraint for the column is functionally equivalent # to creating a CHECK constraint `CHECK (column IS NOT NULL)` for the table add_not_null_constraint(table_name, column_name, validate: false) validate_not_null_constraint(table_name, column_name) if database_version >= 12_00_00 # In PostgreSQL 12+ it is safe to "promote" a CHECK constraint to `NOT NULL` for the column change_column_null(table_name, column_name, false) remove_not_null_constraint(table_name, column_name) end end end end |
#add_exclusion_constraint(table_name, expression, **options) ⇒ Object
892 893 894 895 896 897 898 899 900 901 |
# File 'lib/online_migrations/schema_statements.rb', line 892 def add_exclusion_constraint(table_name, expression, **) if __exclusion_constraint_exists?(table_name, expression: expression, **) Utils.say(<<~MSG.squish) Exclusion constraint was not created because it already exists (this may be due to an aborted migration or similar). table_name: #{table_name}, expression: #{expression} MSG else super end end |
#add_foreign_key(from_table, to_table, **options) ⇒ Object
Extends default method to be idempotent.
805 806 807 808 809 810 811 812 813 814 815 816 |
# File 'lib/online_migrations/schema_statements.rb', line 805 def add_foreign_key(from_table, to_table, **) # Do not consider validation for idempotency. if foreign_key_exists?(from_table, to_table, **.except(:validate)) = +"Foreign key was not created because it already exists " \ "(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}" << ", #{.inspect}" if .any? Utils.say() else super end end |
#add_index(table_name, column_name, **options) ⇒ Object
Extends default method to be idempotent and automatically recreate invalid indexes.
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 |
# File 'lib/online_migrations/schema_statements.rb', line 696 def add_index(table_name, column_name, **) __ensure_not_in_transaction! if [:algorithm] == :concurrently index = if column_name.is_a?(String) && column_name.match?(/\W/) # Use only name to check if index exists, because it does not work for complex expressions. index_name = ([:name] || index_name(table_name, column_name)).to_s indexes(table_name).find { |i| i.name == index_name } else # Rewrite this with `IndexDefinition#defined_for?` when Active Record >= 7.1 is supported. # See https://github.com/rails/rails/pull/45160. indexes(table_name).find { |i| __index_defined_for?(i, column_name, **) } end if index schema = __schema_for_table(table_name) if __index_valid?(index.name, schema: schema) Utils.say("Index was not created because it already exists.") return else Utils.say("Recreating invalid index: table_name: #{table_name}, column_name: #{column_name}") remove_index(table_name, column_name, **) end end if OnlineMigrations.config.statement_timeout # "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock. # It only conflicts with constraint validations, creating/removing indexes, # and some other "ALTER TABLE"s. super else OnlineMigrations.deprecator.warn(<<~MSG) Running `add_index` without a statement timeout is deprecated. Configure an explicit statement timeout in the initializer file via `config.statement_timeout` or the default database statement timeout will be used. Example, `config.statement_timeout = 1.hour`. MSG disable_statement_timeout do super end end end |
#add_not_null_constraint(table_name, column_name, name: nil, validate: true) ⇒ void
This method returns an undefined value.
Adds a NOT NULL constraint to the column
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 |
# File 'lib/online_migrations/schema_statements.rb', line 490 def add_not_null_constraint(table_name, column_name, name: nil, validate: true) column = column_for(table_name, column_name) if column.null == false || __not_null_constraint_exists?(table_name, column_name, name: name) Utils.say("NOT NULL constraint was not created: column #{table_name}.#{column_name} is already defined as `NOT NULL`") else expression = "#{quote_column_name(column_name)} IS NOT NULL" name ||= __not_null_constraint_name(table_name, column_name) add_check_constraint(table_name, expression, name: name, validate: false) if validate validate_not_null_constraint(table_name, column_name, name: name) end end end |
#add_reference_concurrently(table_name, ref_name, **options) ⇒ void
This method should not be run within a transaction
This method returns an undefined value.
Adds a reference to the table with minimal locking
Active Record adds an index non-‘CONCURRENTLY` to references by default, which blocks writes. It also adds a validated foreign key by default, which blocks writes on both tables while validating existing rows.
This method makes sure that an index is added ‘CONCURRENTLY` and the foreign key creation is performed in 2 steps: addition of invalid foreign key and a separate validation.
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 |
# File 'lib/online_migrations/schema_statements.rb', line 646 def add_reference_concurrently(table_name, ref_name, **) __ensure_not_in_transaction! column_name = "#{ref_name}_id" type_column_name = "#{ref_name}_type" if !column_exists?(table_name, column_name) type = [:type] || :bigint allow_null = .fetch(:null, true) add_column(table_name, column_name, type, null: allow_null) end if [:polymorphic] && !column_exists?(table_name, type_column_name) allow_null = [:polymorphic].is_a?(Hash) ? [:polymorphic][:null] : true add_column(table_name, type_column_name, :string, null: allow_null) end # Always added by default in 5.0+ index = .fetch(:index, true) if index index = {} if index == true index_columns = [column_name] if [:polymorphic] index_columns.unshift(type_column_name) index[:name] ||= "index_#{table_name}_on_#{ref_name}" end add_index(table_name, index_columns, **index, algorithm: :concurrently) end foreign_key = [:foreign_key] if foreign_key foreign_key = {} if foreign_key == true foreign_table_name = Utils.foreign_table_name(ref_name, foreign_key) add_foreign_key(table_name, foreign_table_name, **foreign_key, column: column_name, validate: false) if foreign_key[:validate] != false validate_foreign_key(table_name, foreign_table_name, **foreign_key) end end end |
#add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true) ⇒ void
This helper must be used only with text columns
This method returns an undefined value.
Adds a limit constraint to the text column
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 |
# File 'lib/online_migrations/schema_statements.rb', line 561 def add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true) column = column_for(table_name, column_name) if column.type != :text raise "add_text_limit_constraint must be used only with :text columns" end name ||= __text_limit_constraint_name(table_name, column_name) if __text_limit_constraint_exists?(table_name, column_name, name: name) Utils.say("Text limit constraint was not created: #{table_name}.#{column_name} is already has a limit") else add_check_constraint( table_name, "char_length(#{column_name}) <= #{limit}", name: name, validate: false ) if validate validate_text_limit_constraint(table_name, column_name, name: name) end end end |
#disable_statement_timeout ⇒ Object
929 930 931 932 933 934 935 936 937 938 939 940 941 |
# File 'lib/online_migrations/schema_statements.rb', line 929 def disable_statement_timeout OnlineMigrations.deprecator.warn(<<~MSG) `disable_statement_timeout` is deprecated and will be removed. Configure an explicit statement timeout in the initializer file via `config.statement_timeout` or the default database statement timeout will be used. Example, `config.statement_timeout = 1.hour`. MSG prev_value = select_value("SHOW statement_timeout") __set_statement_timeout(0) yield ensure __set_statement_timeout(prev_value) end |
#finalize_column_rename(table_name, column_name, new_column_name) ⇒ void
This method returns an undefined value.
Finishes the process of column rename
231 232 233 |
# File 'lib/online_migrations/schema_statements.rb', line 231 def finalize_column_rename(table_name, column_name, new_column_name) finalize_columns_rename(table_name, { column_name => new_column_name }) end |
#finalize_columns_rename(table_name, old_new_column_hash) ⇒ void
This method returns an undefined value.
Same as ‘finalize_column_rename` but for multiple columns.
243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/online_migrations/schema_statements.rb', line 243 def finalize_columns_rename(table_name, old_new_column_hash) transaction do execute("DROP VIEW #{quote_table_name(table_name)}") tmp_table = __tmp_table_name_for_column_rename(table_name) rename_table(tmp_table, table_name) old_new_column_hash.each do |column_name, new_column_name| rename_column(table_name, column_name, new_column_name) end end end |
#finalize_table_rename(table_name, _new_name = nil) ⇒ void
This method returns an undefined value.
Finishes the process of table rename
360 361 362 |
# File 'lib/online_migrations/schema_statements.rb', line 360 def finalize_table_rename(table_name, _new_name = nil) execute("DROP VIEW IF EXISTS #{quote_table_name(table_name)}") end |
#index_name(table_name, options) ⇒ Object
From ActiveRecord. Will not be needed for ActiveRecord >= 7.1.
787 788 789 790 791 792 793 794 795 796 797 798 799 |
# File 'lib/online_migrations/schema_statements.rb', line 787 def index_name(table_name, ) if .is_a?(Hash) if [:column] Utils.index_name(table_name, [:column]) elsif [:name] [:name] else raise ArgumentError, "You must specify the index name" end else index_name(table_name, column: ) end end |
#initialize_column_rename(table_name, column_name, new_column_name) ⇒ void
Prior to using this method, you need to register the database table so that it instructs Active Record to fetch the database table information (for SchemaCache) using the original table name (if it’s present). Otherwise, fall back to the old table name:
“‘OnlineMigrations.config.column_renames = { old_column_name => new_column_name }“`
Deploy this change before proceeding with this helper. This is necessary to avoid errors during a zero-downtime deployment.
None of the DDL operations involving original table name can be performed until ‘finalize_column_rename` is run
This method returns an undefined value.
Renames a column without requiring downtime
The technique is built on top of database views, using the following steps:
1. Rename the table to some temporary name
2. Create a VIEW using the old table name with addition of a new column as an alias of the old one
3. Add a workaround for Active Record's schema cache
For example, to rename ‘name` column to `first_name` of the `users` table, we can run:
BEGIN;
ALTER TABLE users RENAME TO users_column_rename;
CREATE VIEW users AS SELECT *, first_name AS name FROM users;
COMMIT;
As database views do not expose the underlying table schema (default values, not null constraints, indexes, etc), further steps are needed to update the application to use the new table name. Active Record heavily relies on this data, for example, to initialize new models.
To work around this limitation, we need to tell Active Record to acquire this information from original table using the new table name (see notes).
163 164 165 |
# File 'lib/online_migrations/schema_statements.rb', line 163 def initialize_column_rename(table_name, column_name, new_column_name) initialize_columns_rename(table_name, { column_name => new_column_name }) end |
#initialize_columns_rename(table_name, old_new_column_hash) ⇒ Object
Same as ‘initialize_column_rename` but for multiple columns.
This is useful to avoid multiple iterations of the safe column rename steps when renaming multiple columns.
180 181 182 183 184 |
# File 'lib/online_migrations/schema_statements.rb', line 180 def initialize_columns_rename(table_name, old_new_column_hash) transaction do __rename_table_and_create_view(table_name, old_new_column_hash) end end |
#initialize_table_rename(table_name, new_name) ⇒ void
Prior to using this method, you need to register the database table so that it instructs Active Record to fetch the database table information (for SchemaCache) using the new table name (if it’s present). Otherwise, fall back to the old table name:
“‘
OnlineMigrations.config.table_renames[old_table_name] = new_table_name
“‘
Deploy this change before proceeding with this helper. This is necessary to avoid errors during a zero-downtime deployment.
None of the DDL operations involving original table name can be performed until ‘finalize_table_rename` is run
This method returns an undefined value.
Renames a table without requiring downtime
The technique is built on top of database views, using the following steps:
1. Rename the database table
2. Create a database view using the old table name by pointing to the new table name
3. Add a workaround for Active Record's schema cache
For example, to rename ‘clients` table name to `users`, we can run:
BEGIN;
ALTER TABLE clients RENAME TO users;
CREATE VIEW clients AS SELECT * FROM users;
COMMIT;
As database views do not expose the underlying table schema (default values, not null constraints, indexes, etc), further steps are needed to update the application to use the new table name. Active Record heavily relies on this data, for example, to initialize new models.
To work around this limitation, we need to tell Active Record to acquire this information from original table using the new table name (see notes).
328 329 330 331 332 333 |
# File 'lib/online_migrations/schema_statements.rb', line 328 def initialize_table_rename(table_name, new_name) transaction do rename_table(table_name, new_name) execute("CREATE VIEW #{quote_table_name(table_name)} AS SELECT * FROM #{quote_table_name(new_name)}") end end |
#pk_and_sequence_for(table) ⇒ Object
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 |
# File 'lib/online_migrations/schema_statements.rb', line 905 def pk_and_sequence_for(table) views = self.views table_renames = OnlineMigrations.config.table_renames renamed_tables = table_renames.select do |old_name, _| views.include?(old_name) end column_renames = OnlineMigrations.config.column_renames renamed_columns = column_renames.select do |table_name, _| views.include?(table_name) end if renamed_tables.key?(table) super(renamed_tables[table]) elsif renamed_columns.key?(table) tmp_table = __tmp_table_name_for_column_rename(table) super(tmp_table) else super end end |
#remove_index(table_name, column_name = nil, **options) ⇒ Object
Extends default method to be idempotent.
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 |
# File 'lib/online_migrations/schema_statements.rb', line 745 def remove_index(table_name, column_name = nil, **) if column_name.blank? && [:column].blank? && [:name].blank? raise ArgumentError, "No name or columns specified" end __ensure_not_in_transaction! if [:algorithm] == :concurrently column = column_name || [:column] index_exists = if column.is_a?(String) && column.match?(/\W/) # Use only name to check if index exists, because it does not work for complex expressions. index_name = [:name] || index_name(table_name, column) index_name_exists?(table_name, index_name) else index_exists?(table_name, column_name, **) end if index_exists if OnlineMigrations.config.statement_timeout # "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock. # It only conflicts with constraint validations, other creating/removing indexes, # and some "ALTER TABLE"s. super else OnlineMigrations.deprecator.warn(<<~MSG) Running `remove_index` without a statement timeout is deprecated. Configure an explicit statement timeout in the initializer file via `config.statement_timeout` or the default database statement timeout will be used. Example, `config.statement_timeout = 1.hour`. MSG disable_statement_timeout do super end end else Utils.say("Index was not removed because it does not exist.") end end |
#remove_not_null_constraint(table_name, column_name, name: nil) ⇒ void
This method returns an undefined value.
Removes a NOT NULL constraint from the column
541 542 543 544 |
# File 'lib/online_migrations/schema_statements.rb', line 541 def remove_not_null_constraint(table_name, column_name, name: nil) name ||= __not_null_constraint_name(table_name, column_name) remove_check_constraint(table_name, name: name) end |
#remove_text_limit_constraint(table_name, column_name, _limit = nil, name: nil) ⇒ void
This method returns an undefined value.
Removes a limit constraint from the text column
620 621 622 623 |
# File 'lib/online_migrations/schema_statements.rb', line 620 def remove_text_limit_constraint(table_name, column_name, _limit = nil, name: nil) name ||= __text_limit_constraint_name(table_name, column_name) remove_check_constraint(table_name, name: name) end |
#revert_finalize_column_rename(table_name, column_name, new_column_name) ⇒ void
This method returns an undefined value.
Reverts operations performed by finalize_column_rename
263 264 265 |
# File 'lib/online_migrations/schema_statements.rb', line 263 def revert_finalize_column_rename(table_name, column_name, new_column_name) revert_finalize_columns_rename(table_name, { column_name => new_column_name }) end |
#revert_finalize_columns_rename(table_name, old_new_column_hash) ⇒ void
This method returns an undefined value.
Same as ‘revert_finalize_column_rename` but for multiple columns.
275 276 277 278 279 280 281 282 |
# File 'lib/online_migrations/schema_statements.rb', line 275 def revert_finalize_columns_rename(table_name, old_new_column_hash) transaction do old_new_column_hash.each do |column_name, new_column_name| rename_column(table_name, new_column_name, column_name) end __rename_table_and_create_view(table_name, old_new_column_hash) end end |
#revert_finalize_table_rename(table_name, new_name) ⇒ void
This method returns an undefined value.
Reverts operations performed by finalize_table_rename
373 374 375 |
# File 'lib/online_migrations/schema_statements.rb', line 373 def revert_finalize_table_rename(table_name, new_name) execute("CREATE VIEW #{quote_table_name(table_name)} AS SELECT * FROM #{quote_table_name(new_name)}") end |
#revert_initialize_column_rename(table_name, column_name = nil, new_column_name = nil) ⇒ void
This method returns an undefined value.
Reverts operations performed by initialize_column_rename
199 200 201 |
# File 'lib/online_migrations/schema_statements.rb', line 199 def revert_initialize_column_rename(table_name, column_name = nil, new_column_name = nil) revert_initialize_columns_rename(table_name, { column_name => new_column_name }) end |
#revert_initialize_columns_rename(table_name, _old_new_column_hash = nil) ⇒ void
This method returns an undefined value.
Same as ‘revert_initialize_column_rename` but for multiple columns.
214 215 216 217 218 219 220 221 |
# File 'lib/online_migrations/schema_statements.rb', line 214 def revert_initialize_columns_rename(table_name, _old_new_column_hash = nil) transaction do execute("DROP VIEW #{quote_table_name(table_name)}") tmp_table = __tmp_table_name_for_column_rename(table_name) rename_table(tmp_table, table_name) end end |
#revert_initialize_table_rename(table_name, new_name) ⇒ void
This method returns an undefined value.
Reverts operations performed by initialize_table_rename
343 344 345 346 347 348 |
# File 'lib/online_migrations/schema_statements.rb', line 343 def revert_initialize_table_rename(table_name, new_name) transaction do execute("DROP VIEW IF EXISTS #{quote_table_name(table_name)}") rename_table(new_name, table_name) end end |
#swap_column_names(table_name, column1, column2) ⇒ void
This method returns an undefined value.
Swaps two column names in a table
This method is mostly intended for use as one of the steps for concurrent column type change
390 391 392 393 394 395 396 |
# File 'lib/online_migrations/schema_statements.rb', line 390 def swap_column_names(table_name, column1, column2) transaction do rename_column(table_name, column1, "#{column1}_tmp") rename_column(table_name, column2, column1) rename_column(table_name, "#{column1}_tmp", column2) end end |
#update_column_in_batches(table_name, column_name, value, **options) {|relation| ... } ⇒ void
This method should not be run within a transaction
Consider ‘update_columns_in_batches` when updating multiple columns to avoid rewriting the table multiple times.
For large tables (10/100s of millions of records) you may consider using ‘backfill_column_in_background` or `copy_column_in_background`.
This method returns an undefined value.
Updates the value of a column in batches.
55 56 57 |
# File 'lib/online_migrations/schema_statements.rb', line 55 def update_column_in_batches(table_name, column_name, value, **, &block) update_columns_in_batches(table_name, [[column_name, value]], **, &block) end |
#update_columns_in_batches(table_name, columns_and_values, batch_size: 1000, batch_column_name: primary_key(table_name), progress: false, pause_ms: 50) ⇒ Object
Same as ‘update_column_in_batches`, but for multiple columns.
This is useful to avoid multiple costly disk rewrites of large tables when updating each column separately.
columns_and_values is an array of arrays (first item is a column name, second - new value)
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 |
# File 'lib/online_migrations/schema_statements.rb', line 69 def update_columns_in_batches(table_name, columns_and_values, batch_size: 1000, batch_column_name: primary_key(table_name), progress: false, pause_ms: 50) __ensure_not_in_transaction! if !columns_and_values.is_a?(Array) || !columns_and_values.all?(Array) raise ArgumentError, "columns_and_values must be an array of arrays" end if progress if progress == true progress = ->(_) { print(".") } elsif !progress.respond_to?(:call) raise ArgumentError, "The progress body needs to be a callable." end end model = Utils.define_model(table_name) conditions = columns_and_values.filter_map do |(column_name, value)| value = Arel.sql(value.call.to_s) if value.is_a?(Proc) # Ignore subqueries in conditions if !value.is_a?(Arel::Nodes::SqlLiteral) || !value.to_s.match?(/select\s+/i) arel_column = model.arel_table[column_name] if value.nil? arel_column.not_eq(nil) else arel_column.not_eq(value).or(arel_column.eq(nil)) end end end batch_relation = model.where(conditions.inject(:or)) batch_relation = yield batch_relation if block_given? iterator = BatchIterator.new(batch_relation) iterator.each_batch(of: batch_size, column: batch_column_name) do |relation| updates = columns_and_values.to_h do |(column, value)| value = Arel.sql(value.call.to_s) if value.is_a?(Proc) [column, value] end relation.update_all(updates) progress.call(relation) if progress sleep(pause_ms * 0.001) if pause_ms > 0 end end |
#validate_check_constraint(table_name, **options) ⇒ Object
Extends default method with disabled statement timeout while validation is run
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 |
# File 'lib/online_migrations/schema_statements.rb', line 866 def validate_check_constraint(table_name, **) check_constraint = check_constraint_for!(table_name, **) # Skip costly operation if already validated. return if check_constraint.validated? if OnlineMigrations.config.statement_timeout # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock. # It only conflicts with other validations, creating/removing indexes, # and some other "ALTER TABLE"s. super else OnlineMigrations.deprecator.warn(<<~MSG) Running `validate_check_constraint` without a statement timeout is deprecated. Configure an explicit statement timeout in the initializer file via `config.statement_timeout` or the default database statement timeout will be used. Example, `config.statement_timeout = 1.hour`. MSG disable_statement_timeout do super end end end |
#validate_foreign_key(from_table, to_table = nil, **options) ⇒ Object
Extends default method with disabled statement timeout while validation is run
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 |
# File 'lib/online_migrations/schema_statements.rb', line 822 def validate_foreign_key(from_table, to_table = nil, **) foreign_key = foreign_key_for!(from_table, to_table: to_table, **) # Skip costly operation if already validated. return if foreign_key.validated? if OnlineMigrations.config.statement_timeout # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock. # It only conflicts with other validations, creating/removing indexes, # and some other "ALTER TABLE"s. super else OnlineMigrations.deprecator.warn(<<~MSG) Running `validate_foreign_key` without a statement timeout is deprecated. Configure an explicit statement timeout in the initializer file via `config.statement_timeout` or the default database statement timeout will be used. Example, `config.statement_timeout = 1.hour`. MSG disable_statement_timeout do super end end end |
#validate_not_null_constraint(table_name, column_name, name: nil) ⇒ void
This method returns an undefined value.
Validates a NOT NULL constraint on the column
521 522 523 524 |
# File 'lib/online_migrations/schema_statements.rb', line 521 def validate_not_null_constraint(table_name, column_name, name: nil) name ||= __not_null_constraint_name(table_name, column_name) validate_check_constraint(table_name, name: name) end |
#validate_text_limit_constraint(table_name, column_name, name: nil) ⇒ void
This method returns an undefined value.
Validates a limit constraint on the text column
600 601 602 603 |
# File 'lib/online_migrations/schema_statements.rb', line 600 def validate_text_limit_constraint(table_name, column_name, name: nil) name ||= __text_limit_constraint_name(table_name, column_name) validate_check_constraint(table_name, name: name) end |
#with_lock_retries(&block) ⇒ Object
Executes the block with a retry mechanism that alters the ‘lock_timeout` and sleep time between attempts.
954 955 956 957 958 959 960 |
# File 'lib/online_migrations/schema_statements.rb', line 954 def with_lock_retries(&block) __ensure_not_in_transaction! retrier = OnlineMigrations.config.lock_retrier retrier.connection = self retrier.with_lock_retries(&block) end |