Skip to content

Commit 3577968

Browse files
committed
fix: correctly identify missing verification for bi-directional pacts
1 parent b37b86f commit 3577968

9 files changed

+414
-38
lines changed
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
module PactBroker
2+
module Matrix
3+
class QueryBuilder
4+
LV = :latest_verification_id_for_pact_version_and_provider_version
5+
LP = :latest_pact_publication_ids_for_consumer_versions
6+
7+
def self.provider_or_provider_version_or_verification_in(selectors, allow_null_provider_version = false, qualifier)
8+
most_specific_criteria = selectors.collect(&:most_specific_provider_criterion)
9+
10+
verification_ids = collect_ids(most_specific_criteria, :verification_ids)
11+
provider_version_ids = collect_ids(most_specific_criteria, :pacticipant_version_id)
12+
provider_ids = collect_ids(most_specific_criteria, :pacticipant_id)
13+
14+
ors = [
15+
{ verification_id: verification_ids },
16+
{ Sequel[qualifier][:provider_version_id] => provider_version_ids },
17+
{ Sequel[qualifier][:provider_id] => provider_ids }
18+
]
19+
20+
if allow_null_provider_version
21+
ors << {
22+
Sequel[qualifier][:provider_id] => selectors.collect{ |s| s[:pacticipant_id] },
23+
Sequel[qualifier][:provider_version_id] => nil
24+
}
25+
end
26+
27+
Sequel.|(*ors)
28+
end
29+
30+
def self.consumer_in_pacticipant_ids(selectors)
31+
{ consumer_id: selectors.collect(&:pacticipant_id) }
32+
end
33+
34+
def self.consumer_or_consumer_version_or_pact_publication_in(selectors, qualifier)
35+
most_specific_criteria = selectors.collect(&:most_specific_consumer_criterion)
36+
pact_publication_ids = collect_ids(most_specific_criteria, :pact_publication_ids)
37+
consumer_version_ids = collect_ids(most_specific_criteria, :pacticipant_version_id)
38+
consumer_ids = collect_ids(most_specific_criteria, :pacticipant_id)
39+
40+
Sequel.|(
41+
{ Sequel[qualifier][:pact_publication_id] => pact_publication_ids },
42+
{ Sequel[qualifier][:consumer_version_id] => consumer_version_ids },
43+
{ Sequel[qualifier][:consumer_id] => consumer_ids }
44+
)
45+
end
46+
47+
# Some selecters are specified in the query, others are implied (when only one pacticipant is specified,
48+
# the integrations are automatically worked out, and the selectors for these are of type :implied )
49+
# When there are 3 pacticipants that each have dependencies on each other (A->B, A->C, B->C), the query
50+
# to deploy C (implied A, implied B, specified C) was returning the A->B row because it matched the
51+
# implied selectors as well.
52+
# This extra filter makes sure that every row that is returned actually matches one of the specified
53+
# selectors.
54+
def self.either_consumer_or_provider_was_specified_in_query(selectors, qualifier = nil)
55+
consumer_id_field = qualifier ? Sequel[qualifier][:consumer_id] : CONSUMER_ID
56+
provider_id_field = qualifier ? Sequel[qualifier][:provider_id] : PROVIDER_ID
57+
specified_pacticipant_ids = selectors.select(&:specified?).collect(&:pacticipant_id)
58+
Sequel.|({ consumer_id_field => specified_pacticipant_ids } , { provider_id_field => specified_pacticipant_ids })
59+
end
60+
61+
def self.consumer_or_consumer_version_or_provider_or_provider_or_provider_version_match_selector(s)
62+
consumer_or_consumer_version_match = s[:pacticipant_version_id] ? { Sequel[LP][:consumer_version_id] => s[:pacticipant_version_id] } : { Sequel[LP][:consumer_id] => s[:pacticipant_id] }
63+
provider_or_provider_version_match = s[:pacticipant_version_id] ? { Sequel[:lv][:provider_version_id] => s[:pacticipant_version_id] } : { Sequel[LP][:provider_id] => s[:pacticipant_id] }
64+
Sequel.|(consumer_or_consumer_version_match , provider_or_provider_version_match)
65+
end
66+
67+
def self.all_pacticipant_ids selectors
68+
selectors.collect(&:pacticipant_id)
69+
end
70+
71+
def self.collect_ids(hashes, key)
72+
ids = hashes.collect{ |s| s[key] }.flatten.compact
73+
# must avoid an empty IN list, or Sequel makes a (0 = 1) clause for us
74+
ids.empty? ? [-1] : ids
75+
end
76+
77+
def self.collect_the_ids selectors
78+
most_specific_criteria = selectors.collect(&:most_specific_consumer_criterion)
79+
verification_ids = collect_ids(most_specific_criteria, :verification_ids)
80+
pact_publication_ids = collect_ids(most_specific_criteria, :pact_publication_ids)
81+
pacticipant_version_ids = collect_ids(most_specific_criteria, :pacticipant_version_id)
82+
pacticipant_ids = collect_ids(most_specific_criteria, :pacticipant_id)
83+
all_pacticipant_ids = selectors.collect(&:pacticipant_id)
84+
85+
specified_pacticipant_ids = selectors.select(&:specified?).collect(&:pacticipant_id)
86+
87+
{
88+
verification_ids: verification_ids,
89+
pact_publication_ids: pact_publication_ids,
90+
consumer_version_ids: pacticipant_version_ids,
91+
provider_version_ids: pacticipant_version_ids,
92+
consumer_ids: pacticipant_ids,
93+
provider_ids: pacticipant_ids,
94+
all_pacticipant_ids: all_pacticipant_ids,
95+
specified_pacticipant_ids: specified_pacticipant_ids
96+
}
97+
end
98+
end
99+
end
100+
end

