Skip to content

Commit 78b8a5d

Browse files
committed
feat(matrix): optimise query for can-i-deploy, again
1 parent 29dcbac commit 78b8a5d

File tree

6 files changed

+308
-20
lines changed

6 files changed

+308
-20
lines changed

lib/pact_broker/matrix/quick_row.rb

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
=begin
2+
The Matrix::Row is based on the matrix view which does a join of every pact/verification
3+
and then selects the relevant ones.
4+
5+
The Matrix::QuickRow starts with the relevant rows, and builds the matrix query from that,
6+
making it much quicker.
7+
8+
=end
9+
require 'sequel'
10+
require 'pact_broker/repositories/helpers'
11+
require 'pact_broker/logging'
12+
require 'pact_broker/pacts/pact_version'
13+
require 'pact_broker/domain/pacticipant'
14+
require 'pact_broker/domain/version'
15+
require 'pact_broker/domain/verification'
16+
require 'pact_broker/pacts/pact_publication'
17+
require 'pact_broker/tags/tag_with_latest_flag'
18+
19+
module PactBroker
20+
module Matrix
21+
LV = :latest_verification_id_for_pact_version_and_provider_version
22+
LP = :latest_pact_publication_ids_for_consumer_versions
23+
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)]
27+
PROVIDER_VERSION_COLUMNS = [Sequel[:lv][:provider_version_id], Sequel[:pv][:number].as(:provider_version_number), Sequel[:pv][:order].as(:provider_version_order)]
28+
ALL_COLUMNS = CONSUMER_COLUMNS + CONSUMER_VERSION_COLUMNS + PROVIDER_COLUMNS + PROVIDER_VERSION_COLUMNS
29+
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] }
34+
PROVIDER_VERSION_JOIN = { Sequel[:lv][:provider_version_id] => Sequel[:pv][:id] }
35+
36+
RAW_QUERY = Sequel::Model.db[Sequel.as(LP, :lp)]
37+
.select(*ALL_COLUMNS)
38+
.left_outer_join(LV, LP_LV_JOIN, { table_alias: :lv } )
39+
.join(:pacticipants, CONSUMER_JOIN, { table_alias: :consumers })
40+
.join(:pacticipants, PROVIDER_JOIN, { table_alias: :providers })
41+
.join(:versions, CONSUMER_VERSION_JOIN, { table_alias: :cv })
42+
.left_outer_join(:versions, PROVIDER_VERSION_JOIN, { table_alias: :pv } )
43+
44+
ALIASED_QUERY = Sequel.as(RAW_QUERY, :smart_rows)
45+
46+
class QuickRow < Sequel::Model(ALIASED_QUERY)
47+
CONSUMER_ID = Sequel[:smart_rows][:consumer_id]
48+
PROVIDER_ID = Sequel[:smart_rows][:provider_id]
49+
CONSUMER_VERSION_ID = Sequel[:smart_rows][:consumer_version_id]
50+
PROVIDER_VERSION_ID = Sequel[:smart_rows][:provider_version_id]
51+
PACT_PUBLICATION_ID = Sequel[:smart_rows][:pact_publication_id]
52+
VERIFICATION_ID = Sequel[:smart_rows][:verification_id]
53+
54+
associate(:many_to_one, :pact_publication, :class => "PactBroker::Pacts::PactPublication", :key => :pact_publication_id, :primary_key => :id)
55+
associate(:many_to_one, :provider, :class => "PactBroker::Domain::Pacticipant", :key => :provider_id, :primary_key => :id)
56+
associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id)
57+
associate(:many_to_one, :consumer_version, :class => "PactBroker::Domain::Version", :key => :consumer_version_id, :primary_key => :id)
58+
associate(:many_to_one, :provider_version, :class => "PactBroker::Domain::Version", :key => :provider_version_id, :primary_key => :id)
59+
associate(:many_to_one, :pact_version, class: "PactBroker::Pacts::PactVersion", :key => :pact_version_id, :primary_key => :id)
60+
associate(:many_to_one, :verification, class: "PactBroker::Domain::Verification", :key => :verification_id, :primary_key => :id)
61+
associate(:one_to_many, :consumer_version_tags, :class => "PactBroker::Tags::TagWithLatestFlag", primary_key: :consumer_version_id, key: :version_id)
62+
associate(:one_to_many, :provider_version_tags, :class => "PactBroker::Tags::TagWithLatestFlag", primary_key: :provider_version_id, key: :version_id)
63+
64+
dataset_module do
65+
include PactBroker::Repositories::Helpers
66+
include PactBroker::Logging
67+
68+
def consumer_id consumer_id
69+
where(CONSUMER_ID => consumer_id)
70+
end
71+
72+
def matching_selectors selectors
73+
if selectors.size == 1
74+
where_consumer_or_provider_is(selectors.first)
75+
else
76+
where_consumer_and_provider_in(selectors)
77+
end
78+
end
79+
80+
# find rows where (the consumer (and optional version) matches any of the selectors)
81+
# AND
82+
# the (provider (and optional version) matches any of the selectors OR the provider matches
83+
# and the verification is missing (and hence the provider version is null))
84+
def where_consumer_and_provider_in selectors
85+
where{
86+
Sequel.&(
87+
Sequel.|(
88+
*QueryHelper.consumer_and_maybe_consumer_version_match_any_selector(selectors)
89+
),
90+
Sequel.|(
91+
*QueryHelper.provider_and_maybe_provider_version_match_any_selector_or_verification_is_missing(selectors)
92+
),
93+
QueryHelper.either_consumer_or_provider_was_specified_in_query(selectors)
94+
)
95+
}
96+
end
97+
98+
# Can't access other dataset_module methods from inside the Sequel `where{ ... }` block, so make a private class
99+
# with some helper methods
100+
class QueryHelper
101+
def self.consumer_and_maybe_consumer_version_match_any_selector(selectors)
102+
selectors.collect { |s| consumer_and_maybe_consumer_version_match_selector(s) }
103+
end
104+
105+
def self.consumer_and_maybe_consumer_version_match_selector(s)
106+
if s[:pact_publication_ids]
107+
Sequel.&(PACT_PUBLICATION_ID => s[:pact_publication_ids])
108+
elsif s[:pacticipant_version_id]
109+
Sequel.&(CONSUMER_ID => s[:pacticipant_id], CONSUMER_VERSION_ID => s[:pacticipant_version_id])
110+
else
111+
Sequel.&(CONSUMER_ID => s[:pacticipant_id])
112+
end
113+
end
114+
115+
def self.provider_and_maybe_provider_version_match_selector(s)
116+
if s[:verification_ids]
117+
Sequel.&(VERIFICATION_ID => s[:verification_ids])
118+
elsif s[:pacticipant_version_id]
119+
Sequel.&(PROVIDER_ID => s[:pacticipant_id], PROVIDER_VERSION_ID => s[:pacticipant_version_id])
120+
else
121+
Sequel.&(PROVIDER_ID => s[:pacticipant_id])
122+
end
123+
end
124+
125+
# if the pact for a consumer version has never been verified, it exists in the matrix as a row
126+
# with a blank provider version id
127+
def self.provider_verification_is_missing_for_matching_selector(s)
128+
Sequel.&(PROVIDER_ID => s[:pacticipant_id], PROVIDER_VERSION_ID => nil)
129+
end
130+
131+
def self.provider_and_maybe_provider_version_match_any_selector_or_verification_is_missing(selectors)
132+
selectors.collect { |s|
133+
provider_and_maybe_provider_version_match_selector(s)
134+
} + selectors.collect { |s|
135+
provider_verification_is_missing_for_matching_selector(s)
136+
}
137+
end
138+
139+
# Some selecters are specified in the query, others are implied (when only one pacticipant is specified,
140+
# the integrations are automatically worked out, and the selectors for these are of type :implied )
141+
# When there are 3 pacticipants that each have dependencies on each other (A->B, A->C, B->C), the query
142+
# to deploy C (implied A, implied B, specified C) was returning the A->B row because it matched the
143+
# implied selectors as well.
144+
# This extra filter makes sure that every row that is returned actually matches one of the specified
145+
# selectors.
146+
def self.either_consumer_or_provider_was_specified_in_query(selectors)
147+
specified_pacticipant_ids = selectors.select{ |s| s[:type] == :specified }.collect{ |s| s[:pacticipant_id] }
148+
Sequel.|({ CONSUMER_ID => specified_pacticipant_ids } , { PROVIDER_ID => specified_pacticipant_ids })
149+
end
150+
end
151+
152+
def where_consumer_or_provider_is s
153+
where{
154+
Sequel.|(
155+
s[:pacticipant_version_id] ? Sequel.&(CONSUMER_ID => s[:pacticipant_id], CONSUMER_VERSION_ID => s[:pacticipant_version_id]) : Sequel.&(CONSUMER_ID => s[:pacticipant_id]),
156+
s[:pacticipant_version_id] ? Sequel.&(PROVIDER_ID => s[:pacticipant_id], PROVIDER_VERSION_ID => s[:pacticipant_version_id]) : Sequel.&(PROVIDER_ID => s[:pacticipant_id])
157+
)
158+
}
159+
end
160+
161+
def order_by_names_ascending_most_recent_first
162+
order(
163+
Sequel.asc(:consumer_name),
164+
Sequel.desc(:consumer_version_order),
165+
Sequel.asc(:provider_name),
166+
Sequel.desc(:provider_version_order),
167+
Sequel.desc(:verification_id))
168+
end
169+
170+
def eager_all_the_things
171+
eager(:consumer)
172+
.eager(:provider)
173+
.eager(:consumer_version)
174+
.eager(:provider_version)
175+
.eager(:verification)
176+
.eager(:pact_publication)
177+
.eager(:pact_version)
178+
end
179+
180+
end
181+
182+
def success
183+
verification&.success
184+
end
185+
186+
def pact_version_sha
187+
pact_version.sha
188+
end
189+
190+
def pact_revision_number
191+
pact_publication.revision_number
192+
end
193+
194+
def verification_number
195+
verification&.number
196+
end
197+
198+
def success
199+
verification&.success
200+
end
201+
202+
def pact_created_at
203+
pact_publication.created_at
204+
end
205+
206+
def verification_executed_at
207+
verification.execution_date
208+
end
209+
210+
# Add logic for ignoring case
211+
def <=> other
212+
comparisons = [
213+
compare_name_asc(consumer_name, other.consumer_name),
214+
compare_number_desc(consumer_version_order, other.consumer_version_order),
215+
compare_number_desc(pact_revision_number, other.pact_revision_number),
216+
compare_name_asc(provider_name, other.provider_name),
217+
compare_number_desc(provider_version_order, other.provider_version_order),
218+
compare_number_desc(verification_id, other.verification_id)
219+
]
220+
221+
comparisons.find{|c| c != 0 } || 0
222+
end
223+
224+
def compare_name_asc name1, name2
225+
name1 <=> name2
226+
end
227+
228+
def to_s
229+
"#{consumer_name} v#{consumer_version_number} #{provider_name} #{provider_version_number} #{success}"
230+
end
231+
232+
def compare_number_desc number1, number2
233+
if number1 && number2
234+
number2 <=> number1
235+
elsif number1
236+
1
237+
else
238+
-1
239+
end
240+
end
241+
242+
def eql?(obj)
243+
(obj.class == model) && (obj.values == values)
244+
end
245+
246+
def pacticipant_names
247+
[consumer_name, provider_name]
248+
end
249+
250+
def involves_pacticipant_with_name?(pacticipant_name)
251+
pacticipant_name.include?(pacticipant_name)
252+
end
253+
end
254+
end
255+
end

