migration_helpers_spec.rb 9.08 KB
Newer Older
1 2 3 4
require 'spec_helper'

describe Gitlab::Database::MigrationHelpers, lib: true do
  let(:model) do
5 6 7
    ActiveRecord::Migration.new.extend(
      Gitlab::Database::MigrationHelpers
    )
8 9
  end

10 11
  before { allow(model).to receive(:puts) }

12 13 14
  describe '#add_concurrent_index' do
    context 'outside a transaction' do
      before do
15
        allow(model).to receive(:transaction_open?).and_return(false)
16 17 18
      end

      context 'using PostgreSQL' do
19 20 21 22
        before do
          allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
          allow(model).to receive(:disable_statement_timeout)
        end
23

24
        it 'creates the index concurrently' do
25 26 27 28 29
          expect(model).to receive(:add_index).
            with(:users, :foo, algorithm: :concurrently)

          model.add_concurrent_index(:users, :foo)
        end
30 31 32 33 34 35 36

        it 'creates unique index concurrently' do
          expect(model).to receive(:add_index).
            with(:users, :foo, { algorithm: :concurrently, unique: true })

          model.add_concurrent_index(:users, :foo, unique: true)
        end
37 38 39 40 41 42 43
      end

      context 'using MySQL' do
        it 'creates a regular index' do
          expect(Gitlab::Database).to receive(:postgresql?).and_return(false)

          expect(model).to receive(:add_index).
44
            with(:users, :foo, {})
45 46 47 48 49 50 51 52 53 54 55 56 57 58

          model.add_concurrent_index(:users, :foo)
        end
      end
    end

    context 'inside a transaction' do
      it 'raises RuntimeError' do
        expect(model).to receive(:transaction_open?).and_return(true)

        expect { model.add_concurrent_index(:users, :foo) }.
          to raise_error(RuntimeError)
      end
    end
59 60
  end

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
  describe '#remove_concurrent_index' do
    context 'outside a transaction' do
      before do
        allow(model).to receive(:transaction_open?).and_return(false)
      end

      context 'using PostgreSQL' do
        before do
          allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
          allow(model).to receive(:disable_statement_timeout)
        end

        it 'removes the index concurrently' do
          expect(model).to receive(:remove_index).
            with(:users, { algorithm: :concurrently, column: :foo })

          model.remove_concurrent_index(:users, :foo)
        end
      end

      context 'using MySQL' do
        it 'removes an index' do
          expect(Gitlab::Database).to receive(:postgresql?).and_return(false)

          expect(model).to receive(:remove_index).
            with(:users, { column: :foo })

          model.remove_concurrent_index(:users, :foo)
        end
      end
    end

    context 'inside a transaction' do
      it 'raises RuntimeError' do
        expect(model).to receive(:transaction_open?).and_return(true)

        expect { model.remove_concurrent_index(:users, :foo) }.
          to raise_error(RuntimeError)
      end
    end
  end

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
  describe '#add_concurrent_foreign_key' do
    context 'inside a transaction' do
      it 'raises an error' do
        expect(model).to receive(:transaction_open?).and_return(true)

        expect do
          model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
        end.to raise_error(RuntimeError)
      end
    end

    context 'outside a transaction' do
      before do
        allow(model).to receive(:transaction_open?).and_return(false)
      end

      context 'using MySQL' do
        it 'creates a regular foreign key' do
          allow(Gitlab::Database).to receive(:mysql?).and_return(true)

          expect(model).to receive(:add_foreign_key).
            with(:projects, :users, column: :user_id, on_delete: :cascade)

          model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
        end
      end

      context 'using PostgreSQL' do
        before do
          allow(Gitlab::Database).to receive(:mysql?).and_return(false)
        end

        it 'creates a concurrent foreign key' do
          expect(model).to receive(:disable_statement_timeout)
          expect(model).to receive(:execute).ordered.with(/NOT VALID/)
          expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)

          model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
        end
      end
    end
  end

146 147 148 149 150 151 152 153 154 155
  describe '#concurrent_foreign_key_name' do
    it 'returns the name for a foreign key' do
      name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name,
                                               :with_a_very_long_column_name)

      expect(name).to be_an_instance_of(String)
      expect(name.length).to eq(13)
    end
  end

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
  describe '#disable_statement_timeout' do
    context 'using PostgreSQL' do
      it 'disables statement timeouts' do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(true)

        expect(model).to receive(:execute).with('SET statement_timeout TO 0')

        model.disable_statement_timeout
      end
    end

    context 'using MySQL' do
      it 'does nothing' do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(false)

        expect(model).not_to receive(:execute)

        model.disable_statement_timeout
      end
    end
176 177 178 179 180 181 182 183 184 185 186 187
  end

  describe '#update_column_in_batches' do
    before do
      create_list(:empty_project, 5)
    end

    it 'updates all the rows in a table' do
      model.update_column_in_batches(:projects, :import_error, 'foo')

      expect(Project.where(import_error: 'foo').count).to eq(5)
    end
188 189 190 191 192 193

    it 'updates boolean values correctly' do
      model.update_column_in_batches(:projects, :archived, true)

      expect(Project.where(archived: true).count).to eq(5)
    end
194 195 196 197 198 199 200 201 202 203 204 205

    context 'when a block is supplied' do
      it 'yields an Arel table and query object to the supplied block' do
        first_id = Project.first.id

        model.update_column_in_batches(:projects, :archived, true) do |t, query|
          query.where(t[:id].eq(first_id))
        end

        expect(Project.where(archived: true).count).to eq(1)
      end
    end
206 207 208 209
  end

  describe '#add_column_with_default' do
    context 'outside of a transaction' do
210 211 212
      context 'when a column limit is not set' do
        before do
          expect(model).to receive(:transaction_open?).and_return(false)
213

214
          expect(model).to receive(:transaction).and_yield
215

216 217
          expect(model).to receive(:add_column).
            with(:projects, :foo, :integer, default: nil)
218

219 220 221
          expect(model).to receive(:change_column_default).
            with(:projects, :foo, 10)
        end
222

223 224 225
        it 'adds the column while allowing NULL values' do
          expect(model).to receive(:update_column_in_batches).
            with(:projects, :foo, 10)
226

227
          expect(model).not_to receive(:change_column_null)
228

229 230 231 232
          model.add_column_with_default(:projects, :foo, :integer,
                                        default: 10,
                                        allow_null: true)
        end
233

234 235 236
        it 'adds the column while not allowing NULL values' do
          expect(model).to receive(:update_column_in_batches).
            with(:projects, :foo, 10)
237

238 239
          expect(model).to receive(:change_column_null).
            with(:projects, :foo, false)
240

241 242
          model.add_column_with_default(:projects, :foo, :integer, default: 10)
        end
243

244 245 246 247
        it 'removes the added column whenever updating the rows fails' do
          expect(model).to receive(:update_column_in_batches).
            with(:projects, :foo, 10).
            and_raise(RuntimeError)
248

249 250
          expect(model).to receive(:remove_column).
            with(:projects, :foo)
251

252 253 254 255
          expect do
            model.add_column_with_default(:projects, :foo, :integer, default: 10)
          end.to raise_error(RuntimeError)
        end
256

257 258 259 260
        it 'removes the added column whenever changing a column NULL constraint fails' do
          expect(model).to receive(:change_column_null).
            with(:projects, :foo, false).
            and_raise(RuntimeError)
261

262 263
          expect(model).to receive(:remove_column).
            with(:projects, :foo)
264

265 266 267 268 269 270 271 272 273 274
          expect do
            model.add_column_with_default(:projects, :foo, :integer, default: 10)
          end.to raise_error(RuntimeError)
        end
      end

      context 'when a column limit is set' do
        it 'adds the column with a limit' do
          allow(model).to receive(:transaction_open?).and_return(false)
          allow(model).to receive(:transaction).and_yield
Drew Blessing's avatar
fix  
Drew Blessing committed
275 276 277
          allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10)
          allow(model).to receive(:change_column_null).with(:projects, :foo, false)
          allow(model).to receive(:change_column_default).with(:projects, :foo, 10)
278 279 280 281 282 283

          expect(model).to receive(:add_column).
            with(:projects, :foo, :integer, default: nil, limit: 8)

          model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
        end
284
      end
285 286 287 288 289 290
    end

    context 'inside a transaction' do
      it 'raises RuntimeError' do
        expect(model).to receive(:transaction_open?).and_return(true)

291
        expect do
292
          model.add_column_with_default(:projects, :foo, :integer, default: 10)
293
        end.to raise_error(RuntimeError)
294 295 296 297
      end
    end
  end
end