migration_helpers_spec.rb 26.8 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 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
  before do
    allow(model).to receive(:puts)
  end

  describe '#add_timestamps_with_timezone' 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 'adds "created_at" and "updated_at" fields with the "datetime_with_timezone" data type' do
        expect(model).to receive(:add_column).with(:foo, :created_at, :datetime_with_timezone, { null: false })
        expect(model).to receive(:add_column).with(:foo, :updated_at, :datetime_with_timezone, { null: false })

        model.add_timestamps_with_timezone(:foo)
      end
    end

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

      it 'adds "created_at" and "updated_at" fields with "datetime_with_timezone" data type' do
        expect(model).to receive(:add_column).with(:foo, :created_at, :datetime_with_timezone, { null: false })
        expect(model).to receive(:add_column).with(:foo, :updated_at, :datetime_with_timezone, { null: false })

        model.add_timestamps_with_timezone(:foo)
      end
    end
  end
46

47 48 49
  describe '#add_concurrent_index' do
    context 'outside a transaction' do
      before do
50
        allow(model).to receive(:transaction_open?).and_return(false)
51 52 53
      end

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

59
        it 'creates the index concurrently' do
60 61
          expect(model).to receive(:add_index)
            .with(:users, :foo, algorithm: :concurrently)
62 63 64

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

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

          model.add_concurrent_index(:users, :foo, unique: true)
        end
72 73 74 75 76 77
      end

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

78 79
          expect(model).to receive(:add_index)
            .with(:users, :foo, {})
80 81 82 83 84 85 86 87 88 89

          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)

90 91
        expect { model.add_concurrent_index(:users, :foo) }
          .to raise_error(RuntimeError)
92 93
      end
    end
94 95
  end

96 97 98 99 100 101 102 103
  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
104
          allow(model).to receive(:supports_drop_index_concurrently?).and_return(true)
105 106 107
          allow(model).to receive(:disable_statement_timeout)
        end

108
        it 'removes the index concurrently by column name' do
109 110
          expect(model).to receive(:remove_index)
            .with(:users, { algorithm: :concurrently, column: :foo })
111 112 113

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

        it 'removes the index concurrently by index name' do
116 117
          expect(model).to receive(:remove_index)
            .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
118 119 120

          model.remove_concurrent_index_by_name(:users, "index_x_by_y")
        end
121 122 123 124 125 126
      end

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

127 128
          expect(model).to receive(:remove_index)
            .with(:users, { column: :foo })
129 130 131 132 133 134 135 136 137 138

          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)

139 140
        expect { model.remove_concurrent_index(:users, :foo) }
          .to raise_error(RuntimeError)
141 142 143 144
      end
    end
  end

145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
  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)

165 166
          expect(model).to receive(:add_foreign_key)
            .with(:projects, :users, column: :user_id, on_delete: :cascade)
167 168 169 170 171 172 173 174 175 176

          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

177
        it 'creates a concurrent foreign key and validates it' do
178 179 180 181 182 183
          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
184 185 186 187 188 189 190 191 192 193

        it 'appends a valid ON DELETE statement' do
          expect(model).to receive(:disable_statement_timeout)
          expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
          expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)

          model.add_concurrent_foreign_key(:projects, :users,
                                           column: :user_id,
                                           on_delete: :nullify)
        end
194 195 196 197
      end
    end
  end

198 199 200 201 202 203 204 205 206 207
  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

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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
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
  end

  describe '#true_value' do
    context 'using PostgreSQL' do
      before do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
      end

      it 'returns the appropriate value' do
        expect(model.true_value).to eq("'t'")
      end
    end

    context 'using MySQL' do
      before do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
      end

      it 'returns the appropriate value' do
        expect(model.true_value).to eq(1)
      end
    end
  end

  describe '#false_value' do
    context 'using PostgreSQL' do
      before do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
      end

      it 'returns the appropriate value' do
        expect(model.false_value).to eq("'f'")
      end
    end

    context 'using MySQL' do
      before do
        expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
      end

      it 'returns the appropriate value' do
        expect(model.false_value).to eq(0)
      end
    end
