Skip to content

Commit 41cac0c

Browse files
committed
feat: unique constraint added on db level for sidecar table
- db migration added on columns app_guid and name. Unit testting added too - sequel validate uniqueness removed from model and around save added instead
1 parent d16757b commit 41cac0c

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

app/models/runtime/sidecar_model.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@ class SidecarModel < Sequel::Model(:sidecars)
1616
key: :sidecar_guid,
1717
primary_key: :guid
1818

19+
def around_save
20+
yield
21+
rescue Sequel::UniqueConstraintViolation => e
22+
raise e unless e.message.include?('sidecars_app_guid_name_index')
23+
24+
errors.add(%i[app_guid name], Sequel.lit("Sidecar with name '#{name}' already exists for given app"))
25+
raise validation_failed_error
26+
end
27+
1928
def validate
2029
super
2130
validates_presence %i[name command]
2231
validates_max_length 255, :name, message: Sequel.lit('Name is too long (maximum is 255 characters)')
2332
validates_max_length 4096, :command, message: Sequel.lit('Command is too long (maximum is 4096 characters)')
24-
validates_unique %i[app_guid name], message: Sequel.lit("Sidecar with name '#{name}' already exists for given app")
2533
end
2634
end
2735
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Sequel.migration do
2+
up do
3+
transaction do
4+
duplicates = self[:sidecars].
5+
select(:app_guid, :name).
6+
group(:app_guid, :name).
7+
having { count(1) > 1 }
8+
duplicates.each do |dup|
9+
ids_to_remove = self[:sidecars].
10+
where(app_guid: dup[:app_guid], name: dup[:name]).
11+
select(:id).
12+
order(:id).
13+
offset(1).
14+
map(:id)
15+
16+
self[:sidecars].where(id: ids_to_remove).delete
17+
end
18+
19+
alter_table(:sidecars) do
20+
unless @db.indexes(:sidecars).key?(:sidecars_app_guid_name_index)
21+
add_unique_constraint %i[app_guid name],
22+
name: :sidecars_app_guid_name_index
23+
end
24+
end
25+
end
26+
end
27+
28+
down do
29+
alter_table(:sidecars) do
30+
drop_constraint(:sidecars_app_guid_name_index, type: :unique) if @db.indexes(:sidecars).key?(:sidecars_app_guid_name_index)
31+
end
32+
end
33+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
require 'spec_helper'
2+
require 'migrations/helpers/migration_shared_context'
3+
RSpec.describe 'add unique constraint to sidecars', isolation: :truncation, type: :migration do
4+
include_context 'migration' do
5+
let(:migration_filename) { '20260323092954_add_unique_constraint_to_sidecars.rb' }
6+
end
7+
8+
let!(:app) { VCAP::CloudController::AppModel.make }
9+
10+
it 'remove dublicates, add constraint and revert migration' do
11+
# =========================================================================================
12+
# SETUP: Create duplicate entries to test the de-duplication logic.
13+
# =========================================================================================
14+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
15+
db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid)
16+
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(2)
17+
18+
# =========================================================================================
19+
# UP MIGRATION: Run the migration to apply the unique constraints.
20+
# ========================================================================================
21+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
22+
23+
# =========================================================================================
24+
# ASSERT UP MIGRATION: Verify that duplicates are removed and constraints are enforced.
25+
# =========================================================================================
26+
expect(db[:sidecars].where(name: 'app', app_guid: app.guid).count).to eq(1)
27+
expect(db.indexes(:sidecars)).to include(:sidecars_app_guid_name_index)
28+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.to raise_error(Sequel::UniqueConstraintViolation)
29+
30+
# =========================================================================================
31+
# TEST IDEMPOTENCY: Running the migration again should not cause any errors.
32+
# =========================================================================================
33+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
34+
35+
# =========================================================================================
36+
# DOWN MIGRATION: Roll back the migration to remove the constraints.
37+
# =========================================================================================
38+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true)
39+
40+
# =========================================================================================
41+
# ASSERT DOWN MIGRATION: Verify that constraints are removed and duplicates can be re-inserted.
42+
# =========================================================================================
43+
expect(db.indexes(:sidecars)).not_to include(:sidecars_app_guid_name_index)
44+
expect { db[:sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', app_guid: app.guid) }.not_to raise_error
45+
end
46+
end

spec/unit/models/runtime/sidecar_model_spec.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,24 @@ module VCAP::CloudController
1414
end
1515

1616
describe 'validations' do
17+
it { is_expected.to validate_presence :name }
18+
it { is_expected.to validate_presence :command }
19+
end
20+
21+
describe 'sidecar model #around_save' do
1722
let(:app_model) { AppModel.make }
18-
let!(:sidecar) { SidecarModel.make(name: 'my_sidecar', app: app_model) }
23+
let!(:sidecar) { SidecarModel.make(app_guid: app_model.guid, name: 'sidecar1', command: 'exec') }
1924

20-
it 'validates unique sidecar name per app' do
21-
expect { SidecarModel.create(app: app_model, name: 'my_sidecar', command: 'some-command') }.
22-
to raise_error(Sequel::ValidationFailed, /Sidecar with name 'my_sidecar' already exists for given app/)
25+
it 'raises validation error on unique constraint violation for sidecar' do
26+
expect do
27+
SidecarModel.create(guid: SecureRandom.uuid, app_guid: app_model.guid, name: sidecar.name, command: 'exec')
28+
end.to raise_error(Sequel::ValidationFailed) { |error| expect(error.message).to include("Sidecar with name 'sidecar1' already exists for given app") }
29+
end
30+
31+
it 'raises the original error on other unique constraint violations' do
32+
expect do
33+
SidecarModel.create(guid: sidecar.guid, app_guid: app_model.guid, name: 'sidecar2', command: 'exec')
34+
end.to raise_error(Sequel::UniqueConstraintViolation)
2335
end
2436
end
2537

0 commit comments

Comments
 (0)