Skip to content

Commit 186cc14

Browse files
jeremylenzclaude
andcommitted
Fixes #39002 - Handle nil hostgroup in GroupParameter search
When searching for hosts by parameters, the search_by_params method would crash with "NoMethodError: undefined method 'subtree_ids' for nil:NilClass" if it encountered a GroupParameter with a nil hostgroup association (orphaned parameter). This can occur due to the polymorphic nature of the Parameter model's reference_id column in the STI table design, where database-level foreign key constraints are not possible. If a hostgroup is deleted outside of Rails or cascade delete fails, orphaned GroupParameters can exist. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> This commit properly handles negated searches like "not params.foo = bar". When conditions were blank (due to orphaned parameters) but negate was present, the code would create a string starting with " NOT(...)" which caused invalid SQL generation: "NOT COALESCE(, false)". This commit fixes the issue by ensuring we always build valid SQL: - If conditions is blank but negate is present: "NOT(negate)" - If both conditions and negate are present: "conditions AND NOT(negate)" - If only conditions is present: "conditions" (unchanged) - If both are blank: "1 = 0" (no results) This prevents SQL syntax errors while maintaining correct search semantics. Also added test coverage for negated searches with orphaned parameters. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 628206f commit 186cc14

File tree

2 files changed

+65
-3
lines changed

2 files changed

+65
-3
lines changed

app/models/concerns/hostext/search.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,18 @@ def search_by_params(key, operator, value)
196196
conditions = param_conditions(p)
197197
negate = param_conditions(n)
198198

199-
conditions += " AND " unless conditions.blank? || negate.blank?
200-
conditions += " NOT(#{negate})" if negate.present?
199+
# If we have parameters but no valid conditions (e.g., all GroupParameters have nil hostgroup),
200+
# return no results instead of matching all hosts
201+
return {:conditions => '1 = 0'} if conditions.blank? && negate.blank?
202+
203+
# If only the positive conditions are blank (orphaned), but we have negations,
204+
# we need to negate from a base of "all hosts"
205+
if conditions.blank? && negate.present?
206+
conditions = "NOT(#{negate})"
207+
elsif conditions.present? && negate.present?
208+
conditions += " AND NOT(#{negate})"
209+
end
210+
201211
{:joins => :primary_interface, :conditions => conditions}
202212
end
203213

@@ -274,7 +284,9 @@ def param_conditions(p)
274284
when 'OsParameter'
275285
conditions << "hosts.operatingsystem_id = #{param.reference_id}"
276286
when 'GroupParameter'
277-
conditions << "hosts.hostgroup_id IN (#{param.hostgroup.subtree_ids.join(', ')})"
287+
if param.hostgroup.present?
288+
conditions << "hosts.hostgroup_id IN (#{param.hostgroup.subtree_ids.join(', ')})"
289+
end
278290
when 'HostParameter'
279291
conditions << "hosts.id = #{param.reference_id}"
280292
when 'OrganizationParameter'

test/models/concerns/hostext/search_test.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,56 @@ class SearchTest < ActiveSupport::TestCase
271271
it { assert_includes(subject, built_status.host) }
272272
it { assert_not_includes(subject, build_failed_status.host) }
273273
end
274+
275+
context "search by parameters" do
276+
let(:hostgroup) { FactoryBot.create(:hostgroup) }
277+
let(:group_param) { FactoryBot.create(:hostgroup_parameter, name: 'test_param', value: 'test_value', hostgroup: hostgroup) }
278+
let(:host_with_param) { FactoryBot.create(:host, hostgroup: hostgroup) }
279+
let(:host_without_param) { FactoryBot.create(:host) }
280+
281+
setup do
282+
group_param
283+
host_with_param
284+
host_without_param
285+
end
286+
287+
test "can search hosts by GroupParameter value" do
288+
result = Host.search_for("params.test_param = test_value")
289+
assert_includes result, host_with_param
290+
assert_not_includes result, host_without_param
291+
end
292+
293+
test "handles orphaned GroupParameter with nil hostgroup gracefully" do
294+
# Create an orphaned GroupParameter (bypassing validation)
295+
orphaned_param = GroupParameter.new(name: 'orphaned_param', value: 'orphaned_value')
296+
orphaned_param.save(validate: false)
297+
298+
# Search should not crash even with orphaned parameter
299+
assert_nothing_raised do
300+
Host.search_for("params.orphaned_param = orphaned_value")
301+
end
302+
303+
# Should return empty results since no host has this orphaned param
304+
result = Host.search_for("params.orphaned_param = orphaned_value")
305+
assert_empty result
306+
end
307+
308+
test "handles negated search with orphaned GroupParameter" do
309+
# Create an orphaned GroupParameter with value 'f'
310+
orphaned_param = GroupParameter.new(name: 'test_negation', value: 'f')
311+
orphaned_param.save(validate: false)
312+
313+
# Search "not params.test_negation = f" should not crash with SQL syntax error
314+
assert_nothing_raised do
315+
Host.search_for("not params.test_negation = f")
316+
end
317+
318+
# Since the orphaned param doesn't apply to any host, no host has test_negation=f,
319+
# therefore all hosts match "not params.test_negation = f"
320+
result = Host.search_for("not params.test_negation = f")
321+
assert_equal Host.count, result.count
322+
end
323+
end
274324
end
275325
end
276326
end

0 commit comments

Comments
 (0)