lib/pact_broker/matrix/quick_row.rb

+33-31
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,27 @@ module Matrix
2121
LV = :latest_verification_id_for_pact_version_and_provider_version
2222
LP = :latest_pact_publication_ids_for_consumer_versions
2323

24-
CONSUMER_COLUMNS = [Sequel[:lp][:consumer_id], Sequel[:consumers][:name].as(:consumer_name), Sequel[:lp][:pact_publication_id], Sequel[:lp][:pact_version_id]]
25-
PROVIDER_COLUMNS = [Sequel[:lp][:provider_id], Sequel[:providers][:name].as(:provider_name), Sequel[:lv][:verification_id]]
26-
CONSUMER_VERSION_COLUMNS = [Sequel[:lp][:consumer_version_id], Sequel[:cv][:number].as(:consumer_version_number), Sequel[:cv][:order].as(:consumer_version_order)]
24+
CONSUMER_COLUMNS = [Sequel[LP][:consumer_id], Sequel[:consumers][:name].as(:consumer_name), Sequel[LP][:pact_publication_id], Sequel[LP][:pact_version_id]]
25+
PROVIDER_COLUMNS = [Sequel[LP][:provider_id], Sequel[:providers][:name].as(:provider_name), Sequel[:lv][:verification_id]]
26+
CONSUMER_VERSION_COLUMNS = [Sequel[LP][:consumer_version_id], Sequel[:cv][:number].as(:consumer_version_number), Sequel[:cv][:order].as(:consumer_version_order)]
2727
PROVIDER_VERSION_COLUMNS = [Sequel[:lv][:provider_version_id], Sequel[:pv][:number].as(:provider_version_number), Sequel[:pv][:order].as(:provider_version_order)]
2828
ALL_COLUMNS = CONSUMER_COLUMNS + CONSUMER_VERSION_COLUMNS + PROVIDER_COLUMNS + PROVIDER_VERSION_COLUMNS
2929

30-
LP_LV_JOIN = { Sequel[:lp][:pact_version_id] => Sequel[:lv][:pact_version_id] }
31-
CONSUMER_JOIN = { Sequel[:lp][:consumer_id] => Sequel[:consumers][:id] }
32-
PROVIDER_JOIN = { Sequel[:lp][:provider_id] => Sequel[:providers][:id] }
33-
CONSUMER_VERSION_JOIN = { Sequel[:lp][:consumer_version_id] => Sequel[:cv][:id] }
30+
LP_LV_JOIN = { Sequel[LP][:pact_version_id] => Sequel[:lv][:pact_version_id] }
31+
CONSUMER_JOIN = { Sequel[LP][:consumer_id] => Sequel[:consumers][:id] }
32+
PROVIDER_JOIN = { Sequel[LP][:provider_id] => Sequel[:providers][:id] }
33+
CONSUMER_VERSION_JOIN = { Sequel[LP][:consumer_version_id] => Sequel[:cv][:id] }
3434
PROVIDER_VERSION_JOIN = { Sequel[:lv][:provider_version_id] => Sequel[:pv][:id] }
3535