272 273 274
  end

  describe '#update_column_in_batches' do
275 276 277
    context 'when running outside of a transaction' do
      before do
        expect(model).to receive(:transaction_open?).and_return(false)
278

279 280
        create_list(:empty_project, 5)
      end
281

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

285 286
        expect(Project.where(import_error: 'foo').count).to eq(5)
      end
287

288 289 290 291 292
      it 'updates boolean values correctly' do
        model.update_column_in_batches(:projects, :archived, true)

        expect(Project.where(archived: true).count).to eq(5)
      end
293

294 295 296
      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
297

298 299 300 301 302
          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)
303
        end
304
      end
305

306 307 308 309 310 311
      context 'when the value is Arel.sql (Arel::Nodes::SqlLiteral)' do
        it 'updates the value as a SQL expression' do
          model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1'))

          expect(Project.sum(:star_count)).to eq(2 * Project.count)
        end
312 313
      end
    end
314

315 316 317
    context 'when running inside the transaction' do
      it 'raises RuntimeError' do
        expect(model).to receive(:transaction_open?).and_return(true)
318

319 320 321
        expect do
          model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1'))
        end.to raise_error(RuntimeError)
322 323
      end
    end
324 325 326 327
  end

  describe '#add_column_with_default' do
    context 'outside of a transaction' do
328 329
      context 'when a column limit is not set' do
        before do
330 331 332
          expect(model).to receive(:transaction_open?)
            .and_return(false)
            .at_least(:once)
333

334
          expect(model).to receive(:transaction).and_yield
335

336 337
          expect(model).to receive(:add_column)
            .with(:projects, :foo, :integer, default: nil)
338

339 340
          expect(model).to receive(:change_column_default)
            .with(:projects, :foo, 10)
341
        end
342

343
        it 'adds the column while allowing NULL values' do
344 345
          expect(model).to receive(:update_column_in_batches)
            .with(:projects, :foo, 10)
346

347
          expect(model).not_to receive(:change_column_null)
348

349 350 351 352
          model.add_column_with_default(:projects, :foo, :integer,
                                        default: 10,
                                        allow_null: true)
        end
353

354
        it 'adds the column while not allowing NULL values' do
355 356
          expect(model).to receive(:update_column_in_batches)
            .with(:projects, :foo, 10)
357

358 359
          expect(model).to receive(:change_column_null)
            .with(:projects, :foo, false)
360

361 362
          model.add_column_with_default(:projects, :foo, :integer, default: 10)
        end
363

364
        it 'removes the added column whenever updating the rows fails' do
365 366 367
          expect(model).to receive(:update_column_in_batches)
            .with(:projects, :foo, 10)
            .and_raise(RuntimeError)
368

369 370
          expect(model).to receive(:remove_column)
            .with(:projects, :foo)
371

372 373 374 375
          expect do
            model.add_column_with_default(:projects, :foo, :integer, default: 10)
          end.to raise_error(RuntimeError)
        end
376

377
        it 'removes the added column whenever changing a column NULL constraint fails' do
378 379 380
          expect(model).to receive(:change_column_null)
            .with(:projects, :foo, false)
            .and_raise(RuntimeError)
381

382 383
          expect(model).to receive(:remove_column)
            .with(:projects, :foo)
384

385 386 387 388 389 390 391 392 393 394
          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
395 396 397
          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)
398

399 400
          expect(model).to receive(:add_column)
            .with(:projects, :foo, :integer, default: nil, limit: 8)
401 402 403

          model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
        end
404
      end
405 406 407 408 409 410
    end

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

411
        expect do
412
          model.add_column_with_default(:projects, :foo, :integer, default: 10)
413
        end.to raise_error(RuntimeError)
414 415 416
      end
    end
  end
417 418 419 420 421 422

  describe '#rename_column_concurrently' do
    context 'in a transaction' do
      it 'raises RuntimeError' do
        allow(model).to receive(:transaction_open?).and_return(true)

