169 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			169 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
module Gitlab
 | 
						|
  module Database
 | 
						|
    module MigrationHelpers
 | 
						|
      # Creates a new index, concurrently when supported
 | 
						|
      #
 | 
						|
      # On PostgreSQL this method creates an index concurrently, on MySQL this
 | 
						|
      # creates a regular index.
 | 
						|
      #
 | 
						|
      # Example:
 | 
						|
      #
 | 
						|
      #     add_concurrent_index :users, :some_column
 | 
						|
      #
 | 
						|
      # See Rails' `add_index` for more info on the available arguments.
 | 
						|
      def add_concurrent_index(table_name, column_name, options = {})
 | 
						|
        if transaction_open?
 | 
						|
          raise 'add_concurrent_index can not be run inside a transaction, ' \
 | 
						|
            'you can disable transactions by calling disable_ddl_transaction! ' \
 | 
						|
            'in the body of your migration class'
 | 
						|
        end
 | 
						|
 | 
						|
        if Database.postgresql?
 | 
						|
          options = options.merge({ algorithm: :concurrently })
 | 
						|
          disable_statement_timeout
 | 
						|
        end
 | 
						|
 | 
						|
        add_index(table_name, column_name, options)
 | 
						|
      end
 | 
						|
 | 
						|
      # Long-running migrations may take more than the timeout allowed by
 | 
						|
      # the database. Disable the session's statement timeout to ensure
 | 
						|
      # migrations don't get killed prematurely. (PostgreSQL only)
 | 
						|
      def disable_statement_timeout
 | 
						|
        ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql?
 | 
						|
      end
 | 
						|
 | 
						|
      # Updates the value of a column in batches.
 | 
						|
      #
 | 
						|
      # This method updates the table in batches of 5% of the total row count.
 | 
						|
      # This method will continue updating rows until no rows remain.
 | 
						|
      #
 | 
						|
      # When given a block this method will yield two values to the block:
 | 
						|
      #
 | 
						|
      # 1. An instance of `Arel::Table` for the table that is being updated.
 | 
						|
      # 2. The query to run as an Arel object.
 | 
						|
      #
 | 
						|
      # By supplying a block one can add extra conditions to the queries being
 | 
						|
      # executed. Note that the same block is used for _all_ queries.
 | 
						|
      #
 | 
						|
      # Example:
 | 
						|
      #
 | 
						|
      #     update_column_in_batches(:projects, :foo, 10) do |table, query|
 | 
						|
      #       query.where(table[:some_column].eq('hello'))
 | 
						|
      #     end
 | 
						|
      #
 | 
						|
      # This would result in this method updating only rows where
 | 
						|
      # `projects.some_column` equals "hello".
 | 
						|
      #
 | 
						|
      # table - The name of the table.
 | 
						|
      # column - The name of the column to update.
 | 
						|
      # value - The value for the column.
 | 
						|
      #
 | 
						|
      # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop
 | 
						|
      # determines this method to be too complex while there's no way to make it
 | 
						|
      # less "complex" without introducing extra methods (which actually will
 | 
						|
      # make things _more_ complex).
 | 
						|
      #
 | 
						|
      # rubocop: disable Metrics/AbcSize
 | 
						|
      def update_column_in_batches(table, column, value)
 | 
						|
        table = Arel::Table.new(table)
 | 
						|
 | 
						|
        count_arel = table.project(Arel.star.count.as('count'))
 | 
						|
        count_arel = yield table, count_arel if block_given?
 | 
						|
 | 
						|
        total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i
 | 
						|
 | 
						|
        return if total == 0
 | 
						|
 | 
						|
        # Update in batches of 5% until we run out of any rows to update.
 | 
						|
        batch_size = ((total / 100.0) * 5.0).ceil
 | 
						|
 | 
						|
        start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
 | 
						|
        start_arel = yield table, start_arel if block_given?
 | 
						|
        start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i
 | 
						|
 | 
						|
        loop do
 | 
						|
          stop_arel = table.project(table[:id]).
 | 
						|
            where(table[:id].gteq(start_id)).
 | 
						|
            order(table[:id].asc).
 | 
						|
            take(1).
 | 
						|
            skip(batch_size)
 | 
						|
 | 
						|
          stop_arel = yield table, stop_arel if block_given?
 | 
						|
          stop_row = exec_query(stop_arel.to_sql).to_hash.first
 | 
						|
 | 
						|
          update_arel = Arel::UpdateManager.new(ActiveRecord::Base).
 | 
						|
            table(table).
 | 
						|
            set([[table[column], value]]).
 | 
						|
            where(table[:id].gteq(start_id))
 | 
						|
 | 
						|
          if stop_row
 | 
						|
            stop_id = stop_row['id'].to_i
 | 
						|
            start_id = stop_id
 | 
						|
            update_arel = update_arel.where(table[:id].lt(stop_id))
 | 
						|
          end
 | 
						|
 | 
						|
          update_arel = yield table, update_arel if block_given?
 | 
						|
 | 
						|
          execute(update_arel.to_sql)
 | 
						|
 | 
						|
          # There are no more rows left to update.
 | 
						|
          break unless stop_row
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Adds a column with a default value without locking an entire table.
 | 
						|
      #
 | 
						|
      # This method runs the following steps:
 | 
						|
      #
 | 
						|
      # 1. Add the column with a default value of NULL.
 | 
						|
      # 2. Change the default value of the column to the specified value.
 | 
						|
      # 3. Update all existing rows in batches.
 | 
						|
      # 4. 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.
 | 
						|
      #
 | 
						|
      # table - The name of the table to update.
 | 
						|
      # column - The name of the column to add.
 | 
						|
      # type - The column type (e.g. `:integer`).
 | 
						|
      # default - The default value for the column.
 | 
						|
      # allow_null - When set to `true` the column will allow NULL values, the
 | 
						|
      #              default is to not allow NULL values.
 | 
						|
      #
 | 
						|
      # This method can also take a block which is passed directly to the
 | 
						|
      # `update_column_in_batches` method.
 | 
						|
      def add_column_with_default(table, column, type, default:, allow_null: false, &block)
 | 
						|
        if transaction_open?
 | 
						|
          raise 'add_column_with_default can not be run inside a transaction, ' \
 | 
						|
            'you can disable transactions by calling disable_ddl_transaction! ' \
 | 
						|
            'in the body of your migration class'
 | 
						|
        end
 | 
						|
 | 
						|
        disable_statement_timeout
 | 
						|
 | 
						|
        transaction do
 | 
						|
          add_column(table, column, type, default: nil)
 | 
						|
 | 
						|
          # Changing the default before the update ensures any newly inserted
 | 
						|
          # rows already use the proper default value.
 | 
						|
          change_column_default(table, column, default)
 | 
						|
        end
 | 
						|
 | 
						|
        begin
 | 
						|
          update_column_in_batches(table, column, default, &block)
 | 
						|
 | 
						|
          change_column_null(table, column, false) unless allow_null
 | 
						|
        # We want to rescue _all_ exceptions here, even those that don't inherit
 | 
						|
        # from StandardError.
 | 
						|
        rescue Exception => error # rubocop: disable all
 | 
						|
          remove_column(table, column)
 | 
						|
 | 
						|
          raise error
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |