Skip to content
1 change: 1 addition & 0 deletions changes/11506.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Honor `AND`/`OR`/`NOT` clauses in `myDeployments` and `projectDeployments` GraphQL filters, which were previously ignored and caused multi-condition deployment queries to return unfiltered results.
1 change: 1 addition & 0 deletions changes/11509.enhance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Centralize v2 entity filter `AND` / `OR` / `NOT` clauses on a shared `BaseFilter` and add `convert_and` / `convert_or` / `convert_not` helpers on `BaseFilterAdapter`, applying the corrected `(A AND B) OR (C AND D)` and per-sub-filter `NOT` semantics across every adapter.
15 changes: 2 additions & 13 deletions src/ai/backend/common/dto/manager/v2/agent/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
AgentStatusFilter,
OrderDirection,
)
from ai.backend.common.dto.manager.v2.common import BaseFilter

__all__ = (
"AdminSearchAgentsInput",
Expand All @@ -42,7 +43,7 @@ class AgentPathParam(BaseRequestModel):
# ---------------------------------------------------------------------------


class AgentFilter(BaseRequestModel):
class AgentFilter(BaseFilter):
"""Filter conditions for agent search."""

id: StringFilter | None = Field(
Expand All @@ -65,18 +66,6 @@ class AgentFilter(BaseRequestModel):
"and their case-insensitive and negated variants."
),
)
AND: list[AgentFilter] | None = Field(
default=None, description="All sub-conditions must match."
)
OR: list[AgentFilter] | None = Field(
default=None, description="At least one sub-condition must match."
)
NOT: list[AgentFilter] | None = Field(
default=None, description="None of the sub-conditions must match."
)


AgentFilter.model_rebuild()


class AgentOrder(BaseRequestModel):
Expand Down
17 changes: 3 additions & 14 deletions src/ai/backend/common/dto/manager/v2/artifact/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel
from ai.backend.common.dto.manager.query import IntFilter, StringFilter, UUIDFilter
from ai.backend.common.dto.manager.v2.common import BaseFilter

from .types import (
ArtifactAvailability,
Expand Down Expand Up @@ -291,20 +292,14 @@ class ArtifactStatusChangedInputDTO(BaseRequestModel):
artifact_revision_ids: list[UUID] = Field(description="List of artifact revision IDs to watch.")


class ArtifactGQLFilterInputDTO(BaseRequestModel):
class ArtifactGQLFilterInputDTO(BaseFilter):
"""GQL-facing filter for artifacts."""

type: list[ArtifactType] | None = Field(default=None)
name: StringFilter | None = Field(default=None)
registry: StringFilter | None = Field(default=None)
source: StringFilter | None = Field(default=None)
availability: list[ArtifactAvailability] | None = Field(default=None)
AND: list[ArtifactGQLFilterInputDTO] | None = Field(default=None)
OR: list[ArtifactGQLFilterInputDTO] | None = Field(default=None)
NOT: list[ArtifactGQLFilterInputDTO] | None = Field(default=None)


ArtifactGQLFilterInputDTO.model_rebuild()


class ArtifactGQLOrderByInputDTO(BaseRequestModel):
Expand All @@ -328,20 +323,14 @@ class ArtifactRevisionRemoteStatusFilterDTO(BaseRequestModel):
equals: ArtifactRemoteStatus | None = Field(default=None)


class ArtifactRevisionGQLFilterInputDTO(BaseRequestModel):
class ArtifactRevisionGQLFilterInputDTO(BaseFilter):
"""GQL-facing filter for artifact revisions."""

status: ArtifactRevisionStatusFilterDTO | None = Field(default=None)
remote_status: ArtifactRevisionRemoteStatusFilterDTO | None = Field(default=None)
version: StringFilter | None = Field(default=None)
artifact_id: UUIDFilter | None = Field(default=None)
size: IntFilter | None = Field(default=None)
AND: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None)
OR: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None)
NOT: list[ArtifactRevisionGQLFilterInputDTO] | None = Field(default=None)


ArtifactRevisionGQLFilterInputDTO.model_rebuild()


class ArtifactRevisionGQLOrderByInputDTO(BaseRequestModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ai.backend.common.api_handlers import BaseRequestModel
from ai.backend.common.data.storage.registries.types import ModelTarget
from ai.backend.common.dto.manager.query import StringFilter
from ai.backend.common.dto.manager.v2.common import BaseFilter

from .types import ArtifactRegistryType

Expand Down Expand Up @@ -98,7 +99,7 @@ class ArtifactOrderingInput(BaseRequestModel):
)


class ArtifactFilterInput(BaseRequestModel):
class ArtifactFilterInput(BaseFilter):
"""Filtering options for artifacts, supporting recursive AND/OR/NOT composition."""

