Skip to content

Commit 4303e4f

Browse files
committed
fix: duplicate key value violates unique constraint uq_ver_ppt_ord
1 parent a1d96b6 commit 4303e4f

File tree

11 files changed

+180
-9
lines changed

11 files changed

+180
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require 'pact_broker/db/data_migrations/set_latest_version_sequence_value'
2+
Sequel.migration do
3+
change do
4+
create_table(:version_sequence_number) do
5+
Integer :value, null: false
6+
end
7+
end
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require 'pact_broker/db/data_migrations/set_latest_version_sequence_value'
2+
Sequel.migration do
3+
up do
4+
PactBroker::DB::DataMigrations::SetLatestVersionSequenceValue.call(self)
5+
end
6+
7+
down do
8+
end
9+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module PactBroker
2+
module DB
3+
module DataMigrations
4+
class SetLatestVersionSequenceValue
5+
def self.call connection
6+
if columns_exist?(connection)
7+
max_order = connection[:versions].max(:order) || 0
8+
sequence_row = connection[:version_sequence_number].first
9+
if sequence_row.nil? || sequence_row[:value] <= max_order
10+
new_value = max_order + 100
11+
connection[:version_sequence_number].insert(value: new_value)
12+
# Make sure there is only ever one row in case there is a race condition
13+
connection[:version_sequence_number].exclude(value: new_value).delete
14+
end
15+
end
16+
end
17+
18+
def self.columns_exist?(connection)
19+
column_exists?(connection, :versions, :order) &&
20+
column_exists?(connection, :version_sequence_number, :value)
21+
end
22+
23+
def self.column_exists?(connection, table, column)
24+
connection.table_exists?(table) && connection.schema(table).find{|col| col.first == column }
25+
end
26+
end
27+
end
28+
end
29+
end

lib/pact_broker/db/migrate_data.rb

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class MigrateData
1616
def self.call database_connection, options = {}
1717
DataMigrations::SetPacticipantIdsForVerifications.call(database_connection)
1818
DataMigrations::SetConsumerIdsForPactPublications.call(database_connection)
19+
DataMigrations::SetLatestVersionSequenceValue.call(database_connection)
1920
end
2021
end
2122
end

lib/pact_broker/domain/order_versions.rb

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
require 'pact_broker/configuration'
2+
require 'pact_broker/versions/sequence'
23

34
module PactBroker
45
module Domain
56
class OrderVersions
6-
77
include PactBroker::Logging
88

99
def self.call new_version
1010
new_version.lock!
11+
12+
if PactBroker.configuration.order_versions_by_date
13+
set_sequential_order(new_version)
14+
else
15+
set_semantic_order(new_version)
16+
end
17+
end
18+
19+
def self.set_sequential_order(new_version)
20+
set_order new_version, PactBroker::Versions::Sequence.next_val
21+
end
22+
23+
def self.set_semantic_order(new_version)
1124
order_set = false
1225

1326
PactBroker::Domain::Version.for_update.where(pacticipant_id: new_version.pacticipant_id).exclude(order: nil).reverse(:order).each do | existing_version |

lib/pact_broker/verifications/sequence.rb

-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
21
require 'sequel'
32

43
module PactBroker
54
module Verifications
65
class Sequence < Sequel::Model(:verification_sequence_number)
7-
86
dataset_module do
97
# The easiest way to implement a cross database compatible sequence.
108
# Sad, I know.

