Skip to content

Commit e055785

Browse files
authored
Replacing validate uniqueness with db constraint on RevisionSidecar model (#4950)
* 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 * fix: migration files are renamed * feat: concurrent migration for postgres db * fix: typo fixed * fix: comments are fixed
1 parent df9bc0d commit e055785

4 files changed

Lines changed: 124 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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Sequel.migration do
2+
no_transaction # required for concurrently option on postgres
3+
4+
up do
5+
transaction do
6+
# remove duplicate entries if they exist
7+
duplicates = self[:revision_sidecars].
8+
select(:revision_guid, :name).
9+
group(:revision_guid, :name).
10+
having { count(1) > 1 }
11+
12+
duplicates.each do |dup|
13+
ids_to_remove = self[:revision_sidecars].
14+
where(revision_guid: dup[:revision_guid], name: dup[:name]).
15+
select(:id).
16+
order(:id).
17+
offset(1).
18+
map(:id)
19+
self[:revision_sidecars].where(id: ids_to_remove).delete
20+
end
21+
end
22+
23+
if database_type == :postgres
24+
VCAP::Migration.with_concurrent_timeout(self) do
25+
add_index :revision_sidecars, %i[revision_guid name],
26+
name: :revision_sidecars_revision_guid_name_index,
27+
unique: true,
28+
concurrently: true,
29+
if_not_exists: true
30+
end
31+
else
32+
alter_table(:revision_sidecars) do
33+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
34+
unless @db.indexes(:revision_sidecars).key?(:revision_sidecars_revision_guid_name_index)
35+
add_index %i[revision_guid name], unique: true,
36+
name: :revision_sidecars_revision_guid_name_index
37+
end
38+
# rubocop:enable Sequel/ConcurrentIndex
39+
end
40+
end
41+
end
42+
43+
down do
44+
if database_type == :postgres
45+
VCAP::Migration.with_concurrent_timeout(self) do
46+
drop_index :revision_sidecars, nil,
47+
name: :revision_sidecars_revision_guid_name_index,
48+
concurrently: true,
49+
if_exists: true
50+
end
51+
else
52+
alter_table(:revision_sidecars) do
53+
# rubocop:disable Sequel/ConcurrentIndex -- MySQL does not support concurrent index operations
54+
drop_index %i[revision_guid name], name: :revision_sidecars_revision_guid_name_index if @db.indexes(:revision_sidecars).key?(:revision_sidecars_revision_guid_name_index)
55+
# rubocop:enable Sequel/ConcurrentIndex
56+
end
57+
end
58+
end
59+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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_sidecars.rb' }
6+
end
7+
8+
let!(:app) { VCAP::CloudController::AppModel.make }
9+
let!(:revision) { VCAP::CloudController::RevisionModel.make(:app) }
10+
11+
it 'remove duplicates, add constraint and revert migration' do
12+
# create duplicate entries
13+
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid)
14+
db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid)
15+
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision.guid).count).to eq(2)
16+
17+
# run the migration
18+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true)
19+
20+
# verify duplicates are removed and constraint is enforced
21+
expect(db[:revision_sidecars].where(name: 'app', revision_guid: revision.guid).count).to eq(1)
22+
expect(db.indexes(:revision_sidecars)).to include(:revision_sidecars_revision_guid_name_index)
23+
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid) }.to raise_error(Sequel::UniqueConstraintViolation)
24+
25+
# running the migration again should not cause any errors
26+
expect { Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) }.not_to raise_error
27+
28+
# roll back the migration
29+
Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true)
30+
31+
# verify constraint is removed and duplicates can be re-inserted
32+
expect(db.indexes(:revision_sidecars)).not_to include(:revision_sidecars_revision_guid_name_index)
33+
expect { db[:revision_sidecars].insert(guid: SecureRandom.uuid, name: 'app', command: 'command', revision_guid: revision.guid) }.not_to raise_error
34+
end
35+
end

spec/unit/models/runtime/revision_sidecar_model_spec.rb

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

0 commit comments

Comments
 (0)