|
| 1 | +require 'spec_helper' |
| 2 | +require 'migrations/helpers/migration_shared_context' |
| 3 | + |
| 4 | +RSpec.describe 'add unique constraint to buildpacks', isolation: :truncation, type: :migration do |
| 5 | + include_context 'migration' do |
| 6 | + let(:migration_filename) { '20260318083940_add_unique_constraint_to_buildpacks.rb' } |
| 7 | + end |
| 8 | + |
| 9 | + describe 'buildpacks table' do |
| 10 | + it 'removes duplicates, swaps indexes, and handles idempotency' do |
| 11 | + # Drop old unique index so we can insert duplicates |
| 12 | + db.alter_table(:buildpacks) { drop_index %i[name stack], name: :unique_name_and_stack } |
| 13 | + |
| 14 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 1) |
| 15 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 2) |
| 16 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'cnb', position: 3) |
| 17 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs4', lifecycle: 'buildpack', position: 4) |
| 18 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'go', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 5) |
| 19 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'go', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 6) |
| 20 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'go', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 7) |
| 21 | + |
| 22 | + expect(db[:buildpacks].where(name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'buildpack').count).to eq(2) |
| 23 | + expect(db[:buildpacks].where(name: 'go', stack: 'cflinuxfs3', lifecycle: 'buildpack').count).to eq(3) |
| 24 | + |
| 25 | + # === UP MIGRATION === |
| 26 | + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error |
| 27 | + |
| 28 | + # Verify duplicates are removed, keeping one per (name, stack, lifecycle) |
| 29 | + expect(db[:buildpacks].where(name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'buildpack').count).to eq(1) |
| 30 | + expect(db[:buildpacks].where(name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'cnb').count).to eq(1) |
| 31 | + expect(db[:buildpacks].where(name: 'ruby', stack: 'cflinuxfs4', lifecycle: 'buildpack').count).to eq(1) |
| 32 | + expect(db[:buildpacks].where(name: 'go', stack: 'cflinuxfs3', lifecycle: 'buildpack').count).to eq(1) |
| 33 | + |
| 34 | + # Verify old index is dropped and new index is added |
| 35 | + expect(db.indexes(:buildpacks)).not_to include(:unique_name_and_stack) |
| 36 | + expect(db.indexes(:buildpacks)).to include(:buildpacks_name_stack_lifecycle_index) |
| 37 | + |
| 38 | + # Test up migration idempotency |
| 39 | + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error |
| 40 | + |
| 41 | + # === DOWN MIGRATION === |
| 42 | + # First remove test data that would conflict with the old (name, stack) unique index |
| 43 | + db[:buildpacks].delete |
| 44 | + |
| 45 | + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error |
| 46 | + |
| 47 | + # Verify new index is dropped and old index is restored |
| 48 | + expect(db.indexes(:buildpacks)).not_to include(:buildpacks_name_stack_lifecycle_index) |
| 49 | + expect(db.indexes(:buildpacks)).to include(:unique_name_and_stack) |
| 50 | + |
| 51 | + # Verify the restored index enforces uniqueness on (name, stack) |
| 52 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'buildpack', position: 1) |
| 53 | + expect do |
| 54 | + db[:buildpacks].insert(guid: SecureRandom.uuid, name: 'ruby', stack: 'cflinuxfs3', lifecycle: 'cnb', position: 2) |
| 55 | + end.to raise_error(Sequel::UniqueConstraintViolation) |
| 56 | + db[:buildpacks].delete |
| 57 | + |
| 58 | + # Test down migration idempotency |
| 59 | + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error |
| 60 | + expect(db.indexes(:buildpacks)).not_to include(:buildpacks_name_stack_lifecycle_index) |
| 61 | + end |
| 62 | + end |
| 63 | +end |
0 commit comments