diff --git a/db/migrations/20251015071027_allow_multiple_service_bindings.rb b/db/migrations/20251015071027_allow_multiple_service_bindings.rb new file mode 100644 index 0000000000..cbd9f2aeb9 --- /dev/null +++ b/db/migrations/20251015071027_allow_multiple_service_bindings.rb @@ -0,0 +1,55 @@ +Sequel.migration do + no_transaction # adding an index concurrently cannot be done within a transaction + + up do + alter_table(:service_bindings) do + drop_constraint(:unique_service_binding_service_instance_guid_app_guid) if @db.indexes(:service_bindings).key?(:unique_service_binding_service_instance_guid_app_guid) + drop_constraint(:unique_service_binding_app_guid_name) if @db.indexes(:service_bindings).key?(:unique_service_binding_app_guid_name) + end + + if database_type == :postgres + VCAP::Migration.with_concurrent_timeout(self) do + add_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, concurrently: true, if_not_exists: true + add_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, concurrently: true, if_not_exists: true + end + elsif database_type == :mysql + alter_table(:service_bindings) do + # rubocop:disable Sequel/ConcurrentIndex + unless @db.indexes(:service_bindings).key?(:service_bindings_app_guid_service_instance_guid_index) + add_index %i[app_guid service_instance_guid], + name: :service_bindings_app_guid_service_instance_guid_index + end + add_index %i[app_guid name], name: :service_bindings_app_guid_name_index unless @db.indexes(:service_bindings).key?(:service_bindings_app_guid_name_index) + # rubocop:enable Sequel/ConcurrentIndex + end + end + end + + down do + alter_table(:service_bindings) do + if @db.indexes(:service_bindings)[:unique_service_binding_service_instance_guid_app_guid].blank? + add_unique_constraint %i[service_instance_guid app_guid], + name: :unique_service_binding_service_instance_guid_app_guid + end + end + alter_table(:service_bindings) do + add_unique_constraint %i[app_guid name], name: :unique_service_binding_app_guid_name if @db.indexes(:service_bindings)[:unique_service_binding_app_guid_name].blank? + end + + if database_type == :postgres + VCAP::Migration.with_concurrent_timeout(self) do + drop_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, concurrently: true, if_exists: true + drop_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, concurrently: true, if_exists: true + end + elsif database_type == :mysql + alter_table(:service_bindings) do + # rubocop:disable Sequel/ConcurrentIndex + if @db.indexes(:service_bindings).key?(:service_bindings_app_guid_service_instance_guid_index) + drop_index %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index + end + drop_index %i[app_guid name], name: :service_bindings_app_guid_name_index if @db.indexes(:service_bindings).key?(:service_bindings_app_guid_name_index) + # rubocop:enable Sequel/ConcurrentIndex + end + end + end +end diff --git a/spec/migrations/20251015071027_allow_multiple_service_bindings_spec.rb b/spec/migrations/20251015071027_allow_multiple_service_bindings_spec.rb new file mode 100644 index 0000000000..707a727609 --- /dev/null +++ b/spec/migrations/20251015071027_allow_multiple_service_bindings_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' +require 'migrations/helpers/migration_shared_context' + +RSpec.describe 'migration to allow multiple service bindings', isolation: :truncation, type: :migration do + include_context 'migration' do + let(:migration_filename) { '20251015071027_allow_multiple_service_bindings.rb' } + end + + describe 'service_bindings table' do + context 'up migration' do + it 'is in the correct state before migration' do + expect(db.indexes(:service_bindings)).to include(:unique_service_binding_service_instance_guid_app_guid) + expect(db.indexes(:service_bindings)).to include(:unique_service_binding_app_guid_name) + expect(db.indexes(:service_bindings)).not_to include(:service_bindings_app_guid_service_instance_guid_index) + expect(db.indexes(:service_bindings)).not_to include(:service_bindings_app_guid_name_index) + end + + it 'migrates successfully' do + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error + expect(db.indexes(:service_bindings)).not_to include(:unique_service_binding_app_guid_name) + expect(db.indexes(:service_bindings)).not_to include(:unique_service_binding_service_instance_guid_app_guid) + expect(db.indexes(:service_bindings)).to include(:service_bindings_app_guid_service_instance_guid_index) + expect(db.indexes(:service_bindings)).to include(:service_bindings_app_guid_name_index) + end + + it 'does not fail if indexes/constraints are already in desired state' do + db.alter_table :service_bindings do + drop_constraint :unique_service_binding_service_instance_guid_app_guid + drop_constraint :unique_service_binding_app_guid_name + end + if db.database_type == :postgres + db.add_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, if_not_exists: true, concurrently: true + db.add_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, if_not_exists: true, concurrently: true + else + db.add_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, if_not_exists: true + db.add_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, if_not_exists: true + end + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error + end + end + + context 'down migration' do + it 'rolls back successfully' do + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error + expect(db.indexes(:service_bindings)).to include(:unique_service_binding_service_instance_guid_app_guid) + expect(db.indexes(:service_bindings)).to include(:unique_service_binding_app_guid_name) + expect(db.indexes(:service_bindings)).not_to include(:service_bindings_app_guid_service_instance_guid_index) + expect(db.indexes(:service_bindings)).not_to include(:service_bindings_app_guid_name_index) + end + + it 'does not fail if indexes/constraints are already in desired state' do + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error + db.alter_table :service_bindings do + add_unique_constraint %i[service_instance_guid app_guid], name: :unique_service_binding_service_instance_guid_app_guid + add_unique_constraint %i[app_guid name], name: :unique_service_binding_app_guid_name + end + if db.database_type == :postgres + db.drop_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, if_exists: true, concurrently: true + db.drop_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, if_exists: true, concurrently: true + else + db.drop_index :service_bindings, %i[app_guid service_instance_guid], name: :service_bindings_app_guid_service_instance_guid_index, if_exists: true + db.drop_index :service_bindings, %i[app_guid name], name: :service_bindings_app_guid_name_index, if_exists: true + end + + expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) }.not_to raise_error + end + end + end +end