36-
RAW_QUERY = Sequel::Model.db[Sequel.as(LP, :lp)]
36+
RAW_QUERY = Sequel::Model.db[LP]
3737
.select(*ALL_COLUMNS)
3838
.left_outer_join(LV, LP_LV_JOIN, { table_alias: :lv } )
3939
.join(:pacticipants, CONSUMER_JOIN, { table_alias: :consumers })
4040
.join(:pacticipants, PROVIDER_JOIN, { table_alias: :providers })
4141
.join(:versions, CONSUMER_VERSION_JOIN, { table_alias: :cv })
4242
.left_outer_join(:versions, PROVIDER_VERSION_JOIN, { table_alias: :pv } )
4343

44+
4445
ALIASED_QUERY = Sequel.as(RAW_QUERY, :quick_rows)
4546

4647
class QuickRow < Sequel::Model(ALIASED_QUERY)
@@ -101,36 +102,36 @@ class QueryHelper
101102
def self.consumer_and_provider_in selectors
102103
Sequel.&(
103104
Sequel.|(
104-
*consumer_and_maybe_consumer_version_match_any_selector(selectors)
105+
*consumer_or_consumer_version_match_any_selector(selectors)
105106
),
106107
Sequel.|(
107-
*provider_and_maybe_provider_version_match_any_selector_or_verification_is_missing(selectors)
108+
*provider_or_provider_version_match_any_selector_or_verification_is_missing(selectors)
108109
),
109110
either_consumer_or_provider_was_specified_in_query(selectors)
110111
)
111112
end
112113

113-
def self.consumer_and_maybe_consumer_version_match_any_selector(selectors)
114-
selectors.collect { |s| consumer_and_maybe_consumer_version_match_selector(s) }
114+
def self.consumer_or_consumer_version_match_any_selector(selectors)
115+
selectors.collect { |s| most_specific_consumer_criterion(s) }
115116
end
116117

117-
def self.consumer_and_maybe_consumer_version_match_selector(s)
118+
def self.most_specific_consumer_criterion(s, qualifier = :quick_rows)
118119
if s[:pact_publication_ids]
119-
{ PACT_PUBLICATION_ID => s[:pact_publication_ids] }
120+
{ Sequel[qualifier][:pact_publication_id] => s[:pact_publication_ids] }
120121
elsif s[:pacticipant_version_id]
121-
{ CONSUMER_ID => s[:pacticipant_id], CONSUMER_VERSION_ID => s[:pacticipant_version_id] }
122+
{ Sequel[qualifier][:consumer_version_id] => s[:pacticipant_version_id] }
122123
else
123-
{ CONSUMER_ID => s[:pacticipant_id] }
124+
{ Sequel[qualifier][:consumer_id] => s[:pacticipant_id] }
124125
end
125126
end
126127

127-
def self.provider_and_maybe_provider_version_match_selector(s)
128-
if s[:verification_ids]
129-
{ VERIFICATION_ID => s[:verification_ids] }
130-
elsif s[:pacticipant_version_id]
131-
{ PROVIDER_ID => s[:pacticipant_id], PROVIDER_VERSION_ID => s[:pacticipant_version_id] }
128+
def self.most_specific_provider_criterion(selector, qualifier = :quick_rows)
129+
if selector[:verification_ids]
130+
{ Sequel[qualifier][:verification_id] => selector[:verification_ids] }
131+
elsif selector[:pacticipant_version_id]
132+
{ Sequel[qualifier][:provider_version_id] => selector[:pacticipant_version_id] }
132133
else
133-
{ PROVIDER_ID => s[:pacticipant_id] }
134+
{ Sequel[qualifier][:provider_id] => selector[:pacticipant_id] }
134135
end
135136
end
136137

@@ -140,9 +141,9 @@ def self.provider_verification_is_missing_for_matching_selector(s)
140141
{ PROVIDER_ID => s[:pacticipant_id], PROVIDER_VERSION_ID => nil }
141142
end
142143

