Skip to content

Commit 2fa162e

Browse files
committed
feat: unique constraint for revision_guid and name added at db level
- DB migration added for new uniqueness constraint. Tests for the new migration is added too. - around_save method added into RevisionSidecarModel to catch new constraint and augment it with message. Sequel:validate_uniqueness removed from the model
1 parent 5d45b2a commit 2fa162e

4 files changed

Lines changed: 113 additions & 1 deletion

File tree

app/models/runtime/revision_sidecar_model.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ class RevisionSidecarModel < Sequel::Model(:revision_sidecars)
1717

1818
add_association_dependencies revision_sidecar_process_types: :destroy
1919

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

spec/unit/models/runtime/revision_sidecar_model_spec.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,28 @@ module VCAP::CloudController
4343
end.to raise_error(Sequel::UniqueConstraintViolation)
4444
end
4545
end
46+
47+
describe 'Validations' do
48+
let!(:revision_sidecar) { RevisionSidecarModel.make(name: 'revision1', command: 'exec') }
49+
50+
it { is_expected.to validate_presence :name }
51+
it { is_expected.to validate_presence :command }
52+
end
53+
54+
describe 'revision_sidecar_model #around_save' do
55+
it 'raises validation error on unique constraint violation for revision sidecar' do
56+
revision_sidecar = RevisionSidecarModel.make(name: 'revision2', command: 'exec')
57+
expect do
58+
RevisionSidecarModel.make(name: revision_sidecar.name, revision_guid: revision_sidecar.revision_guid, command: 'exec')
59+
end.to raise_error(Sequel::ValidationFailed) { |error| expect(error.message).to include('already exists for revision') }
60+
end
61+
62+
it 'raises the original error on other unique constraint violations' do
63+
revision_sidecar = RevisionSidecarModel.make(name: 'revision3', command: 'exec')
64+
expect do
65+
RevisionSidecarModel.make(guid: revision_sidecar.guid, name: 'revision4', command: 'exe')
66+
end.to raise_error(Sequel::UniqueConstraintViolation)
67+
end
68+
end
4669
end
4770
end

0 commit comments

Comments
 (0)