Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 147 additions & 62 deletions src/sentry/search/eap/spans/filter_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,51 +83,89 @@ def semver_filter_converter(params: SnubaParams, search_filter: SearchFilter) ->
organization_id = params.organization_id
if organization_id is None:
raise ValueError("organization is a required param")
# We explicitly use `raw_value` here to avoid converting wildcards to shell values
if not isinstance(search_filter.value.raw_value, str):
raise InvalidSearchQuery(
f"{search_filter.key.name}: Invalid value: {search_filter.value.raw_value}. Expected a semver version."
)
version: str = search_filter.value.raw_value

operator: str = search_filter.operator

# Note that we sort this such that if we end up fetching more than
# MAX_SEMVER_SEARCH_RELEASES, we will return the releases that are closest to
# the passed filter.
order_by = Release.SEMVER_COLS
if operator.startswith("<"):
order_by = list(map(_flip_field_sort, order_by))
qs = (
Release.objects.filter_by_semver(
organization_id,
parse_semver(version, operator),
project_ids=params.project_ids,
)
.values_list("version", flat=True)
.order_by(*order_by)[: constants.MAX_SEARCH_RELEASES]
)
versions = list(qs)
final_operator: Literal["IN", "NOT IN"] = "IN"
if len(versions) == constants.MAX_SEARCH_RELEASES:
# We want to limit how many versions we pass through to Snuba. If we've hit
# the limit, make an extra query and see whether the inverse has fewer ids.
# If so, we can do a NOT IN query with these ids instead. Otherwise, we just
# do our best.
operator = constants.OPERATOR_NEGATION_MAP[operator]
# Note that the `order_by` here is important for index usage. Postgres seems
# to seq scan with this query if the `order_by` isn't included, so we
# include it even though we don't really care about order for this query
qs_flipped = (
Release.objects.filter_by_semver(organization_id, parse_semver(version, operator))
.order_by(*map(_flip_field_sort, order_by))
.values_list("version", flat=True)[: constants.MAX_SEARCH_RELEASES]
# Handle IN operator by processing each version separately
if operator in ["IN", "NOT IN"]:
raw_versions = search_filter.value.raw_value
if not isinstance(raw_versions, list):
raw_versions_list = [raw_versions]
else:
raw_versions_list = raw_versions

all_versions = set()
for version_item in raw_versions_list:
try:
if not isinstance(version_item, str):
continue
# For each version in the IN clause, create a separate query using = operator
individual_qs = (
Release.objects.filter_by_semver(
organization_id,
parse_semver(version_item, "="),
project_ids=params.project_ids,
)
.values_list("version", flat=True)
.order_by(*Release.SEMVER_COLS)[: constants.MAX_SEARCH_RELEASES]
)
all_versions.update(individual_qs)
except InvalidSearchQuery:
# Skip invalid semver versions in the IN clause
continue

versions = list(all_versions)
final_op_in: Literal["IN", "NOT IN"] = "IN" if operator == "IN" else "NOT IN"
else:
# Original logic for non-IN operators
# We explicitly use `raw_value` here to avoid converting wildcards to shell values
if not isinstance(search_filter.value.raw_value, str):
raise InvalidSearchQuery(
f"{search_filter.key.name}: Invalid value: {search_filter.value.raw_value}. Expected a semver version."
)
version_str: str = search_filter.value.raw_value

# Note that we sort this such that if we end up fetching more than
# MAX_SEMVER_SEARCH_RELEASES, we will return the releases that are closest to
# the passed filter.
order_by = Release.SEMVER_COLS
if operator.startswith("<"):
order_by = list(map(_flip_field_sort, order_by))
qs = (
Release.objects.filter_by_semver(
organization_id,
parse_semver(version_str, operator),
project_ids=params.project_ids,
)
.values_list("version", flat=True)
.order_by(*order_by)[: constants.MAX_SEARCH_RELEASES]
)
versions = list(qs)
final_op_other: Literal["IN", "NOT IN"] = "IN"

# Apply the optimization logic for non-IN operators only
if len(versions) == constants.MAX_SEARCH_RELEASES:
# We want to limit how many versions we pass through to Snuba. If we've hit
# the limit, make an extra query and see whether the inverse has fewer ids.
# If so, we can do a NOT IN query with these ids instead. Otherwise, we just
# do our best.
operator = constants.OPERATOR_NEGATION_MAP[operator]
# Note that the `order_by` here is important for index usage. Postgres seems
# to seq scan with this query if the `order_by` isn't included, so we
# include it even though we don't really care about order for this query
qs_flipped = (
Release.objects.filter_by_semver(
organization_id, parse_semver(version_str, operator)
)
.order_by(*map(_flip_field_sort, order_by))
.values_list("version", flat=True)[: constants.MAX_SEARCH_RELEASES]
)

exclude_versions = list(qs_flipped)
if exclude_versions and len(exclude_versions) < len(versions):
# Do a negative search instead
final_operator = "NOT IN"
versions = exclude_versions
exclude_versions = list(qs_flipped)
if exclude_versions and len(exclude_versions) < len(versions):
# Do a negative search instead
final_op_other = "NOT IN"
versions = exclude_versions

if not validate_snuba_array_parameter(versions):
raise InvalidSearchQuery(
Expand All @@ -138,7 +176,13 @@ def semver_filter_converter(params: SnubaParams, search_filter: SearchFilter) ->
# XXX: Just return a filter that will return no results if we have no versions
versions = [constants.SEMVER_EMPTY_RELEASE]

return [SearchFilter(SearchKey(constants.RELEASE_ALIAS), final_operator, SearchValue(versions))]
return [
SearchFilter(
SearchKey(constants.RELEASE_ALIAS),
final_op_in if operator in ["IN", "NOT IN"] else final_op_other,
SearchValue(versions),
)
]


def semver_package_filter_converter(
Expand Down Expand Up @@ -181,26 +225,61 @@ def semver_build_filter_converter(
if organization_id is None:
raise ValueError("organization is a required param")

if not isinstance(search_filter.value.raw_value, str):
raise InvalidSearchQuery(
f"{search_filter.key.name}: Invalid value: {search_filter.value.raw_value}. Expected a semver build."
)
build: str = search_filter.value.raw_value
operator = search_filter.operator

operator, negated = handle_operator_negation(search_filter.operator)
try:
django_op = constants.OPERATOR_TO_DJANGO[operator]
except KeyError:
raise InvalidSearchQuery("Invalid operation 'IN' for semantic version filter.")
versions = list(
Release.objects.filter_by_semver_build(
organization_id,
django_op,
build,
project_ids=params.project_ids,
negated=negated,
).values_list("version", flat=True)[: constants.MAX_SEARCH_RELEASES]
)
# Handle IN operator by processing each build separately
if operator in ["IN", "NOT IN"]:
raw_builds = search_filter.value.raw_value
if not isinstance(raw_builds, list):
raw_builds_list = [raw_builds]
else:
raw_builds_list = raw_builds

all_versions = set()
for build_item in raw_builds_list:
try:
if not isinstance(build_item, str):
continue
# For each build in the IN clause, create a separate query using = operator
op, negated = handle_operator_negation("=")
django_op = constants.OPERATOR_TO_DJANGO[op]
individual_qs = Release.objects.filter_by_semver_build(
organization_id,
django_op,
build_item,
project_ids=params.project_ids,
negated=negated,
).values_list("version", flat=True)[: constants.MAX_SEARCH_RELEASES]
all_versions.update(individual_qs)
except (InvalidSearchQuery, KeyError):
# Skip invalid build values in the IN clause
continue

versions = list(all_versions)
final_op_build_in = "IN" if operator == "IN" else "NOT IN"
else:
# Original logic for non-IN operators
if not isinstance(search_filter.value.raw_value, str):
raise InvalidSearchQuery(
f"{search_filter.key.name}: Invalid value: {search_filter.value.raw_value}. Expected a semver build."
)
build_str: str = search_filter.value.raw_value

operator, negated = handle_operator_negation(search_filter.operator)
try:
django_op = constants.OPERATOR_TO_DJANGO[operator]
except KeyError:
raise InvalidSearchQuery("Invalid operation 'IN' for semantic version filter.")
versions = list(
Release.objects.filter_by_semver_build(
organization_id,
django_op,
build_str,
project_ids=params.project_ids,
negated=negated,
).values_list("version", flat=True)[: constants.MAX_SEARCH_RELEASES]
)
final_op_build_other = "IN"

if not validate_snuba_array_parameter(versions):
raise InvalidSearchQuery(
Expand All @@ -211,7 +290,13 @@ def semver_build_filter_converter(
# XXX: Just return a filter that will return no results if we have no versions
versions = [constants.SEMVER_EMPTY_RELEASE]

return [SearchFilter(SearchKey(constants.RELEASE_ALIAS), "IN", SearchValue(versions))]
return [
SearchFilter(
SearchKey(constants.RELEASE_ALIAS),
final_op_build_in if operator in ["IN", "NOT IN"] else final_op_build_other,
SearchValue(versions),
)
]


def trace_filter_converter(params: SnubaParams, search_filter: SearchFilter) -> list[SearchFilter]:
Expand Down
Loading
Loading