143-
def self.provider_and_maybe_provider_version_match_any_selector_or_verification_is_missing(selectors)
144+
def self.provider_or_provider_version_match_any_selector_or_verification_is_missing(selectors)
144145
selectors.collect { |s|
145-
provider_and_maybe_provider_version_match_selector(s)
146+
most_specific_provider_criterion(s)
146147
} + selectors.collect { |s|
147148
provider_verification_is_missing_for_matching_selector(s)
148149
}
@@ -155,16 +156,17 @@ def self.provider_and_maybe_provider_version_match_any_selector_or_verification_
155156
# implied selectors as well.
156157
# This extra filter makes sure that every row that is returned actually matches one of the specified
157158
# selectors.
158-
def self.either_consumer_or_provider_was_specified_in_query(selectors)
159-
specified_pacticipant_ids = selectors.select{ |s| s[:type] == :specified }.collect{ |s| s[:pacticipant_id] }
160-
Sequel.|({ CONSUMER_ID => specified_pacticipant_ids } , { PROVIDER_ID => specified_pacticipant_ids })
159+
def self.either_consumer_or_provider_was_specified_in_query(selectors, qualifier = nil)
160+
consumer_id_field = qualifier ? Sequel[qualifier][:consumer_id] : CONSUMER_ID
161+
provider_id_field = qualifier ? Sequel[qualifier][:provider_id] : PROVIDER_ID
162+
specified_pacticipant_ids = selectors.select(&:specified?).collect(&:pacticipant_id)
163+
Sequel.|({ consumer_id_field => specified_pacticipant_ids } , { provider_id_field => specified_pacticipant_ids })
161164
end
162165

163166
def self.consumer_or_consumer_version_or_provider_or_provider_or_provider_version_match_selector(s)
164-
Sequel.|(
165-
s[:pacticipant_version_id] ? { CONSUMER_VERSION_ID => s[:pacticipant_version_id] } : { CONSUMER_ID => s[:pacticipant_id] },
166-
s[:pacticipant_version_id] ? { PROVIDER_VERSION_ID => s[:pacticipant_version_id] } : { PROVIDER_ID => s[:pacticipant_id] }
167-
)
167+
consumer_or_consumer_version_match = s[:pacticipant_version_id] ? { CONSUMER_VERSION_ID => s[:pacticipant_version_id] } : { CONSUMER_ID => s[:pacticipant_id] }
168+
provider_or_provider_version_match = s[:pacticipant_version_id] ? { PROVIDER_VERSION_ID => s[:pacticipant_version_id] } : { PROVIDER_ID => s[:pacticipant_id] }
169+
Sequel.|(consumer_or_consumer_version_match , provider_or_provider_version_match)
168170
end
169171
end
170172