artifact_type: list[str] | None = Field(default=None, description="Filter by artifact type(s)")
Expand All @@ -120,9 +121,6 @@ class ArtifactFilterInput(BaseRequestModel):
availability: list[str] | None = Field(
default=None, description="Filter by availability status"
)
AND: list[ArtifactFilterInput] | None = Field(default=None, description="AND filter group")
OR: list[ArtifactFilterInput] | None = Field(default=None, description="OR filter group")
NOT: list[ArtifactFilterInput] | None = Field(default=None, description="NOT filter group")


# ---------------------------------------------------------------------------
Expand Down
13 changes: 2 additions & 11 deletions src/ai/backend/common/dto/manager/v2/audit_log/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ai.backend.common.api_handlers import BaseRequestModel
from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter
from ai.backend.common.dto.manager.v2.common import BaseFilter

from .types import AuditLogOrderField, AuditLogStatus, OrderDirection

Expand All @@ -30,7 +31,7 @@ class AuditLogStatusFilter(BaseRequestModel):
not_in: list[AuditLogStatus] | None = Field(default=None, description="Status is not in list")


class AuditLogFilter(BaseRequestModel):
class AuditLogFilter(BaseFilter):
"""Filter for audit logs."""

entity_type: StringFilter | None = Field(default=None, description="Entity type filter")
Expand All @@ -40,16 +41,6 @@ class AuditLogFilter(BaseRequestModel):
created_at: DateTimeFilter | None = Field(
default=None, description="Filter logs by created_at datetime"
)
AND: list[AuditLogFilter] | None = Field(default=None, description="All conditions must match")
OR: list[AuditLogFilter] | None = Field(
default=None, description="At least one condition must match"
)
NOT: list[AuditLogFilter] | None = Field(
default=None, description="None of the conditions must match"
)


AuditLogFilter.model_rebuild()


class AuditLogOrder(BaseRequestModel):
Expand Down
17 changes: 17 additions & 0 deletions src/ai/backend/common/dto/manager/v2/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
from decimal import Decimal
from enum import StrEnum
from functools import cached_property
from typing import Self

from pydantic import Field

from ai.backend.common.api_handlers import BaseRequestModel, BaseResponseModel

__all__ = (
"BaseFilter",
"BinarySizeInfo",
"BinarySizeInput",
"EnvironmentVariableEntryInfo",
Expand All @@ -27,6 +29,21 @@
)


class BaseFilter(BaseRequestModel):
"""Base class for v2 entity filter DTOs.

Provides the recursive ``AND`` / ``OR`` / ``NOT`` boolean clause fields shared
by every filter so subclasses only declare their typed leaf fields. The
clauses are typed as ``list[Self] | None``: each subclass automatically
references its own concrete type without requiring ``model_rebuild()``
or repeating the three fields.
"""

AND: list[Self] | None = Field(default=None, description="AND conjunction of sub-filters.")
OR: list[Self] | None = Field(default=None, description="OR conjunction of sub-filters.")
NOT: list[Self] | None = Field(default=None, description="NOT negation of sub-filters.")