423 424
        expect { model.rename_column_concurrently(:users, :old, :new) }
          .to raise_error(RuntimeError)
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
      end
    end

    context 'outside a transaction' do
      let(:old_column) do
        double(:column,
               type: :integer,
               limit: 8,
               default: 0,
               null: false,
               precision: 5,
               scale: 1)
      end

      let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) }

      before do
        allow(model).to receive(:transaction_open?).and_return(false)
        allow(model).to receive(:column_for).and_return(old_column)

        # Since MySQL and PostgreSQL use different quoting styles we'll just
        # stub the methods used for this to make testing easier.
        allow(model).to receive(:quote_column_name) { |name| name.to_s }
        allow(model).to receive(:quote_table_name) { |name| name.to_s }
      end

      context 'using MySQL' do
        it 'renames a column concurrently' do
          allow(Gitlab::Database).to receive(:postgresql?).and_return(false)

455 456
          expect(model).to receive(:install_rename_triggers_for_mysql)
            .with(trigger_name, 'users', 'old', 'new')
457

458 459
          expect(model).to receive(:add_column)
            .with(:users, :new, :integer,
460 461 462 463
                 limit: old_column.limit,
                 precision: old_column.precision,
                 scale: old_column.scale)

464 465
          expect(model).to receive(:change_column_default)
            .with(:users, :new, old_column.default)
466

467 468
          expect(model).to receive(:update_column_in_batches)

469 470
          expect(model).to receive(:change_column_null).with(:users, :new, false)

471 472 473 474 475 476 477 478 479 480 481
          expect(model).to receive(:copy_indexes).with(:users, :old, :new)
          expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)

          model.rename_column_concurrently(:users, :old, :new)
        end
      end

      context 'using PostgreSQL' do
        it 'renames a column concurrently' do
          allow(Gitlab::Database).to receive(:postgresql?).and_return(true)

482 483
          expect(model).to receive(:install_rename_triggers_for_postgresql)
            .with(trigger_name, 'users', 'old', 'new')
484

485 486
          expect(model).to receive(:add_column)
            .with(:users, :new, :integer,
487 488 489 490
                 limit: old_column.limit,
                 precision: old_column.precision,
                 scale: old_column.scale)

491 492
          expect(model).to receive(:change_column_default)
            .with(:users, :new, old_column.default)
493

494 495
          expect(model).to receive(:update_column_in_batches)

496 497
          expect(model).to receive(:change_column_null).with(:users, :new, false)

498 499 500 501 502 503 504 505 506 507 508 509 510
          expect(model).to receive(:copy_indexes).with(:users, :old, :new)
          expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)

          model.rename_column_concurrently(:users, :old, :new)
        end
      end
    end
  end

  describe '#cleanup_concurrent_column_rename' do
    it 'cleans up the renaming procedure for PostgreSQL' do
      allow(Gitlab::Database).to receive(:postgresql?).and_return(true)

511 512
      expect(model).to receive(:remove_rename_triggers_for_postgresql)
        .with(:users, /trigger_.{12}/)
513 514 515 516 517 518 519 520 521

      expect(model).to receive(:remove_column).with(:users, :old)

      model.cleanup_concurrent_column_rename(:users, :old, :new)
    end

    it 'cleans up the renaming procedure for MySQL' do
      allow(Gitlab::Database).to receive(:postgresql?).and_return(false)

522 523
      expect(model).to receive(:remove_rename_triggers_for_mysql)
        .with(/trigger_.{12}/)
524 525 526 527 528 529 530 531 532

      expect(model).to receive(:remove_column).with(:users, :old)

      model.cleanup_concurrent_column_rename(:users, :old, :new)
    end
  end

  describe '#change_column_type_concurrently' do
    it 'changes the column type' do
533 534
      expect(model).to receive(:rename_column_concurrently)
        .with('users', 'username', 'username_for_type_change', type: :text)
535 536 537 538 539 540 541

      model.change_column_type_concurrently('users', 'username', :text)
    end
  end

  describe '#cleanup_concurrent_column_type_change' do
    it 'cleans up the type changing procedure' do
542 543
      expect(model).to receive(:cleanup_concurrent_column_rename)
        .with('users', 'username', 'username_for_type_change')
544