lib/pact_broker/matrix/repository.rb

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'pact_broker/repositories/helpers'
22
require 'pact_broker/matrix/row'
3+
require 'pact_broker/matrix/quick_row'
34
require 'pact_broker/matrix/head_row'
45
require 'pact_broker/error'
56
require 'pact_broker/matrix/query_results'
@@ -80,7 +81,7 @@ def find_compatible_pacticipant_versions selectors
8081
# If two or more are specified, just return the integrations that involve the specified pacticipants
8182
def find_integrations_for_specified_selectors(resolved_specified_selectors)
8283
specified_pacticipant_names = resolved_specified_selectors.collect(&:pacticipant_name)
83-
Row
84+
QuickRow
8485
.select(:consumer_name, :consumer_id, :provider_name, :provider_id)
8586
.matching_selectors(resolved_specified_selectors)
8687
.distinct
@@ -123,13 +124,8 @@ def apply_latestby options, selectors, lines
123124
end
124125

125126
def query_matrix selectors, options
126-
query = Row
127-
if options[:latestby]
128-
query = query.where(pact_publication_id: Row.db[:latest_pact_publication_ids_for_consumer_versions].select(:pact_publication_id))
129-
end
130-
127+
query = options[:latestby] ? QuickRow.eager_all_the_things : Row
131128
query = query.select_all.matching_selectors(selectors)
132-
133129
query = query.limit(options[:limit]) if options[:limit]
134130
query
135131
.order_by_names_ascending_most_recent_first
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require 'pact_broker/matrix/quick_row'
2+
3+
module PactBroker
4+
module Matrix
5+
describe QuickRow do
6+
before do
7+
td.create_pact_with_hierarchy("A", "1", "B")
8+
.create_verification(provider_version: '1', success: false)
9+
.create_verification(provider_version: '1', number: 2, success: true)
10+
.create_verification(provider_version: '2', number: 3, success: true)
11+
.create_provider("C")
12+
.create_pact
13+
.create_verification(provider_version: '1')
14+
.create_consumer_version("2")
15+
.create_pact
16+
.create_verification(provider_version: '3')
17+
.use_provider("B")
18+
.create_pact
19+
end
20+
21+
it "behaves like a Row, except smarter" do
22+
a_id = QuickRow.db[:pacticipants].where(name: "A").select(:id).single_record[:id]
23+
rows = QuickRow.consumer_id(a_id).eager(:consumer).eager(:verification).all
24+
expect(rows.first.consumer).to be rows.last.consumer
25+
expect(rows.first.verification).to_not be nil
26+
expect(rows.first.consumer_name).to_not be nil
27+
expect(rows.first.provider_name).to_not be nil
28+
end
29+
end
30+
end
31+
end