lib/pact_broker/versions/sequence.rb

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'sequel'
2+
3+
module PactBroker
4+
module Versions
5+
class Sequence < Sequel::Model(:version_sequence_number)
6+
7+
dataset_module do
8+
# The easiest way to implement a cross database compatible sequence.
9+
# Sad, I know.
10+
def next_val
11+
db.transaction do
12+
for_update.first
13+
select_all.update(value: Sequel[:value]+1)
14+
row = first
15+
if row
16+
row.value
17+
else
18+
# The first row should have been created in the migration, so this code
19+
# should only ever be executed in a test context, after we've truncated all the
20+
# tables after a test.
21+
# There would be a risk of a race condition creating two rows if this
22+
# code executed in prod, as I don't think you can lock an empty table
23+
# to prevent another record being inserted.
24+
max_version_order = PactBroker::Domain::Version.max(:order)
25+
value = max_version_order ? max_version_order + 100 : 1
26+
insert(value: value)
27+
value
28+
end
29+
end
30+
end
31+
end
32+
end
33+
end
34+
end
35+
36+
# Table: version_sequence_number
37+
# Columns:
38+
# value | integer | NOT NULL
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require 'pact_broker/db/data_migrations/set_latest_version_sequence_value'
2+
3+
module PactBroker
4+
module DB
5+
module DataMigrations
6+
describe SetLatestVersionSequenceValue, data_migration: true do
7+
include MigrationHelpers
8+
9+
describe ".call" do
10+
before (:all) do
11+
PactBroker::Database.migrate(20190509)
12+
end
13+
14+
let(:now) { DateTime.new(2018, 2, 2) }
15+
16+
subject { SetLatestVersionSequenceValue.call(database) }
17+
18+
context "when there is no sequence value set" do
19+
context "when there are no versions" do
20+
it "initializes the sequence value - this is required at start up each time in case someone has changed the ordering configuration (date vs semantic)" do
21+
subject
22+
expect(database[:version_sequence_number].first[:value]).to eq 100
23+
end
24+
end
25+
26+
context "when there are pre-existing versions" do
27+
let!(:consumer) { create(:pacticipants, {name: 'Consumer', created_at: now, updated_at: now}) }
28+
let!(:consumer_version) { create(:versions, {number: '1.2.3', order: 1, pacticipant_id: consumer[:id], created_at: now, updated_at: now}) }
29+
let!(:consumer_version) { create(:versions, {number: '1.2.5', order: 3, pacticipant_id: consumer[:id], created_at: now, updated_at: now}) }
30+
31+
it "initializes the sequence value to the max version order with a margin of error" do
32+
subject
33+
expect(database[:version_sequence_number].first[:value]).to eq 103
34+
end
35+
end
36+
end
37+
38+
context "when a value already exists and it is already higher than the max_order" do
39+
before do
40+
database[:version_sequence_number].insert(value: 5)
41+
end
42+
43+
it "does not update the value" do
44+
subject
45+
expect(database[:version_sequence_number].first[:value]).to eq 5
46+
expect(database[:version_sequence_number].count).to eq 1
47+
end
48+
end
49+
50+
context "when a value already exists and it not higher than the max_order" do
51+
before do
52+
database[:version_sequence_number].insert(value: 3)
53+
end
54+
55+
let!(:consumer) { create(:pacticipants, {name: 'Consumer', created_at: now, updated_at: now}) }
56+
let!(:consumer_version) { create(:versions, {number: '1.2.3', order: 1, pacticipant_id: consumer[:id], created_at: now, updated_at: now}) }
57+
let!(:consumer_version) { create(:versions, {number: '1.2.5', order: 3, pacticipant_id: consumer[:id], created_at: now, updated_at: now}) }
58+
59+
it "updates the value" do
60+
subject
61+
expect(database[:version_sequence_number].first[:value]).to eq 103
62+
end
63+
end
64+
end
65+
end
66+
end
67+
end
68+
end

spec/lib/pact_broker/domain/order_versions_spec.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
end
2525
end
2626

27-
context "when order_versions_by_date is true (not recommended)" do
27+
context "when order_versions_by_date is true (recommended)" do
2828
before do
2929
allow(PactBroker.configuration).to receive(:order_versions_by_date).and_return(true)
3030
end
@@ -69,7 +69,6 @@
6969
end
7070

7171
context "when the new version is considered to be earlier than the previous latest version" do
72-
7372
before do
7473
Sequel::Model.db[:versions].where(number: '2').update(number: 'z')
7574
Sequel::Model.db[:versions].where(number: '3').update(number: 'a')
@@ -80,8 +79,6 @@
8079
PactBroker::Domain::Version.create(number: '2', pacticipant_id: consumer.id)
8180
expect(ordered_versions).to eq(['1', 'z', 'a', '2', '4'])
8281
end
83-
8482
end
8583
end
86-
8784
end

spec/lib/pact_broker/versions/repository_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ module Versions
9393
expect(subject.number).to eq version_number
9494
expect(subject.pacticipant.name).to eq pacticipant_name
9595
expect(subject.tags.first.name).to eq "prod"
96-
expect(subject.order).to eq 0
96+
expect(subject.order).to_not be nil
9797
end
9898

9999
context "when case sensitivity is turned off and names with different cases are used" do

spec/support/database_cleaner.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
RSpec.configure do |config|
55

6-
config.include MigrationHelpers, migration: true
6+
config.include MigrationHelpers, migration: true, data_migration: true
77

88
config.before(:suite) do
99
if defined?(::DB)
@@ -20,11 +20,21 @@
2020
PactBroker::Database.drop_objects
2121
end
2222

23+
2324
config.after :each, migration: true do
2425
PactBroker::Database.migrate
2526
PactBroker::Database.truncate
2627
end
2728

29+
config.after :each, data_migration: true do
30+
PactBroker::Database.truncate
31+
end
32+
33+
config.after :all, data_migration: true do
34+
PactBroker::Database.migrate
35+
PactBroker::Database.truncate
36+
end
37+
2838
config.before(:each) do | example |
2939
unless example.metadata[:no_db_clean] || example.metadata[:migration]
3040
DatabaseCleaner.start if defined?(::DB)

0 commit comments

Comments
 (0)