lib/pact_broker/matrix/quick_row_2.rb

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
require 'pact_broker/pacts/all_pact_publications'
2+
require 'pact_broker/repositories/helpers'
3+
require 'pact_broker/matrix/query_builder'
4+
5+
# The difference between this query and the query for QuickRow2 is that
6+
# the left outer join is done on a pre-filtered dataset so that we
7+
# get a row with null verification fields for a pact that has not been verified
8+
# by the particular providers where're interested in, rather than being excluded
9+
# from the dataset.
10+
11+
module PactBroker
12+
module Matrix
13+
class QuickRow2 < Sequel::Model(:latest_pact_publication_ids_for_consumer_versions)
14+
LV = :latest_verification_id_for_pact_version_and_provider_version
15+
LP = :latest_pact_publication_ids_for_consumer_versions
16+
17+
LP_LV_JOIN = { Sequel[LP][:pact_version_id] => Sequel[:lv][:pact_version_id] }
18+
CONSUMER_JOIN = { Sequel[LP][:consumer_id] => Sequel[:consumers][:id] }
19+
PROVIDER_JOIN = { Sequel[LP][:provider_id] => Sequel[:providers][:id] }
20+
CONSUMER_VERSION_JOIN = { Sequel[LP][:consumer_version_id] => Sequel[:cv][:id] }
21+
PROVIDER_VERSION_JOIN = { Sequel[:lv][:provider_version_id] => Sequel[:pv][:id] }
22+
23+
CONSUMER_COLUMNS = [Sequel[LP][:consumer_id], Sequel[:consumers][:name].as(:consumer_name), Sequel[LP][:pact_publication_id], Sequel[LP][:pact_version_id]]
24+
PROVIDER_COLUMNS = [Sequel[LP][:provider_id], Sequel[:providers][:name].as(:provider_name), Sequel[:lv][:verification_id]]
25+
CONSUMER_VERSION_COLUMNS = [Sequel[LP][:consumer_version_id], Sequel[:cv][:number].as(:consumer_version_number), Sequel[:cv][:order].as(:consumer_version_order)]
26+
PROVIDER_VERSION_COLUMNS = [Sequel[:lv][:provider_version_id], Sequel[:pv][:number].as(:provider_version_number), Sequel[:pv][:order].as(:provider_version_order)]
27+
ALL_COLUMNS = CONSUMER_COLUMNS + CONSUMER_VERSION_COLUMNS + PROVIDER_COLUMNS + PROVIDER_VERSION_COLUMNS
28+
29+
SELECT_ALL_COLUMN_ARGS = [:select_all_columns] + ALL_COLUMNS
30+
31+
dataset_module do
32+
include PactBroker::Repositories::Helpers
33+
34+
select *SELECT_ALL_COLUMN_ARGS
35+
36+
def matching_selectors selectors
37+
if selectors.size == 1
38+
matching_one_selector(selectors.first)
39+
else
40+
matching_multiple_selectors(selectors)
41+
end
42+
end
43+
44+
# When we have one selector, we need to join ALL the verifications to find out
45+
# what integrations exist
46+
def matching_one_selector(selector)
47+
select_all_columns
48+
.join_verifications
49+
.join_pacticipants_and_pacticipant_versions
50+
.where {
51+
QueryBuilder.consumer_or_consumer_version_or_provider_or_provider_or_provider_version_match_selector(selector)
52+
}
53+
end
54+
55+
# When the user has specified multiple selectors, we only want to join the verifications for
56+
# the specified selectors. This is because of the behaviour of the left outer join.
57+
# Imagine a pact has been verified by a provider version that was NOT specified in the selectors.
58+
# If we join all the verifications and THEN filter the rows to only show the versions specified
59+
# in the selectors, we won't get a row for that pact, and hence, we won't
60+
# know that it hasn't been verified by the provider version we're interested in.
61+
# Instead, we need to filter the verifications dataset down to only the ones specified in the selectors first,
62+
# and THEN join them to the pacts, so that we get a row for the pact with null provider version
63+
# and verification fields.
64+
def matching_multiple_selectors(selectors)
65+
select_all_columns
66+
.join_verifications_for(selectors)
67+
.join_pacticipants_and_pacticipant_versions
68+
.where {
69+
Sequel.&(
70+
QueryBuilder.consumer_or_consumer_version_or_pact_publication_in(selectors, LP),
71+
QueryBuilder.either_consumer_or_provider_was_specified_in_query(selectors, LP)
72+
)
73+
}
74+
.from_self(alias: :t9)
75+
.where {
76+
QueryBuilder.provider_or_provider_version_or_verification_in(selectors, true, :t9)
77+
}
78+
end
79+
80+
def join_pacticipants_and_pacticipant_versions
81+
join_consumers
82+
.join_providers
83+
.join_consumer_versions
84+
.join_provider_versions
85+
end
86+
87+
def join_consumers
88+
join(:pacticipants, CONSUMER_JOIN, { table_alias: :consumers })
89+
end
90+
91+
def join_providers
92+
join(:pacticipants, PROVIDER_JOIN, { table_alias: :providers })
93+
end
94+
95+
def join_consumer_versions
96+
join(:versions, CONSUMER_VERSION_JOIN, { table_alias: :cv })
97+
end
98+
99+
def join_provider_versions
100+
left_outer_join(:versions, PROVIDER_VERSION_JOIN, { table_alias: :pv } )
101+
end
102+
103+
def join_verifications_for(selectors)
104+
left_outer_join(verifications_for(selectors), LP_LV_JOIN, { table_alias: :lv } )
105+
end
106+
107+
def join_verifications
108+
left_outer_join(LV, LP_LV_JOIN, { table_alias: :lv } )
109+
end
110+
111+
def verifications_for(selectors)
112+
db[LV]
113+
.select(:verification_id, :provider_version_id, :pact_version_id)
114+
.where {
115+
Sequel.&(
116+
QueryBuilder.consumer_in_pacticipant_ids(selectors),
117+
QueryBuilder.provider_or_provider_version_or_verification_in(selectors, false, LV)
118+
)
119+
}
120+
end
121+
end
122+
end
123+
end
124+
end

lib/pact_broker/matrix/repository.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'pact_broker/matrix/resolved_selector'
1010
require 'pact_broker/verifications/latest_verification_id_for_pact_version_and_provider_version'
1111
require 'pact_broker/pacts/latest_pact_publications_by_consumer_version'
12+
require 'pact_broker/matrix/quick_row_2'
1213

1314
module PactBroker
1415
module Matrix
@@ -81,8 +82,8 @@ def find_compatible_pacticipant_versions selectors
8182
# If two or more are specified, just return the integrations that involve the specified pacticipants
8283
def find_integrations_for_specified_selectors(resolved_specified_selectors)
8384
specified_pacticipant_names = resolved_specified_selectors.collect(&:pacticipant_name)
84-
QuickRow
85-
.pacticipant_names_and_ids
85+
86+
QuickRow2
8687
.matching_selectors(resolved_specified_selectors)
8788
.distinct
8889
.all

0 commit comments

Comments
 (0)