545 546
      expect(model).to receive(:rename_column)
        .with('users', 'username_for_type_change', 'username')
547 548 549 550 551 552 553

      model.cleanup_concurrent_column_type_change('users', 'username')
    end
  end

  describe '#install_rename_triggers_for_postgresql' do
    it 'installs the triggers for PostgreSQL' do
554 555
      expect(model).to receive(:execute)
        .with(/CREATE OR REPLACE FUNCTION foo()/m)
556

557 558
      expect(model).to receive(:execute)
        .with(/CREATE TRIGGER foo/m)
559 560 561 562 563 564 565

      model.install_rename_triggers_for_postgresql('foo', :users, :old, :new)
    end
  end

  describe '#install_rename_triggers_for_mysql' do
    it 'installs the triggers for MySQL' do
566 567
      expect(model).to receive(:execute)
        .with(/CREATE TRIGGER foo_insert.+ON users/m)
568

569 570
      expect(model).to receive(:execute)
        .with(/CREATE TRIGGER foo_update.+ON users/m)
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595

      model.install_rename_triggers_for_mysql('foo', :users, :old, :new)
    end
  end

  describe '#remove_rename_triggers_for_postgresql' do
    it 'removes the function and trigger' do
      expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
      expect(model).to receive(:execute).with('DROP FUNCTION foo()')

      model.remove_rename_triggers_for_postgresql('bar', 'foo')
    end
  end

  describe '#remove_rename_triggers_for_mysql' do
    it 'removes the triggers' do
      expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
      expect(model).to receive(:execute).with('DROP TRIGGER foo_update')

      model.remove_rename_triggers_for_mysql('foo')
    end
  end

  describe '#rename_trigger_name' do
    it 'returns a String' do
596 597
      expect(model.rename_trigger_name(:users, :foo, :bar))
        .to match(/trigger_.{12}/)
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
    end
  end

  describe '#indexes_for' do
    it 'returns the indexes for a column' do
      idx1 = double(:idx, columns: %w(project_id))
      idx2 = double(:idx, columns: %w(user_id))

      allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])

      expect(model.indexes_for('table', :user_id)).to eq([idx2])
    end
  end

  describe '#foreign_keys_for' do
    it 'returns the foreign keys for a column' do
      fk1 = double(:fk, column: 'project_id')
      fk2 = double(:fk, column: 'user_id')

      allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2])

      expect(model.foreign_keys_for('table', :user_id)).to eq([fk2])
    end
  end

  describe '#copy_indexes' do
    context 'using a regular index using a single column' do
      it 'copies the index' do
        index = double(:index,
                       columns: %w(project_id),
                       name: 'index_on_issues_project_id',
                       using: nil,
                       where: nil,
                       opclasses: {},
                       unique: false,
                       lengths: [],
                       orders: [])

636 637
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
638

639 640
        expect(model).to receive(:add_concurrent_index)
          .with(:issues,
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
               %w(gl_project_id),
               unique: false,
               name: 'index_on_issues_gl_project_id',
               length: [],
               order: [])

        model.copy_indexes(:issues, :project_id, :gl_project_id)
      end
    end

    context 'using a regular index with multiple columns' do
      it 'copies the index' do
        index = double(:index,
                       columns: %w(project_id foobar),
                       name: 'index_on_issues_project_id_foobar',
                       using: nil,
                       where: nil,
                       opclasses: {},
                       unique: false,
                       lengths: [],
                       orders: [])

663 664
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
665

666 667
        expect(model).to receive(:add_concurrent_index)
          .with(:issues,
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
               %w(gl_project_id foobar),
               unique: false,
               name: 'index_on_issues_gl_project_id_foobar',
               length: [],
               order: [])

        model.copy_indexes(:issues, :project_id, :gl_project_id)
      end
    end

    context 'using an index with a WHERE clause' do
      it 'copies the index' do
        index = double(:index,
                       columns: %w(project_id),
                       name: 'index_on_issues_project_id',
                       using: nil,
                       where: 'foo',
                       opclasses: {},
                       unique: false,
                       lengths: [],
                       orders: [])

690 691
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
692

