Skip to content

Commit c97a537

Browse files
committed
[COMMS-938] In numeric mode, search semantic identifiers only when hash-prefixed
https://community.openproject.org/wp/COMMS-938 This has two advantages: 1. It's easier to understand since it works the same way in classic identifier mode: Only when the number is prefixed with a hash the ID is searched 2. Otherwise in classic identifier mode the WorkPackageSemanticAlias table would be hit for every string that starts with a letter, which is unnecessary and makes the search even slower
1 parent c0b2ecc commit c97a537

7 files changed

Lines changed: 82 additions & 44 deletions

File tree

app/models/queries/work_packages/filter/typeahead_filter.rb

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@ def where
4444
parts.map { |part| "(#{conditions_for(part).join(' OR ')})" }.join(" AND ")
4545
end
4646

47-
def conditions_for(part)
47+
def conditions_for(part) # rubocop:disable Metrics/AbcSize
4848
conditions = [subject_condition(part),
4949
project_name_condition(part),
5050
type_name_condition(part),
5151
status_condition(part)]
5252

5353
if (match = part.match(/\A(#)?(#{Projects::Identifier::SEMANTIC_FORMAT.source}-?\d*)\z/i))
54-
conditions << work_package_identifier_condition(match[2])
54+
conditions << work_package_identifier_condition(hash_prefixed: match[1].present?, search_term: match[2])
5555
end
5656

5757
if (match = part.match(/\A(#)?(\d+)\z/))
5858
conditions << id_or_sequence_number_condition(hash_prefixed: match[1].present?, search_term: match[2])
5959
end
6060

61-
conditions
61+
conditions.compact
6262
end
6363

6464
def subject_condition(string)
@@ -69,14 +69,6 @@ def project_name_condition(string)
6969
Queries::Operators::Contains.sql_for_field([string], Project.table_name, "name")
7070
end
7171

72-
def work_package_identifier_condition(string)
73-
alias_condition = Queries::Operators::StartsWith.sql_for_field(
74-
[string], WorkPackageSemanticAlias.table_name, "identifier"
75-
)
76-
"#{WorkPackage.table_name}.id IN " \
77-
"(SELECT work_package_id FROM #{WorkPackageSemanticAlias.table_name} WHERE #{alias_condition})"
78-
end
79-
8072
def type_name_condition(string)
8173
Queries::Operators::Contains.sql_for_field([string], Type.table_name, "name")
8274
end
@@ -99,6 +91,16 @@ def status_condition(string)
9991
end
10092
end
10193

94+
def work_package_identifier_condition(hash_prefixed:, search_term:)
95+
return unless Setting::WorkPackageIdentifier.semantic? || hash_prefixed
96+
97+
alias_condition = Queries::Operators::StartsWith.sql_for_field(
98+
[search_term], WorkPackageSemanticAlias.table_name, "identifier"
99+
)
100+
"#{WorkPackage.table_name}.id IN " \
101+
"(SELECT work_package_id FROM #{WorkPackageSemanticAlias.table_name} WHERE #{alias_condition})"
102+
end
103+
102104
def id_or_sequence_number_condition(hash_prefixed:, search_term:)
103105
if Setting::WorkPackageIdentifier.classic? || hash_prefixed
104106
id_condition(search_term)

app/models/queries/work_packages/selects/exact_match_select.rb

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,13 @@ def self.exact_match_condition_sql(query_string)
6565
stripped = query_string.to_s.strip
6666
return nil if stripped.blank? || stripped.match?(/\s/)
6767

68+
hash_prefixed = stripped.start_with?("#")
6869
candidate = stripped.delete_prefix("#")
6970
condition =
7071
if candidate.match?(/\A[1-9]\d*\z/)
71-
numeric_exact_match_condition(candidate, hash_prefixed: stripped.start_with?("#"))
72+
numeric_exact_match_condition(candidate, hash_prefixed:)
7273
elsif candidate.match?(/\A#{WorkPackage::SemanticIdentifier::SEMANTIC_ID_PATTERN.source}\z/i)
73-
# So far, semantic identifiers are always upper case.
74-
# We can leverage this to match in a way that allows index usage.
75-
OpenProject::SqlSanitization.sanitize(
76-
"#{WorkPackage.table_name}.id IN (SELECT work_package_id FROM " \
77-
"#{WorkPackageSemanticAlias.table_name} WHERE identifier = ?)",
78-
candidate.upcase
79-
)
74+
semantic_exact_match_condition(candidate, hash_prefixed:)
8075
end
8176

8277
return nil unless condition
@@ -95,4 +90,16 @@ def self.numeric_exact_match_condition(candidate, hash_prefixed:)
9590
)
9691
end
9792
end
93+
94+
def self.semantic_exact_match_condition(candidate, hash_prefixed:)
95+
return nil unless Setting::WorkPackageIdentifier.semantic? || hash_prefixed
96+
97+
# So far, semantic identifiers are always upper case.
98+
# We can leverage this to match in a way that allows index usage.
99+
OpenProject::SqlSanitization.sanitize(
100+
"#{WorkPackage.table_name}.id IN (SELECT work_package_id FROM " \
101+
"#{WorkPackageSemanticAlias.table_name} WHERE identifier = ?)",
102+
candidate.upcase
103+
)
104+
end
98105
end

spec/models/queries/work_packages/filter/typeahead_filter_spec.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,23 @@
352352
identifier: "PHO-2")
353353
end
354354

355-
context "and there are still entries in the semantic alias registry" do
356-
let(:values) { [identifier_work_package1_semantic_alias.identifier] }
355+
context "when searching by a semantic identifier prefixed with '#'" do
356+
let(:values) { ["##{identifier_work_package1_semantic_alias.identifier}"] }
357357

358358
it "still finds by existing identifiers" do
359359
expect(subject).to include(identifier_work_package1)
360360
expect(subject).not_to include(identifier_work_package2)
361361
end
362362
end
363+
364+
context "when searching by a semantic identifier not prefixed with '#'" do
365+
let(:values) { [identifier_work_package1_semantic_alias.identifier] }
366+
367+
it "does not find by existing identifiers" do
368+
expect(subject).not_to include(identifier_work_package1)
369+
expect(subject).not_to include(identifier_work_package2)
370+
end
371+
end
363372
end
364373

365374
context "when searching by status" do

spec/models/queries/work_packages/selects/exact_match_select_spec.rb

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -121,34 +121,54 @@ def ranked_ids(ids)
121121
end
122122
let(:query_string) { "COM-5" }
123123

124-
it "ranks the exact identifier match above one that only shares the prefix" do
125-
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
126-
.to eq(exact_work_package.id)
127-
end
128-
129-
context "when the query is given in lower case" do
130-
let(:query_string) { "com-5" }
131-
132-
it "still matches COM-5" do
124+
context "and the instance is in semantic identifier mode",
125+
with_settings: { work_packages_identifier: Setting::WorkPackageIdentifier::SEMANTIC } do
126+
it "ranks the exact identifier match above one that only shares the prefix" do
133127
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
134128
.to eq(exact_work_package.id)
135129
end
136-
end
137130

138-
context "when matching a historical alias in classic mode",
139-
with_settings: { work_packages_identifier: Setting::WorkPackageIdentifier::CLASSIC } do
140-
it "still matches through the alias table" do
141-
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
142-
.to eq(exact_work_package.id)
131+
context "when the query is given in lower case" do
132+
let(:query_string) { "com-5" }
133+
134+
it "still matches COM-5" do
135+
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
136+
.to eq(exact_work_package.id)
137+
end
138+
end
139+
140+
# context "when matching a historical alias in classic mode",
141+
# with_settings: { work_packages_identifier: Setting::WorkPackageIdentifier::CLASSIC } do
142+
# it "still matches through the alias table" do
143+
# expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
144+
# .to eq(exact_work_package.id)
145+
# end
146+
# end
147+
148+
context "when the query is hash-prefixed" do
149+
let(:query_string) { "#COM-5" }
150+
151+
it "still ranks the exact identifier match above one that only shares the prefix" do
152+
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
153+
.to eq(exact_work_package.id)
154+
end
143155
end
144156
end
145157

146-
context "when the query is hash-prefixed" do
147-
let(:query_string) { "#COM-5" }
158+
context "and the instance is in classic identifier mode" do
159+
context "when the query is hash-prefixed" do
160+
let(:query_string) { "#COM-5" }
148161

149-
it "still ranks the exact identifier match above one that only shares the prefix" do
150-
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
151-
.to eq(exact_work_package.id)
162+
it "still ranks the exact identifier match above one that only shares the prefix" do
163+
expect(ranked_ids([exact_work_package.id, prefix_work_package.id]).first)
164+
.to eq(exact_work_package.id)
165+
end
166+
end
167+
168+
context "when the query is not hash-prefixed" do
169+
let(:query_string) { "COM-5" }
170+
171+
it { is_expected.to be_nil }
152172
end
153173
end
154174
end

spec/models/query/sort_criteria_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
context "when a typeahead filter is active on the query" do
9898
let(:query) do
9999
build_stubbed(:query, show_hierarchies: false).tap do |q|
100-
q.add_filter(:typeahead, "**", "COM-5")
100+
q.add_filter(:typeahead, "**", "#COM-5")
101101
end
102102
end
103103

spec/requests/api/v3/work_packages/available_relation_candidates_resource_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def work_packages
138138

139139
describe "relation candidates for wp1 (in hierarchy) with exact_match sorting" do
140140
let(:href) do
141-
"/api/v3/work_packages/#{wp1.id}/available_relation_candidates?query=RELCAND-5" \
141+
"/api/v3/work_packages/#{wp1.id}/available_relation_candidates?query=%23RELCAND-5" \
142142
"&sortBy=[[\"exactMatch\",\"desc\"],[\"updatedAt\",\"desc\"]]"
143143
end
144144

spec/requests/api/v3/work_packages/index_resource_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
{
129129
typeahead: {
130130
operator: "**",
131-
values: "COM-5"
131+
values: "#COM-5"
132132
}
133133
}
134134
]

0 commit comments

Comments
 (0)