spec/lib/pact_broker/matrix/repository_spec.rb

+8-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def build_selectors(hash)
1212
end
1313

1414
def shorten_row row
15-
"#{row[:consumer_name]}#{row[:consumer_version_number]} #{row[:provider_name]}#{row[:provider_version_number] || '?'} n#{row[:verification_number] || '?'}"
15+
"#{row.consumer_name}#{row.consumer_version_number} #{row.provider_name}#{row.provider_version_number || '?'} n#{row.verification_number || '?'}"
1616
end
1717

1818
def shorten_rows rows
@@ -86,7 +86,7 @@ def shorten_rows rows
8686
end
8787
end
8888

89-
context "when latestby=cvp" do
89+
context "when latestby=cvp", can_i_deploy: true do
9090
let(:latestby) { 'cvp' }
9191

9292
it "returns the latest row for each provider for each consumer version" do
@@ -132,7 +132,7 @@ def shorten_rows rows
132132
end
133133
end
134134

135-
context "when latestby=cvp" do
135+
context "when latestby=cvp", can_i_deploy: true do
136136
let(:latestby) { 'cvp' }
137137

138138
it "returns the latest verifications for each provider for the specified consumer version" do
@@ -267,7 +267,7 @@ def shorten_rows rows
267267

268268
it "only includes the row for the latest revision" do
269269
expect(subject.size).to eq 1
270-
expect(subject).to contain_hash(pact_revision_number: 3)
270+
expect(subject.find { | s | s.pact_revision_number == 3} ).to_not be nil
271271
end
272272
end
273273