693 694
        expect(model).to receive(:add_concurrent_index)
          .with(:issues,
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
               %w(gl_project_id),
               unique: false,
               name: 'index_on_issues_gl_project_id',
               length: [],
               order: [],
               where: 'foo')

        model.copy_indexes(:issues, :project_id, :gl_project_id)
      end
    end

    context 'using an index with a USING clause' do
      it 'copies the index' do
        index = double(:index,
                       columns: %w(project_id),
                       name: 'index_on_issues_project_id',
                       where: nil,
                       using: 'foo',
                       opclasses: {},
                       unique: false,
                       lengths: [],
                       orders: [])

718 719
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
720

721 722
        expect(model).to receive(:add_concurrent_index)
          .with(:issues,
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
               %w(gl_project_id),
               unique: false,
               name: 'index_on_issues_gl_project_id',
               length: [],
               order: [],
               using: 'foo')

        model.copy_indexes(:issues, :project_id, :gl_project_id)
      end
    end

    context 'using an index with custom operator classes' do
      it 'copies the index' do
        index = double(:index,
                       columns: %w(project_id),
                       name: 'index_on_issues_project_id',
                       using: nil,
                       where: nil,
                       opclasses: { 'project_id' => 'bar' },
                       unique: false,
                       lengths: [],
                       orders: [])

746 747
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
748

749 750
        expect(model).to receive(:add_concurrent_index)
          .with(:issues,
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
               %w(gl_project_id),
               unique: false,
               name: 'index_on_issues_gl_project_id',
               length: [],
               order: [],
               opclasses: { 'gl_project_id' => 'bar' })

        model.copy_indexes(:issues, :project_id, :gl_project_id)
      end
    end

    describe 'using an index of which the name does not contain the source column' do
      it 'raises RuntimeError' do
        index = double(:index,
                       columns: %w(project_id),
                       name: 'index_foobar_index',
                       using: nil,
                       where: nil,
                       opclasses: {},
                       unique: false,
                       lengths: [],
                       orders: [])

774 775
        allow(model).to receive(:indexes_for).with(:issues, 'project_id')
          .and_return([index])
776

777 778
        expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }
          .to raise_error(RuntimeError)
779 780 781 782 783 784 785 786 787 788 789
      end
    end
  end

  describe '#copy_foreign_keys' do
    it 'copies foreign keys from one column to another' do
      fk = double(:fk,
                  from_table: 'issues',
                  to_table: 'projects',
                  on_delete: :cascade)

790 791
      allow(model).to receive(:foreign_keys_for).with(:issues, :project_id)
        .and_return([fk])
792

793 794
      expect(model).to receive(:add_concurrent_foreign_key)
        .with('issues', 'projects', column: :gl_project_id, on_delete: :cascade)
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810

      model.copy_foreign_keys(:issues, :project_id, :gl_project_id)
    end
  end

  describe '#column_for' do
    it 'returns a column object for an existing column' do
      column = model.column_for(:users, :id)

      expect(column.name).to eq('id')
    end

    it 'returns nil when a column does not exist' do
      expect(model.column_for(:users, :kittens)).to be_nil
    end
  end
811 812 813 814 815 816 817 818

  describe '#replace_sql' do
    context 'using postgres' do
      before do
        allow(Gitlab::Database).to receive(:mysql?).and_return(false)
      end

      it 'builds the sql with correct functions' do
819 820
        expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s)
          .to include('regexp_replace')
821 822 823 824 825 826 827 828 829
      end
    end

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

      it 'builds the sql with the correct functions' do
830 831
        expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s)
          .to include('locate', 'insert')
832 833 834 835 836 837 838
      end
    end

    describe 'results' do
      let!(:user) { create(:user, name: 'Kathy Alice Aliceson') }

      it 'replaces the correct part of the string' do
839
        allow(model).to receive(:transaction_open?).and_return(false)
840
        query = model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve')
841 842 843

        model.update_column_in_batches(:users, :name, query)

844 845 846 847
        expect(user.reload.name).to eq('Kathy Eve Aliceson')
      end
    end
  end
848
end