class BinarySizeInput(BaseRequestModel):
"""Binary size input accepting bytes integer or human-readable string.

Expand Down
50 changes: 7 additions & 43 deletions src/ai/backend/common/dto/manager/v2/deployment/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
StringFilter,
UUIDFilter,
)
from ai.backend.common.dto.manager.v2.common import ResourceSlotEntryInput
from ai.backend.common.dto.manager.v2.common import BaseFilter, ResourceSlotEntryInput
from ai.backend.common.dto.manager.v2.deployment.types import (
AccessTokenOrderField,
AutoScalingRuleOrderField,
Expand Down Expand Up @@ -542,7 +542,7 @@ class ReplicaTrafficStatusFilter(BaseRequestModel):
)


class DeploymentFilter(BaseRequestModel):
class DeploymentFilter(BaseFilter):
"""Filter for deployments."""

name: StringFilter | None = Field(default=None, description="Name filter")
Expand All @@ -562,15 +562,9 @@ class DeploymentFilter(BaseRequestModel):
destroyed_at: NullableDateTimeFilter | None = Field(
default=None, description="Destruction datetime filter (supports is_null)"
)
AND: list[DeploymentFilter] | None = Field(default=None, description="AND conjunction")
OR: list[DeploymentFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[DeploymentFilter] | None = Field(default=None, description="NOT negation")


DeploymentFilter.model_rebuild()


class RevisionFilter(BaseRequestModel):
class RevisionFilter(BaseFilter):
"""Filter for deployment revisions."""

revision_number: IntFilter | None = Field(default=None, description="Filter by revision number")
Expand All @@ -584,15 +578,9 @@ class RevisionFilter(BaseRequestModel):
)
cluster_mode: StringFilter | None = Field(default=None, description="Cluster mode filter")
created_at: DateTimeFilter | None = Field(default=None, description="Creation datetime filter")
AND: list[RevisionFilter] | None = Field(default=None, description="AND conjunction")
OR: list[RevisionFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[RevisionFilter] | None = Field(default=None, description="NOT negation")


RevisionFilter.model_rebuild()


class RouteFilter(BaseRequestModel):
class RouteFilter(BaseFilter):
"""Filter for deployment routes."""

deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID")
Expand All @@ -605,15 +593,9 @@ class RouteFilter(BaseRequestModel):
traffic_status: list[RouteTrafficStatus] | None = Field(
default=None, description="Traffic status filter"
)
AND: list[RouteFilter] | None = Field(default=None, description="AND conjunction")
OR: list[RouteFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[RouteFilter] | None = Field(default=None, description="NOT negation")


RouteFilter.model_rebuild()


class AccessTokenFilter(BaseRequestModel):
class AccessTokenFilter(BaseFilter):
"""Filter for access tokens."""

deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID")
Expand All @@ -622,44 +604,26 @@ class AccessTokenFilter(BaseRequestModel):
default=None, description="Expiration datetime filter"
)
created_at: DateTimeFilter | None = Field(default=None, description="Creation datetime filter")
AND: list[AccessTokenFilter] | None = Field(default=None, description="AND conjunction")
OR: list[AccessTokenFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[AccessTokenFilter] | None = Field(default=None, description="NOT negation")


AccessTokenFilter.model_rebuild()


class AutoScalingRuleFilter(BaseRequestModel):
class AutoScalingRuleFilter(BaseFilter):
"""Filter for auto-scaling rules."""

deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID")
created_at: DateTimeFilter | None = Field(default=None, description="Creation datetime filter")
last_triggered_at: NullableDateTimeFilter | None = Field(
default=None, description="Last triggered datetime filter"
)
AND: list[AutoScalingRuleFilter] | None = Field(default=None, description="AND conjunction")
OR: list[AutoScalingRuleFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[AutoScalingRuleFilter] | None = Field(default=None, description="NOT negation")


AutoScalingRuleFilter.model_rebuild()


class ReplicaFilter(BaseRequestModel):
class ReplicaFilter(BaseFilter):
"""Filter for deployment replicas."""

deployment_id: UUID | None = Field(default=None, description="Filter by deployment ID")
status: ReplicaStatusFilter | None = Field(default=None, description="Replica status filter")
traffic_status: ReplicaTrafficStatusFilter | None = Field(
default=None, description="Replica traffic status filter"
)
AND: list[ReplicaFilter] | None = Field(default=None, description="AND conjunction")
OR: list[ReplicaFilter] | None = Field(default=None, description="OR conjunction")
NOT: list[ReplicaFilter] | None = Field(default=None, description="NOT negation")


ReplicaFilter.model_rebuild()


class DeploymentPolicyFilter(BaseRequestModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ai.backend.common.config import ModelDefinition
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
from ai.backend.common.dto.manager.v2.common import (
BaseFilter,
EnvironmentVariableEntryInput,
OrderDirection,
ResourceSlotEntryInput,
Expand Down Expand Up @@ -92,16 +93,10 @@ class UpdateDeploymentRevisionPresetInput(BaseRequestModel):
deployment_strategy: DeploymentStrategyInput | Sentinel | None = Field(default=SENTINEL)


class DeploymentRevisionPresetFilter(BaseRequestModel):
class DeploymentRevisionPresetFilter(BaseFilter):
id: UUIDFilter | None = Field(default=None, description="Filter by preset ID.")
name: StringFilter | None = Field(default=None)
runtime_variant_id: UUIDFilter | None = Field(default=None)
AND: list[DeploymentRevisionPresetFilter] | None = Field(default=None)
OR: list[DeploymentRevisionPresetFilter] | None = Field(default=None)
NOT: list[DeploymentRevisionPresetFilter] | None = Field(default=None)


DeploymentRevisionPresetFilter.model_rebuild()


class DeploymentRevisionPresetOrder(BaseRequestModel):
Expand Down
9 changes: 2 additions & 7 deletions src/ai/backend/common/dto/manager/v2/domain/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel
from ai.backend.common.dto.manager.defs import DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT
from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter
from ai.backend.common.dto.manager.v2.common import BaseFilter
from ai.backend.common.dto.manager.v2.domain.types import (
DomainOrderField,
DomainProjectFilter,
Expand Down Expand Up @@ -92,7 +93,7 @@ class PurgeDomainInput(BaseRequestModel):
name: str = Field(description="Name of the domain to permanently purge.")


class DomainFilter(BaseRequestModel):
class DomainFilter(BaseFilter):
"""Filter criteria for searching domains."""

name: StringFilter | None = Field(default=None, description="Filter by domain name.")
Expand All @@ -108,12 +109,6 @@ class DomainFilter(BaseRequestModel):
user: DomainUserFilter | None = Field(
default=None, description="Filter by nested user conditions."
)
AND: list[DomainFilter] | None = Field(default=None, description="AND logical combinator.")
OR: list[DomainFilter] | None = Field(default=None, description="OR logical combinator.")
NOT: list[DomainFilter] | None = Field(default=None, description="NOT logical combinator.")


DomainFilter.model_rebuild()


class DomainOrder(BaseRequestModel):
Expand Down
Loading
Loading