@@ -276,7 +276,7 @@ def shorten_rows rows
276276

277277
it "only includes the row for the latest revision" do
278278
expect(subject.size).to eq 1
279-
expect(subject).to contain_hash(pact_revision_number: 3)
279+
expect(subject.find { | s | s.pact_revision_number == 3} ).to_not be nil
280280
end
281281
end
282282

@@ -800,12 +800,12 @@ def shorten_rows rows
800800

801801
it "returns the latest revision of each pact" do
802802
expect(subject.count).to eq 2
803-
expect(subject[0][:consumer_version_number]).to eq "2.0.0"
804-
expect(subject[1][:consumer_version_number]).to eq "1.2.3"
803+
expect(subject[0].consumer_version_number).to eq "2.0.0"
804+
expect(subject[1].consumer_version_number).to eq "1.2.3"
805805
end
806806

807807
it "returns the latest verification for the pact version" do
808-
expect(subject[1][:provider_version_number]).to eq "4.5.6"
808+
expect(subject[1].provider_version_number).to eq "4.5.6"
809809
end
810810

811811
it "doesn't matter which way you order the pacticipant names" do

spec/lib/pact_broker/matrix/service_spec.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ module Matrix
109109
end
110110

111111
it "returns the row" do
112-
expect(subject[:consumer_name]).to eq 'consumer'
113-
expect(subject[:provider_name]).to eq 'provider'
114-
expect(subject[:consumer_version_number]).to eq '1'
115-
expect(subject[:provider_version_number]).to eq '2'
112+
expect(subject.consumer_name).to eq 'consumer'
113+
expect(subject.provider_name).to eq 'provider'
114+
expect(subject.consumer_version_number).to eq '1'
115+
expect(subject.provider_version_number).to eq '2'
116116
end
117117
end
118118

0 commit comments

Comments
 (0)